最近从新看了一遍 webpack 提取公共文件的配置。原来以为这东西是个玄学,都是 “凭感受” 配置。这篇文章将以解决实际开发遇到的问题为核心,悉数利用 webpack 提取独立文件(模块)的应用。javascript
独立文件在实际开发中通常有两种:html
分离出独立文件的目的:vue
// 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 的配置能够去官网查看,这种方法不算是打包提取第三方模块,只是一个变量引入,不是本文讨论的重点。java
配置属性 | 配置介绍 |
---|---|
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 才会被建立。非必填项。 |
建立一个以下图的目录node
package.json 以下react
{ "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分离为独立文件。jquery
// 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 : [独立文件名]。webpack
固然也能够不用在入口定义,如vue-cli 就是在 在CommonsChunk中配置了minChunks。咱们的第三方模块都是经过npm 安装在node_modules 目录下,咱们能够经过minChunks 判断模块路径是否含有node_module 来返回true 或 false,前文有介绍minChunks的含义。配置以下:web
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从新打包。那么如何解决这个问题呢。vue-cli
咱们将 a.js 作一个简单修改:
// 原来 - console.log('unique:' +arr); // 修改后 + console.log(arr);
从新打包发现vendor的hash变化了至关于从新打包了underscore,解决的方法是利用一个 manifest 来记录 vendor 的 id ,若是vendor没改变,则不须要从新打包。这就有两种解决方式 :
利用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。
既然动态打包的时候创建 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, }),