本文需要你对webpack有一定的了解,如果你比较感兴趣,可以参考我们之前的webpack源码解析系列:webpack系列-总览。
一些概念说明
Compilation 初始化的时候会初始化下面几个变量:
this.mainTemplate = new MainTemplate(...)
this.chunkTemplate = new ChunkTemplate(...)
this.runtimeTemplate = new RuntimeTemplate
this.moduleTemplates = {
javascript: new ModuleTemplate(this.runtimeTemplate, "javascript"),
webassembly: new ModuleTemplate(this.runtimeTemplate, "webassembly")
}
this.hotUpdateChunkTemplate // 暂时不关注
mainTemplate: 用来生成执行主流程的代码,里面包含了 webpack 的启动代码等等。 chunkTemplate: 得到的最终代码则会通过 JsonP 的方式来加载。
下面的例子: 我们有一个入口文件:
// main.js
import { Vue } from 'vue'
new Vue(...)
这样的文件打包后生成一个 app.js ,一个 chunk-vendor.js。
app.js 结构如下:
(function(modules) { // webpackBootstrap
// webpack 的启动函数
// webpack 内置的方法
}){{
moduleId: (function(module, exports, __webpack_require__) {
// 我们写的 js 代码都在各个 module 中
},
// ...
})
chunk-vendors.js 结构如下:
(window["webpackJsonp"] = window["webpackJsonp"] || []).push([["chunk-vendors"],{
moduleId: (function(module, exports, __webpack_require__) {
// ...
},
// ...
})
app.js 里面包含了 webpack 的 bootstrap 代码,这个代码整体的框架就在 mainTemplate。
app.js 会通过 jonsP 的方式加载 chunk-vendor.js ,这个 js 代码的框架就放在 chunkTemplate 中。
app.js 和 chunk-vendors.js 中各个 module 的代码生成过程就在 ModuleTempalte 中。
代码生成主流程
chunk 代码生成在 seal 阶段。从 Compilation.createChunkAssets
中开始。
主流程图如下
首先确定当前结构是使用 mainTemplate 还是走 chunkTemplate。这两个 Tempalte 中会有自己的 render 流程。我们以 mainTempalte 为例,看 render 的流程。
render 主流程中会生成主结构的代码,也就是前面我们 app.js demo 生成的代码框架部分。然后生成各个 moulde 的代码。这个流程由 ModuleTemplate 中的函数完成。
在 module 生成的时候,会调用 hook.content, hook.module, hook.render, hook.package
这几个 hook。在每一个 hook 得到结果之后,传入到下一个 hook 中。hook.module
这个 hook 执行完后,会得到 module 的代码。然后在 hook.render
中,将这些代码包裹成一个函数。如果我们在 webpack.config.js
中配置了 output.pathinfo=true
(配置说明),那么在 hook.package
这里就会给最终生成的代码添加一些路径和 tree-shaking 相关的注释,可以方便我们阅读代码。
得到所有的 module 代码之后,将它们包裹成数组或者对象。
修改代码
- 利用上面文件生成的 hook, 在某个 module 中添加额外内容
BannerPlugin 是在 chunk 文件开头添加额外的内容。如果我们仅仅是希望在某个 module 中添加内容如何做呢?回顾一下上面代码生成的流程图,module 代码生成有几个关键的 hook 例如 hook.content,hook.module,hook.render
。可以在这几个 hook 中注册函数来进行修改。一个简单的 demo 如下
const { ConcatSource } = require("webpack-sources");
class AddExternalPlugin {
constructor(options) {
// plugin 初始化。这里处理一些参数格式化等
this.content = options.content // 获取要添加的内容
}
apply(compiler) {
const content = this.content
compiler.hooks.compilation.tap('AddExternal', compilation => {
compilation.moduleTemplates.javascript.hooks.render.tap('AddExternal', (
moduleSource,
module ) => {
// 这里会传入 module 参数,我们可以配置,指定在某一 module 中执行下面的逻辑
// ConcatSource 意味着最后处理的时候,我们 add 到里面的代码,会直接拼接。
const source = new ConcatSource()
// 在最开始插入我们要添加的内容
source.add(content)
// 插入源码
source.add(moduleSource)
// 返回新的源码
return source
})
})
}
}
- 在 chunk 执行代码外再包裹一层额外的逻辑。
我们曾经配置过 umd 的模式,或者 output.library
参数。配置了这俩内容之后,最后生成的代码结构就和最开始 app.js demo 中的结果不一样了。以 output.library='someLibName'
为例,会变成下面这样
var someLibName =
(function(modules){
// webpackBootstrap
})([
//... 各个module
])
这个的实现,就是在上面 hooks.renderWithEntry
环节对 mainTemplate 生成的代码进行了修改。
如果我们在某些情况下,想额外包裹一些自己的逻辑。可以就在这里处理。给一个简单的 demo
const { ConcatSource } = require("webpack-sources");
class MyWrapPlugin {
constructor(options) {
}
apply(compiler) {
const onRenderWithEntry = (source, chunk, hash) => {
const newSource = new ConcatSource()
newSource.add(`var myLib =`)
newSource.add(source)
newSource.add(`\nconsole.log(myLib)`)
return newSource
}
compiler.hooks.compilation.tap('MyWrapPlugin', compilation => {
const { mainTemplate } = compilation
mainTemplate.hooks.renderWithEntry.tap(
"MyWrapPlugin",
onRenderWithEntry
)
// 如果我们支持一些变量的配置化,那么就需要把我们配置的信息写入 hash 中。否则,当我们修改配置的时候,会发现 hash 值不会变化。
// mainTemplate.hooks.hash.tap("SetVarMainTemplatePlugin", hash => {
// hash.update()
// });
})
}
}
module.exports = MyWrapPlugin
webpack编译后结果
var myLib =/******/ (function(modules) {
//... webpack bootstrap 代码
/******/ return __webpack_require__(__webpack_require__.s = 0);
/******/ })
/************************************************************************/
/******/ ([
/* 0 */
/***/ (function(module, exports) {
// ...
/***/ })
/******/ ])
console.log(myLib);
- BannerPlugin
类似内置的 BannerPlugin。在上面 chunk 文件生成之后,也就是createChunkAssets
执行完成之后,对整体的 chunk 文件内容进行修改。例如 bannerPlugin 是在 optimizaChunkAssets
hook 中
在这个 hook 里面可以拿到一个参数 chunks
所有的 chunk,然后在这里可以添加额外的内容。
chunkAssets 后文件内容修改
createChunkAssets 执行过后,其他的 hook 中可可以拿到文件内容,进行修改。
-
sourcemap 的影响,afterOptimizeChunkAssets 这个 hook 之后,webpack 生成了 sourcemap。如果在这个之后进行代码的修改,例如 optimizeAssets 或者更后面的 emit hook 中,会发现 sourcemap 不对了。像下面的例子
compiler.hooks.compilation.tap('AddExternal', compilation => { compilation.hooks.optimizeAssets.tap('AddExternal', assets => { let main = assets["main.js"] main = main.children.unshift('//test\n//test\n') }) })
-
对 hash 的影响。当上面 chunk 代码生成结束后,其实 hash 也就随着生成了。在hash生成完之后的 hook 中对代码的修改,比如增加点啥,不会影响到 hash 的结果。例如上面修改 chunk 代码的例子。假如我们的 plugin 进行了升级,修改的内容变了,但是生成的 hash 并不会随着改变。所以需要在 hash 生成相关的 hook 中,把 plugin 的内容写入 hash 中。
module-source 生成
module-source 的过程中会对 parser 阶段生成的各个 dependency 进行处理,根据 dependency.Template 实现对我们缩写的源码的转换。这里我们结合最开始 parser 来一起看 module-source。以下面 demo 为例:
// main.js
import { test } from './b.js'
function some() {
test()
}
some()
// b.js
export function test() {
console.log('b2')
}
main.js parser 中转成的 AST:
对 ast 进行 parser ,这个过程中,会经历
if (this.hooks.program.call(ast, comments) === undefined) {
this.detectMode(ast.body);
this.prewalkStatements(ast.body);
this.blockPrewalkStatements(ast.body);
this.walkStatements(ast.body);
}
-
program
检测有没有用到 import/export ,会增加 HarmonyCompatibilityDependency, HarmonyInitDependency(作用后面介绍)
-
detectMode
检测最开始是否有
use strict
和use asm
,为了保证我们代码编译之后开头写的 use strict 仍然在最开始 -
prewalkStatements
遍历当前作用域下所有的变量定义。这个过程中
import { test } from './b.js'
中 test 也是在当前作用域下的,所以 import 在这里会被处理(过程见 javascript-parser)。针对这句 import 会额外被添加ConstDependency
和HarmonyImportSideEffectDependency
-
blockPrewalk
处理当前作用域下 let/const(在 prewalk 的时候只会处理var),class 名,export 和 export default
-
walkStatements
开始深入每一个节点进行处理。这里会找到代码中所有使用
test
的地方,然后添加HarmonyImportSpecifierDependency
经理过这些之后,对于上面的 demo 就会加入
这些 dependency 分成两大类:
-
moduleDependency: 有对应的 denpendencyFactory,在 processModuleDependencies 过程中会对这个 dependency 进行处理,得到对应的 module
HarmonyImportSideEffectDependency --> NormalModuleFactory
HarmonyImportSpecifierDependency --> NormalModuleFactory
两个指向的是同一个 module(./b.js),所以会被去重。然后 webpack 沿着 dependency ,处理 b.js... 直到将所有的 moduleDependency 处理完
-
仅文件生成的时候,用来生成代码
module.source
首先拿到源码代码,然后处理各个 dependency
-
HarmonyCompatibilityDependency
在开始插入
__webpack_require__.r(__webpack_exports__);
,标识这是一个 esModule -
HarmonyInitDependency
遍历所有的 dependency, 负责生成
import {test} from './b.js'
对应的引入 './b.js' 模块的代码/* harmony import */ var _b_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(0);
-
ConstDependency
在 HarmonyInitDependency 阶段中,已经插入了
import
语句对应的内容,所以源码中的import {test} from './b.js'
需要删除掉。ConstDependency 的作用就是把这句替换成空,即删除 -
HarmonyImportSideEffectDependency
作用阶段在 HarmonyInitDependency 过程中
-
HarmonyImportSpecifierDependency
代码中
test()
所生成的依赖。作用就是替换代码中的test
-
获取到 './b.js' 模块对应的变量名
_b_js__WEBPACK_IMPORTED_MODULE_0__
-
获取 test 对应到 b.js 中的属性名(因为经过 webpack 编译,为了简化代码,我们在 b.js 中的 export test,可能会被转为 export a = test)
Object(_b_js__WEBPACK_IMPORTED_MODULE_0__[/* test */ "a"])
-
然后替换代码中 test
-
经过所有的 dependency 之后:
了解了这个过程之后,如果我们需要对源代码中进行一些简单的修改,可以利用 parser 阶段的各个 hook 来实现。在这里修改有一个好处,不用担心搞坏 sourcemap 和 影响 hash 的生成。
- parser 中插入代码的 demo
例如,我们使用某个插件的时候,需要下面的写法
import MainFunction from './a.js'
import { test } from './b.js'
MainFunction.use(test)
实际中利用 webpack 插件,在检测到有 test 引入时候,自动插入
import MainFunction from './a.js'
MainFunction.use(test)
实现的关键,就是上面提到的 HarmonyImportSideEffectDependency
, HarmonyImportSpecifierDependency
和 ConstDependency
代码如下
const path = require('path')
const ConstDependency = require("webpack/lib/dependencies/ConstDependency");
const HarmonyImportSideEffectDependency = require("webpack/lib/dependencies/HarmonyImportSideEffectDependency")
const HarmonyImportSpecifierDependency = require("webpack/lib/dependencies/HarmonyImportSpecifierDependency")
const NullFactory = require("webpack/lib/NullFactory");
// 要引入的 a.js 的路径。这个路径后面会经过 webpack 的 resolve
const externalJSPath = `${path.join(__dirname, './a.js')}`
class ProvidePlugin {
constructor() {
}
apply(compiler) {
compiler.hooks.compilation.tap(
"InjectPlugin",
(compilation, { normalModuleFactory }) => {
const handler = (parser, parserOptions) => {
// 在 parser 处理 import 语句的时候
parser.hooks.import.tap('InjectPlugin', (statement, source) => {
parser.state.lastHarmonyImportOrder = (parser.state.lastHarmonyImportOrder || 0) + 1;
// 新建一个 './a.js' 的依赖
const sideEffectDep = new HarmonyImportSideEffectDependency(
externalJSPath,
parser.state.module,
parser.state.lastHarmonyImportOrder,
parser.state.harmonyParserScope
);
// 为 dependency 设置一个位置。这里设置为和 import { test } from './b.js' 相同的位置,在代码进行插入的时候会插入到改句所在的地方。
sideEffectDep.loc = {
start: statement.start,
end: statement.end
}
// 设置一下 renames,标识代码中 mainFunction 是从外部引入的
parser.scope.renames.set('mainFunction', "imported var");
// 把这个依赖加入到 module 的依赖中
parser.state.module.addDependency(sideEffectDep);
// -------------处理插入 mainFunction.use(test)------------
if (!parser.state.harmonySpecifier) {
parser.state.harmonySpecifier = new Map()
}
parser.state.harmonySpecifier.set('mainFunction', {
source: externalJSPath,
id: 'default',
sourceOrder: parser.state.lastHarmonyImportOrder
})
// 针对 mainFunction.use 中的 mainFunction
const mainFunction = new HarmonyImportSpecifierDependency(
externalJSPath,
parser.state.module,
-1,
parser.state.harmonyParserScope,
'default',
'mainFunction',
[-1, -1], // 插入到代码最开始
false
)
parser.state.module.addDependency(mainFunction)
// 插入代码片段 .use(
const constDep1 = new ConstDependency(
'.use(',
-1,
true
)
parser.state.module.addDependency(constDep1)
// 插入代码片段 test
const useArgument = new HarmonyImportSpecifierDependency(
source,
parser.state.module,
-1,
parser.state.harmonyParserScope,
'test',
'test',
[-1, -1],
false
)
parser.state.module.addDependency(useArgument)
// 插入代码片段 )
const constDep2 = new ConstDependency(
')\n',
-1,
true
)
parser.state.module.addDependency(constDep2)
});
}
normalModuleFactory.hooks.parser
.for("javascript/auto")
.tap("ProvidePlugin", handler);
normalModuleFactory.hooks.parser
.for("javascript/dynamic")
.tap("ProvidePlugin", handler);
}
);
}
}
module.exports = ProvidePlugin;
生成的代码如下
/* 1 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
const mainFunction = function () {
console.log('mainFunction')
}
mainFunction.use = function(name) {
console.log('load something')
}
/* harmony default export */ __webpack_exports__["a"] = (mainFunction);
/***/ }),
/* 2 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony import */ var _Users_didi_Documents_learn_webpack_4_demo_banner_demo_a_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1);
/* harmony import */ var _b_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(0);
_Users_didi_Documents_learn_webpack_4_demo_banner_demo_a_js__WEBPACK_IMPORTED_MODULE_0__[/* default */ "a"].use(_b_js__WEBPACK_IMPORTED_MODULE_1__[/* test */ "a"])
Object(_b_js__WEBPACK_IMPORTED_MODULE_1__[/* test */ "a"])()
/***/ })
-
DefinePlugin
DefinePlugin 介绍
可以使用这个插件在编译阶段对一些常量进行替换的时候,例如:
- 常用到的 js 代码中根据
process.env.NODE_ENV
的值,区分不同 dev 环境 和 production 环境。从而实现在不同环境下走不同的分支逻辑。
new DefinePlugin({ 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV) })
-
可以配置 api URL
new DefinePlugin({ API_DOMAIN: process.env.NODE_ENV === 'dev' ? '"//10.96.95.200"' : '"//api.didi.cn"' })
实现 dev 和 production 下 api 请求域名的切换。
简单介绍一些原理:一个最简单的例子
new DefinePlugin({ 'TEST': "'test'" })
代码中使用
const a = TEST
, 在 parser 的时候遍历到 = 号右边的时候,会触发表达式解析的钩子// key 是 TEST parser.hooks.expression.for(key).tap("DefinePlugin", expr => { const strCode = toCode(code, parser); // 结果为我们设置的 'test' if (/__webpack_require__/.test(strCode)) { // 如果用到了 __webpack_require__ ,生成的 ConstantDependency 中 requireWebpackRequire=true // 在后期生成代码,用 function(module, exports){} 将代码包裹起来的时候,参数里面会有 __webpack_require__,即 function(module, exports, __webpack_require__){} return ParserHelpers.toConstantDependencyWithWebpackRequire( parser, strCode )(expr); } else { // ParserHelpers.toConstantDependency 会生成一个 ConstDependency,并且添加到当前的 module 中 // ConstDependency.expression = "'test'",位置就是我们代码中 TEST 对应的位置 return ParserHelpers.toConstantDependency( parser, strCode )(expr); } });
前面说过,ConstDependency 会对源码对应内容进行替换。所以在后面代码生成阶段执行下面的操作
ConstDependency.Template = class ConstDependencyTemplate { apply(dep, source) { // 如果 range 是一个数字,则为插入;如果是一个区间,则为替换 if (typeof dep.range === "number") { source.insert(dep.range, dep.expression); return; } // 把源码中对应的地方替换成了 dep.expression,即 "test" source.replace(dep.range[0], dep.range[1] - 1, dep.expression); } };
这样便实现了,对源码中 TEST 的替换。
- 常用到的 js 代码中根据
总结
相信通过上边详细的过程分析以及对应的一些demo的实践,对于webpack是如何生成静态文件的整个过程都已经了解了。希望在未来里,你遇到类似的场景,且现有的生态插件不能满足需求的时候,是可以自己动手搞定。
我们想要深入了解一个细节的最大动力就是来自于我们的需求,在我们开源的小程序框架mpx中就有很多很多上述静态文件生成的大量应用。如果你感兴趣,欢迎大家去了解、去使用、去共建。
额外的,滴滴前端技术团队的团队号也已经上线,我们也同步了一定的招聘信息,我们也会持续增加更多职位,有兴趣的同学可以一起聊聊。
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!