wepack 透视——提高工程化(实践篇)

让人想犯罪 __ 提交于 2019-12-09 13:52:28

wepack 透视——提高工程化(实践篇)

webpack 是我们前端工程师必须掌握的一项技能,我们的日常开发已经离不开这个配置。关于这方面的文章已经很多,但还是想把自己的学习过程总结记录下来。 上一篇文章介绍了webpack 构建原理,这篇文章将基于这个原理之上,讲述在我们实际工程配置中可以去优化的2 个方向。

  • 提升构建速度,也就是减少整个打包构建的时间,
  • 优化构建输出,也就是减小我们最终构建输出的文件体积。

1. 提升构建速度

1.1 哪些阶段可以提速?

我们先回顾下整个构建过程,首先从入口文件开始递归生成所有文件的module实例,再针对所有module 实例的依赖关系进行分析优化,划分为一个或多个 chunk 去生成最终的打包文件输出。那哪些阶段我们可以去节约时间呢?

module 实例的优化和处理的时间我们并不好做提速,这里往往涉及到最终输出文件的大小,我们做的优化操作越多,输出的文件体积越小,这是我们希望看到的,所以只能从生成 module 实例阶段去入手提速,在这个阶段文件会经过以下处理:
  • resovle阶段: 获取文件所在的绝对路径以及文件要被哪些loaders编译转换
  • run-loader阶段:执行对应的 loaders对文件执行编译转换
  • parse 阶段:解析文件是否存在依赖,以及对应的依赖文件。

在这个过程中,我们可以节约时间的方向:

  • resolve 阶段: 减少查找文件绝对路径的时间
  • run-loader阶段: 减少要被 loader 执行编译转换的文件数量

1.2 如何配置?

  • resolve 阶段
 resolve: {
    // 位于 src 文件夹下常用模块,创建别名,准确匹配
    alias: {
      xyz$: path.resolve(__dirname, 'path/to/file.js')
    },
    modules: ['node_modules'],  // 查找范围的减小
    extensions: ['.js', '.json'],  // import 文件未加文件后缀是,webpack 根据 extensions 定义的文件后缀进行依次查找
    mainFields: ['loader', 'main'],  // 减少入口文件的搜索步骤,设置第三方模块的入口文件位置
  },
  • 充分利用别名,配置别名的路径,可准确匹配到对应文件路径,减少文件查找时间
  • 配置 modules,减少文件查找范围
  • extensions 配置项要充分利用,在我们引入的文件没有添加后缀信息时,webpack 会遍历此配置项,依次加上配置项数组里的后缀去查找匹配文件,所以在我们日常的开发中最好加上文件后缀,可以省略添加后缀查找的步骤,或者我们把高频的文件后缀放在数组前面,这样通过减少遍历次数去节约时间
  • run-loader 阶段
 module: {
    rules: [
      {
        test: /\.js$/, // 匹配的文件
        use: 'babel-loader',
        // 在此文件范围内去查找
        include: [],
        // 此文件范围内不去查找
        exclude: file => (
          /node_modules/.test(file) &&
          !/\.vue\.js/.test(file)
        )
      }
    ]
  }

在此阶段要充分利用include和exclude配置项,将需要经过 loader 转换的文件限定在某个范围内,或者把不需要经过此 loader 执行的文件过滤掉。

2. 优化构建输出

减小最终输出的包体积可以从如下几个方向着手:

  • tree-shaking
  • 代码压缩

2.1 Tree-shaking

Tree-shaking的意思是将我们没有用到的代码剔除掉,从而减少总的打包体积。在 webpack4.0 生产模式中,已默认帮我开启了 tree-shaking,我们先了解下它tree-shaking的原理

// a.js
import {
  add
} from './b.js'

add(1, 2)
// b.js
export function add(n1, n2) {
  return n1 + n2
}

export function square(x) {
  return x * x;
}

export function cube(x) {
  return x * x * x;
}

我们可以看到 b.js中的square和cube方法并没有被使用到,我们在开发模式下开启 usedExports的配置,

 mode: 'development',
 optimization: {
   usedExports: true
 }

b.js 最后打包的结果如下:

/***/ "./src/chunk/b.js":
/*!************************!*\
  !*** ./src/chunk/b.js ***!
  \************************/
