# 万字长文系统梳理 Webpack 基础(中)

# 代码分片

  代码分片(code splitting)是webpack所特有的一项技术,可以将代码按照特定的形式进行拆分,不用一次全部加载而是按需加载,能有效降低首屏加载资源的大小。

# CommonsChunkPlugin

  ;CommonsChunkPluginwebpack4-内部自带的插件,可以将多个chunk中的公共部分提取出来。从而减少开发过程中模块的重复打包,提升开发速度。资源整体体积也减小了,并且可以有效的利用客户端缓存。

  ;CommonsChunkPlugin可配置属性如下。

  • name:将chunks属性对应的source chunk中的公共模块提取到name中,若未指定chunks,默认将提取entry chunks中的公共模块
  • chunks:指定source chunk,即表示从哪些chunk中查找公共模块,省略此选项默认为entry chunks
  • filename:提取后的资源文件名,支持模板语言的形式动态生成
  • minChunks:可以为数字、函数或者Infinity

# 非插件打包

  根目录下包括package.jsonwebpack.config.jssrc等,src目录下包括foo.jsbar.jsutils.js

// package.json
{
  ...
  "scripts": {
    "build": "webpack"
  },
  "devDependencies": {
    "webpack": "^3.10.0"
  },
  "dependencies": {
    "jquery": "^3.2.1"
  }
}

// webpack.config.js
module.exports = {
  entry: {
    foo: './src/foo.js',
    bar: './src/bar.js',
  },
  output: {
    filename: './dist/[name].js',
  },
}

// src/bar.js
import jquery from 'jquery'
import { log } from './utils.js'

console.log(jquery, log, 'bar')

// src/foo.js
import jquery from 'jquery'
import { log } from './utils.js'

console.log(jquery, log, 'foo')

// src/utils.js
export function log() {
  console.log('log')
}

  运行打包后将在根目录下dist文件夹生成bar.jsfoo.js,其中jqueryutils均被打包进了这两个文件中。

  最后需要在页面添加script标签引入foo.jsbar.js

// src/index.html
<html lang="zh-CN">
  ...
<body>
  <script src="./bar.js"></script>
  <script src="./foo.js"></script>
</body>

</html>

# 提取公共代码

  修改webpack.config.js,新增CommonsChunkPlugin插件提取公共模块。

// webpack.config.js
const webpack = require('webpack')

module.exports = {
  entry: {
    foo: './src/foo.js',
    bar: './src/bar.js',
  },
  output: {
    filename: './dist/[name].js',
  },
  plugins: [
    new webpack.optimize.CommonsChunkPlugin({
      name: 'vendor',
      filename: './dist/[name].js',
    }),
  ],
}

  如下foo.jsbar.js中的第三方模块jquery和项目内公共模块utilswebpack运行文件都被打包进了vendor.jsfoo.jsbar.js体积明显减小。

  页面中vendor.js要在其他js之前引入。

// src/index.html
<html lang="zh-CN">
  ...
<body>
  <script src="./vendor.js"></script>
  <script src="./bar.js"></script>
  <script src="./foo.js"></script>
</body>

</html>

# 提取运行时

  当使用插件提取公共模块时,提取后的资源内部不仅仅包括模块代码,还包含webpack运行时。webpack运行时指的是初始化环境的代码,如创建模块缓存对象、声明模块加载函数等。

  如下首个CommonsChunkPlugin实例会提取出foo.jsbar.js中的第三方模块jquery、本地模块utilswebpack运行时文件到vendor中。

  然后次个CommonsChunkPlugin实例再在vendor中提取出运行时文件到runtime中,最终vendor中包括第三方模块jquery和本地模块utilsruntime中包含webpack运行时文件。

  注意runtimeCommonsChunkPlugin必须出现在plugins最后,否则webpack将无法正常提取模块。

// webpack.config.js
const webpack = require('webpack')

module.exports = {
  entry: {
    foo: './src/foo.js',
    bar: './src/bar.js',
  },
  output: {
    filename: './dist/[name].js',
  },
  plugins: [
    new webpack.optimize.CommonsChunkPlugin({
      name: 'vendor',
      filename: './dist/[name].js',
    }),
    new webpack.optimize.CommonsChunkPlugin({
      name: 'runtime',
      filename: './dist/[name].js',
      chunks: ['vendor'],
    }),
  ],
}

  运行打包将在dist目录下生成runtime.jsrendor.js等文件。

  上述plugins等价于如下方式。

plugins: [
  new webpack.optimize.CommonsChunkPlugin({
    name: ['vendor', 'runtime'],
    filename: './dist/[name].js',
  }),
],

  也可以通过minChunks来达到提取出运行时文件的目的,其中当设置minChunksn时,表示某个模块只有被nchunks同时引用才会进行提取,CommonsChunkPlugin默认是只要一个模块被两个入口chunk引用就会被提取出来,即minChunk默认值为2

  设置为Infinity表示提取的阈值无限高,即所有模块都不会被提取。

  设置为Infinity一般有两个作用,第一个是可以让webpack只提取特定的模块,另一个则是为了生成一个没有任何模块而是仅仅包含webpack运行时的文件runtime

  如下表示从入口chunk中提取出运行时文件,然后再从foobar中提取出第三方模块和本地utils模块。

plugins: [
  new webpack.optimize.CommonsChunkPlugin({
    name: 'runtime',
    filename: './dist/[name].js',
    minChunks: Infinity,
  }),
  new webpack.optimize.CommonsChunkPlugin({
    name: 'vendor',
    filename: './dist/[name].js',
    chunks: ['foo', 'bar'],
  }),
],

# 提取第三方模块和本地模块

  首个CommonsChunkPlugin实例的minChunks设置为Infinity,即表示所有模块都不会被提取,此时name设置为runtime是可以提取出webpack运行时文件的。但是由于name指定为vendor,且vendor在入口entry中声明了,即表示只提取出vendor数组对应的模块(jquery)和webpack运行时文件。

  次个CommonsChunkPlugin实例即从vendor中提取出webpack运行时文件,此时vendor中仅仅只包含第三方模块jqueryruntime中包含运行时文件。

  末个CommonsChunkPlugin实例最后从foobar中提取出本地模块到utils中。

// webpack.config.js
const webpack = require('webpack')

module.exports = {
  entry: {
    foo: './src/foo.js',
    bar: './src/bar.js',
    vendor: ['jquery'],
  },
  output: {
    filename: './dist/[name].js',
  },
  plugins: [
    new webpack.optimize.CommonsChunkPlugin({
      name: 'vendor',
      filename: './dist/[name].js',
      minChunks: Infinity,
    }),
    new webpack.optimize.CommonsChunkPlugin({
      name: 'runtime',
      filename: './dist/[name].js',
      chunks: ['vendor'],
    }),
    new webpack.optimize.CommonsChunkPlugin({
      name: 'utils',
      filename: './dist/[name].js',
      chunks: ['foo', 'bar'],
    }),
  ],
}

  运行打包将在dist目录下生成vendor.jsutils.jsruntime.js

  上述plugins等价于如下方式。

plugins: [
  new webpack.optimize.CommonsChunkPlugin({
    name: ['vendor', 'runtime'],
    filename: './dist/[name].js',
    minChunks: Infinity,
  }),
  new webpack.optimize.CommonsChunkPlugin({
    name: 'utils',
    filename: './dist/[name].js',
    chunks: ['foo', 'bar'],
  }),
],

  另一种方式是使用minChunks的函数方式,其中首个CommonsChunkPlugin实例的minChunks函数中,module.resource为包含模块名的完整路径,count为模块被引用的次数,通过对入口文件及其依赖的模块进行遍历,如果模块在node_modules中,就会被提取到vendor中,实质此方式可以让vendor只保留第三方模块。

// webpack.config.js
const webpack = require('webpack')

