知多一点 webpack 的 CommonsChunkPlugin

hello~亲爱的看官老爷们大家好~ 最近一直在学习 webpack 的相关知识。曾几何时我总觉得 webpack 的体系庞大而难以掌握,一直回避不愿去学。然而伟人鲁迅曾说过: 世上太多事会因无法掌握而使你狂躁不安,最好的解决方法就是硬着头皮开始做! 因而就从比较简单的 CommonsChunkPlugin 开始学起吧~

虽然本文比较简单,但还是需要一点 webpack 知识的,如若完全没接触过 webpack ,建议先移步 官方文档Webpack 3,从入门到放弃 了解一下 webpack 基础为佳~

基础配置

CommonsChunkPlugin 插件,是一个可选的用于建立一个独立文件(又称作 chunk)的功能,这个文件包括多个入口 chunk 的公共模块。通过将公共模块拆出来,最终合成的文件能够在最开始的时候加载一次,便存起来到缓存中供后续使用。这个带来速度上的提升,因为浏览器会迅速将公共的代码从缓存中取出来,而不是每次访问一个新页面时,再去加载一个更大的文件。

简单来说,这有点像封装函数。把不变的与变化的分开,使得不变的可以高效复用,变化的灵活配置。接下来会根据这个原则优化我们的项目,现在先看看虚拟的项目长成什么样吧~

新建一个 index.html 模板与入口 index.js文件,简单配置如下:

index.html

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>
    <div id="app">
        <p>{{ vue_test }}</p>
    </div>
    <div class="jq_test"></div>
</body>
</html>

index.js:

import Vue from 'vue';
import $ from 'jquery';

new Vue({
  el: '#app',
  data: {
    vue_test: 'vue is loaded!'
  }
})

$(function() {
  $('.jq_test').html('jquery is loaded!')
})

为演示起见,代码十分简单,相信不用多加解释。接下来先简单配置一下 webpack.config.js,代码如下:

const path = require('path');
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;

module.exports = {
  entry: {
    index: path.join(__dirname, 'index.js')
  },
  output: {
    path: path.join(__dirname, '/dist'),
    filename: 'js/[name].[chunkhash].js'
  },
  resolve: { alias: { 'vue': 'vue/dist/vue.js' } },
  plugins: [
    new CleanWebpackPlugin(['./dist']),
    new HtmlWebpackPlugin({
      filename: 'index.html',
      template: 'index.html',
      inject: true
    }),
    new BundleAnalyzerPlugin(),
  ]
};

CleanWebpackPlugin 主要用于清除 dist 目录下的文件,这样每次打包就不必手动清除了。HtmlWebpackPlugin 则是为了在 dist 目录下新建 html 模板并自动插入依赖的 jsBundleAnalyzerPlugin 主要是为了生成打包后的 js 文件包含的依赖,如此时进行打包,则生成:

可以看到生成的 index.js 文件包含了 vuejquery

首次优化

一般而言,我们项目中的类库变化较少,业务代码倒是多变的。需要想办法把类库抽离出来,把业务代码单独打包。这样加伤 hash 后浏览器就能缓存类库的 js 文件,优化用户体验。此时我们的主角 CommonsChunkPlugin 就正式登场了。我们在 webpack.config.js 文件的 plugins 中添加 CommonsChunkPlugin,配置如下:

plugins: [
    //...此前的代码
    new webpack.optimize.CommonsChunkPlugin({
      name: 'vendor',
      minChunks: function(module) {
        return (
          module.resource &&
          /\.js$/.test(module.resource) &&
          module.resource.indexOf(
            path.join(__dirname, './node_modules')
          ) === 0
        )
      }
    }),
]

上述配置,是通过 CommonsChunkPlugin 生成一个名为 vendorjs 文件,它抽取入口文件也就是 index.js 中来源于 node_modules 的依赖组成。此例中就是 vuejquery。打包出来画风是这样的:

此时看上去解决了我们的问题,将依赖的类库抽取抽来独立打包,加上缓存就能被浏览器缓存了。然而事情没那么简单,不行你随意改一下入口的 index.js 代码,再次打包:

