webpack

340 阅读10分钟

核心概念

webpack是js的模块打包工具,通过分析模块之间的依赖,将所有模块打包成一份或者多分代码块,供HTML直接引用。webpack仅提供了打包功能和文件处理机制,可以通过生态中的各种Loader和Plugin对代码进行预编译和打包。

  • 概念
    • Entry: 入口文件,webpack会从该文件开始进行分析和编译
    • Output: 出口文件,打包后创建的bundler文件的路径以及文件名
    • Module: 模块,在webpack中任何文件都可以作为一个模块,根据配置的不同Loader进行加载和打包
    • Chunk: 代码块,可以根据配置将所有模块代码合并成一个或者多个代码块,以便按需加载,提高性能。是 webpack 处理过程中的一组模块
    • bundle:捆绑好的最终文件。如果说,chunk 是各种片段,那么 bundle 就是一堆 chunk 组成的“集大成者”,比如上面说的 main.js 就属于 bundle。当然它也类似于电路上原先是各种散乱的零件,最终组成一个集成块的感觉。它经历了加载和编译的过程,是源文件的最终版本。bundle 是一个或多个 chunk 组成的集合。
    • Loader: 模块加载器,进行各种文件类型的加载与转换
    • Plugin: 拓展插件,可以通过webpack响应的事件钩子,介入到打包过程中的任意环节,从而对代码按需修改
    • optimization: 优化
  • Compiler
    • 可以理解为Webpack的实例,全局唯一,从启动生存到结束
    • 包含当前的所有配置信息
      • options, loaders, plugins
  • Compilation
    • 当监听到文件发生改变时,Webpack 会创建一个新的 Compilation 对象,开始一次新的编译
    • 包含了当前的输入资源,输出资源,变化的文件等
    • 通过它提供的 api,可以监听每次编译过程中触发的事件钩子
    • 通过 Compilation 也能读取到 Compiler 对象
  • 工作流程
    • 初始化参数:从配置文件和 Shell 语句中读取与合并参数,得出最终的参数;执行配置文件中的插件实例化语句 new Plugin()
    • 开始编译:用上一步得到的参数初始化 Compiler 实例,调用插件的apply方法,挂载插件监听,执行对象的 run 方法从入口文件开始执行编译
    • 编译模块:从入口文件出发,调用所有配置的 Loader 对模块进行翻译,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理
    • 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表
    • 输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统
    • 在以上过程中,Webpack 会在特定的时间点广播出特定的事件,插件在监听到感兴趣的事件后会执行特定的逻辑,并且插件可以调用 Webpack 提供的 API 改变 Webpack 的运行结果
  • 特性
    • 通过链式调用,按顺序串起一个个Loader
    • 通过事件流机制,让Plugin可以插入到整个过程中的每个步骤中

无标题.png

使用 tree shaking

  • 使用 ES2015 模块语法(即 import 和 export):因为es6模块的依赖关系是确定的是固定的,不依靠运行时的静态分析。而对于commonJS类型的模块需要依赖第三方的插件才能实现动态删除无效代码 lodash使用commonJS所以需要替换为lodash-es6
  • 以default方式引入的模块,无法treeShaking。引入单个导出对象的方式,使用import * as xxx 的语法,还是import {xxx} 的语法
  • webpackconfig 中 rule.sideEffects 默认为false,指代在要处理的模块中是没有有副作用。通过import 引入的css文件会被 tree shaking,可以通过设置 rules.sudeEffects: true 告诉webpack 这些文件有副作用
  • optimzation.sideEffects 默认为true,指代在优化过程中是否遵循依赖模块的副作用描述。
  • 在 Babel 7 之前的 babel-env中,modules 的默认项为 'commonjs';在 Babel 7 之后的 @babel/preset-env 中,modules 的选项默认为 'auto'
  • 配置
    • 开发环境: mode: 'development',optimization: { usedExports: true}
    • 生产环境:mode: 'production'
    • package.json 中的 sideEffects 属性来确认对应的依赖包是否会产生副作用,默认为 true, 告诉 Webpack ,所有文件都有副作用,他们不能被 Tree Shaking。sideEffects 为一个数组时,告诉 Webpack ,数组中那些文件不要进行 Tree Shaking,其他的可以 Tree Shaking。
  • 相应的操作是在优化阶段进行的,并不能减少编辑模块编译阶段的构建时间,消除那些被引用了但未被使用的模块代码 :参考

