Skip to content

webpack

Webpack 是一个 JavaScript 打包和构建工具,可帮助你编译、打包和优化代码,使其可供生产环境使用。Webpack 本身只能处理 JavaScript 文件。但是通过使用加载程序(loader)和插件(plugins),我们可以将其扩展以处理另外的文件也,包括 CSS、HTML 或图片文件。

提示

无论是 rollup.js 还是 webpack,在寻找资源时,如果 package.json 中存在 module 字段,那么会优先使用 module 字段指向的资源来代替 main 字段指向的资源。

基本概念

  • Chunk 是 webpack 生成的文件块,用来拆分代码,它可以被多个 bundle 引用;
  • Bundle 是 webpack 生成的最终需要被加载的文件,它通过复用多个 chunk 实现代码复用;
  • Module 是一个 JavaScript 对象,它包含了整个应用程序所需的资源,例如图像、CSS、JavaScript 行为等;

构建流程

Webpack 从启动到结束会依次执⾏以下流程:

  1. 初始化参数:从配置⽂件和 Shell 语句中读取与合并参数,得出最终的参数;
  2. 开始编译:⽤上⼀步得到的参数初始化 Compiler 对象,加载所有配置的插件,执⾏对象的 run ⽅法开始执⾏编译;
  3. 确定⼊⼝:根据配置中的 entry 找出所有的⼊⼝⽂件;
  4. 编译模块:从⼊⼝⽂件出发,调⽤所有配置的 Loader 对模块进⾏翻译,找出该模块依赖的模块,再递归本步骤直到所有⼊⼝依赖的⽂件都经过了本步骤的处理;
  5. 输出资源:根据⼊⼝和模块之间的依赖关系,组装成⼀个个包含多个模块的 Chunk,加⼊到输出列表,这步是可以修改输出内容的最后机会;
  6. 输出完成:在确定好输出内容后,根据配置确定输出的路径和⽂件名,把⽂件内容写⼊到⽂件系统。

在以上过程中,Webpack 会在特定的时间点⼴播出特定的事件,插件在监听到感兴趣的事件后会执⾏特定的逻辑,并且插件可以调⽤ Webpack 提供的 API 改变 Webpack 的运⾏结果。

loader 与 plugin 区别

  • loader 处理指定类型的文件;
  • plugin 是拓展功能;

编写一个 loader

一个 source 为参数的方法,返回处理完成后的 source。

js
module.exports = function (source) {
  // todo
  return source
}

编写一个 plugin

Tapable 是 Webpack 实现所有生命周期回调和插件方式的核心。它提供了 apply 方法可以注册某个函数到某个事件,让这个函数可以在某个时间节点执行。

常用生命周期

  • entryOption 生命周期发生在 Webpack 配置文件 entry 属性配置之前。如果当前项目是多页应用,会使用 entryOption 生命周期来通过 Node 动态添加多个 entry,用来设置多个 html-webpack-plugin 进行多页面处理。
  • afterPlugins 生命周期发生在 Plugins 加载完毕以后,可以通过此生命周期修改 plugins 内部嵌套的 options 进行优化或增加代码。
  • beforeRun 生命周期发生在 compiler.run 之前,在此阶段可以修改 options,添加 loaders 或 plugins。
  • run 生命周期在 compiler.run 执行时触发,可以修改 beforeRun 生命周期加载的 Loaders 和 Plugins 。
  • emit 生命周期发生在编译完成,打包可执行文件输出前,可以获得 complier 和 compilation 的编译实例,修改打包输出文件。
  • done 生命周期是编译完成后,可以修改打包文件后最后一步,可以做一些输出文件的处理。

以下是 emit 生命周期列子:

  1. 一个 JavaScript 类函数。
  2. 在函数原型 (prototype)中定义一个注入 compiler 对象的 apply 方法。
  3. apply 函数中通过 compiler 插入指定的事件钩子,在钩子回调中拿到 compilation 对象。
  4. 使用 compilation 操纵修改 webapack 内部实例数据。
js
module.exports = class RemoveCommentPlugin {
  constructor(opts) {}
  apply(compiler) {
    compiler.hooks.emit.tap('RemoveComment', compilation => {
      let content = compilation.assets[item].source()
      Object.keys(compilation.assets).forEach(item => {
        compilation.assets[item] = {
          source: () => content,
          size: () => content.length
        }
      })
    })
  }
}

优化前端性能

  • 压缩代码:删除多余的代码、注释、简化代码的写法等等⽅式。可以利⽤ webpack 的 UglifyJsPlugin 和 ParallelUglifyPlugin 来压缩 JS ⽂件, 利⽤ cssnano (css-loader?minimize)来压缩 css
  • 利⽤ CDN 加速: 在构建过程中,将引⽤的静态资源路径修改为 CDN 上对应的路径。可以利⽤ webpack 对于 output 参数和各 loader 的 publicPath 参数来修改资源路径
  • Tree Shaking: 将代码中永远不会⾛到的⽚段删除掉。可以通过在启动 webpack 时追加参数 --optimize-minimize 来实现
  • Code Splitting: 将代码按路由维度或者组件分块(chunk),这样做到按需加载,同时可以充分利⽤浏览器缓存
  • 提取公共第三⽅库: SplitChunksPlugin 插件来进⾏公共模块抽取,利⽤浏览器缓存可以⻓期缓存这些⽆需频繁变动的公共代码

优化打包速度

  • 外部扩展(externals): 将不怎么需要更新的第三⽅库脱离 webpack 打包,不被打⼊ bundle 中,从⽽减少打包时间,⽐如 jQuery ⽤ script 标签引⼊;
  • 动态库(dll): 采⽤ webpack 的 DllPlugin 和 DllReferencePlugin 引⼊ dll,让⼀些基本不会改动的代码先打包成静态资源,避免反复编译浪费时间
  • 利⽤缓存: webpack.cache 、babel-loader.cacheDirectory、 HappyPack.cache 都可以利⽤缓存提⾼ rebuild
  • 限制 loader 文件范围:include;
  • 多线程打包:thread-loader;

优化打包体积

文件数量尽可能少、文件大小尽可能小。

  • 模块懒加载:一般都是返回一个 promise;
  • Tree Shaking;
  • 代码压缩;