/*! exports provided: add, square, cube */
/*! exports used: add */
/***/ (function(module, __webpack_exports__, __webpack_require__) {

"use strict";
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "a", function() { return add; });
/* unused harmony export square */
/* unused harmony export cube */
// import('./d.js').then(mod => mod(100, 11))

function add(n1, n2) {
  return n1 + n2
}

function square(x) {
  return x * x;
}

function cube(x) {
  return x * x * x;
}

可以看到square和cube函数都被标记了/**unused harmony export **/, webpack4.0 就是根据此标记集合生产模式下默认开启的压缩 minification进行 tree-shaking的。

当然,他有一些需要注意的点,否则很多时候我们可能会发现 treeshaking 无效

  • 使用 ES6 的模块化语法(import/esport)

正是由于 ES6 模块的静态特性,使我们对依赖的静态分析有了可能,才有了上面的/** harmony **/标记,因此webpack4.0 的 treeshaking 必须基于 ES6 模块化语法。

  • 避免将 ES6 模块化语法经过 babel 编译转换为 commonJs 语法
  • 在项目 package.json文件中,添加一个“sideEffects:false”属性.

告知 webpack 所有代码不包含 side effect,可以安全删除未用到的 export,当然我们 也可以配置指定文件无side effect

  "sideEffects": [
    "./src/some-side-effectful-file.js"
  ]
  • 开启代码压缩,集合 ES6 模块化语法才能 tree-shaking

在生产模式中,默认开启代码压缩minification,默认加载的压缩插件是TerserPlugin,当然也可以使用其他具有删除未引用代码能力的插件来替换默认插件,比如UglifyJsPlugin

const UglifyJsPlugin = require('uglifyjs-webpack-plugin');

module.exports = {
  optimization: {
    minimizer: [new UglifyJsPlugin()],
  },
};

2.2 代码压缩

  • js 代码压缩

在 webpack 4.0的生产环境中 js 代码压缩是默认开启的,

module.exports = {
  optimization: {
    minimizer: [
      new TerserPlugin({
        test: /\.js(\?.*)?$/i,
        cache: true,
        parallel: true,
      }),
    ],
  },
};

此处注意webpack 是在 v4.26.0 将默认的压缩插件从 uglifyjs-webpack-plugin 改成 teaser-webpack-plugin 的(https://github.com/webpack/webpack/releases?after=v4.26.1 )因为uglifyjs-webpack-plugin 使用的 uglify-es 已经不再被维护,teaser是其一个分支。

  • css 代码压缩

目前 webpack4 并未对 css 有内置任何优化,据说 webpack5 会内置 css 文件的压缩。目前可以使用'mini-css-extract-plugin'插件进行压缩。

const MiniCssExtractPlugin = require("mini-css-extract-plugin");
module.exports = {
  plugins: [
    new MiniCssExtractPlugin({
      // Options similar to the same options in webpackOptions.output
      // both options are optional
      filename: "[name].css",
      chunkFilename: "[id].css"
    })
  ],
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          {
            loader: MiniCssExtractPlugin.loader,
            options: {
              // you can specify a publicPath here
              // by default it use publicPath in webpackOptions.output
              publicPath: '../'
            }
          },
          "css-loader"
        ]
      }
    ]
  }
}

再来看一个来自官方文档的demo,集合了 js 和 css 的压缩。

const TerserJSPlugin = require("terser-webpack-plugin");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const OptimizeCSSAssetsPlugin = require("optimize-css-assets-webpack-plugin");
module.exports = {
  optimization: {
    minimizer: [
      new TerserJSPlugin({}),
      new OptimizeCSSAssetsPlugin({})
    ]
  },
  plugins: [
    new MiniCssExtractPlugin({
      filename: "[name].css",
      chunkFilename: "[id].css"
    })
  ],
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          MiniCssExtractPlugin.loader,
          "css-loader"
        ]
      }
    ]
  }
}

2.3 拆包

前面介绍了 tree-shaking 和代码压缩,那打包出来的体积是不是已经优化到了极致呢?异步加载出现了。。。 比如vue-router 支持异步路由。整体思想是将进入首页所需要的所有资源打包到一个 chunk 中,其余的资源异步引用,在需要时再加载。

// a.js
import('./c').then(del => del(1, 2))

回顾下原理篇我们讲的拆包原理,遇到异步进行分 chunkgroup,在这个 c.js就会被单独打包成一个 chunk 输出。最后打包的结果如下:

(window["webpackJsonp"] = window["webpackJsonp"] || []).push([[0],{

/***/ "./src/chunk/c.js":
/*!************************!*\
  !*** ./src/chunk/c.js ***!
  \************************/
/*! exports provided: default */
/***/ (function(module, __webpack_exports__, __webpack_require__) {

"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "default", function() { return del; });
/* harmony import */ var _d_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./d.js */ "./src/chunk/d.js");


Object(_d_js__WEBPACK_IMPORTED_MODULE_0__["default"])(100, 11)

Promise.resolve(/*! import() */).then(__webpack_require__.bind(null, /*! ./b.js */ "./src/chunk/b.js")).then(add => add(1, 2))

function del(n1, n2) {
  return n1 - n2
}

/***/ })

}]);

在执行到 c.js 时,webpack 使用jsonp 的原理调用异步引用的文件c 。通过 script 标签加载,src 指向 c 文件的标识来调用执行。

涉及到拆包的配置如下,来自官方文档的 demo:

module.exports = {
  //...
  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
        }
      }
    }
  }
};

通过对splitChunks的一些配置,可以设置我们拆包的一些规则。 在这里有一 点需要注意:

  • webpack 默认把第三方库对应的node_modules里的文件抽出单独作为一个 chunk输出,当然我们还可以讲自己写的公用工具类的代码也这么抽出作为单独chunk 输出,这么做的原因这些代码不太会变动,单纯抽出可以充分利用浏览起的缓存,加快下一次的加载速度。

3. 总结

webpack的配置优化可以从以下 2 个方向入手

  • 提高构建速度
    • Resolve阶段提速,加快文件查找速度
    • loader 执行阶段提速,减少要被 loader 执行的文件数量
  • 优化构建输出
    • tree-shaking
    • 代码压缩
      • js 代码压缩
      • css 代码压缩
    • 拆包
      • 异步加载
      • 抽出第三方库

作者:吴海元

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!