Loader

webpack是基于Node,因此只能识别js模块,如css/html /图片等类型的文件无法加载。而Loader就是文件转换器,对webpack传入的字符串进行按需修改

  • 步骤
    • 对代码进行分析,构建AST(抽象语法树)
    • 遍历进行定向的修改
    • 生成新的代码字符串
  • 特性
    • 链式传递: 从右到左顺序执行
    • 基于Node环境,拥有较高权限,比如文件增删改查
    • 可同步也可异步
  • 常用loader
    • file-loader: 加载文件资源,如字体/图片等
    • url-loader: 加载图片,可将小图片直接转换为Date Url,减少请求
    • babel-loader: 加载js/jsx文件,将ES6/ES7代码转换为ES5
    • style-loader: 将css代码以<style>标签的形式插入到html中
    • css-loader: 分析@importurl(),引用css文件与对应的资源
    • postcss-loader: 用于css的兼容性处理,例如添加前缀,单位转换等
    • less-loader/sass-loader: css预处理器,在css中新增了新语法,提高开发效率
  • 编写原则
    • 单一原则: 每个 Loader 只做一件事
    • 链式调用: Webpack 会按顺序链式调用每个 Loader
    • 统一原则: 遵循 Webpack 制定的设计规则和结构,输入与输出均为字符串,各个 Loader 完全独立,即插即用

Plugin

  • 原理
    • 在 Webpack 运行的生命周期中会广播出许多事件
    • Plugin 可以监听这些事件
    • 在合适的时机通过 Webpack 提供的 API 改变输出结果
  • 常用 Plugin:
    • uglifyjs-webpack-plugin: 压缩、混淆代码
    • clean-webpack-plugin: 每次打包前自动清理输入文件夹
    • CommonsChunkPlugin: 代码分割
    • html-webpack-plugin: 加载 html 文件,并引入 css / js 文件
    • extract-text-webpack-plugin / mini-css-extract-plugin: 抽离样式,生成 css 文件
  • 常用API
    • emit 修改输出资源 源文件的转换和组装已经完成,在这里可以读取到最终将输出的资源、代码块、模块及其依赖,并且可以修改输出资源的内容
    • after-compile 监听文件变化 默认情况下 Webpack 只会监视入口和其依赖的模块是否发生变化,在有些情况下项目可能需要引入新的文件,例如引入一个 HTML 文件
    • done 在成功构建并且输出了文件后,Webpack 即将退出时发生
    • failed 在构建出现异常导致构建失败,Webpack 即将退出时发生

事件流机制

  • 核心是基础类Tapable,观察者模式通过事件的订阅与广播
  • Compiler 和 Compilation 都继承自 Tapable。可以直接在 Compiler 和 Compilation 对象上广播和监听事件
  • 传给每个插件的 Compiler 和 Compilation 对象都是同一个引用,在一个插件中修改了 Compiler 或 Compilation 对象上的属性,会影响到后面的插件
  • 有些事件是异步的,这些异步的事件会附带两个参数,第二个参数为回调函数,在插件处理完任务时需要调用回调函数通知 Webpack,才会进入下一处理流程

编译流程

  • Webpack 启动后,在读取配置的过程中会先执行 new BasicPlugin(options) 初始化一个 BasicPlugin 获得其实例
  • 在初始化 compiler 对象后,再调用 basicPlugin.apply(compiler)给插件实例传入 compiler 对象
  • 插件实例在获取到 compiler 对象后,就可以通过compiler.plugin(事件名称, 回调函数) 监听到 Webpack 广播出来的事件
  • 并且可以通过 compiler 对象去操作 Webpack

plugin 代码

/**
* 广播出事件
* event-name 为事件名称,注意不要和现有的事件重名
* params 为附带的参数
*/
compiler.apply('event-name',params);