module.exports = {
  entry: {
    foo: './src/foo.js',
    bar: './src/bar.js',
  },
  output: {
    filename: './dist/[name].js',
  },
  plugins: [
    new webpack.optimize.CommonsChunkPlugin({
      name: 'vendor',
      filename: './dist/[name].js',
      minChunks: function (module, count) {
        return module.resource && module.resource.includes('node_modules')
      },
    }),
    new webpack.optimize.CommonsChunkPlugin({
      name: 'runtime',
      filename: './dist/[name].js',
      chunks: ['vendor'],
    }),
    new webpack.optimize.CommonsChunkPlugin({
      name: 'utils',
      filename: './dist/[name].js',
      chunks: ['foo', 'bar'],
    }),
  ],
}

  页面中引用各js方式如下。

<html lang="zh-CN">
  ...
<body>
  <script src="./runtime.js"></script>
  <script src="./utils.js"></script>
  <script src="./vendor.js"></script>
  <script src="./foo.js"></script>
  <script src="./bar.js"></script>
</body>

</html>

# 单页应用提取第三方模块

  单页应用单独创建一个入口即可,如下vendor中包含第三方模块vuewebpack运行时文件。

// webpack.config.js
const webpack = require('webpack')

module.exports = {
  entry: {
    main: './src/main.js',
    vendor: ['vue'],
  },
  output: {
    filename: './dist/[name].js',
  },
  plugins: [
    new webpack.optimize.CommonsChunkPlugin({
      name: 'vendor',
      filename: './dist/[name].js',
    }),
  ],
}

  打包后最终dist目录下生成main.jsvendor.js

  页面中引用js方式如下。

<html lang="zh-CN">
  ...
<body>
  <script src="./vendor.js"></script>
  <script src="./main.js"></script>
</body>

</html>

# 资源异步加载

  当模块数量过多、资源体积过大时,一部分暂时不用的模块延迟加载。使页面初次渲染的时候下载的资源尽可能小,后续模块等到恰当的时机再去触发加载,此方式即按需加载。

  ;webpack官方推荐使用import函数来异步加载模块,并返回Promise对象。

  如下根目录包括webpack.config.jssrc文件夹等,其中src下包括main.jsutils.jsmain.js中通过import异步加载utils.js

// src/main.js
setTimeout(() => {
  import(/* webpackChunkName: "utils-chunk" */ './utils.js')
}, 2000)
console.log('main')

// src/utils.js
import jquery from 'jquery'

console.log(jquery, 'utils')

// webpack.config.js
module.exports = {
  entry: './src/main.js',
  output: {
    filename: './dist/[name].js',
    chunkFilename: './dist/[name].js',
    publicPath: '../',
  },
}

  ;output.chunkFilename用来指定异步chunk的文件名,支持模板语言的方式,异步chunk默认没有名称,其默认值为[id].js,如0.js1.js等。

  通过webpack所特有的注释可以让webpack获取到异步chunk的名字,如上述中/* webpackChunkName: "utils-chunk" */,打包后chunk名称为utils-chunk

  打包后main.js作为首屏加载的资源,页面中通过script标签来引用,而间接资源(utils-chunk.js)的请求路径要通过output.publicPath来指定。

  页面引用方式如下。

<html lang="zh-CN">
  ...
<body>
  <script src="./main.js"></script>
</body>

</html>

  ;2s后会在页面head标签内部动态插入script标签,标签引用utils-chunk.js

# optimization.splitChunks

  ;CommonsChunkPlugin可以在很多场景下提取公共模块,但是其缺陷也非常明显。

  • 单个CommonsChunkPlugin实例只能提取单个vendor,若要提取多个vendor需要新增多个CommonsChunkPlugin实例,容易造成配置代码重复
  • 多个CommonsChunkPlugin实例之间可能存在逻辑关系,只有正确的逻辑关系才能保证提取的代码按照预期,而且部分配置不容易理解,无法做到开箱即用

  ;webpack4删除了CommonsChunkPlugin,改进了CommonsChunkPlugin并重新设计和实现了代码分片特性,准备通过optimization.splitChunks属性来简化代码分割的配置。

# 提取公共代码

  将webpack版本升级,对CommonsChunkPlugin打包异步资源的场景换成splitChunks,调整webpack.config.js

// webpack.config.js
module.exports = {
  entry: './src/main.js',
  output: {
    filename: '[name].js',
  },
  optimization: { splitChunks: { chunks: 'all' } },
  mode: 'none',
}

// package.json
{
  ...
  "devDependencies": {
    "webpack": "4.29.4",
    "webpack-cli": "3.2.3"
  }
}

  运行打包结果如下,其中main.jsutils.js被单独打包出来,utils.js中引用的第三方模块jquery被打包进了vendors~utils-chunk.js

  页面引用main.js后,2s后会在页面head标签内部依次插入vendors~utils-chunk.jsutils-chunk.js,此时并行请求的数量为2

  ;CommonsChunkPlugin场景未提取出utils中的jquery模块,倘若仅修改utils中一行代码,客户端就只有重新下载整个utils-chunk.js。而splitChunks提取出jquery模块,jquery不会经常变动,修改utils中一行代码,客户端只需要重新下载utils-chunk.js,而此文件的体积仅仅1 KiB不到,并且vendor~utils-chunk.js也能很好的利用浏览器缓存。

# 提取条件

  ;CommonsChunkPlugin通过配置项将特定模块提取出来,其方式更贴近于命令式。而splitChunks的不同之处在于仅仅只设置部分提取条件,如模块体积、模块位置等,当某些模块达到这些条件就会被自动提取出来,其方式更贴近于声明式。splitChunks默认提取条件如下。

  • 提取后的chunk来自node_modules目录,处于node_modules的模块一般为通用模块,比较适合被提取出来
  • 提取后的javascript chunk体积大于30KBCSS chunk体积大于50KB,一般提取后的资源体积太小,带来的优化效果也比较一般
  • 按需加载过程中,并行请求的资源最大值小于等于5
  • 首次加载时,并行请求的资源数最大值小于等于3

# 提取多异步资源

  根目录下包括webpack.config.jssrc文件夹,src下包括main.jsfoo.jsbar.js

// src/main.js
setTimeout(() => {
  import(/* webpackChunkName: "bar-chunk" */ './bar.js')
  import(/* webpackChunkName: "foo-chunk" */ './foo.js')
}, 2000)
console.log('main')

// src/bar.js
import react from 'react'

console.log(react, 'bar')

// src/foo.js
import vue from 'vue'
import react from 'react'

console.log(vue, react, 'foo')

  运行打包后,webpack会创建包含vue的代码块(vendor~foo-chunk),foo-chunk依赖此代码块,同时也会创建包含react的代码块(vendors~bar-chunk~foo-chunk),foo-chunkbar-chunk依赖此代码块。

  在import('./bar.js)调用的时候,vendors~bar-chunk~foo-chunk.jsbar-chunk.js并行加载。在import('./foo.js)调用的时候,vendor~foo-chunk.jsfoo-chunk.js并行加载,此时不用再加载vendors~bar-chunk~foo-chunk.js,直接读取缓存即可。

# splitChunks 配置参数

  ;webpack官方给出的splitChunks默认值如下。

optimization: {
  splitChunks: {
    chunks: 'async',
    minSize: 30000,
    maxSize: 0,
    minChunks: 1,
    maxAsyncRequests: 5,
    maxInitialRequests: 3,
    automaticNameDelimiter: '~',
    name: true,
    cacheGroups: {
      vendors: {
        test: /[\\/]node_modules[\\/]/,
        priority: -10,
      },
      default: {
        minChunks: 2,
        priority: -20,
        reuseExistingChunk: true,
      },
    },
  },
},

# chunks

  ;chunks可配置splitChunks工作模式,包括三个可选值async(默认)、initialall,其中async即只提取异步chunkinitial只提取入口同步chunkall则是两种模式同时开启。

  根目录下包括webpack.config.jssrcsrc下包括main.jsbar.jsfoo.js

// src/main.js
import jquery from 'jquery'

