使用webpack打包多页应用
引言
对于传统的多页应用,有着诸多比较棘手的问题,如公共模块的复用、样式兼容性、语法兼容性等等问题,而我们可以通过webpack进行构建,通过预编译的方式赋予多页应用更多的功能,如:模块化、代码检查、预处理器、最新语法、模板引擎等,本文将带大家使用webpack去配置多页应用。
多页入口的配置
多页入口的配置,其实就是配置[代码]entry[代码]属性为一个对象,先来看一个例子:
[代码]// webpack.config.js
module.exports = {
entry: {
pageOne: './src/pageOne/index.js',
pageTwo: './src/pageTwo/index.js',
pageThree: './src/pageThree/index.js'
}
};
[代码]
如上配置示例告诉 webpack 需要三个独立分离的依赖图,运行后,输出如下:
[代码]$ webpack
Hash: abc3dca0a96fc92ffdf9
Version: webpack 4.32.2
Time: 246ms
Built at: 2019-05-26 11:13:15
Asset Size Chunks Chunk Names
pageOne.js 952 bytes 0 [emitted] pageOne
pageThree.js 956 bytes 1 [emitted] pageThree
pageTwo.js 953 bytes 2 [emitted] pageTwo
Entrypoint pageOne = pageOne.js
Entrypoint pageTwo = pageTwo.js
Entrypoint pageThree = pageThree.js
[0] ./src/pageOne/index.js 24 bytes {0} [built]
[1] ./src/pageTwo/index.js 24 bytes {2} [built]
[2] ./src/pageThree/index.js 25 bytes {1} [built]
[代码]
之后可以在[代码]dist[代码]目录下看到,wepback生成了[代码]pageOne.js[代码]、[代码]pageTwo.js[代码]、[代码]pageThree.js[代码]这三个文件。
多页入口的配置方式
在多页应用中,一般配置多页入口,有两种配置方式:
使用配置文件进行配置
优点:入口文件可以随意放置,只需要配置正确即可
缺点:每次新增入口都需要先进行配置
约定一个目录存放入口文件
优点:约定大于配置,消除了配置成本
缺点:没有配置文件那么灵活,入口需要放置在约定目录
本文中我们将采用第2种方式进行配置。
首先,约定[代码]src/views[代码]目录为页面的入口,如下所示:
[代码]src
├──views
│ ├──home
│ │ ├──home.js
│ │ ├──home.pug 使用 pug 模板引擎
│ │ └──home.scss 使用 scss 预处理器处理样式
│ ├──page1
│ │ ├──page1.js
│ │ ├──page1.pug 使用 pug 模板引擎
│ │ └──page1.scss 使用 scss 预处理器处理样式
│ ...
...
[代码]
实现收集逻辑
我们会使用[代码]glob[代码]库去引入这些入口文件,关于[代码]glob[代码]的用法,请戳GitHub - isaacs/node-glob: glob functionality for node.js
新建[代码]build/entry.js[代码]文件,内容如下:
[代码]// build/entry.js
const path = require('path');
// 此处需要 "npm install glob"
const glob = require('glob');
// 相对于根目录取路径
const resolve = dir => path.join(__dirname, '../', dir);
// 收集的入口
const entry = {};
// 收集入口文件的目录
const filePaths = glob.sync(resolve('src/views/*'));
filePaths.forEach(filePath => {
// 目录的名称,也就是打包的文件名
const filename = path.basename(filePath);
entry[filename] = `${filePath}/${filename}.js`;
});
exports.getEntry = function getEntry() {
return entry;
};
[代码]
修改 [代码]webpack.config.js[代码]文件,内容如下:
[代码]// webpack.config.js
const path = require('path');
const { getEntry } = require('./build/entry');
const resolve = dir => path.join(__dirname, dir);
module.exports = {
entry: getEntry(),
// 配置输出的路径
output: {
// 输出到 dist 目录下
path: resolve('dist'),
// 输出到 dist/js 目录下
filename: 'js/[name].js'
}
};
[代码]
实现HTML的生成
要生成HTML文件,我们需要借助[代码]html-webpack-plugin[代码]插件。
因为我们这里使用了[代码]pug[代码]模板引擎,所以需要先经过[代码]pug-loader[代码]进行解析,关于[代码]pug[代码]更多信息,请戳入门指南 – Pug。
修改 [代码]build/entry.js[代码]文件, 内容如下:
[代码]// build/entry.js
...
// 此处需要 "npm install html-webpack-plugin"
const HtmlWebpackPlugin = require('html-webpack-plugin');
...
exports.getEntryHTMLPlugins = function getEntryHTMLPlugins() {
return Object.keys(entry).map(filename => {
// 根据入口文件生成 HtmlWebpackPlugin 实例
// 一个实例对应一个 html 文件
return new HtmlWebpackPlugin({
// 模板文件
template: `${path.dirname(entry[filename])}/${filename}.pug`,
// 输出的文件名
filename: `${filename}.html`,
chunks: [filename]
});
});
};
[代码]
修改 [代码]webpack.config.js[代码]文件,内容如下:
[代码]// webpack.config.js
...
const { getEntry, getEntryHTMLPlugins } = require('./build/entry');
module.exports = {
...,
module: {
rules: [
{
// 此处需要 "npm install pug pug-loader"
// 配置 pug-loader 去解析转换 pug 文件
test: /\.(pug|jade)$/,
loader: 'pug-loader'
}
]
},
plugins: [
...getEntryHTMLPlugins()
]
};
[代码]
至此,多页入口的配置已经完成。
赋予多页应用更多的功能
入口配置完成之后,我们将完善 webpack 配置,为多页应用添加语法检查、样式预处理器、以及ES6语法转换,这些功能都是通过 loader 去处理的,我们可以简单的把 loader 看成是一个转换器。
webpack 在编译时,会对 import 的模块(文件)进行检查(也就是使用配置中的 rules 里配置的 test 正则去匹配模块),如果某个文件满足 rules 配置的规则,则会使用设置的 loader 去处理这个文件。
添加语法检查
通过 eslint 对编译前的脚本进行检查,修改 [代码]webpack.config.js[代码]文件,内容如下:
[代码]// webpack.config.js
...
module.exports = {
...,
module: {
rules: [
...,
{
// 对 js 后缀的文件进行检查
test: /\.js$/,
// 此处需要 "npm install eslint eslint-loader"
loader: 'eslint-loader',
// 加载器的执行循序,这里设置先执行
// 也就是检查编译前的代码
enfore: 'pre',
exclude: /(node_modules)/
}
]
},
...
};
[代码]
需要在根目录下创建[代码].eslintrc[代码]文件,对 eslint 语法检查的规则进行配置,本文不对 eslint 配置做详细介绍,关于 eslint 的更多配置,请戳Configuring ESLint - ESLint - Pluggable JavaScript linter
ES6语法转换
对于 js 文件,通过 babel 进行转换,修改 [代码]webpack.config.js[代码]文件,内容如下:
[代码]// webpack.config.js
...
module.exports = {
...,
module: {
rules: [
...,
{
// 对 js 后缀的文件进行转换
test: /\.js$/,
// 此处需要 "npm install babel-loader @babel/core"
loader: 'babel-loader',
exclude: /(node_modules)/
}
]
},
...
};
[代码]
需要在根目录下创建[代码].babelrc[代码]文件,对 babel 的转换规则进行配置,本文不对 babel 配置做详细介绍,关于 babel 配置的更多信息,请戳Configure Babel · Babel
样式预处理器
这里样式的 loader 配置和上面两种不太一样,需要链式调用多个 loader,以确保样式能正常运作。修改 [代码]webpack.config.js[代码]文件,内容如下:
[代码]// webpack.config.js
...
// 此处需要 "npm install mini-css-extract-plugin"
// 此插件用于提取 css
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = {
...,
module: {
rules: [
...,
{
// 对 sass 或 scss 文件进行处理
test: /\.s(c|a)ss$/,
use: [
// 当配置了多个 loader 时,webpack 的处理方式是从后到前的顺序(也就是 sass-loader、postcss-loader、css-loader)
// 这里先经过了 sass 解析、再通过 postcss 补全样式
// 然后通过 css-loader 去处理一些 css module 或 @import 等问题
// 最后通过插件的 loader 把样式提取出来
MiniCssExtractPlugin.loader,
// 此处需要 "npm install css-loader postcss-loader sass-loader"
'css-loader',
'postcss-loader',
{
loader: 'sass-loader',
options: {
// 此处需要 "npm install dart-sass"
// 这里配置 sass 的解析器为 dart-sass,而不是 node-sass,主要是安装方便和处理速度的提升
implementation: require('dart-sass')
}
}
]
}
]
},
plugins: [
...,
// 初始化插件实例,配置提出来的 css 路径和名称
new MiniCssExtractPlugin({
filename: 'css/[name].css'
})
]
...
};
[代码]
之后,我们需要在根目录下创建 postcss 的配置文件 [代码]postcss.config.js[代码] 去告知 postcss 去做什么样的处理,如下所示:
[代码]// postcss.config.js
module.exports = {
plugins: {
// 此处需要 "npm install autoprefixer"
// 配置自动补全插件
autoprefixer: {
// 指定补全的浏览器版本,可以有效的减少补全的代码
browsers: ['> 2%', 'last 5 versions', 'IE >= 10']
}
}
};
[代码]
关于 browsers 匹配到的浏览器,可以戳这里进行查询:browserl.ist: A page to display compatible browsers from a browserslist string.
提取公共 js 代码
在多个页面中,我们可能会在 A 和 B 页面中都引入同一个 js 文件,为了复用这个 js 文件,我们可以通过配置,提取出该文件。
修改 [代码]webpack.config.js[代码],内容如下:
[代码]// webpack.config.js
module.exports = {
...,
optimization: {
// 提取异步加载代码至 manifest 块中
// 当我们代码中有异步加载的文件时,会在该文件内生成异步文件加载的逻辑
// 当多个文件有异步加载时,每次在该文件生成的代码实际上大部分是相同的(基本上除了 chunkId 和 chunknamne)
// 所以我们可以提取出相同的逻辑,放到一个文件中进行加载,以减少单个文件大小
runtimeChunk: : {
name: 'manifest'
},
splitChunks: {
cacheGroups: {
// 提取公共的 node_modules 代码至 vendors 块中
// 通常文件中可能会通过 npm 安装一些资源包,如果有多个文件同时引入了相同的资源包,可以通过这种方式去提取该资源包,以减少单个文件的大小和复用相同的代码
vendor: {
name: 'vendors',
test: /[\\/]node_modules[\\/]/
}
}
}
}
};
[代码]
提取了公共的代码块之后,我们还需要去 [代码]HtmlWebpackPlugin[代码]的实例中引用这些公共的代码块,修改[代码]build/entry.js[代码]文件,内容如下:
[代码]// build/entry.js
...
exports.getEntryHTMLPlugins = function getEntryHTMLPlugins() {
...
return new HtmlWebpackPlugin({
...,
chunks: ['mainfest', 'vendor', filename]
});
};
[代码]
开发环境和生产环境的配置
开发环境和生产环境稍有不同,主要区别在于:
生产环境:代码压缩
开发环境:监听文件变动实时编译、soucemap
要针对不同环境进行编译处理,我们可以借助 node 的[代码]npm script[代码]来区分(通过加载不同的配置文件),在根目录下找到(如果已经有该文件)或新建[代码]package.json[代码]文件,内容如下:
[代码]{
"scripts": {
"dev": "webpack --config webpack.dev.conf.js --watch",
"build": "webpack --config webpack.prod.conf.js"
}
}
[代码]
多于多个配置文件,有部分配置可能会重复,我们可以利用[代码]webpack-merge[代码]库对相同的配置进行合并,以减少不必要的重复配置。
开发环境的配置
根目录下新建[代码]webpack.dev.conf.js[代码]文件,内容如下:
[代码]// webpack.dev.conf.js
// 此处需要 "npm install webpack-merge"
const merge = require('webpack-merge');
const baseConf = require('./webpack.config.js');
// 在原有的基础上扩展 dev 时需要的配置
module.exports = merge(baseConf, {
// webpack 4 新增的选项,会根据环境添加一些环境需要的配置
// 如:
// 添加了 NamedModulesPlugin(把 chunkid 换成 chunkname)
// 设置 devtool 为 eval(为每个 module 会封装到 eval 里包裹起来执行,并且会在末尾追加注释//@ sourceURL.)
// 公共代码的提取配置(optimization.splitChunks 选项)等
mode: 'development'
});
[代码]
关于[代码]mode[代码]属性的详细信息,请戳这里: https://webpack.js.org/configuration/mode/
生产环境的配置
根目录下新建[代码]webpack.prod.conf.js[代码]文件,内容如下:
[代码]// webpack.prod.conf.js
// 此处需要 "npm install webpack-merge"
const merge = require('webpack-merge');
const baseConf = require('./webpack.config.js');
// 在原有的基础上扩展 dev 时需要的配置
module.exports = merge(baseConf, {
// 相比于 development,production 主要是添加了对代码的压缩
mode: 'production'
});
[代码]
至此,多页应用的配置就完成的差不多了。这就是全部了吗,当然不,如果有需要比如 vue 的单文件组件(SFC)也可以通过 vue-loader 进行配置,还有开发环境的热模块替换、静态资源文件的引入等。
总结
多页入口其实就是设置[代码]entry[代码]属性为一个对象,对于这个对象,我们可以通过配置文件或者是约定目录自动匹配的方式进行获取(又或者是直接在webpack配置上写)。
webpack 中的 loader 类似一个转换器,对于满足给定规则的文件会使用相应的 loader 去处理,也就是说,不管什么类型的文件,只要有相应的 loader,就可以处理。