I have a lot of questions about Webpack… This article will briefly take a look at how WebPack handles ESM, and try to understand the general implementation of WebPack’s ESM ass we Can
The Webpack version for this analysis is 4.41.2
Warm up
- Tapable can be used like EventEmitter (if you want to use tapable, it can be used to sync, Async, waterfall… , I guess for debugging convenience)
- Learn about the Webpack process (Webpack 3.x)
Webpack is the process it packages through “hooks” of several key classes, divided into “core stages”
- Understand that WebPack-Sources provides several types of CachedSource, PrefixSource, ConcatSource, and ReplaceSource that can be used in combination, It is easy to add, replace, and connect code, and it also has some source-Map related apis, such as updateHash, for internal invocation of WebPack
The sample
How does WebPack build bundle.js from the following example (a single entry ESM module)
src/index.js
// Introduce the hello function
import sayHello from './hello.js'
let str = sayHello('1')
console.log(str)
Copy the code
The above code has three statements
src/hello.js
export default function sayHelloFn(. args) {
return 'Hello, parameter value:' + args[0]}Copy the code
The above code has 1 statement and the webpack configuration is the most basic
module.exports = {
mode: 'development'.devtool: 'source-map'.entry: {
app: path.resolve(__dirname, './src/index.js')},output: {
filename: '[name].bundle.js'.path: path.resolve(__dirname, 'dist')}}Copy the code
After analysis, it can be roughly divided into two processes
The parse process
Parse occurs programmatically after the Compilation buildModule hook. The code is found in the doBuild callback of the NormalModule class parser. parse, which calls the Parser static method. In this method, you can see that WebPack uses the Acorn module to parse into ast
Now comes the core
if (this.hooks.program.call(ast, comments) === undefined) {
this.detectStrictMode(ast.body);
// Prewalking iterates the scope for variable declarations
this.prewalkStatements(ast.body);
// Block-Prewalking iterates the scope for block variable declarations
this.blockPrewalkStatements(ast.body);
// Walking iterates the statements and expressions and processes them
this.walkStatements(ast.body);
}
Copy the code
The main job here is to iterate through statement after statement in the ast module, do some processing when it encounters some types, and possibly call handlers for various “expressions “tap on the evaluate hook (HookMap defined in the Parser constructor). These expressions are “Literal”, “LogicalExpression”, “BinaryExpression”, “UnaryExpression”… At the same time will call plug-ins (HarmonyImportDependencyParserPlugin HarmonyExportDependencyParserPlugin…) Prewalk, blockPrewalk, and walk all parse each statement and operate on scope objects before parsing, so handling import and export here is a bit like js scope raising
this.scope = {
topLevelScope: true.inTry: false.inShorthand: false.isStrict: false.definitions: new StackedSetMap(),
renames: new StackedSetMap()
};
Copy the code
So even if I write it this way, it’s ok
let str = sayHello('1')
console.log(str)
import sayHello from './hello.js'
Copy the code
So here’s the main analysis
index.js
index ast
First statement
Second statement
The third statement
In prewalk, the first statement is preparsed, namely, statement.type is ImportDeclaration. Get source = statement.source.value, ‘./hello.js, ‘by calling the prewalkImportDeclaration method. this.hooks.import.call(statement, source)
Calling the Import hook
Parser. State. LastHarmonyImportOrder = (parser. State. LastHarmonyImportOrder | | 0) + 1 ConstDependency is added to the module relies on HarmonyImportSideEffectDependency is added to the module relies on
Traversal, specifiers, namely preliminary analytical indicator enclosing scope. Renames. Set (” sayHello “, null) enclosing scope. Definitions. The add (‘ sayHello) type, Namely specifiers. Type ImportDefaultSpecifier, can call this. Hooks. ImportSpecifier. Call (statement, source, ‘default’ name)
Call the importSpecifier hook
parser.scope.definitions.delete('sayHello')
parser.scope.renames.set('sayHello'.'imported var')
parser.state.harmonySpecifier.set('sayHello', {
source: './hello.js'.id: 'default'./ / 1
sourceOrder: parser.state.lastHarmonyImportOrder
})
Copy the code
The second statement of the blockPrewalk procedure is the pre-parsed-statement type, i.e. Statement. type is VariableDeclaration. Will call blockPrewalkVariableDeclaration method traversal. Declarations, here is a case, according to the declarator enclosing scope. Renames. Set (” STR “, null); this.scope.definitions.add(‘str’)
Call callHook = this.imports.call. get(‘imported var’); callHook.call(expression)
Calling the Call hook
Parser. State. HarmonySpecifier. Get (” sayHello “), its set in importSpecifier hook, As a parameter to HarmonyImportSpecifierDependency HarmonyImportSpecifierDependency is added to the module to rely on
hello.js
hello.js ast
A statement
Prewalk process type, in the process of the parse the statement function declarations that statement. The declaration, the type of FunctionDeclartion, This. Will call hook hooks. ExportSpecifier. Call (the statement, ‘sayHelloFn’, ‘default’)
Call the exportSpecifier hook
HarmonyExportSpecifierDependency is added to the module dependencies
Walk process parse the statement types, namely the statement. The type of ExportDefaultDeclaration, calls the hook enclosing hooks. Export. Call (statement)
Call the export hook
HarmonyExportHeaderDependency is added to the module dependencies
Declarator.type = functiontion This will call hook. Hooks. ExportDeclaration. Call (the statement, the statement. The declaration)
Call the exportDeclaration hook
The empty hook function is not currently processed
After these hook calls, we add scope and module dependencies to the property Dependencies list of the current module object, which will be dealt with later. The generate procedure calls the template class corresponding to the Dependecy class in Dependencies
The generate process
Genernate occurs after the beforeChunkAssets hook of the Compilation process before the chunkAsset hook, In MainTemplate’s renderManifest hook and modules hook, hello.js should be handled first. In buildChunkGraph, hello.js is the module that index.js depends on. We’ll talk about that later
Next the paper template calls (note: oh, found a “bug”, HarmonyCompatibilityDependency corresponding template class called HarmonyExportDependencyTemplate, HarmonyExportHeaderDependency corresponding template class also called HarmonyExportDependencyTemplate, what is the international seek bug engineer? Tactical backward)
hello.js
/ / call runtimeTemplate defineEsModuleFlagStatement which parameters exportsArgument as "__webpack_exports__"
/ / get the content = "__webpack_require__. R (__webpack_exports__)"
const content = runtime.defineEsModuleFlagStatement({
exportsArgument: dep.originModule.exportsArgument
});
source.insert(- 10, content) // -10 indicates the priority. The smaller the priority is, the higher the priority is
Copy the code
(2) HarmonyInitDependency it corresponds to the Template class HarmonyInitDependencyTemplate calls to apply for the module. The dependencies traversal, GetHarmonyInitOrder = getHarmonyInitOrder = getHarmonyInitOrder = getHarmonyInitOrder = getHarmonyInitOrder Call the Template harmonyInit method in turn
(2.1) HarmonyExportSpecifierDependency it corresponds to the Template class invokes HarmonyExportSpecifierDependencyTemplate getHarmonyInitOrder Direct return 0 returned to invoke harmonyInit HarmonyInitDependencyTemplate
const content = `/* harmony export (binding) */ webpack_require.d({exportsName}, {JSON.stringify(
used
)}, function() { return ${dep.id}; }); \n`
source.insert(- 1, content); // -1 indicates the priority. The smaller the priority is, the higher the priority is
Copy the code
(3) HarmonyExportHeaderDependency it corresponds to the Template class for HarmonyExportDependencyTemplate call the apply (x)
source.replace(dep.rangeStatement[0], replaceUntil, content);
Copy the code
The effect is to replace “export default” with an empty string
Processed code
__webpack_require__.r(__webpack_exports__);
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "default".function() { return sayHelloFn; });
function sayHelloFn(. args) {
return 'Hello, parameter value:' + args[0]}Copy the code
Line 5 of index.js (Dependency – means none) is not in module Dependency, only shown here
hello.js
(2) HarmonyInitDependency it corresponds to the Template class call the apply for HarmonyInitDependencyTemplate with hello. Js (2) call getHarmonyInitOrder Template, HarmonyInit method
(2.1) HarmonyImportSideEffectDependency inherited from HarmonyImportDependency, It corresponds to the Template class HarmonyImportSideEffectDependencyTemplate it inherited from getHarmonyInitOrder HarmonyImportDependencyTemplate calls Return dep. SourceOrder, this property is to add module based on new HarmonyImportSideEffectDependency incoming, its value for the parser. State. LastHarmonyImportOrder, Here is 1 to call getHarmonyInit
let sourceInfo = importEmittedMap.get(source);
if(! sourceInfo) { importEmittedMap.set( source, (sourceInfo = {emittedImports: new Map()})); }const key = dep._module || dep.request;
if (key && sourceInfo.emittedImports.get(key)) return;
sourceInfo.emittedImports.set(key, true);
/ / dep for HarmonyImportSideEffectDependency instance, getImportStatement method is defined in HarmonyImportSideEffectDependency the superclass
const content = dep.getImportStatement(false, runtime);
source.insert(- 1, content);
Copy the code
The content is /* harmony import */ var _hello_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./hello.js */ “./analysis/harmonymodule-analysis/hello.js”);
(2.2) HarmonyImportSpecifierDependency inherited from HarmonyImportDependency, It corresponds to the Template class HarmonyImportSideEffectDependencyTemplate it inherited from HarmonyImportDependencyTemplate call getHarmonyInitOrder with (2.1) Call getHarmonyInit
/ / due to (2.1)
// This condition is true and no processing is performed
if (key && sourceInfo.emittedImports.get(key)) return;
Copy the code
(3) ConstDependency: the Template class is ConstDependencyTemplate and calls Apply
// dep is an instance of ConstDependency
source.replace(dep.range[0], dep.range[1] - 1, dep.expression);
Copy the code
It replaces positions 13 to 46(import sayHello from ‘./hello.js’) with an empty string
(4) HarmonyImportSpecifierDependency inherited from HarmonyImportDependency, It corresponds to the Template class HarmonyImportSideEffectDependencyTemplate it inherited from HarmonyImportDependencyTemplate
HarmonyImportSpecifierDependency (2.2) is not processed? Didn’t see it wrong template class inherits from HarmonyImportDependencyTemplate its apply method is empty function, getHarmonyInitOrder, getHarmonyInit are defined, So here is the apply of HarmonyImportSpecifierDependencyTemplate apply call method calls the apply
/ / dep is HarmonyImportSpecifierDependency instance
const content = this.getContent(dep, runtime);
source.replace(dep.range[0], dep.range[1] - 1, content)
Copy the code
It replaces positions 58 to 65(sayHello) with Object(_hello_js__WEBPACK_IMPORTED_MODULE_0__[“default”])
Processed code
__webpack_require__.r(__webpack_exports__);
/* harmony import */ var _hello_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/ *! ./hello.js */ "./analysis/harmonymodule-analysis/hello.js");
// Introduce the hello function
let str = Object(_hello_js__WEBPACK_IMPORTED_MODULE_0__["default"]) ('1')
console.log(str)
Copy the code
Finally, they are spliced together with the bootstrap code into the entire bundle in the Render hook of the MainTemplate
A question
Didn’t it say there were a lot of questions? What questions do you have when you see this? come in… No? All right, let me ask you a question
Q: WebPack will parse ESM, and Babel-Loader will let Babel convert ES6 to ES5, so don’t they “conflict” when converting ESM? Webpack adds configuration
module: {
rules: [{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: [
'@babel/preset-env'
]
}
}
}]
}
Copy the code
{caller: object. assign({name: “babel-loader”, supportsStaticESM: true, supportsDynamicImport: True}, opts.caller)} This is the caller metadata feature provided by babel7, so that @babel/core is passed to presets/plugins. Here @babel/preset-env doesn’t use @babel/transform-modules-commonjs to transform the import export code, and the parse is after runLoader.
conclusion
Contact our daily work, like a project module, analyze the requirements (parse) to determine the Dependency needs, such as design, front end and back end, each with its own responsibilities to generate the project. Some projects might only need the front end and the back end, and some of the front end might even help the back end, and the back end might say, you help me, you lose your job, Backend quickly to finish the task in hand (HarmonyImportSideEffectDependency and HarmonyImportSpecifierDependency), and of course project (hello. Js) and project (index. Js) is a dependent.
Dig a hole for the rest and explore it later.