setTimeout(() => {
  import(/* webpackChunkName: "bar-chunk" */ './bar.js')
  import(/* webpackChunkName: "foo-chunk" */ './foo.js')
}, 2000)
console.log(jquery, 'main')

// src/bar.js
import react from 'react'

console.log(react, 'bar')

// src/foo.js
import vue from 'vue'
import react from 'react'

console.log(vue, react, 'foo')

// webpack.config.js
module.exports = {
  entry: './src/main.js',
  output: {
    filename: '[name].js',
  },
  optimization: { splitChunks: { chunks: 'all' } },
  mode: 'none',
}

  运行打包后,main.js中的第三方模块jquery被提取到vendor~main.js中,剩余代码保留至main.js中,foo.js中第三方模块vue被提取至vendor~foo-chunk.js,剩余代码保留至foo-chunk.js中,foo.jsbar.js中第三方模块react被提取至vendor~foo-chunk~bar-chunk.jsbar.js剩余代码保留至bar-chunk.js

  修改chunks属性为initial后再次打包,其中仅main.js中的第三方模块jquery被提取至vendor~main.js,剩余代码保留至main.jsfoo.jsbar.js中第三方模块未被提取,只是将其保留至foo-chunk.jsbar-chunk.js

  修改chunks属性为async后再次打包,main.js中代码保留至main.js,其中的第三方模块未做提取,foo.js中的第三方模块vue被提取至vendor~foo-chunk.js,剩余代码保留至foo-chunk.jsfoo.jsbar.js中第三方模块react被提取至vendor~bar-chunk~foo-chunk.jsbar.js中剩余代码保留至bar-chunk.js

# 匹配条件

  • minSize:提取出来的代码块在压缩前的最小大小,默认为3000030KB
  • maxSize:提取出来的代码块在压缩前的最大大小,默认为0,即不限制大小
  • minChunks:模块被引用次数,默认为1
  • maxAsyncRequests:最大的按需加载次数,默认为5
  • maxInitialRequests:最大的首次初始化加载次数,默认为3
  • automaticNameDelimiter:提取出来的代码块自动生成名字的分隔符,默认为 ~
  • name:提取出的代码块文件的名字,默认为true,即自动生成文件名

# 缓存组

  缓存组cacheGroups默认包括vendorsdefault两种规则,vendors用于提取node_modules中符合条件的模块,default则作用于被多次引用的模块,若要禁用某种规则,可直接将其置为false

  ;cacheGroups中的每一项都会继承或者覆盖外层splitChunks中的参数值,例如cacheGroups.vendors项中无minChunks属性,则它将继承外层splitChunks.minChunks的属性值,即cacheGroups.vendors.minChunks1cacheGroups.default项中有minChunks属性,则它将覆盖外层splitChunks.minChunks的属性。

  除了上述参数值外,cacheGroups额外提供了三个配置属性。

  • test:可匹配模块路径或者chunk名称,默认为所有的模块
  • priority:表示提取权重,数值越大优先级越高。一个模块可能满足多个cacheGroups,则提取到哪个就由权重最高的控制
  • reuseExistingChunk:是否使用已有的chunktrue表示如果当前的chunk包含的模块已经被提取出去了,则不会再重新生成新的

  根目录下包括webpack.config.jssrc文件夹,src下包括main.jsutils.js

// src/main.js
import Vue from 'vue'
import Vuex from 'vuex'
import VueRouter from 'vue-router'
import { log } from './utils.js'

console.log(Vue, Vuex, VueRouter, log, 'main')

// src/utils.js
export function log() {
  console.log('log')
}

// webpack.config.js
module.exports = {
  entry: './src/main.js',
  output: {
    filename: '[name].js',
  },
  optimization: {
    splitChunks: {
      chunks: 'all',
    },
  },
  mode: 'none',
}

  运行打包后第三方模块vuevuexvue-router被提取至vendors~main.jsmain.js剩余的代码和utils.js代码被保留至main.js中。

  如果想要单独提取出utils.js,则可以自定义如下cacheGroups,其中module.resource为包含模块名的完整路径。

// webpack.config.js
splitChunks: {
  chunks: 'all',
  cacheGroups: {
    utils: {
      test: module => {
        return /src\\utils/.test(module.resource)
      },
      priority: -20,
      minSize: 0,
    },
    default: false,
  },
},

  运行打包后第三方模块vuevuexvue-router被提取至vendors~main.js中,utils.js中代码被提取至utils~main.js中,main.js中剩余代码保留。

# 生产环境

  生产环境不同于开发环境,生产环境关注的是用户体验,如何让用户更快地加载资源,包括如何资源压缩、添加环境变量优化打包、如何最大限度利用浏览器缓存等。

# 环境配置

# 单一配置

  单一配置即不管在什么环境均使用webpack.config.js,仅仅是在构建初传递环境变量参数,然后在webpack.config.js中通过条件来决定使用哪个配置。

  注意windows不支持使用ENV=development的方式,命令会被阻塞导致报错,第三方插件cross-env可解决此问题。

npm i cross-env --save-dev

  根目录下包括webpack.config.jssrc文件夹等,其中src下包括index.htmlmain.js

// package.json
{
  ...
  "scripts": {
    "dev": "cross-env ENV=development webpack-dev-server",
    "build": "cross-env ENV=production webpack"
  },
  "devDependencies": {
    "webpack": "4.29.4",
    "webpack-cli": "3.2.3",
    "webpack-dev-server": "^3.1.14",
    "html-webpack-plugin": "^3.2.0"
  }
}

// webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin')

const ENV = process.env.ENV
const isProd = ENV === 'production'

module.exports = {
  entry: './src/main.js',
  output: {
    filename: isProd ? './[name].[chunkhash:8].js' : './[name].js',
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: './src/index.html',
    }),
  ],
}

// src/index.html
<html lang="zh-CN">

<head>
  <title>hello world</title>
</head>

<body>
  <p>hello world</p>
</body>

</html>

// src/main.js
console.log('hello world')

  运行dev脚本命令,控制台可查看开发环境打包后的输出文件。

  运行build脚本命令,查看打包后输出文件。

# 多环境配置

  可以为每个环境单独创建一个配置文件,例如开发环境为webpack.dev.config.js,生产环境为webpack.prod.config.js,但是两个配置文件一般会有重复的部分,一改都要改,不利于维护。

  也可以单独创建一个webpack.config.js,然后另外两个js分别引用该文件并添加上自身环境的配置。但是一般都使用第三方插件webpack-merge,用来做webpack的配置合并,便于对不同环境的配置进行管理。

  根目录下为buildpackage.jsonsrc文件夹,src下包括index.htmlmain.js

// package.json
{
  ...
  "scripts": {
    "dev": "webpack-dev-server --config=./build/webpack.dev.config.js",
    "build": "webpack --config=./build/webpack.prod.config.js"
  }
}

  ;build中包括webpack.config.jswebpack.dev.config.jswebpack.prod.config.js

// build/webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin')

module.exports = {
  entry: './src/main.js',
  plugins: [
    new HtmlWebpackPlugin({
      template: './src/index.html',
    }),
  ],
}

// build/webpack.dev.config.js
const webpackConfig = require('./webpack.config.js')

module.exports = {
  ...webpackConfig,
  output: {
    filename: './[name].js',
  },
}

// build/webpack.prod.config.js
const webpackConfig = require('./webpack.config.js')

module.exports = {
  ...webpackConfig,
  output: {
    filename: './[name].[chunkhash:8].js',
  },
}

  分别运行devbuild脚本命令,输出结果与单一配置一致。

# production 模式

  早期的webpack版本中,不同的环境使用的配置项太多,无法做到开箱即用。webpack4中新增了mode配置项,可以通过它来切换打包模式。

  如下当前处于生产环境,webpack会自动添加适用于生产环境的配置项。

// webpack.config.js
module.exports = {
  ...
  mode: 'production',
}

