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,
    }),
相关文章
相关标签/搜索