/**
* 监听名称为 event-name 的事件,当 event-name 事件发生时,函数就会被执行。
* 同时函数中的 params 参数为广播事件时附带的参数。
*/
compiler.plugin('event-name',function(params) {
});

module.exports = {
plugins:[
    	// 在初始化 EndWebpackPlugin 时传入了两个参数,分别是在成功时的回调函数和失败时的回调函数;
    	new EndWebpackPlugin(() => {
      		// Webpack 构建成功,并且文件输出了后会执行到这里,在这里可以做发布文件操作
    	}, (err) => {
      		// Webpack 构建失败,err 是导致错误的原因
      		console.error(err);        
    	})
    ]
}
class EndWebpackPlugin {
    constructor(doneCallback, failCallback) {
        // 存下在构造函数中传入的回调函数
        this.doneCallback = doneCallback;
        this.failCallback = failCallback;
    }
    apply(compiler) {
        compiler.plugin('done', (stats) => {
            // 在 done 事件中回调 doneCallback
            this.doneCallback(stats);
        });
        compiler.plugin('failed', (err) => {
            // 在 failed 事件中回调 failCallback
            this.failCallback(err);
        });
    }
}
// 导出插件 
module.exports = EndWebpackPlugin;

编译优化

  • 代码优化
    • 无用代码消除: UglifyJs 生产环境中删除不可能被执行的代码
    • 摇树优化(tree shaking):
        • import, ESM:摇树的关键
        • sideEffects mode: 'development',optimization: { usedExports: true} :开发环境下的基础配置
      • mode: 'production':生产环境
      • sideEffects:摇树的作用范围
          • package.json 中的配置:sideEffects: false(全都摇)
          • 规则配置中的字段:sideEffects: true(控制全局文件不被摇掉)
  • code-spliting: 代码分割
    • split chunks 是指在chunk生成之后将原先以入口点来划分的chunks根据一定的规则分离出子chunk的过程
    • 路由分割: React Router
    • 剥离公共组件: Loading
    • 项剥离第三方库: 使用 optimization.splitChunks 配置
    • 剥离wenpack运行态: 使用 optimization.runtimeChunk 配置项
  • 使用chunkhash,利用浏览器缓存
  • resolve 在构建时指定查找文件的规则
    • resolve.modules 指定查找模块的目录范围
    • resolve.extensions 指定查找模块的文件类型范围
    • resolve.mainFields 指定查找模块的package.json中主文件的属性名
    • resolve.symlinks 指定在查找模块时是否处理软连接
  • 编译性能优化
    • 减少编译模块
      • ignorePlugin 忽略不需要编译的模块,比如引入moment时不需要local语言包
      • lodash 导入时声明特定的模块,babel-plugin-lodash
      • DLLPlugin
      • Externals
    • 缩小编译范围
      • modules: 指定模块路径,减少递归搜索
      • mainFields: 指定入口文件描述字段,减少搜索
      • includes/exclude: 指定搜索范围/排除不必要的搜索范围
      • alias: 缓存目录,避免重复寻址
    • babel-loader
      • 忽略node_moudles,避免编译第三方库中已经被编译过的代码
      • 使用cacheDirectory,可以缓存编译结果,避免多次重复编译
    • 多进程并发
      • webpack-parallel-uglify-plugin: 压缩 JS 需要先将代码解析成 AST 语法树,然后需要根据复杂的规则去分析和处理 AST,最后将 AST 还原成 JS,这个过程涉及到大量计算比较好使,可使用此插件多进程并发压缩 js 文件,提高压缩速度
      • HappyPack :本身就是对node开启了一个多进程打包,将不再被维
      • thread-loader: 多进程并发文件的 Loader 解析,只要把 thread-loader 放置在其他 loader 之前即可
    • 使用分析
      • speed-measure-webpack-plugin 分析构建过程每个生命周期的用时时间,是一个基于时间的分析工具
      • Webpack Analyse / webpack-bundle-analyzer 对打包后的文件进行分析,寻找可优化的地方
      • 配置profile:true,对各个编译阶段耗时进行监控,寻找耗时最多的地方
    • source-map
      • 开发: cheap-module-eval-source-map
      • 生产: hidden-source-map