# 环境变量

  在webpack中可以使用DefinePlugin为生产环境和开发环境添加不同的环境变量,即webpack.DefinePlugin是用来设置浏览器环境下的全局变量(不会被挂载到window上)。

  ;webpack.DefinePlugin作用于所有模块,会将模块中的环境变量完全替换为设置的值。

  根目录下为webpack.config.jspackage.jsonsrcsrc下包括main.js

// package.json
{
  ...
  "scripts": {
    "build": "webpack"
  },
  "devDependencies": {
    "webpack": "4.29.4",
    "webpack-cli": "3.2.3"
  }
}

// webpack.config.js
const webpack = require('webpack')

module.exports = {
  entry: './src/main.js',
  output: {
    filename: './[name].js',
  },
  plugins: [
    new webpack.DefinePlugin({
      ENV: JSON.stringify('production'),
    }),
  ],
  mode: 'development',
  devtool: 'none',
}

// src/main.js
console.log(ENV)

  打包后查看输出目录dist中的main.js文件,其中ENV被完全替换为"production"

// dist/main.js
'./src/main.js': function (module, exports) {
  console.log('production')
},

  注意对于字符串或者包含字符串的对象都要加上JSON.stringify,若不添加JSON.stringify,在替换之后就会成为变量名,而非字符串值,即上述ENV会被替换为production(无字符串引号)。

  除了字符串类型的值外,也可以设置其他类型的环境变量。

new webpack.DefinePlugin({
  ENV: JSON.stringify('production'),
  ENVIR: '"development"',
  IS_PRODUCTION: true,
  ENV_ID: 1038,
  CONSTANTS: JSON.stringify({
    TYPES: ['foo', 'bar'],
  }),
})

  很多框架和库都使用process.env.NODE_ENV作为区别开发环境和生产环境的变量,其值为production即表示当前环境为生产环境,库和框架在打包时会去掉诸如警告信息和日志等的开发环境的代码,将有助于减小代码体积提升运行速度。

  如下可配置process.env.NODE_ENV变量,注意若启用了mode: 'production',则webapck会自动设置process.env.NODE_ENV的值,不用再重复设置。

new webpack.DefinePlugin({
  'process.env.NODE_ENV': JSON.stringify('development'),
})

# 区分环境方式

# scripts

  ;webpack4版本引入mode属性,包括developmentproductionnone三种模式。

  ;development模式会将模块内process.env.NODE_ENV的值设为development,同时启用开发环境的webpack插件。production模式会将模块内process.env.NODE_ENV的值设为production,同时启用生产环境的webpack插件。

  可在webpack.config.js中设置mode,也可在package.jsonscripts脚本命令中设置--mode

// package.json
"scripts": {
  "build-dev": "webpack --mode=development",
  "build-prod": "webpack --mode=production"
}

  特殊脚本也会默认设置mode,如下"dev": "webpack-dev-server"modedevelopment"build": "webpack"modeproduction

// package.json
"scripts": {
  "dev": "webpack-dev-server",
  "build": "webpack"
}

  而在模块内部就可根据process.env.NODE_ENV的值判断当前的环境,注意mode的方式是可以在任意模块内通过process.env.NODE_ENV获取当前的环境变量,但是无法在node环境(webpack配置文件)中获取当前的环境变量。

// package.json
"scripts": {
  "build": "webpack"
}

// src/mian.js
console.log(process.env.NODE_ENV) // production

// webpack.config.js
console.log(process.env.NODE_ENV) // undefined

module.exports = {
  entry: './src/main.js',
  ...
}

  如下webpack.config.js中可以通过函数的方式获取环境变量,但是也无法在函数外获取环境变量,main.js中能输出production是因为特殊脚本"build": "webpack"会默认设置modeproduction模式。

// package.json
"scripts": {
  "build-dev": "webpack --env=development",
  "build-prod": "webpack --env=production"
}

// webpack.config.js
console.log(process.env.NODE_ENV) // undefined

module.exports = (env) => {
  console.log(env) // development | production

  return {
    entry: './src/main.js',
    ...
  }
}

// src/main.js
console.log(process.env.NODE_ENV) // production

# webpack.DefinePlugin

  ;scripts的方式模块内的process.env.NODE_ENV只能为固定的几个值。

  ;webpack.DefinePlugin则可以设置模块内process.env.NODE_ENV为任意值,但是也无法在node环境中获取当前的环境变量。

// package.json
"scripts": {
  "build": "webpack"
}

// webpack.config.js
const webpack = require('webpack')

console.log(process.env.NODE_ENV) // undefined

module.exports = {
  entry: './src/main.js',
  output: {
    filename: './[name].js',
  },
  plugins: [
    new webpack.DefinePlugin({
      'process.env.NODE_ENV': JSON.stringify('dev'),
    }),
  ],
}

// src/main.js
console.log(process.env.NODE_ENV) // dev

# cross-env

  上述两种方式均无法在node环境(webpak配置文件)中获取环境变量,scripts方式也只能通过函数的方式在函数内部获取,无法在webpack.config.js中获取到当前的环境变量。

  借助第三方插件cross-env可以在node环境获取当前的环境变量,并且可以任意设置环境变量的值。

// package.json
"scripts": {
  "build": "cross-env ENVIR=prod webpack"
},
"devDependencies": {
  "cross-env": "^7.0.3",
  ...
}

// webpack.config.js
console.log(process.env.ENVIR) // prod

module.exports = {
  entry: './src/main.js',
  ...
}

// src/main.js
console.log(process.env.ENVIR) // undefined

# source map

  ;source map指的是将编译、压缩、打包后的代码映射回源代码的过程,webpack打包压缩后的代码基本不具备可读性,此时代码抛出错误,想要回溯其调用栈非常困难。

# 未启用配置

  根目录下包括package.jsonwebpack.config.jssrc文件夹,src下包括index.htmlmain.jsstyle.scss,其中MiniCssExtractPlugin插件主要作用是提取出css样式文件。

// package.json
{
  ...
  "scripts": {
    "build": "webpack"
  },
  "devDependencies": {
    "css-loader": "^0.28.9",
    "html-webpack-plugin": "^3.2.0",
    "node-sass": "^4.7.2",
    "sass-loader": "^6.0.7",
    "mini-css-extract-plugin": "^0.5.0",
    "style-loader": "^0.19.0",
    "webpack": "4.29.4",
    "webpack-cli": "^3.2.3"
  }
}

// webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')

module.exports = {
  entry: './src/main.js',
  plugins: [
    new HtmlWebpackPlugin({
      template: './src/index.html',
    }),
    new MiniCssExtractPlugin({
      filename: '[name].css',
      chunkFilename: '[id].css',
    }),
  ],
  module: {
    rules: [
      {
        test: /\.scss$/,
        use: [
          {
            loader: MiniCssExtractPlugin.loader,
          },
          'css-loader',
          'sass-loader',
        ],
      },
    ],
  },
}

// src/index.html
<html lang="zh-CN">

<body>
  <p>hello world</p>
</body>

</html>

// src/main.js
import './index.scss'

console.log('source-map')

// src/index.scss
$color: red;

p {
  color: $color;
}

  执行build脚本打包后,根目录下输出index.htmlmain.cssmain.js,利用VS Code编辑器插件Live Serverindex.html内部右击Open with Live Server,查看index.html部署到服务器上的情况。

  如下控制台查看输出结果。

  单击mian.js:formatted:76跳转至如下位置,但是仅能查看此输出语句在打包后的代码中的位置,无法回溯到源代码。

  然后在控制台查看样式。

  单击mian.css:1跳转至如下位置,也仅能查看打包后的样式代码,无法回溯。

# 启用配置

  在webpack.config.js中添加devtool开启js文件的source map,而对于scsscss则需要添加额外的source map配置项。

