Lianer

大概有两个月没有更新了,这两个月在忙一个比较复杂的项目,目前项目已临近发布,才得以空闲写这篇文章,很快还会有更多文章发布。

背景

有一个项目有如下依赖:

"dependencies": {
  "axios": "^0.16.1",
  "crypto-js": "^3.1.9-1",
  "element-ui": "^1.2.3",
  "fastclick": "^1.0.6",
  "file-saver": "^1.3.3",
  "interactjs": "^1.2.8",
  "keymaster": "^1.6.2",
  "lodash": "^4.17.4",
  "markdown-it": "^8.3.1",
  "moment": "^2.18.1",
  "store": "^2.0.4",
  "swiper": "^3.4.2",
  "vue": "^2.2.1",
  "vuedraggable": "^2.10.0",
  "vuex": "^2.2.1",
  "weixin-js-sdk": "^1.2.0",
  "xlsx": "^0.9.13"
}

Webpack 编译整个项目所耗费的时间大约是 75s。

在引入 DllPlugin 之后,Webpack 编译业务代码所耗费的时间大约是 27s,提升明显。

显然,编译速度提升非常的明显。

那么,DllPlugin 是如何工作的问题?。

一个项目通常都会使用很多第三方 npm 模块,我们并不会去修改他们的源码,但 Webpack 仍然会去编译他们,这非常耗时。DllPlugin 可以将这些 npm 模块单独前置打包,当我们再编译项目的时候,Webpack 就会跳过这些 DllPlugin 打包过的模块,只编译业务代码。

DllPlugin 需要配合 DllReferencePlugin 使用。

首先,DllPlugin 编译得出 manifest.json(依赖配置文件)和 dll.js (npm 模块),然后,DllReferencePlugin 去解析 manifest.json 来使用 dll.js。

配置 DllPlugin

需要注意的是,DllPlugin 的编译是独立的,所以它需要一套独立的 Webpack Config 和入口文件。

第一步,创建一个 DllPlugin 使用的入口文件,它的作用就是引入项目中使用到的 npm 依赖。

// src/vendor.js

require('axios')
require('crypto-js')
require('element-ui')
require('fastclick')
require('file-saver')
require('interactjs')
require('keymaster')
require('lodash')
require('markdown-it')
require('moment')
require('store')
require('swiper')
require('vue')
require('vuedraggable')
require('vuex')
require('weixin-js-sdk')
require('xlsx')

第二步,创建一个 DllPlugin 使用的 Webpack 配置文件。

// webpack.dll.conf.js

var path = require('path')
var utils = require('./utils')
var webpack = require('webpack')
var config = require('../config')

var webpackConfig = {
  entry: {
    // 第一步创建的文件
    vendor: ['./src/vendor.js']
  },
  output: {
    path: path.resolve(__dirname, '../dist'),
    // 输出的文件名为 vendor_[hash].js
    filename: 'vendor_[hash].js',
    // 暴露到 window 的全局变量,这里是 vendor_[hash],与 filename 同名,方便在后面插入 html 使用
    library: 'vendor_[hash]'
  },
  plugins: [
    new webpack.DllPlugin({
      // 生成的 manifest.json 文件,这个文件在后面的 DllReferencePlugin 中需要使用到
      path: path.resolve(__dirname, '../dist/vendor-manifest.json'),
      // 保持与上面的 library 配置相同
      name: 'vendor_[hash]'
    }),
    new webpack.optimize.UglifyJsPlugin()
  ],
  resolve: {
    extensions: ['.js', '.vue', '.json'],
    alias: {
      'vue$': 'vue/dist/vue.esm.js',
      '@': path.resolve(__dirname, '../src'),
    }
  }
}

module.exports = webpackConfig

配置 DllReferencePlugin

在 plugins 中添加这一段。

new webpack.DllReferencePlugin({
  // 指定 node_modules 所在路径
  context: path.resolve(__dirname, '../'),
  // 通过 vendor-manifest.json 来告诉 Webpack 如何使用上面 DllPlugin 编译出来的包
  manifest: require('../dist/vendor-manifest.json')
})

配置 HtmlWebpackPlugin 增加 vendor 自定义参数。

new HTMLPlugin({
  filename: `html/${staticVersion}/production.html`,
  template: 'index.html',
  inject: true,
  minify: {
    removeComments: true,
    collapseWhitespace: true,
    removeAttributeQuotes: true
  },
  chunksSortMode: 'dependency',
  chunks: ['manifest', 'vendor', 'production'],
  // vendor 是一个自定义的参数,在 index.html 会使用
  vendor: '/static/js/' + require('../dist/vendor-manifest.json').name + '.js'
})

在 index.html 中插入 HtmlWebpackPlugin 的模板语句,将上面的自定义变量 vendor 渲染到 script 的 src 中。

<script src="<%= htmlWebpackPlugin.options.vendor %>"></script>
<!-- built files will be auto injected -->

到此,所有步骤就都完成了。

DllPlugin 与 DllReferencePlugin 的使用已经讲解完毕,相信你已经能够充分理解它的原理了。

如果有其他更好的 Webpack 打包优化方案欢迎留言与我讨论。