前言
最近重新看了一遍 webpack 提取公共文件的配置。原来觉得这东西是个玄学,都是 “凭感觉” 配置。这篇文章将以解决实际开发遇到的问题为核心,悉数利用 webpack 提取独立文件(模块)的应用。
独立文件在实际开发中一般有两种:
- 第三方模块 如 Vue React jQuery 等
- 项目开发编写的独立模块(模块),对于 MPA 多页面开发来说是封装出的一些方法库比如 utils.getQueryString() 或者是每个页面的共同操作;对于SPA 应用来说没有特别的需要分离出模块,但是针对首屏渲染速度的提升,可以将 某些独立模块分离出来实现按需加载。
分离出独立文件的目的:
- 独立文件一般很少更改或者不会更改,webpack 没必要每次打包进一个文件中,独立文件提取出可以长期缓存。
- 提升 webpack 打包速度
提取第三方模块
- 配置externals
Webpack 可以配置 externals 来将依赖的库指向全局变量,从而不再打包这个库。
// webpack.config.js 中
module.exports = {
entry: {
app: __direname +'/app/index.js'
}
externals: {
jquery: 'window.jQuery'
}
...
}
// 模板 html 中
...
<script src="https://code.jquery.com/jquery-3.1.0.js"></script>
...
// 入口文件 index.js
import $ from 'jquery'
其实就是 script 标签引入的jquery 挂载在window下 其他类型 externals 的配置可以去官网查看,这种方法不算是打包提取第三方模块,只是一个变量引入,不是本文讨论的重点。
- 利用CommonsChunkPlugin
CommonsChunkPlugin 插件是专门用来提取独立文件的,它主要是提取多个入口 chunk 的公共模块。他的配置介绍如下:
配置属性 | 配置介绍 |
---|---|
name 或者 names | chunk 的名称 如果是names数组 相当于对每个name进行插件实例化 |
filename | 这个common chunk 的文件输出名 |
minChunks | 通常情况为一个整数,至少有minChunks个chunk使用了该模块,该模块才会被移入[common chunk]里 minChunks 还可以是Infinity意思为没有任何模块被移入,只是创建当前这个 chunk,这通常用来生成 jquery 等第三方代码库。minChunks还可以是一个返回布尔值的函数,返回 true 该模块会被移入 common chunk,否则不会。默认值是 chunks 的长度。 |
chunks | 元素为chunk名称的数组,插件将从该数组中提取common chunk 可见 minChunks 应该小予chunks的长度,且大于1。如果没有 所有的入口chunks 会被选中 |
children | 默认为false 如果为true 相当于为上一项chunks配置为chunk的子chunk 用于代码分割code split |
async | 默认为false 如果为true 生成的common chunk 为异步加载,这个异步的 common chunk 是 name 这个 chunk 的子 chunk,而且跟 chunks 一起并行加载 |
minSize | 如果有指定大小,那么 common chunk 的文件大小至少有 minSize 才会被创建。非必填项。 |
创建一个如下图的目录
package.json 如下
{
"name": "webpacktest",
"version": "1.0.0",
"description": "",
"directories": {
"doc": "doc"
},
"scripts": {
"start": "webpack"
},
"author": "abzerolee",
"license": "ISC",
"devDependencies": {
"html-webpack-plugin": "^2.30.1",
"webpack": "^3.8.1"
},
"dependencies": {
"underscore": "^1.8.3",
}
}
a.js 引入了 underscore 需要进行了数组去重操作,现在需要将underscore分离为独立文件。
// webpack.config.js
entry: {
a: __dirname +'/app/a.js',
vendor: ['underscore']
},
output: {
path: __dirname +'/dist',
filename: '[name].[chunkhash:6].js',
chunkFilename: '[name].[id].[chunkhash:6].js'
},
plugins: [
new webpack.optimize.CommonsChunkPlugin({
name: 'vendor',
}),
new HtmlWebpackPlugin({
template: __dirname +'/app/index.html'
})
]
// a.js
let _ = require('underscore');
let arr = _.filter([1,2,3,2,3,3,5,5], (v, i, self) => self.indexOf(v) === i);
console.log('unique:' +arr);
这样underscore就分离进了 vendor 块,注意的是需要在入口定义 要输出的 [ 独立文件名 ]: [ 需要分离的模块数组 ], 然后在CommonsChunkPlugin中配置 name : [独立文件名]。
当然也可以不用在入口定义,如vue-cli 就是在 在CommonsChunk中配置了minChunks。我们的第三方模块都是通过npm 安装在node_modules 目录下,我们可以通过minChunks 判断模块路径是否含有node_module 来返回true 或 false,前文有介绍minChunks的含义。配置如下:
entry: {
a: __dirname +'/app/a.js', // **注意** 入口没定义vendor
},
output: {
path: __dirname +'/dist',
filename: '[name].[chunkhash:6].js',
chunkFilename: '[name].[id].[chunkhash:6].js'
},
plugins: [
new webpack.optimize.CommonsChunkPlugin({
name: 'vendor',
minChunks: function(module) {
let flag = module.context && module.context.indexOf('node_modules') !== -1;
console.log(module.context, flag);
return flag;
}
}),
new HtmlWebpackPlugin({
template: __dirname +'/app/index.html'
})
]
上述两种方式,对于多页面还是单页面都是可应用的。但是现在的问题是每次入口文件 a.js 修改之后都会造成 vendor重新打包。那么如何解决这个问题呢。
manifest 处理第三方模块应用
我们将 a.js 做一个简单修改:
// 原来
- console.log('unique:' +arr);
// 修改后
+ console.log(arr);
重新打包发现vendor的hash变化了相当于重新打包了underscore,解决的方法是利用一个 manifest 来记录 vendor 的 id ,如果vendor没改变,则不需要重新打包。这就有两种解决方式 :
1. 利用manifest.js
利用CommonsChunkPlugin的chunks特性,提取出 webpack定义的异步加载代码,配置如下:
entry: {
a: __dirname +'/app/a.js',
},
output: {
path: __dirname +'/dist',
filename: '[name].[chunkhash:6].js',
chunkFilename: '[name].[id].[chunkhash:6].js'
},
plugins: [
new webpack.optimize.CommonsChunkPlugin({
name: 'vendor',
minChunks: function(module) {
let flag = module.context && module.context.indexOf('node_modules') !== -1;
console.log(module.context, flag);
return flag;
}
}),
new webpack.optimize.CommonsChunkPlugin({
name: 'manifest',
chunks: ['vendor'],
}),
new HtmlWebpackPlugin({
template: __dirname +'/app/index.html'
})
]
还是修改了 a.js 之后发现 vendor的 hash 值没有变化,如下图:
这里要注意的是chunks: [ 独立文件名 ]。但是,又有但是,要是这么就配置没问题了,就不能叫做玄学了,修改 a.js 的内部代码没问题,如果修改了 require 的模块引入,vendor的hash又有变化了,当然我们可以尽量避免修改文件的依赖引入,但是终归不是最完美的方式。那么终极解决方法是什么呢?DllReferencePlugin,DllPlugin。
2. 利用DllReferencePlugin,DllPlugin
既然动态打包的时候建立 manifest 不行,那么能不能直接把他打包成一个纯净的依赖库,本身无法运行,只是让我们的app 来引入。
那么我们需要完成两步,先webpack.DllPlugin打包dll(纯净的第三方独立文件),然后用DllReferencePlugin 在我们的应用中引用,这样的好处是如果下一个项目还是使用一样的依赖比如react react-dom react-router,可以直接引入这个dll。
配置文件如下:
entry: {
vendor: ['underscore']
},
output: {
path: __dirname +'/dist',
filename: '[name].js',
library: '[name]',
},
plugins: [
new webpack.DllPlugin({
path: __dirname +'/dist/manifest.json',
name: '[name]',
context: __dirname,
}),
],
根据上述配置打包结果如上图,dist目录下现在有一个vender.js 和 manifest.json 注意这里输出的路径配置。DllPlugin配置介绍如下:
配置项 | 介绍 |
---|---|
path | path 是 manifest.json 文件的输出路径,这个文件会用于后续的业务代码打包; |
name | name 是 dll 暴露的对象名,要跟 output.library 保持一致; |
context | context 是解析包路径的上下文,这个要跟接下来配置的 webpack.config.js 一致。 |
之后在我们的应用中引入中,配置如下:
entry: {
a: __dirname +'/app/a.js',
},
output: {
path: __dirname +'/dist',
filename: '[name].[chunkhash:6].js',
chunkFilename: '[name].[id].[chunkhash:6].js'
},
plugins: [
new webpack.DllReferencePlugin({
context: __dirname,
manifest: require('./dist/manifest.json'),
}),
new HtmlWebpackPlugin({
template: __dirname +'/app/index.html'
})
]
根据上述配置打包得到a.3e6285.js index.html 如上图,浏览器中打开index.html会显示 Uncaught ReferenceError: vendor is not defined
这里需要在 index.html 中 a.3e6285.js 插入 script 标签
<script type="text/javascript" src="vendor.js" ></script>
<script type="text/javascript" src="a.3e6285.js"></script>
再打开index.html 可以控制台打印出了数组去重的结果。插入标签的这一步可以在打包好独立文件之前,就在模板html 中插入。
到了这里,提取第三方模块的方法,避免重复打包的方法都介绍完毕了。接下来是配置提取自己编写的公共模块方法。
提取项目公共模块
单页面应用的公共模块没有必要提取出单独的文件,因为不必考虑复用的情况。但是对于打包生成的文件过大,我们又想分离出几个模块有需要的时候才加载,其实这并不是提取公共模块,而是代码分割,通过:
require.ensure(dependencies: String[], callback: function(require), chunkName: String)
在callback中定义的 require的模块将会独立打包,并且插入在 html 的head标签,这里就不做更多介绍了。
多页面应用是有必要抽取公共模块的,比如a.js 引用了lib1, b.js 也引用了 lib1 那么lib1,那么我们肯定希望在提取出 lib1 同时还可以提取出第三方库,配置文件如下:
// a.js
let _ = require('underscore');
let lib1 = require('./lib1');
console.log('this is entry_a import lib1');
let arr = _.filter([1,2,3,2,3,3,5,5], (v, i, self) => self.indexOf(v) === i);
console.log(arr);
// b.js
require('./lib1');
var b = 'b';
console.log('this is entry_b import lib1');
// webpack.config.js
entry: {
a: __dirname +'/app/a.js',
b: __dirname +'/app/b.js',
vendor: ['underscore'],
},
output: {
path: __dirname +'/dist',
filename: '[name].[chunkhash:6].js',
chunkFilename: '[name].[id].[chunkhash:6].js'
},
plugins: [
new webpack.optimize.CommonsChunkPlugin({
name: ['chunk', 'vendor'],
minChunks: 2,
}),
new webpack.optimize.CommonsChunkPlugin({
name: 'manifest',
chunks: ['vendor']
}),
new HtmlWebpackPlugin({
template: __dirname +'/app/index.html',
filename: __dirname +'/dist/a.html',
chunks: ['a', 'chunk', 'vendor', 'manifest'],
}),
new HtmlWebpackPlugin({
template: __dirname +'/app/index.html',
filename: __dirname +'/dist/b.html',
chunks: ['b', 'chunk', 'vendor', 'manifest'],
}),
]
}
通过打包后发现生成了如下文件:
可以明确看出生成了chunk.d09623.js 而且 其中就是我们的lib1.js 的库的代码。这里要注意的是Commons.ChunkPlugin的配置 当name 给定数组之后从入口文件中选取 共同引用超过 minChunks 次数的模块打包进name 数组的第一个模块,然后name 数组后面的块 'vendor' 依次打包(查找entry里的key,没有找到相关的key就生成一个空的块),最后一个块包含webpack生成的在浏览器上使用各个块的加载代码,所以插入到页面中最后一个块要最先加载,加载顺序由name数组自右向左。
这里我们使用manifest 去提取了 webpackJsonp 的加载代码,为了防止重复打包库文件,这在前文已经提到过。所以vendor中的加载代码在mainfest.js 中,修改a.js 的console.log, 重新打包后的文件可以发现chunk.d0962e.js, vendor.98054b.js都没有重新打包
所以总结来讲就是多入口配置CommonsChunk
new webpack.optimize.CommonsChunkPlugin({
name: ['生成的项目公共模块文件名', '第三方模块文件名'],
minChunks: 2,
}),
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。