// webpack.config.js
module.exports = {
  ...
  module: {
    rules: [
      {
        test: /\.scss$/,
        use: [
          {
            loader: MiniCssExtractPlugin.loader,
          },
          {
            loader: 'css-loader',
            options: {
              sourceMap: true,
            },
          },
          {
            loader: 'sass-loader',
            options: {
              sourceMap: true,
            },
          },
        ],
      },
    ],
  },
  devtool: 'source-map',
}

  再次执行build脚本打包,dist目录下会多出几个map文件,由于启用了devtool配置项,source map就会跟随源代码一步步被传递,直到生成最后的map文件,其文件名默认为输出文件加上.map后缀,如main.js.mapmain.js尾部还会默认追加一句注释来标识map文件的位置。

// main.js
...
//# sourceMappingURL=main.js.map

  控制台再次查看输出结果,已经可以看到此语句在源代码中的具体行数。

  单击main.js:2跳转至如下,可看见此语句的具体情况。

  样式则完全可以回溯到scss文件中。

  单击index.scss:3跳转至如下。

# 安全性能

  开启source map之后,开发者工具中的webpack://目录中可以找到解析后的工程源码。

  注意当打开浏览器的开发者工具时,map文件会被同时加载,然后浏览器使用它来解析对应的输出文件,分析出源代码的目录结构和内容。

  ;map文件一般比较大,不打开开发者工具是不会加载的,但是使用source map会有一定的安全隐患。

  ;webpack提供了hidden-source-mapnosources-source-map两种策略来提升source map的安全性。

  ;hidden-source-map仍然会生成完整的map文件,但是输出文件中不会添加对map文件的引用。若要回溯源码,可借助第三方服务(例如 Sentry (opens new window)),将map文件上传上去。

  ;nosources-source-map可以在开发者工具中查看源码的目录结构,但是文件具体内容会被隐藏。可以在控制台查看console日志的准确行数,对于回溯错误基本足够。

# devtool 配置项

  ;devtool用作调试,包括如下十几种等配置。

  • none
  • eval
  • eval-source-map
  • cheap-eval-source-map
  • cheap-module-eval-source-map
  • source-map
  • cheap-source-map
  • cheap-module-source-map
  • inline-source-map
  • inline-cheap-source-map
  • inline-cheap-module-source-map
  • hidden-source-map
  • nosources-source-map

  其中包括evalcheapmodulesource-map等关键字,大部分都是组合而成,具体特性如下。

  • eval:使用eval包裹模块代码,并且存在sourceURLsourceURL指向原文件
  • cheap:打包map文件时,不保存原始代码的列位置信息,只包含行位置信息。忽略loadersource map,并且仅显示转译后的代码
  • module:包括loadersourcemap
  • source-map:生成.map文件

# none

  根目录下包括package.jsonwebpack.config.jssrc文件夹,src下包括index.htmlmain.js

// package.json
{
  ...
  "scripts": {
    "build": "webpack"
  },
  "devDependencies": {
    "@babel/core": "^7.2.2",
    "@babel/preset-env": "^7.3.1",
    "babel-loader": "^8.0.5",
    "html-webpack-plugin": "^3.2.0",
    "webpack": "4.29.4",
    "webpack-cli": "^3.2.3"
  }
}

// webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin')

module.exports = {
  entry: './src/main.js',
  plugins: [
    new HtmlWebpackPlugin({
      template: './src/index.html',
    }),
  ],
  module: {
    rules: [
      {
        test: /\.js$/,
        loader: 'babel-loader',
        exclude: /node_modules/,
        options: {
          cacheDirectory: true,
          presets: [['@babel/preset-env', { modules: false }]],
        },
      },
    ],
  },
  devtool: 'none',
  mode: 'development',
}

// src/index.html
<html lang="zh-CN">

<body>
  <p>hello world</p>
</body>

</html>

// src/main.js
const fn = () => {
  console.log('source map')
}

fn()

  运行build脚本命令后,控制台查看输出结果。

  单击mian.js:97跳转如下。

  ;none配置项即无法回溯到源代码,仅仅是根据配置的loader转译了模块的代码。

# eval

  修改webpack.config.js中的devtool属性为eval,再运行build脚本命令,查看打包输出的代码。

  控制台查看输出结果后单击并跳转。

  ;eval配置项中,打包后模块代码被eval包裹,同时包括sourceURL,回溯的源代码被loader转换过(箭头函数转换为普通函数),并带有光标列信息。

# source-map

  修改webpack.config.js中的devtool属性为source-map,再次运行build脚本命令,dist目录下多生成了.map文件。

  查看打包输出的代码。

  控制台查看输出结果后单击并跳转。

  ;source-map配置项中,生成了.map文件,打包输出的代码尾部追加了sourceMappingURL,回溯的源代码为原始代码,并带有光标列信息。

# cheap-source-map

  修改webpack.config.js中的devtool属性为cheap-source-map,运行build脚本命令,也会在dist目录下生成.map文件,打包输出的代码和source-map一致。

  控制台查看输出结果后单击并跳转。

  ;cheap-source-map配置项中,也会生成.map文件,打包输出的代码尾部也会追加sourceMappingURL,回溯的源代码被loader转换过(箭头函数转换为普通函数),不带有光标列信息(光标位于行首)。

# eval-source-map

  修改webpack.config.js中的devtool属性为eval-source-map,运行build脚本命令,查看打包输出的代码。

  控制台查看输出结果后单击并跳转。

  ;eval-source-map配置项中,打包后模块代码被eval包裹,同时包括sourceURLsourceMappingURL,不会生成.map文件,而是将map文件内容转换为base64编码插入到sourceMappingURL后面,回溯的源代码为原始代码,并带有光标列信息。

# cheap-eval-source-map

  修改webpack.config.js中的devtool属性为cheap-eval-source-map,运行build脚本命令,查看打包输出的代码。

  控制台查看输出结果后单击并跳转。

  ;cheap-eval-source-map配置项中,打包后模块代码被eval包裹,同时包括sourceURLsourceMappingURL,不会生成.map文件,而是将map文件内容转换为base64编码插入到sourceMappingURL后面,回溯的源代码被loader转换过(箭头函数转换为普通函数),不带有光标列信息(光标位于行首)。

# cheap-module-eval-source-map

  修改webpack.config.js中的devtool属性为cheap-module-eval-source-map,运行build脚本命令,查看打包输出的代码。

  控制台查看输出结果后单击并跳转。

  ;cheap-module-eval-source-map配置项中,打包后模块代码被eval包裹,同时包括sourceURLsourceMappingURL,不会生成.map文件,而是将map文件内容转换为base64编码插入到sourceMappingURL后面,回溯的源代码为原始代码,不带有光标列信息(光标位于行首)。

# cheap-module-source-map

  修改webpack.config.js中的devtool属性为cheap-module-source-map,运行build脚本命令,查看打包输出的代码。

  控制台查看输出结果后单击并跳转。

  ;cheap-module-source-map配置项中,也会生成.map文件,打包输出的代码尾部也会追加sourceMappingURL,回溯的源代码为原始代码,不带有光标列信息(光标位于行首)。

# inline-source-map

  修改webpack.config.js中的devtool属性为inline-source-map,运行build脚本命令,查看打包输出的代码。

  控制台查看输出结果后单击并跳转。

  ;inline-source-map配置项中,不会生成.map文件,而是将map文件内容转换为base64编码插入到sourceMappingURL后面,回溯的源代码为原始代码,并带有光标列信息。

# hidden-source-map

  修改webpack.config.js中的devtool属性为hidden-source-map,运行build脚本命令,也会在dist目录下生成.map文件,查看打包输出的代码。

  控制台查看输出结果后单击并跳转。

  ;inline-source-map配置项中,会生成.map文件,但是不会保留对map文件的引用(无sourceMappingURL),无法回溯到源代码。

# nosources-source-map

  修改webpack.config.js中的devtool属性为nosources-source-map,运行build脚本命令后,控制台查看输出结果。

  单击main.js:2跳转如下。

  查看打包输出的代码。

  ;nosources-source-map配置项中,生成了.map文件,打包输出的代码尾部追加了sourceMappingURL,无法回溯到源代码(源代码的目录结构可查看,具体内容被隐藏),但是可以在控制台查看console日志的准确行数。

