webpack 填坑之路--提取獨立文件(模塊)

前言

最近從新看了一遍 webpack 提取公共文件的配置。原來以爲這東西是個玄學,都是 「憑感受」 配置。這篇文章將以解決實際開發遇到的問題爲核心,悉數利用 webpack 提取獨立文件(模塊)的應用。javascript

獨立文件在實際開發中通常有兩種:html

  1. 第三方模塊 如 Vue React jQuery 等
  2. 項目開發編寫的獨立模塊(模塊),對於 MPA 多頁面開發來講是封裝出的一些方法庫好比 utils.getQueryString() 或者是每一個頁面的共同操做;對於SPA 應用來講沒有特別的須要分離出模塊,可是針對首屏渲染速度的提高,能夠將 某些獨立模塊分離出來實現按需加載。

分離出獨立文件的目的:vue

  1. 獨立文件通常不多更改或者不會更改,webpack 不必每次打包進一個文件中,獨立文件提取出能夠長期緩存。
  2. 提高 webpack 打包速度

提取第三方模塊

  1. 配置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 的配置能夠去官網查看,這種方法不算是打包提取第三方模塊,只是一個變量引入,不是本文討論的重點。java

  1. 利用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 纔會被建立。非必填項。

建立一個以下圖的目錄node

clipboard.png

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

manifest 處理第三方模塊應用

咱們將 a.js 作一個簡單修改:

// 原來
-  console.log('unique:' +arr);
// 修改後
+   console.log(arr);

clipboard.png

從新打包發現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 值沒有變化,以下圖:

clipboard.png

這裏要注意的是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,
    }),
  ],

clipboard.png

根據上述配置打包結果如上圖,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'
    })
  ]

clipboard.png

根據上述配置打包獲得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'],
    }),
  ]
}

經過打包後發現生成了以下文件:

clipboard.png

能夠明確看出生成了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都沒有從新打包

clipboard.png

因此總結來說就是多入口配置CommonsChunk

new webpack.optimize.CommonsChunkPlugin({
      name: ['生成的項目公共模塊文件名', '第三方模塊文件名'],
      minChunks: 2,
    }),
相關文章
相關標籤/搜索