Introduce to Webpack
date
Jun 8, 2021
slug
webpack
status
Published
tags
webpack
summary
webpack简介
type
Post
工作流程
- 读取配置参数config.js
- 启动webpack,创建Compiler对象并开始解析项目
- 以entry文件为入口跑dfs,生成依赖关系树
- 对树上的节点根据不同的文件类型,使用不同的loader进行处理,最终输出产物为JavaScript文件,loader可以理解成纯函数。
- 在1-4的流程中,webpack会基于发布订阅模式,抛出一些hook,wepack的插件系统就是基于这些hook,执行插件的功能,plugin可以理解成执行副作用
源码细节
webpack中有两个核心对象,compiler和compilation,compiler是一个singleton,负责控制webpack的构建流程;compliation是每一次构建时的上下文对象,包含了当次构建所需要的所有信息,每次热更新和重新构建,compiler都会重新生成一个compilation对象,来维护这次的构建过程。
在loader处理每个节点的同时,会用acorn库生成节点的AST,将该节点AST合并到总的AST的中,如果当前AST还有依赖,则dfs进入下一层节点。
最终webpack打包出来的bundle文件是一个llfe函数
// webpack 5 打包的bundle文件内容
(() => { // webpackBootstrap
var __webpack_modules__ = ({
'file-A-path': ((modules) => { // ... })
'index-file-path': ((__unused_webpack_module, __unused_webpack_exports, __webpack_require__) => { // ... })
})
// The module cache
var __webpack_module_cache__ = {};
// The require function
function __webpack_require__(moduleId) {
// Check if module is in cache
var cachedModule = __webpack_module_cache__[moduleId];
if (cachedModule !== undefined) {
return cachedModule.exports;
}
// Create a new module (and put it into the cache)
var module = __webpack_module_cache__[moduleId] = {
// no module.id needed
// no module.loaded needed
exports: {}
};
// Execute the module function
__webpack_modules__[moduleId](module, module.exports, __webpack_require__);
// Return the exports of the module
return module.exports;
}
// startup
// Load entry module and return exports
// This entry module can't be inlined because the eval devtool is used.
var __webpack_exports__ = __webpack_require__("./src/index.js");
})整个llfe只有三个变量和一个方法,
__webpack_modules__存放了编译后的各个文件模块的JS内容,__webpack_module_cache__ 用来做模块缓存,__webpack_require__是Webpack内部实现的一套依赖引入函数。最后一句则是代码运行的起点,从入口文件开始,启动整个项目。__webpack_require__模块引入函数存在的原因是,存在有ES Module和CommonJS两种规范导出/引入依赖模块,webpack打包编译的时候,会统一替换成自己的__webpack_require__来实现模块的引入和导出,从而实现模块缓存机制,以及抹平不同模块规范之间的一些差异性Loader
Webpack的最终输出是一份Javascript代码,在Webpack内部也只能够处理Javascript代码,因此当遇到非Javascript文件时,需要将其转换成等效的Javascript文件,才能继续执行打包任务。Loader的作用就是转换非JavaScript文件。
Loader的配置如下
// webpack.config.js
module.exports = {
// ...other config
module: {
rules: [
{
test: /^your-regExp$/,
use: [
{
loader: 'loader-name-A',
},
{
loader: 'loader-name-B',
}
]
},
]
}
}对于某个匹配规则,会有一个Loader数组来链式处理,上一个loader的输出会作为下一个loader的输入。因此loader必须是JavaScript代码字符串。
loader并不是一个纯函数,它的this上下文由webpack提供,this指向loaderContext的loaderRunnder对象
module.exports = function(source) {
const content = doSomeThing2JsString(source);
// 如果 loader 配置了 options 对象,那么this.query将指向 options
const options = this.query;
// 可以用作解析其他模块路径的上下文
console.log('this.context', this.context);
/*
* this.callback 参数:
* error:Error | null,当 loader 出错时向外抛出一个 error
* content:String | Buffer,经过 loader 编译后需要导出的内容
* sourceMap:为方便调试生成的编译后内容的 source map
* ast:本次编译生成的 AST 静态语法树,之后执行的 loader 可以直接使用这个 AST,进而省去重复生成 AST 的过程
*/
this.callback(null, content);
// or return content;
}Plugin
plugin可以扩展webpack的功能
之前提到过webpack的两个核心对象compiler和compilation,其中compiler上暴露了Webpack整个生命周期相关的钩子(compiler hook),compilation则暴露出模块和依赖相关的粒度更小的事件钩子
// Tapable的简单使用
const { SyncHook } = require("tapable");
class Car {
constructor() {
// 在this.hooks中定义所有的钩子事件
this.hooks = {
accelerate: new SyncHook(["newSpeed"]),
brake: new SyncHook(),
calculateRoutes: new AsyncParallelHook(["source", "target", "routesList"])
};
}
/* ... */
}
const myCar = new Car();
// 通过调用tap方法即可增加一个消费者,订阅对应的钩子事件了
myCar.hooks.brake.tap("WarningLampPlugin", () => warningLamp.on());plugin也有一些开发的规范:
- plugin必须是一个函数或者拥有apply方法的对象,这样才能传入compiler实例
- 传入每个plugin的compiler是同一个实例,如果在某个插件中对compiler有修改,会影响到后续的plugin的complier
- 异步的事件需要在插件处理完任务时调用回调函数通知
Webpack进入下一个流程,不然会卡住
class MyPlugin {
apply (compiler) {
// 找到合适的事件钩子,实现自己的插件功能
compiler.hooks.emit.tap('MyPlugin', compilation => {
// compilation: 当前打包构建流程的上下文
console.log(compilation);
// do something...
})
}
}SourceMap
sourceMap可以将被压缩混淆的代码还原出来,通过一份映射记录的map文件可以将混淆前后的代码一一对应。sourceMap文件的数据结构如下
{
"version" : 3, // Source Map版本
"file": "out.js", // 输出文件(可选)
"sourceRoot": "", // 源文件根目录(可选)
"sources": ["foo.js", "bar.js"], // 源文件列表
"sourcesContent": [null, null], // 源内容列表(可选,和源文件列表顺序一致)
"names": ["src", "maps", "are", "fun"], // mappings使用的符号名称列表
"mappings": "A,AAAB;;ABCDE;" // 带有编码映射数据的字符串
}map文件需要以下规则
- 生成文件中的一行的每个组用“;”分隔;
- 每一段用“,”分隔;
- 每个段由1、4或5个可变长度字段组成;
有了sourceMap文件后,可以在混淆后的代码后面加一行
//# sourceURL=/path/to/file.js.map有了这段注释后,浏览器就会通过
sourceURL去获取这份映射文件,通过解释器解析后,实现源码和混淆代码之间的映射,sourceMap也需要浏览器支持才行webpack打包出来的bundle文件,可以发现在默认的
development开发模式下,每个_webpack_modules__文件模块的代码最末端,都会加上//# sourceURL=webpack://file-path?,从而实现对sourceMap的支持。