最近從新看了一遍 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, }),