作者:崔静
本文需求你对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
中开端。
主流程图如下
**阐明1:**在 JavascriptModulePlugin中会确认 render 函数。这个 render 函数后续在 createChunkAssets 中会调用。
**阐明2:**这儿 moduleTemplate 在 Compilation 一开端初始化会生成
this.moduleTemplates = { javascript: new ModuleTemplate(this.runtimeTemplate, "javascript"), webassembly: new ModuleTemplate(this.runtimeTemplate, "webassembly") };
由于走到是 mainTemplate,在最开端获取 render 各种信息的函数中
renderManifest
为触发JavascriptModulesPlugin
中注册的函数,而这个里边确认了 module 所运用的模板为moduleTemplates.javascript
compilation.mainTemplate.hooks.renderManifest.tap( "JavascriptModulesPlugin", (result, options) => { //... result.push({ render: () => compilation.mainTemplate.render( hash, chunk, moduleTemplates.javascript, dependencyTemplates ), //... }); return result; } );
阐明3: module-source 的进程见最终附加内容
首要确认当时结构是运用 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 就会参加
HarmonyCompatibilityDependency
HarmonyInitDependency
ConstDependency
HarmonyImportSideEffectDependency
HarmonyImportSpecifierDependency
这些 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"])
假如是被调用的话,会走一个逻辑??
if (isCall) { if (callContext === false && asiSafe) { return `(0,${access})`; } else if (callContext === false) { return `Object(${access})`; } }
-
然后替换代码中 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中就有许多许多上述静态文件生成的大量应用。假如你感兴趣,欢迎大家去了解、去运用、去共建。
额外的,滴滴前端技能团队的团队号也现已上线,咱们也同步了一定的招聘信息,咱们也会持续添加更多职位,有兴趣的同学能够一同聊聊。