绝望地发现 vendor.js 文件的 hash 改变了。简单说,这是因为模块标识产生了变化所导致的,更具体的原因可以查看相关的中文文档~修正的方法其实也挺简单,就是再使用 CommonsChunkPlugin 抽取一次模块,将不变的类库沉淀下来,将变化的抽离出去。因而添如下代码:

plugins: [
    //...此前的代码
    new webpack.optimize.CommonsChunkPlugin({
      name: 'vendor',
      minChunks: function(module) {
        return (
          module.resource &&
          /\.js$/.test(module.resource) &&
          module.resource.indexOf(
            path.join(__dirname, './node_modules')
          ) === 0
        )
      }
    }),
    new webpack.optimize.CommonsChunkPlugin({
      name: 'manifest',
      chunks: ['vendor', 'index']
    })
]

打包后, dist/js 目录下多出一个名为 manifestjs 文件,此时你无论如何改变 index.js 的代码,打包后的 vendor.jshash 都不再会改变了。

然而稍等,当你想拍拍手收工的时候,思考一下这样的场景:随着项目不断迭代,vendor 中的依赖不断被添加与删除,使得它的 hash 会不断变化,这显然不符合我们的利益,这到底如何解决呢?

再次优化

既然 CommonsChunkPlugin 是可以按照我们的需求抽取模块,而依赖的外部模块可能是不断变化的,那么为何不将基础的依赖模块抽取出来作为一个文件,其他的依赖如插件等作为另一个文件呢?

简单说,如我们的项目中 vue 是基本的依赖,必须用到它,而 jquery 等则是后加的类库,之后可能变更。那么将 vue 独立打包一个文件,有利于浏览器缓存,因为无论此后添加更多的类库或删去 jquery 时, vue 文件的缓存依然是生效的。因而我们可以这么做,首先新建一个入口:

entry: {
    index: path.join(__dirname, 'index.js'),
    vendor: ['vue'],
},

此处主要是用于指明需要独立打包的依赖有哪些。之后在 plugins 中做如下修改:

plugins: [
    //...此前的代码
    new webpack.HashedModuleIdsPlugin(),
    new webpack.optimize.CommonsChunkPlugin({
      name: 'vendor',
      minChunks: Infinity,
    }),
    new webpack.optimize.CommonsChunkPlugin({
      name: 'common',
      minChunks: function(module) {
        return (
          module.resource &&
          /\.js$/.test(module.resource) &&
          module.resource.indexOf(
            path.join(__dirname, './node_modules')
          ) === 0
        )
      },
      chunks: ['index'],
    }),
    new webpack.optimize.CommonsChunkPlugin({
      name: 'manifest',
      chunks: ['vendor', 'common', 'index']
    })
]

插件 HashedModuleIdsPlugin,是用于保持模块引用的 module id 不变。而 CommonsChunkPlugin 则提取入口指定的依赖独立打包,minChunks: Infinity,的用意是让插件别管其他,就按照设置的数组提取文件就好。之后修改一下原来的 vendor,重命名为 common,指定它从入口 index.js 中抽取来自 node_modules 的依赖。最后就是抽取 webpack 运行时的函数及其模块标识组成 manifest。运行一下 webpack,构建出来如图:

可以看到 vuejquery 被分开打包成了两个文件,我们尝试添加一下新的依赖 vuex,打包后结果如下:

如此一来,我们的优化目的就达到了,不变的都提取出来,变化的可以动态配置~

小结

webpack 插件 CommonsChunkPlugin 就介绍到这里了,然而优化还是有很多的,比如开启压缩,去除注释等。而当项目体积逐渐增大时,CommonsChunkPlugin 就不一定是提取代码的最优解了。在打包速度与控制构建的精细程度来说,结合 DLLPlugin 会有更好的表现。根据不同的场景组合不同的插件以达到我们的目的,本来就是 webpack 的魅力之一。

感谢各位看官大人看到这里,知易行难,希望本文对你有所帮助,所有的代码均会被上传到 github 上,滚求 star ~谢谢!