名词简介
Entry
:编译入口,webpack 编译的起点;Compiler
:编译管理器,webpack 启动后会创建compiler
对象,该对象一直存活知道结束退出;Compilation
:单次编辑过程的管理器,比如watch = true
时,运行过程中只有一个compiler
但每次文件变更触发重新编译时,都会创建一个新的compilation
对象;Dependence
:依赖对象,webpack 基于该类型记录模块间依赖关系;Module
:webpack 内部所有资源都会以“module”对象形式存在,所有关于资源的操作、转译、合并都是以 “module” 为基本单位进行的;Chunk
:编译完成准备输出时,webpack 会将module
按特定的规则组织成一个一个的chunk
,这些chunk
某种程度上跟最终输出一一对应;Loader
:资源内容转换器,其实就是实现从内容 A 转换 B 的转换器;Plugin
:webpack构建过程中,会在特定的时机广播对应的事件,插件监听这些事件,在特定时间点介入编译过程。
核心流程
webpack 体系知识可以被抽象为:
- 构建的核心流程;
- loader 的作用;
- plugin 架构与常用套路。
Webpack 最核心的功能是用于现代 JavaScript 应用程序的静态模块打包工具。简单来说,就是把包括img、css、js 等资源模块依赖,转译、组合、拼接、生成一个或多个 bundles。
包括 内容转换 + 资源合并两种核心功能。实现上包含三个阶段:初始化、构建、生成。
初始化
- 初始化参数:从配置文件、 配置对象、Shell 参数中读取,与默认配置结合得出最终的参数;
- 创建编译器对象:用上一步得到的参数创建
Compiler
对象; - 初始化编译环境:包括注入内置插件、注册各种模块工厂、初始化 RuleSet 集合、加载配置的插件等;
- 开始编译:执行
compiler
对象的run
方法; - 确定入口:根据配置中的
entry
找出所有的入口文件,调用compilition.addEntry
将入口文件转换为dependence
对象。
具体基本流程:
- 启动 webpack ,触发
lib/webpack.js
文件中createCompiler
方法; createCompiler
方法内部调用WebpackOptionsApply
插件;WebpackOptionsApply
定义在lib/WebpackOptionsApply.js
文件,内部根据entry
配置决定注入entry
相关的插件,包括:DllEntryPlugin
、DynamicEntryPlugin
、EntryPlugin
、PrefetchPlugin
、ProgressPlugin
、ContainerPlugin;
Entry
相关插件,如lib/EntryPlugin.js
的EntryPlugin
监听compiler.make
钩子;lib/compiler.js
的compile
函数内调用this.hooks.make.callAsync;
- 触发
EntryPlugin
的make
回调,在回调中执行compilation.addEntry
函数; compilation.addEntry
函数内部经过一系列与主流程无关的hook
之后,再调用handleModuleCreate
函数,正式开始构建内容。
构建
- 编译模块(make):根据
entry
对应的dependence
创建module
对象,调用loader
将模块转译为标准 JS 内容,调用 JS 解释器将内容转换为 AST 对象,从中找出该模块依赖的模块,再 递归 本步骤直到所有入口依赖的文件都经过了本步骤的处理; - 完成模块编译:上一步递归处理所有能触达到的模块后,得到了每个模块被翻译后的内容以及它们之间的 依赖关系图。
具体基本流程:
- 调用
handleModuleCreate
,根据文件类型构建module
子类; - 调用 loader-runner 仓库的
runLoaders
根据用户所配置的 loader 集合读取、转译module
内容,通常是将各类资源类型转译为 JavaScript 文本; - 调用 acorn 将 JS 文本解析为AST;
- 遍历 AST,触发各种钩子
- 在
HarmonyExportDependencyParserPlugin
插件监听exportImportSpecifier
钩子,解读 JS 文本对应的资源依赖; - 调用
module
对象的addDependency
将依赖对象加入到module
依赖列表中。
- 在
- AST 遍历完毕后,调用
module.handleParseResult
处理模块依赖; - 对于
module
新增的依赖,调用handleModuleCreate
,控制流回到第一步; - 所有依赖都解析完毕后,构建阶段结束。
生成
- 输出资源(seal):根据入口和模块之间的依赖关系,组装成一个个包含多个模块的
Chunk
,再把每个Chunk
转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会; - 写入文件系统(emitAssets):在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统。
seal
函数主要完成从 module
到 chunks
的转化,具体流程:
- 构建本次编译的
ChunkGraph
对象; - 遍历
compilation.modules
集合,将module
按entry/动态引入
的规则分配给不同的Chunk
对象; compilation.modules
集合遍历完毕后,得到完整的chunks
集合对象,调用createXxxAssets
方法;createXxxAssets
遍历module/chunk
,调用compilation.emitAssets
方法将资assets
信息记录到compilation.assets
对象中;- 触发
seal
回调,控制流回到compiler
对象。
webpack 内置的 chunk
封装规则为:
entry
及 entry 触达到的模块,组合成一个chunk
- 使用动态引入语句引入的模块,各自组合成一个
chunk
按 entry / 动态引入
两种情况组织 chunks
,必然引发不必要的重复打包。webpack 提供了一些插件如 CommonsChunkPlugin
、SplitChunksPlugin
,在基本规则之外进一步优化 chunks
结构。
compilation.seal
函数逻辑:
- 遍历
compilation.modules
,记录下模块与chunk
关系; - 触发各种模块优化钩子,这一步优化的主要是模块依赖关系;
- 遍历
module
构建 chunk 集合; - 触发各种优化钩子。
重点关注得到 chunks
集合后的步骤 4 触发的 optimizeChunks
钩子,SplitChunksPlugin
正是使用这个钩子,分析 chunks
集合的内容,按配置规则增加一些通用的 chunk :
module.exports = class SplitChunksPlugin {
constructor(options = {}) {
// ...
}
_getCacheGroup(cacheGroupSource) {
// ...
}
apply(compiler) {
// ...
compiler.hooks.thisCompilation.tap("SplitChunksPlugin", (compilation) => {
// ...
compilation.hooks.optimizeChunks.tap(
{
name: "SplitChunksPlugin",
stage: STAGE_ADVANCED,
},
(chunks) => {
// ...
}
);
});
}
};
小结
从资源形态扭转的角度看整个流程:
compiler.make
阶段:entry
文件以dependence
对象形式加入compilation
的依赖列表,dependence
对象记录有entry
的类型、路径等信息- 根据
dependence
调用对应的工厂函数创建module
对象,之后读入module
对应的文件内容,调用loader-runner
对内容做转化,转化结果若有其它依赖则继续读入依赖资源,重复此过程直到所有依赖均被转化为module
compilation.seal
阶段:- 遍历
module
集合,根据entry
配置及引入资源的方式,将module
分配到不同的chunk
- 遍历
chunk
集合,调用compilation.emitAsset
方法标记chunk
的输出规则,即转化为assets
集合
- 遍历
compiler.emitAssets
阶段:- 将
assets
写入文件系统
- 将
Plugins
webpack 的钩子体系是一种强耦合架构,它在特定时机触发钩子时会附带上足够的上下文信息,插件定义的钩子回调中,能也只能与这些上下文背后的数据结构、接口交互产生 side effect,进而影响到编译状态和后续流程。
插件通常是一个带有 apply
函数的类,apply
函数运行时会得到参数 compiler
,以此为起点可以调用 hook
对象注册各种钩子回调:
class SomePlugin {
apply(compiler) {
compiler.hooks.thisCompilation.tap('SomePlugin', (compilation) => {
compilation.hooks.optimizeChunkAssets.tapAsync('SomePlugin', ()=>{});
})
}
}
而传递参数与具体的钩子强相关。
compiler
对象触发时机如下:
具体触发时机示例:
compiler.hooks.compilation
:- 时机:启动编译创建出 compilation 对象后触发
- 参数:当前编译的 compilation 对象
- 示例:很多插件基于此事件获取 compilation 实例
compiler.hooks.make
:- 时机:正式开始编译时触发
- 参数:同样是当前编译的
compilation
对象 - 示例:webpack 内置的
EntryPlugin
基于此钩子实现entry
模块的初始化
compilation.hooks.optimizeChunks
:- 时机:
seal
函数中,chunk
集合构建完毕后触发 - 参数:
chunks
集合与chunkGroups
集合 - 示例:
SplitChunksPlugin
插件基于此钩子实现chunk
拆分优化
- 时机:
compiler.hooks.done
:- 时机:编译完成后触发
- 参数:
stats
对象,包含编译过程中的各类统计信息 - 示例:
webpack-bundle-analyzer
插件基于此钩子实现打包分析
webpack 会将上下文信息以参数或 this
(compiler 对象) 形式传递给钩子回调,在回调中可以调用上下文对象的方法或者直接修改上下文对象属性的方式,对原定的流程产生 side effect。
比如,EntryPlugin
插件:
class EntryPlugin {
apply(compiler) {
compiler.hooks.compilation.tap(
"EntryPlugin",
(compilation, { normalModuleFactory }) => {
compilation.dependencyFactories.set(
EntryDependency,
normalModuleFactory
);
}
);
compiler.hooks.make.tapAsync("EntryPlugin", (compilation, callback) => {
const { entry, options, context } = this;
const dep = EntryPlugin.createDependency(entry, options);
compilation.addEntry(context, dep, options, (err) => {
callback(err);
});
});
}
}
基于 webpack 的移动端尺寸适配方案
目前比较常见的移动端适配不同的屏幕宽度方案是采用css中的相对单位,有 rem 和 vw两种。
rem
rem 的定义是 font-size of the root element,是只相对于浏览器的根元素(HTML元素)的 font-size的来确定的单位,即只需要计算出对应的根元素的字体大小,用同样的 css 代码可以实现等比适配:
1 rem = (1 root element font-size) px
一般在 webpack 构建的时候使用插件来实现rem适配:postcss-pxtorem
和 lib-flexible。
首先,在webpack.config.js
中配置 postcss-loader:
module.exports = {
entry: "./src/index.js",
output: {
path: path.join(__dirname,"/dist"),
filename: "bundle.js"
},
module:{
rules:[
{
test: /\.css$/,
use: ['style-loader','css-loader','postcss-loader'] //配置postcss-loader
}
]
},
}
然后,安装:
npm i postcss-pxtorem --D 和 npm i amfe-flexible --S
在项目根目录新建.postcssrc.js
文件,在其中写入 postcss-pxtorem 插件配置:
module.exports = {
"plugins": {
"postcss-pxtorem": {
rootValue: 75, // 750的设计稿
propList: ['*']
}
}
}
在entry指定的入口js文件("./src/index.js"
)中引入lib-flexible:
import 'amfe-flexible'
最后,使用是就可以直接用设计稿尺寸:
.button {
width: 47px;
height: 47px;
}
vw
vw 是 1% of viewport’s width,是相对浏览器可视区域宽度的单位。不需要再像 rem 那样,在 js 去动态设置根元素的 font-size,而是直接相对于屏幕宽度。
比如 750px 的设计稿中,一个元素的宽度是300 px,即:
x / 300px = 100 vw / 750px;
x = 100 vw * (300 px / 750 px);
同样,使用 webpack可以用 postcss-px-to-viewport
插件来实现。
首先,像 rem 一样在webpack.config.js
配置 postcss-loader。
然后,安装:
npm i postcss-px-to-viewport --D
在项目根目录新建.postcssrc.js
文件,在其中写入 postcss-px-to-viewport
插件配置:
module.exports = {
"plugins": {
'postcss-px-to-viewport': {
viewportWidth: 750 //750的设计稿
}
}
}
最后,使用是同样就可以直接用设计稿尺寸:
.button {
width: 47px;
height: 47px;
}
小结
首先是在兼容性上,rem 可以兼容更老的浏览器版本;其次是 rem 需要通过 js 计算根元素的字体大小,vm是纯 css 实现。
如果使用的是第三方框架其自身用的是 px 单位且基于 375px 的设计稿,.postcssrc.js 需要做适配:
const path = require('path')
module.exports = ({file}) => {
/** 比如使用 vant UI 框架 */
const width = file.dirname.includes(path.join('node_modules', 'vant')) ? 375 : 750;
return {
"plugins": {
"postcss-px-to-viewport": {
viewportWidth: width,
}
}
}
}
const path = require('path')
module.exports = ({file}) => {
/** 比如使用 vant UI 框架 */
const rootValue = file.dirname.includes(path.join('node_modules', 'vant')) ? 37.5 : 75;
return {
"plugins": {
"postcss-pxtorem": {
rootValue,
propList: ['*']
}
}
}
}