1 概述
webpack构建工具,webpack真是让人又爱又恨,爱是它是目前功能最为完善的前端打包构建工具,恨是真的TMD难用,而且API一直在变动,我总觉得我之前学过的关于webpack的都没用了。
最新版的webpack 4它对打包这个问题的抽象为以下几个模块。
关键模块:
- mode,总体配置
- entry,入口
- output,出口
- module,模块内容转换器
- plugins,各种各样的插件
其他模块:
- resolve,模块查找器
- optimization,优化器,压缩和分包的优化
- target,目标,umd,node和browser等等
- devServer,开发用服务器
- devTool,开发用工具,主要是sourceMap
模块很多,插件很多,选项也很多,我从未见过这么长的工具文档,简直要吐。
2 功能
2.1 单入口
webpack --mode development
webpack4加入了约定模式,直接指定mode,就能将src目录的index.js入口的打包到dist目录去。
2.2 模块转换
const path = require('path');
module.exports = {
entry:'./src/index.js',
output:{
filename:'index.js',
path:path.resolve(__dirname,'dist')
},
module:{
rules:[
{
test:/\.js$/,
exclude:/node_modules/,
loader:'babel-loader',
options:{
presets:['env','react','stage-0']
}
},
{
test:/\.css$/,
use:[
{loader:'style-loader'},
{loader:'css-loader'}
]
}
]
}
}
使用module将js和css文件进行打包前的预处理,以上的配置文件,使用的是babel-loader,style-loader和css-loader。
2.3 插件
const path = require('path');
const htmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
entry:{
index:'./src/index.js',
index2:'./src/index2.js',
},
output:{
filename:'[name].js',
path:path.resolve(__dirname,'dist')
},
module:{
rules:[
{
test:/\.js$/,
exclude:/node_modules/,
loader:'babel-loader',
options:{
presets:['env','react','stage-0']
}
},
{
test:/\.css$/,
use:[
{loader:'style-loader'},
{loader:'css-loader'}
]
}
]
},
plugins:[
new htmlWebpackPlugin({
filename:'index.html',
chunks:['index'],
template:'./src/index.html',
}),
new htmlWebpackPlugin({
filename:'index2.html',
chunks:['index2'],
template:'./src/index.html',
})
],
devServer:{
compress: true,
proxy:{
"/": {
target:"https://www.baidu.com",
changeOrigin: true
}
}
}
}
使用plugins就可以注入webpack的插件机制,这里的代码使用的是htmlWebpackPlugin,可以从模板中自动生成入口的html文件。
2.4 自动watch
webpack --mode development --watch
webpack命令加入–watch参数,就能侦听文件的变动,自动重新打包。
2.5 开发用服务器
webpack-dev-server --mode development --hot --open
比watch更进一步的是,webpack提供了webpack-dev-server,可以将生成的打包文件暴露到8080端口的http服务器上。其中hot参数代表当文件变动时,自动刷新浏览器,open参数代表开启时,自动打开浏览器。
devServer:{
compress: true,
proxy:{
"/": {
target:"https://www.baidu.com",
changeOrigin: true
}
}
}
另外,在webpack.config.js中可以配置compress,代表开启gzip压缩,加入proxy,代表代理后端服务的api。
2.6 多入口依赖
对于多页面程序,webpack支持多个entry入口,也是相当简单的。要注意它并不支持目录式的entry入口,需要自己combine生成这些entry入口。
2.7 文件名哈希
const path = require('path');
const htmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
entry:'./src/index.js',
output:{
filename:'[name]-[hash].js',
path:path.resolve(__dirname,'dist')
},
module:{
rules:[
{
test:/\.js$/,
exclude:/node_modules/,
loader:'babel-loader',
options:{
presets:['env','react','stage-0']
}
},
{
test:/\.css$/,
use:[
{loader:'style-loader'},
{loader:'css-loader'}
]
}
]
},
plugins:[
new htmlWebpackPlugin({
template:'./src/index.html',
})
]
}
在output的配置上,加入[hash]作为文件名就可以了,这样输出的文件中会带有hash文件名,就能避免缓存造成发布前端文件时没有及时更新的问题。
3 优化
3.1 cdn加速
const path = require('path');
module.exports = {
entry:'./src/index.js',
output:{
filename:'index.js',
path:path.resolve(__dirname,'dist')
},
module:{
rules:[
{
test:/\.js$/,
exclude:/node_modules/,
loader:'babel-loader',
options:{
presets:['env','react','stage-0']
}
},
{
test:/\.css$/,
use:[
{loader:'style-loader'},
{loader:'css-loader'}
]
}
]
},
externals:{
'react':'React',
'react-dom':'ReactDOM'
}
}
<!doctype>
<html>
<head>
<meta charset="utf-8"/>
<title>webpack test1</title>
</head>
<body>
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.4.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.4.0/umd/react-dom.production.min.js"></script>
<script src="dist/index.js"></script>
</body>
</html>
对于像React,ReactDom这些对象,我们希望webpack不要打包到单一的main.js中,而是从cdn中直接拉取。这样启动速度更快,并发拉取js文件效率更高。那么,只需要在配置文件中配置好externals属性就可以了。
另外,这个工具也可以用来解决依赖包是普通的umd模块的问题。
3.2 提取公共chunk
const path = require('path');
const htmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
entry:{
index:'./src/index.js',
index2:'./src/index2.js',
},
output:{
filename:'[name].js',
chunkFilename: '[name].js',
path:path.resolve(__dirname,'dist')
},
module:{
rules:[
{
test:/\.js$/,
exclude:/node_modules/,
loader:'babel-loader',
options:{
presets:['env','react','stage-0']
}
},
{
test:/\.css$/,
use:[
{loader:'style-loader'},
{loader:'css-loader'}
]
}
]
},
optimization:{
splitChunks: {
chunks: 'all',
minSize: 1,
minChunks: 1,
maxAsyncRequests: 5,
maxInitialRequests: 3,
automaticNameDelimiter: '-',
name: true,
cacheGroups: {
vendors: {
name:'vendors',
test: /[\\/]node_modules[\\/]/,
priority: -10
},
commons: {
name:'commons',
minChunks: 2,
priority: -20,
reuseExistingChunk: true
}
}
},
runtimeChunk:{
name:'webpack'
},
},
plugins:[
new htmlWebpackPlugin({
filename:'index.html',
chunks:['index','commons','vendors','webpack'],
template:'./src/index.html',
}),
new htmlWebpackPlugin({
filename:'index2.html',
chunks:['index2','commons','vendors','webpack'],
template:'./src/index.html',
})
],
devServer:{
compress: true,
proxy:{
"/": {
target:"https://www.baidu.com",
changeOrigin: true
}
}
}
}
webpack默认会将所有文件都打包到同一个包上,这会大大降低效率,webpack2版本提供了commonChunkPlugin来解决这个问题,可是相当粗糙。webpack4原生提供了splitChunks和runtimeChunk来解决,不仅可以合并共同包common,而且可以合并node_modules下的vendor包,更可以合并webpack的runtime包。
这是打包后请求
3.3 动态加载chunk
import app from './app';
import dynamic from 'redva/dynamic';
export default [
{
url: '/counter',
name: '计数器',
component: dynamic({
app: app,
models: () => [import('../models/counter')],
component: () => import('../routes/counter'),
}),
},
{
url: '/todo',
name: 'todo列表',
component: dynamic({
app: app,
models: () => [import('../models/todo')],
component: () => import('../routes/todo'),
}),
},
]
我们希望在首页启动时不要一次加载所有页面的模块,而是按需加载各个子页面的模块,这样能大幅提高首页渲染速度。解决方法就是用webpack中的dynamic import方法,该方法将import看成一个函数,然后将文件名传递进去,返回的是一个promise的结果。webpack遇到这些dynamic import的方法会执行自动split chunks,然后在调用时才向后端真正请求模块。
以上就是dynamic import的威力,红色的资源是在加载该页时才出现的,要注意,对于多个异步请求的共同包,它会放在common.js的文件中。
3.4 动态运行chunk
require.context('./models/', true, /\.js$/);
require.context('./routes/', true, /\.js$/);
与dynamic import不同的是,webpack也支持dynamic run。对于像react-native和electron的项目,他们的资源都是放在本地环境中的,dynamic import是没有意义的。我们希望的是一开始就将整个包所有模块都加载到内存里,但是当打开到该页面时才进行模块的语法解析和运行的操作,这样就能提高避免一开始将几个M的数据直接运行的问题。首先,按照webpack的语法,用require.context将需要dynamic run的模块指定出来,代表当前文件依赖这些目录下的模块,但是不执行它们。
import app from './app';
import dynamic from 'redva/dynamic';
const routers = [
{
url: '/counter',
name: '计数器',
models: ['counter'],
component: 'counter',
},
{
url: '/todo',
name: 'todo列表',
models: ['todo'],
component: 'todo',
},
];
export default routers.map(route => {
return {
url: route.url,
name: route.name,
component: dynamic({
app: app,
models: () => {
return route.models.map(model => {
return import(`../models/${model}`);
});
},
component: () => {
return import(`../routes/${route.component}`);
},
}),
};
});
然后用dynamic import的方法加载这些模块,要注意的是,这里加载的模块名称可以是任意的字符串拼接,而不仅仅是一个常量的字符串。
可以看到,启动时并没有异步加载的包。但是从console日志中可以看出,每个页面的模块运行都是在必要时才进行的。
3.5 删除多余依赖
import {Button,Modal} from 'antd'
在使用antd包时,使用以上的方法默认会打包整个antd的包,造成包太大了,我们希望能按需打包antd的包,避免引入整个antd包。
module:{
rules:[
{
test:/\.js$/,
exclude:/node_modules/,
loader:'babel-loader',
options:{
presets:['env','react','stage-0'],
plugins:[["import",{ "libraryName": "antd", style: 'css' }]]
}
},
{
test:/\.css$/,
use:[
{loader:'style-loader'},
{loader:'css-loader'}
]
}
]
}
方法是使用babel-plugin-import,它可以将antd的引入代码改为如下的方式
import {Button,Modal} from 'antd'
↓ ↓ ↓ ↓ ↓ ↓
require('antd/lib/button');
require('antd/lib/button/style/css');
require('antd/lib/modal');
require('antd/lib/modal/style/css');
这就大大减少了需要依赖的包,并自动引入了所对应的css的文件,这种方法也适用于material-design的包。
3.6 tree-shaking
tree-shaking是webpack4的杀手级功能,它可以根据es6的import的静态分析,找出不必要的依赖引入,然后自动剔除他们。使用这种方法,有两个前提:
- 支持tree-shaking的包必须是没有sideEffect的。
- 支持tree-shaking的包必须使用es6 import的方法,不能使用commonjs,amdjs的模块依赖方法。
- webpack最终打包时必须使用uglifyjs进行代码剔除(或使用mode=production的方式)。
module.exports = {
mode: "production",
entry:'./src/index.js',
output:{
filename:'[name]-[hash].js',
path:path.resolve(__dirname,'dist')
},
module:{
rules:[
{
test:/\.js$/,
exclude:/node_modules/,
loader:'babel-loader',
options:{
presets:[['env',{modules:false}],'react','stage-0']
}
},
{
test:/\.css$/,
use:[
{loader:'style-loader'},
{loader:'css-loader'}
]
}
]
},
optimization:{
splitChunks: {
chunks: 'all',
minSize: 1,
minChunks: 1,
maxAsyncRequests: 5,
maxInitialRequests: 3,
automaticNameDelimiter: '-',
name: true,
cacheGroups: {
antd: {
name:'antd',
test: /[\\/]node_modules[\\/]_antd/,
priority: 11
},
vendors: {
name:'vendors',
test: /[\\/]node_modules[\\/]/,
priority: -10
},
commons: {
name:'commons',
minChunks: 2,
priority: -20,
reuseExistingChunk: true
}
}
},
runtimeChunk:{
name:'webpack'
},
},
plugins:[
new htmlWebpackPlugin({
template:'./src/index.html',
})
],
devServer:{
compress: true
}
}
幸好,antd均支持tree-shaking,除了它的dist目录下的antd.css。使用以上的webpack配置后,我们启动一下webpack-dev-server。注意配置中的babel-loader中的配置,modules必须为false。
可以看出,经过antd的包仅4.6k,远远少于引入整个antd包的大小。但是,这种方法并不支持antd的css的tree-shaking,你仍然需要引入整个antd的样式库。
总体来说,这种方法还在早起探索阶段,并没有达到成熟,使用babel-plugin-import是更为可靠和优秀的方法。另外,这里还有一些关于tree-shaking的批评
5 总结
这可能只是webpack 4的开始,现在所学的可能都要被推翻,因为前端打包这个问题仍然在业界不断探索的阶段,并没有一个很好的最佳实践。
参考资料
- 本文作者: fishedee
- 版权声明: 本博客所有文章均采用 CC BY-NC-SA 3.0 CN 许可协议,转载必须注明出处!