# 差异对比

  如下为各个配置项的差异,其中构建速度fastest > fast > ok > slow > slowest

devtool 构建速度 map 方式 eval 包裹 sourceMappingURL 是否有光标列信息 是否可回溯 回溯代码
none fastest - - - - -
eval fast eval函数内sourceURL引用源文件路径 - loader转译后代码
source-map slowest 模块尾部追加sourceMappingURL引用map 链接map文件名 原始代码
cheap-source-map ok 模块尾部追加sourceMappingURL引用map 链接map文件名 loader转译后代码
eval-source-map slowest eval函数内sourceURL引用源文件路径,函数尾部再插入sourceMappingURL map内容base64编码 原始代码
cheap-eval-source-map ok eval函数内sourceURL引用源文件路径,函数尾部再插入sourceMappingURL map内容base64编码 loader转译后代码
cheap-module-eval-source-map slow eval函数内 sourceURL 引用源文件路径,函数尾部再插入sourceMappingURL map内容base64编码 原始代码
cheap-module-source-map slow 模块尾部追加sourceMappingURL引用map 链接map文件名 原始代码
inline-source-map slowest 模块尾部追加sourceMappingURL map内容base64编码 原始代码
hidden-source-map slowest - - - - -
nosources-source-map slowest 模块尾部追加sourceMappingURL引用map 链接map文件名 - - -

# 资源压缩

  一般发布到线上环境的资源,都会进行代码压缩(丑化),即移出多余的空格、换行和执行不到的代码、缩短变量名、移出注释等,在保证执行结果不变的前提下将代码替换为更短的形式。

  代码在压缩后整体体积都会显著减小,同时将基本不可读,一定程度上提升了代码的安全性。

# 压缩 JavaScript

  ;webpack3及以下可通过webpack.optimize.UglifyJsPlugin实现代码压缩。

// package.json
{
  ...
  "scripts": {
    "build": "webpack"
  },
  "devDependencies": {
    "webpack": "^3.10.0"
  }
}

// webpack.config.js
const webpack = require('webpack')

module.exports = {
  entry: './src/main.js',
  output: {
    filename: './dist/[name].js',
  },
  plugins: [new webpack.optimize.UglifyJsPlugin()],
}

// src/main.js
const fn = function () {
  console.log('hello world')
}

fn()

  ;webpack4之后默认使用terser-webpack-plugin作为内置压缩插件,其支持ES6+代码的压缩。在webpack4中可通过optimization.minimize控制是否开启压缩,开发环境下默认关闭,生产环境下默认开启。

// webpack.config.js
module.exports = {
  ...
  optimization: {
    minimize: true,
  },
}

  也可以通过optimization.minimizer自定义压缩插件及其配置项,如下打包时可自动去除console.log

// webpack.config.js
const TerserPlugin = require('terser-webpack-plugin')

module.exports = {
  entry: './src/main.js',
  output: {
    filename: './[name].js',
  },
  optimization: {
    minimize: true,
    minimizer: [
      new TerserPlugin({
        terserOptions: {
          compress: {
            pure_funcs: ['console.log'],
          },
        },
      }),
    ],
  },
}

# 压缩 CSS

  压缩css首先是使用extract-text-webpack-plugin或者mini-css-extract-plugin将样式提取出来,然后使用optimize-css-assets-webpack-plugin来进行压缩。

  根目录下为package.jsonwebpack.config.jssrc文件夹,src下包括main.jsindex.css

// package.json
{
  ...
  "scripts": {
    "build": "webpack"
  },
  "devDependencies": {
    "css-loader": "^0.28.7",
    "mini-css-extract-plugin": "^0.5.0",
    "optimize-css-assets-webpack-plugin": "^4.0.1",
    "webpack": "4.29.4",
    "webpack-cli": "^3.2.3"
  }
}

// webpack.config.js
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin')

module.exports = {
  entry: './src/main.js',
  output: {
    filename: './[name].js',
  },
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          {
            loader: MiniCssExtractPlugin.loader,
          },
          'css-loader',
        ],
      },
    ],
  },
  plugins: [
    new MiniCssExtractPlugin({
      filename: '[name].css',
      chunkFilename: '[id].css',
    }),
  ],
  optimization: {
    minimize: true,
    minimizer: [new OptimizeCSSAssetsPlugin()],
  },
}

// src/main.js
import './index.css'

console.log('hello world')

// src/index.css
p {
  color: red;
}

  执行build脚本打包后,查看输出的css文件。

// dist/main.css
p {
  color: red;
}

# 缓存

  缓存是指重复利用浏览器已经获取过的资源,详细的缓存策略(如缓存时间)由服务器来决定,浏览器则会在资源过期前一直使用本地缓存进行响应。

  但是此方式也会带来一个问题,若代码进行了bug fix,并希望立即更新到所有用户的浏览器上,最好的办法就是更改资源URL,迫使客户端重新下载资源。

  常用方法是每次打包对资源内容计算一次hash,并作为版本号存放在文件名中。

# 版本号

  如下通常使用chunkhash来作为文件版本号,会单独为每一个chunk计算一个hash值。

// package.json
{
  ...
  "scripts": {
    "build": "webpack"
  },
  "devDependencies": {
    "webpack": "4.29.4",
    "webpack-cli": "^3.2.3"
  }
}

// webpack.config.js
module.exports = {
  entry: './src/main.js',
  output: {
    filename: './[name]@[chunkhash:8].js',
  },
}

// src/main.js
console.log('hello world')

  运行build脚本打包后如下。

  资源文件名的改变也就意味着HTML中引用路径的改变,可使用html-webpack-plugin插件,在打包结束后自动同步引用的资源名。

// package.json
"devDependencies": {
  ...
  "html-webpack-plugin": "3.2.0"
}

// webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin')

module.exports = {
  ...
  plugins: [
    new HtmlWebpackPlugin({
      template: './src/index.html',
    }),
  ],
}

// src/html
<html lang="zh-CN">

<body>
  <p>hello world</p>
</body>

</html>

  再次运行build脚本打包,打开distindex.html,其中引用的资源路径已同步。

# CommonsChunkPlugin

  通过CommonsChunkPlugin可以将一些不常变动的代码单独提取出来,与经常迭代的业务代码区别开来,这部分资源可以在客户端一直缓存。

  但是webpack3及以下为每个模块指定的id是按数字递增的,当有新的模块插入进来就会导致其他模块的id发生变化,进而影响chunk中的内容,即最终影响chunkhash值,造成本不用下载的资源重新下载。

  根目录下为package.jsonwebpack.config.jssrc文件夹,src下包括main.js

// package.json
{
  ...
  "scripts": {
    "build": "webpack"
  },
  "devDependencies": {
    "webpack": "^3.10.0"
  },
  "dependencies": {
    "jquery": "^3.2.1"
  }
}

// webpack.config.js
const webpack = require('webpack')

module.exports = {
  entry: {
    main: './src/main.js',
    vendor: ['jquery'],
  },
  output: {
    filename: './dist/[name]@[chunkhash:8].js',
  },
  plugins: [
    new webpack.optimize.CommonsChunkPlugin({
      name: 'vendor',
      filename: './dist/[name]@[chunkhash:8].js',
    }),
  ],
}

// src/main.js
import 'jquery'

console.log('main.js')

  运行build脚本打包,查看输出的资源,其中vendor中仅包含第三方模块jquerywebpack运行时文件。

  然后在src下新建utils.jsmain.js中引入。

// src/main.js
import 'jquery'
import './utils'

console.log('main.js')

// src/utils.js
console.log('utils.js')

  再次执行build脚本打包,查看输出的资源。

  如此就造成了一个问题,vendor中的模块并未变化,其路径名称却发生了改变。

  对比dist目录下vendor@5eb95a94.jsvendor@fe14193b.js,仅如下两处发生了变化。

