webpack
定义
一个用于现代 JavaScript 应用程序的 静态模块打包工具。当 webpack 处理应用程序时,它会在内部从一个或多个入口点构建一个 依赖图(dependency graph),然后将你项目中所需的每一个模块组合成一个或多个 bundles,它们均为静态资源,用于展示你的内容。
作用
把静态模块内容,压缩、转译、打包等(前端工程化)
把 less/sass 转成 css 代码
把 ES6+ 降级成 ES5 等
支持多种模块文件类型,多种模块标准语法
加载器 loader
webpack 默认只识别 js 和 json,要想识别更多内容,就需要使用加载器器(loader)
规则 rules
它有两个属性:
- test,识别出哪些文件会被转换,一般是正则匹配
- use,定义用哪个 loader 进行转换
占位符
- 【hash】对模块内容做算法计算,得到映射的数字字母组合的字符串
- 【ext】使用当前模块原本的占位符,例如:.png / .jpg 等字符串
- 【query】保留引入文件时代码中查询参数(只有 URL 下生效
- 处理图片规则
module.exports = {
// ...
module: {
rules: [
// ...
{
test: /\.(png|jpg|jpeg|gif)$/i,
type: "asset",
generator: {
filename: "assets/[hash][ext][query]",
},
},
],
},
};- 识别 css,
css-loader解析 css 代码,style-loader将解析的 css 代码插入到 DOM 里,mini-css-extract-plugin提取 css 到单文件中,好让浏览器缓存,需实例化到插件里。 less-loader、sass-loader
类型
enforce 属性,分为前置(pre)、普通(normal)、行内(inline)、后置(post)
执行顺序
- Pitching 阶段: Loader 上的 pitch 方法,按照 后置(post)、行内(inline)、普通(normal)、前置(pre) 的顺序调用。
- Normal 阶段: Loader 上的 常规方法,按照 前置(pre)、普通(normal)、行内(inline)、后置(post) 的顺序调用。模块源码的转换, 发生在这个阶段。
- 同等类型下的 normal Loader 执行顺序才是由右向左,或者由下到上执行。picth Loader 从左往右
这里的 normal loader 和 enforce 的不是同一个东西,源码处理转换在这个阶段
内联方式
- 使用 ! 前缀,将禁用所有已配置的 normal loader(普通 loader)
import test from "!c-loader";
const a = 1;使用 !! 前缀,将禁用其他类型的 loader,只要内联 loader
使用 -! 前缀,将禁用所有已配置的 preLoader 和 loader,但是不禁用 postLoaders,也就是不要 pre 和 normal loader:
编写自定义 loader
三个参数 source、map、meta
- loader 是一个函数,接收一个源代码
- 不能是箭头函数因为要复用 this,传递上下文参数
- 同步处理必须返回 buffer 或者 string,异步需要调用特定函数
- 如果 pitch 有返回值,则会中断后面的 loader 链
插件 plugin
loader 用于转换某些类型的模块,而插件则可以用于执行范围更广的任务。包括:打包优化,资源管理,注入环境变量。
hooks
- entry-option 初始化 option
- run 开始编译
- compile 真正开始的编译,在创建 compilation 对象之前
- compilation 生成好了 compilation 对象
- make 从 entry 开始递归分析依赖,准备对每个模块进行 build
- after-compile 编译 build 过程结束
- emit 在将内存中 assets 内容写到磁盘文件夹之前
- after-emit 在将内存中 assets 内容写到磁盘文件夹之后
- done 完成所有的编译过程
- failed 编译失败的时候
- ...还有好多
Hook 类型
- SyncHook 同步钩子 Webpack 共出现 71 次,如 Compiler.hooks.compilation
- SyncBailHook 同步熔断钩子 Webpack 共出现 66 次,如 Compiler.hooks.shouldEmit
- SyncWaterfallHook 同步瀑布流钩子 Webpack 共出现 37 次,如 Compilation.hooks.assetPath
- SyncLoopHook 同步循环钩子 Webpack 中未使用
- AsyncParallelHook 异步并行钩子 Webpack 仅出现 1 次:Compiler.hooks.make
- AsyncParallelBailHook 异步并行熔断钩子 Webpack 中未使用
- AsyncSeriesHook 异步串行钩子 Webpack 共出现 16 次,如 Compiler.hooks.done
- AsyncSeriesBailHook 异步串行熔断钩子 Webpack 中未使用
- AsyncSeriesLoopHook 异步串行循环钩子 Webpack 中未使用
- AsyncSeriesWaterfallHook 异步串行瀑布流钩子 Webpack 共出现 5 次,如 NormalModuleFactory.hooks.beforeResolve
执行顺序
钩子处理,没有执行顺序一说,谁先触发钩子谁先执行
自定义插件
class TestPlugin {
constructor() {
console.log("TestPlugin constructor()");
}
// 1. webpack读取配置时,new TestPlugin() ,会执行插件 constructor 方法
// 2. webpack创建 compiler 对象
// 3. 遍历所有插件,调用插件的 apply 方法
apply(compiler) {
console.log("TestPlugin apply()");
// 从文档可知, compile hook 是 SyncHook, 也就是同步钩子, 只能用tap注册
compiler.hooks.compile.tap("TestPlugin", (compilationParams) => {
console.log("compiler.compile()");
});
// 从文档可知, make 是 AsyncParallelHook, 也就是异步并行钩子, 特点就是异步任务同时执行
// 可以使用 tap、tapAsync、tapPromise 注册。
// 如果使用tap注册的话,进行异步操作是不会等待异步操作执行完成的。
compiler.hooks.make.tap("TestPlugin", (compilation) => {
setTimeout(() => {
console.log("compiler.make() 111");
}, 2000);
});
// 使用tapAsync、tapPromise注册,进行异步操作会等异步操作做完再继续往下执行
compiler.hooks.make.tapAsync("TestPlugin", (compilation, callback) => {
setTimeout(() => {
console.log("compiler.make() 222");
// 必须调用
callback();
}, 1000);
});
compiler.hooks.make.tapPromise("TestPlugin", (compilation) => {
console.log("compiler.make() 333");
// 必须返回promise
return new Promise((resolve) => {
resolve();
});
});
// 从文档可知, emit 是 AsyncSeriesHook, 也就是异步串行钩子,特点就是异步任务顺序执行
compiler.hooks.emit.tapAsync("TestPlugin", (compilation, callback) => {
setTimeout(() => {
console.log("compiler.emit() 111");
callback();
}, 3000);
});
compiler.hooks.emit.tapAsync("TestPlugin", (compilation, callback) => {
setTimeout(() => {
console.log("compiler.emit() 222");
callback();
}, 2000);
});
compiler.hooks.emit.tapAsync("TestPlugin", (compilation, callback) => {
setTimeout(() => {
console.log("compiler.emit() 333");
callback();
}, 1000);
});
}
}
module.exports = TestPlugin;配置 Configuration
模块 Modules
与 Node.js 模块相比,webpack 模块能以各种方式表达它们的依赖关系。下面是一些示例:
- ES2015 import 语句
- CommonJS require() 语句
- AMD define 和 require 语句
- css/sass/less 文件中的 @import 语句。
- stylesheet url(...) 或者 HTML
<img src=...>文件中的图片链接。
优化 Optimization
css-minimizer-webpack-plugin压缩 css,在 optimization 的 minimizer 里实例化
热更新机制
- 本地服务启动时,会启动一个 websocket 服务,与浏览器建立连接
- 服务端监控到文件修改,会去构建新的模块,生成新的编译标识
- 与旧模块做对比,分析出要替换的模块,生成热更新文件
- ws 通知浏览器更新,浏览器根据编译标识获取到更新文件,在浏览器里做模块的更新替换删除
treeshaking 机制
定义
是一种基于 ES Module 规范的 Dead Code Elimination 技术,它会在运行过程中静态分析模块之间的导入导出,确定 ESM 模块中哪些导出值未曾其他模块使用,并将其删除,以此实现打包产物的优化。
实现过程
- 构建阶段收集模块信息,有哪些导出值,形成导出列表,记录到依赖关系图 ModuleGraph 对象里去
- 封装阶段,再次遍历模块,标记出导出列表哪些导出值被用到,哪些没有
- 导出阶段,将未使用和使用的值生成不同的代码
- 删除阶段,根据生成代码的标记,使用 Terser、UglifyJS 等 DCE 工具,删除无效代码
注意
- Tree-Shaking 强依赖于 ESM 模块化方案的静态分析能力,所以应尽量坚持使用 ESM 编写模块代码。
- 禁止 Babel 转译模块导入导出语句
- 使用支持 Tree Shaking 的包
开发环境搭建
使用webpack-dev-server做服务器
// ...
module.exports = {
// ...
mode: "development",
};"scripts": {
"build": "webpack --mode=production",
"dev": "webpack serve --mode=development"
}注意:命令行设置的优先级高于配置文件中的,推荐用命令行设置。 开发模式注重代码热替换更快,让开发调试代码更便捷 生产模式注重项目体积更小,更轻量,适配不同的浏览器环境
注入环境变量
// ...
const webpack = require("webpack");
module.exports = {
// ...
plugins: [
// ...
new webpack.DefinePlugin({
// key 是注入到打包后的前端 JS 代码中作为全局变量
// value 是变量对应的值(在 corss-env 注入在 node.js 中的环境变量字符串)
"process.env.NODE_ENV" : JSON.stringify(process.env. NODE_ENV),
}),
],
};别名
// ...
const config = {
// ...
resolve: {
alias: {
"@": path.resolve(__dirname, "src"),
},
},
};外部扩展 Externals
externals 配置选项提供了「从输出的 bundle 中排除依赖」的方法。相反,所创建的 bundle 依赖于那些存在于用户环境(consumer's environment)中的依赖
获取第三方对应 key,value
key 是 package.json 中安装的包名,value 导出时的全局变量
缺点
没 tree shaking
编译打包过程
概念
- bundle
- Bundle(捆绑包)是指将所有相关的模块和资源打包在一起形成的单个文件。它是应用程序的最终输出,可以在浏览器中加载和执行。
- 捆绑包通常由 Webpack 根据入口点(entry)和它们的依赖关系自动创建。当你运行 Webpack 构建时,它会根据配置将所有模块和资源打包成一个或多个捆绑包。
- Chunk
Chunk(代码块)是 Webpack 在打包过程中生成的中间文件,它代表着一堆 module 的集合。
Webpack 根据代码的拓扑结构和配置将模块组织成不同的代码块。每个代码块可以是一个独立的文件,也可以与其他代码块组合成一个捆绑包。
Webpack 使用代码分割(code splitting)技术将应用程序代码拆分成更小的代码块,以便在需要时进行按需加载。这有助于减小初始加载的文件大小,提高应用程序的性能。
在 Webpack 中,捆绑包和代码块之间存在一对多的关系。一个捆绑包可以包含多个代码块,而一个代码块也可以属于多个不同的捆绑包。这取决于 Webpack 配置中的拆分点(split points)和代码块的依赖关系。
过程
- 初始化参数:从配置文件和 Shell 语句中读取与合并参数,得出最终的参数。
- 开始编译:用上一步得到的参数初始化 Compiler 对象,加载所有配置的插件,执行对象的 run 方法开始执行编译。
- 确定入口:根据配置中的 entry 找出所有的入口文件。
- 编译模块:创建 compilation 对象,和其相关回调钩子,从入口文件出发,调用所有配置的 Loader 对模块进行翻译,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理。
- 完成模块编译:在经过第 4 步使用 Loader 翻译完所有模块后,得到了每个模块被翻译后的最终内容以及它们之间的依赖关系。
- 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会。
- 输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统
- 在以上过程中,Webpack 会在特定的时间点广播出特定的事件,插件在监听到感兴趣的事件后会执行特定的逻辑,并且插件可以调用 Webpack 提供的 API 改变 Webpack 的运行结果。
JStar