8

前言

最近重新看了一遍 webpack 提取公共文件的配置。原来觉得这东西是个玄学,都是 “凭感觉” 配置。这篇文章将以解决实际开发遇到的问题为核心,悉数利用 webpack 提取独立文件(模块)的应用。

独立文件在实际开发中一般有两种:

  1. 第三方模块 如 Vue React jQuery 等
  2. 项目开发编写的独立模块(模块),对于 MPA 多页面开发来说是封装出的一些方法库比如 utils.getQueryString() 或者是每个页面的共同操作;对于SPA 应用来说没有特别的需要分离出模块,但是针对首屏渲染速度的提升,可以将 某些独立模块分离出来实现按需加载。

分离出独立文件的目的:

  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 的配置可以去官网查看,这种方法不算是打包提取第三方模块,只是一个变量引入,不是本文讨论的重点。

  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 才会被创建。非必填项。

创建一个如下图的目录

clipboard.png

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);

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,
    }),

autozerolee
276 声望7 粉丝

If you want to do something, try five minutes first.