# HashedModuleIdsPlugin

  解决的方法在于更改模块id的生成方式,webpack3内部自带了HashedModuleIdsPlugin插件,它可以为每个模块按照其所在路径生成一个字符串类型的hash id

// src/main.js
import 'jquery'

console.log('main.js')

// webpack.config.js
const webpack = require('webpack')

module.exports = {
  entry: {
    main: './src/main.js',
    vendor: ['jquery'],
  },
  output: {
    filename: './dist/[name]@[chunkhash:8].js',
  },
  plugins: [
    new webpack.HashedModuleIdsPlugin(),
    new webpack.optimize.CommonsChunkPlugin({
      name: 'vendor',
      filename: './dist/[name]@[chunkhash:8].js',
    }),
  ],
}

  运行build脚本打包,查看输出的资源。

  ;main.js引入utils.js

import 'jquery'
import './utils'

console.log('main.js')

  再次运行build打包。

  由于vendor仅仅包括第三方模块jquerywebpack运行时,同时jquery的路径始终是固定的,所以其hash id始终是固定的。

  ;webpack3以下的版本,由于其不支持字符串类型的id,可以使用webpack-hashed-module-id-plugin插件。而webpack4+修改了模块id的生成方式,也就不再有此问题。

# 监控分析

  可以使用第三方插件对打包输出的bundle体积进行监控和分析,以防止不必要的冗余模块被添加进来。

# Import Cost

  ;Vs Code中的Import Cost可以对引入模块的大小进行实时检测,当代码中引用一个新的模块(主要是node_modules中的模块),就会计算此模块压缩后以及gzip后所占的体积。

# webpack-bundle-analyzer

  另外一个可视化分析工具为webpack-bundle-analyzer,可以分析一个bundle的构成。

// package.json
{
  ...
  "scripts": {
    "build": "webpack"
  },
  "devDependencies": {
    "vue": "^2.6.12",
    "vue-router": "^3.5.1",
    "vuex": "^3.6.2",
    "webpack": "^3.10.0",
    "webpack-bundle-analyzer": "^4.4.1"
  }
}

// webpack.config.js
const webpack = require('webpack')
const Analyzer = require('webpack-bundle-analyzer').BundleAnalyzerPlugin

module.exports = {
  entry: {
    main: './src/main.js',
    vendor: ['vue', 'vuex', 'vue-router'],
  },
  output: {
    filename: './dist/[name].js',
  },
  plugins: [
    new Analyzer(),
    new webpack.optimize.CommonsChunkPlugin({
      name: 'vendor',
      filename: './dist/[name].js',
    }),
  ],
}

// src/main.js
import Vue from 'vue'
import Vuex from 'vuex'
import VueRouter from 'vue-router'
import { log } from './utils.js'

console.log(Vue, Vuex, VueRouter, log, 'main.js')

// src/utils.js
export function log() {
  console.log('utils.js')
}

  运行build脚本分析结果如下。

# 打包优化

  项目初期不要看到任何优化点就拿到项目中,会增加复杂度优化效果也不理想。一般是项目发展到一定规模,出现性能问题再去具体优化。

# HappyPack

  ;HappyPack是一个通过多线程来提升webpack打包速度的插件。

  打包过程非常耗时的部分就是通过loader转译各种资源,例如babel转译ES6+等,其具体工作流程如下。

  1. 从配置中获取打包入口
  2. 匹配loader规则,并对入口模块进行转译
  3. 对转译后的模块进行依赖查找
  4. 对新找到的模块重复进行步骤2和步骤3,直到没有新的依赖模块

  步骤2到步骤4是一个递归的过程,webpack需要一步步地获取更深层级的资源,然后逐个进行转译。根本问题在于webpack是单线程的,若一个模块依赖于其他几个模块,webpack必须对这些模块逐个进行转译。虽然这些转译任务彼此之间没有任务依赖关系,却必须串行地执行。而HappyPack的核心特性是可以开启多个线程,并行地对不同模块进行转译,充分利用本地资源来提升打包速度。

# 单个 loader

  使用时要用HappyPack提供的loader替换原有的loader,并将原有的loader传给HappyPack插件。

  根目录下为package.jsonwebpack.config.jssrc,其中src下包括main.jsbar.jsfoo.js

// package.json
{
  ...
  "scripts": {
    "build": "webpack"
  },
  "devDependencies": {
    "@babel/core": "^7.2.2",
    "@babel/preset-env": "^7.3.1",
    "babel-loader": "^8.0.5",
    "vue": "^2.6.12",
    "vuex": "^3.6.2",
    "webpack": "4.29.4",
    "webpack-cli": "3.2.3",
    "happypack": "^5.0.0"
  }
}

// src/main.js
import './foo.js'
import './bar.js'

console.log('main.js')

// src/foo.js
import vue from 'vue'

console.log(vue, 'foo.js')

// src/bar.js
import Vuex from 'vuex'

console.log(Vuex, 'bar.js')

  初始webpack配置如下。

// webpack.config.js
module.exports = {
  entry: './src/main.js',
  output: {
    filename: './[name].js',
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        loader: 'babel-loader',
        exclude: /node_modules/,
        options: {
          cacheDirectory: true,
          presets: [['@babel/preset-env', { modules: false }]],
        },
      },
    ],
  },
}

  引入HappyPack插件后修改webpack配置。

// webpack.config.js
const HappyPack = require('happypack')

module.exports = {
  entry: './src/main.js',
  output: {
    filename: './[name].js',
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        loader: 'happypack/loader',
      },
    ],
  },
  plugins: [
    new HappyPack({
      loaders: [
        {
          loader: 'babel-loader',
          options: {
            cacheDirectory: true,
            presets: [['@babel/preset-env', { modules: false }]],
          },
        },
      ],
    }),
  ],
}

# 多个 loader

  ;HappyPack优化多个loader时,需要为每一个loader配置一个id,否则HappyPack无法知晓rulesplugins如何一一对应。

  根目录下为package.jsonwebpack.config.jssrcsrc下包括main.jsindex.css

// package.json
{
  ...
  "scripts": {
    "build": "webpack"
  },
  "devDependencies": {
    "@babel/core": "^7.2.2",
    "@babel/preset-env": "^7.3.1",
    "babel-loader": "^8.0.5",
    "webpack": "4.29.4",
    "webpack-cli": "3.2.3",
    "happypack": "^5.0.0",
    "css-loader": "^0.28.9",
    "style-loader": "^0.19.0"
  }
}

// src/main.js
import './index.css'

console.log('main.js')

// src/index.css
p {
  color: red;
}

  初始webpack配置如下。

// webpack.config.js
module.exports = {
  entry: './src/main.js',
  output: {
    filename: './[name].js',
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        loader: 'babel-loader',
        exclude: /node_modules/,
        options: {
          cacheDirectory: true,
          presets: [['@babel/preset-env', { modules: false }]],
        },
      },
      {
        test: /\.css$/,
        use: ['style-loader', 'css-loader'],
      },
    ],
  },
}

  引入HappyPack插件后修改webpack配置。

// webpack.config.js
const HappyPack = require('happypack')

module.exports = {
  entry: './src/main.js',
  output: {
    filename: './[name].js',
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        loader: 'happypack/loader?id=js',
        exclude: /node_modules/,
      },
      {
        test: /\.css$/,
        loader: 'happypack/loader?id=css',
      },
    ],
  },
  plugins: [
    new HappyPack({
      id: 'js',
      loaders: [
        {
          loader: 'babel-loader',
          options: {
            cacheDirectory: true,
            presets: [['@babel/preset-env', { modules: false }]],
          },
        },
      ],
    }),
    new HappyPack({
      id: 'css',
      loaders: ['style-loader', 'css-loader'],
    }),
  ],
}

# 缩小打包范围

  提升性能一般都是两种方式,增加资源或者缩小范围。增加资源即使用更多的CPU和内存,用更多的计算能力来缩短执行任务的时间。缩小范围则是针对任务本身,例如去掉冗余流程,或者不做重复性的工作等。

# exclude/include

  对于js模块,一般要把node_modules目录排除掉。

# noParse

  部分第三方库是完全不希望webpack去进行解析的,即不希望应用任何loader规则,库的内部也不会有对其他模块的依赖,此时就可以使用noParse对其进行忽略。

  如下表示忽略所有文件名包括lodash的模块,这些模块仍然会被打包进资源,只不过webpack不会对其进行任何解析。

// webpack.config.js
module.exports = {
  ...
  module: {
    noParse: /lodash/,
  },
}

# IgnorePlugin

  ;IgnorePlugin插件则是可以完全排除一些模块,被排除的模块即便被引用了也不会被打包进资源文件中。

  如下moment.js是一个时间处理的库,为了做本地化其加载了很多语言包,一般来说用不上,而且会占用很多体积,可以用IgnorePlugin将其忽略掉。

  根目录下包括package.jsonwebpack.config.jssrcsrc下包括main.js

// package.json
{
  ...
  "scripts": {
    "build": "webpack"
  },
  "devDependencies": {
    "moment": "^2.29.1",
    "webpack": "4.29.4",
    "webpack-cli": "3.2.3"
  }
}

// webpack.config.js
module.exports = {
  entry: './src/main.js',
  output: {
    filename: './[name].js',
  },
}

// src/main.js
import 'moment'

console.log('main.js')

  运行build脚本打包,查看输出的资源。

  修改webpack.config.js,其中resourceRegExp匹配资源文件,contextRegExp匹配检索目录。

// webpack.config.js
const webpack = require('webpack')

module.exports = {
  entry: './src/main.js',
  output: {
    filename: './[name].js',
  },
  plugins: [
    new webpack.IgnorePlugin({
      resourceRegExp: /^\.\/locale$/,
      contextRegExp: /moment$/,
    }),
  ],
}

  再次运行build脚本打包,查看输出的资源。

# DllPlugin

  早期windows系统由于受限于计算机内存空间较小的问题而出现的一种内存优化方法叫动态链接库,当一段相同的子程序被多个程序调用时,为了减少内存消耗,可以将这段子程序存储为一个可执行文件,当被多个程序调用时只在内存中生成和使用同一个实例。

  ;DllPlugin借鉴了此思路,对于第三方模块或者一些不常变化的模块,可以将他们预先编译和打包,然后在项目中实际构建过程中直接取用即可。预先打包的时候还会附加生成一份模块清单,此清单将会在工程业务模块打包时起到链接和索引的作用。

  ;DllPluginCode Splitting类似,都可以用来提取公共模块,但是也有本质区别。Code Splitting是设置一些特定的规则并在打包过程中根据规则提取模块。DllPlugin则是将vendor完全拆分出来,有自己的一整套webpack配置并独立打包,在实际工程构建时就不再对它进行任何处理,直接取用即可。理论上DllPlugin会比Code Splitting在打包速度上更快,但是也相应的增加了配置以及资源管理的复杂度。可以理解为DllPlugin通过两次打包来取代一次打包,理论上速度更快,第一次打包是针对不常变化的模块,第二次打包则是针对业务模块。

# dll 打包

  根目录下包括webpack.config.jspackage.jsonsrcsrc下包括main.jsindex.html

// package.json
{
  ...
  "scripts": {
    "build": "webpack"
  },
  "devDependencies": {
    "vue": "^2.6.12",
    "webpack": "4.29.4",
    "webpack-cli": "3.2.3"
  }
}

// src/main.js
import 'vue'

console.log('main.js')

  首先为动态链接库单独创建一个webpack配置文件,将其命名为webpack.dll.config.js。其中output.filename为动态链接库的名称,output.path为动态链接库的输出路径,output.library必须和DllPlugin中的name一致。DllPlugin中的path为动态链接库的模块清单的输出路径,pathmanifest.json文件中name字段的值。

// webpack.dll.config.js
const path = require('path')
const webpack = require('webpack')

const dllAssetPath = path.join(__dirname, 'dist', 'dll')
const dllLibraryName = 'dll'

module.exports = {
  entry: ['vue'],
  output: {
    path: dllAssetPath,
    filename: 'dll.js',
    library: dllLibraryName,
  },
  plugins: [
    new webpack.DllPlugin({
      name: dllLibraryName,
      path: path.join(dllAssetPath, 'manifest.json'),
    }),
  ],
}

  然后配置package.json,新增一条脚本命令。

// package.json
{
  ...
  "scripts": {
    "dll": "webpack --config webpack.dll.config.js"
  }
}

  运行dll脚本命令,将在dist目录下dll文件夹中生成dll.jsmanifest.json。其中dll.js中的变量名就是上述配置中output.librarymanifest.json中的name就是上述DllPlugin中的name

// dist/dll/dll.js
var dll = (function (params) { 
  ...
})( ... )

// dist/dll/manifest.json
{
  "name": "dll",
  "content": {
    ...
  }
}

  最后需要在业务代码中链接dll.js

// webpack.config.js
const path = require('path')
const webpack = require('webpack')

module.exports = {
  entry: './src/main.js',
  output: {
    filename: './[name].js',
  },
  plugins: [
    new webpack.DllReferencePlugin({
      manifest: require(path.join(__dirname, 'dist/dll/manifest.json')),
    }),
  ],
}

  页面引入script如下,页面执行到dll.js时会声明全局变量dll,而manifest相当于注入main.js的资源地图,main.js会先通过name字段找到名为dlllibrary,故在webpack.dll.config.jsoutput.library必须和DllPlugin中的name一致。

// src/index.html
<html lang="zh-CN">

<body>
  <p>hello world</p>
  <script src="./dll/dll.js"></script>
  <script src="./main.js"></script>
</body>

</html>

# id 问题

  查看manifest.json,其中每个模块都有一个id,其值是按照数字顺序递增的,main.js代码在引用dll.js中的模块的时候也是引用数字id

  工程打包后可能存在dll.js(通过DllPlugin构建)、utils.js@[chunkhash].jsmain.js。其中dll.js中包含vue,其id5。当尝试添加更多的模块到dll.js中时,重新构建后vueid变为6

  由于utils也引用了vue模块,重新构建后其chunkhash则会发生改变,但是其本身内容并未改变,而在客户端用户只有重新下载资源。

  解决此问题的方式则使用HashedModuleIdsPlugin插件。

// webpack.dll.config.js
module.exports = {
  plugins: [
    new webpack.DllPlugin({
      ...
    }),
    new webpack.HashedModuleIdsPlugin()
  ]
}

# tree shaking

  ;ES6 Module的依赖关系的构建是在代码编译时而非运行时,基于此特性,webpack提供了tree shaking功能,它可以在打包过程中检测工程中没有被引用的模块,此部分代码将永远无法被执行到,因此也被称为“死代码”。

  ;webpack会对这部分代码进行标记,并在资源压缩时将它们从最终的bundle中去掉。

  如下webpack打包时会对bar函数添加一个标记,然后通过压缩工具来去除死代码。

// src/main.js
import { foo } from './utils.js'

foo()

// src/utils.js
export function foo() {
  console.log('foo')
}

export function bar() {
  console.log('bar')
}

上一篇

下一篇

# 🎉 写在最后

🍻伙伴们,如果你已经看到了这里,觉得这篇文章有帮助到你的话不妨点赞👍或 Star (opens new window) ✨支持一下哦!

手动码字,如有错误,欢迎在评论区指正💬~

你的支持就是我更新的最大动力💪~

GitHub (opens new window) / Gitee (opens new window)GitHub Pages (opens new window)掘金 (opens new window)CSDN (opens new window) 同步更新,欢迎关注😉~

最后更新时间: 3/6/2022, 9:06:37 PM