Author: Cui Jing
This article requires you to have a certain understanding of Webpack, if you are more interested, you can refer to our previous Webpack source code parsing series: Webpack series – overview.
Some concepts
The Compilation initializes the following variables:
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 // Not yet
Copy the code
MainTemplate: Generates code to execute the main process, including the webpack startup code and so on. ChunkTemplate: The resulting code is loaded via JsonP.
Here’s an example: We have an entry file:
// main.js
import { Vue } from 'vue'
new Vue(...)
Copy the code
Such files are packaged to produce an app.js, a chunk-vendor.js.
The structure of app.js is as follows:
(function(modules) { // webpackBootstrap
// Start function for webpack
// Webpack built-in methods{{})moduleId: (function(module.exports, __webpack_require__) {
// We write js code in each module
},
// ...
})
Copy the code
Chunk-vendors. Js is structured as follows:
(window["webpackJsonp"] = window["webpackJsonp"] || []).push([["chunk-vendors"] and {moduleId: (function(module.exports, __webpack_require__) {
// ...
},
// ...
})
Copy the code
App.js contains the webpack bootstrap code, which is the overall framework of the mainTemplate.
App.js loads chunk-vendor.js via jonsP, the framework of the JS code is placed in the chunkTemplate.
The code generation process for each module in app.js and Chunk-vendors. Js is in ModuleTempalte.
Code generation main process
Chunk code is generated in seal phase. Start from the Compilation. CreateChunkAssets.
The main flow diagram is as follows
** Note 1: ** The Render function is identified in the JavascriptModulePlugin. This render function is later called in createChunkAssets.
The moduleTemplate is generated during initialization during Compilation
this.moduleTemplates = { javascript: new ModuleTemplate(this.runtimeTemplate, "javascript"), webassembly: new ModuleTemplate(this.runtimeTemplate, "webassembly")};Copy the code
RenderManifest is the function registered in JavascriptModulesPlugin because it is mainTemplate. And this determines the template used by the module inside for moduleTemplates. Javascript
compilation.mainTemplate.hooks.renderManifest.tap( "JavascriptModulesPlugin".(result, options) = > { / /... result.push({ render: () = > compilation.mainTemplate.render( hash, chunk, moduleTemplates.javascript, dependencyTemplates ), / /... }); returnresult; });Copy the code
Note 3: The module-source process is appended at the end
First determine whether the current structure uses mainTemplate or chunkTemplate. The two Tempaltes will have their own render flow. Let’s take mainTempalte as an example to see the process of Render.
The main structure code is generated in the Render main flow, which is the framework part of the code generated in our app.js demo. The code for each moulde is then generated. The process is accomplished by a function in the ModuleTemplate.
When module is generated, hook. Content, hook. Module, hook. Render, hook. Package are called. After each hook gets the result, it is passed to the next hook. Hook. Module After the hook is executed, the code for the Module is returned. Then in hook. Render, wrap this code into a function. If we had configured output.pathinfo=true (configuration description) in webpack.config.js, we would have added some path and tree-shaking related comments to the resulting code in hook.package. So we can read the code.
Once you have all the Module code, wrap it in arrays or objects.
Modify the code
- Add additional content to one of the modules using the hooks generated by the above files
BannerPlugin is the addition of additional content at the beginning of the chunk file. What if we just want to add content to a module? Review the above code generation, flow chart, module code generation there are several key hook, such as hook. The content, hook. The module, the hook. The render. You can register functions in these hooks to make changes. A simple demo is shown below
const { ConcatSource } = require("webpack-sources");
class AddExternalPlugin {
constructor(options) {
// Plugin is initialized. This handles some parameter formatting and so on
this.content = options.content // Get the content to add
}
apply(compiler) {
const content = this.content
compiler.hooks.compilation.tap('AddExternal'.compilation= > {
compilation.moduleTemplates.javascript.hooks.render.tap('AddExternal'.(
moduleSource,
module ) = > {
// The module argument is passed, which we can configure to execute the following logic in a module
// ConcatSource means that the code we add to it will be concatenated at the end of the processing.
const source = new ConcatSource()
// Insert what we want to add at the beginning
source.add(content)
// Insert source code
source.add(moduleSource)
// return the new source code
return source
})
})
}
}
Copy the code
- Wrap an extra layer of logic around the chunk execution code.
We have configured the umD mode, or the output.library parameter. After configuring these two things, the resulting code structure is different from the original app.js demo. Library =’someLibName’, for example, would look like this
var someLibName =
(function(modules){
// webpackBootstrap}) ([/ /... Each module
])
Copy the code
The implementation of this is to modify the code generated by mainTemplate in the hooks. RenderWithEntry section above.
If we want to wrap up some of our own logic in some cases. We can do it right here. I’ll give you a simple 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
)
// If we support configuration of some variables, then we need to write our configuration information to the hash. Otherwise, the hash value will not change when you modify the configuration.
// mainTemplate.hooks.hash.tap("SetVarMainTemplatePlugin", hash => {
// hash.update()
// });}}})module.exports = MyWrapPlugin
Copy the code
Webpack compiled results
var myLib =/ * * * * * * / (function(modules) {
/ /... Webpack bootstrap code
/ * * * * * * / return __webpack_require__(__webpack_require__.s = 0);
/ * * * * * * / })
/ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * /
/ * * * * * * / ([
/* 0 */
/ * * * / (function(module.exports) {
// ...
/ * * * / })
/ * * * * * * / ])
console.log(myLib);
Copy the code
- BannerPlugin
Similar to the built-in BannerPlugin. After the above chunk file is generated, that is, after the createChunkAssets execution is completed, the entire chunk file contents are modified. For example, bannerPlugin is in optimizaChunkAssets hook
In this hook you can get chunks of a parameter and then add additional content there.
The contents of chunkAssets are modified
After createChunkAssets is executed, the contents of the file can be retrieved from other hooks for modification.
-
AfterOptimizeChunkAssets hook, webpack generates sourcemap. If you make code changes after this, such as optimizeAssets or later emit hooks, you will find that sourcemap is incorrect. Like the following example
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')})})Copy the code
-
Impact on hash. When the chunk generation is complete, the hash will be generated. Changes made to the code in the hook after the hash is generated, such as additions, do not affect the result of the hash. Take the example above of modifying chunk code. If our plugin is updated, the changes change, but the generated hash does not change with it. Therefore, the content of the plugin needs to be written into the hash in the hash-generating hooks.
The module – source generated
Each dependency generated in the Parser phase is handled in the module-source process, and the source code is converted from dependency.Template. Here we look at module-source in conjunction with the original parser. Take the following demo as an example:
// main.js
import { test } from './b.js'
function some() {
test()
}
some()
// b.js
export function test() {
console.log('b2')}Copy the code
AST converted from main.js parser:
Parser on the AST
if (this.hooks.program.call(ast, comments) === undefined) {
this.detectMode(ast.body);
this.prewalkStatements(ast.body);
this.blockPrewalkStatements(ast.body);
this.walkStatements(ast.body);
}
Copy the code
-
program
Testing have used the import/export, would increase HarmonyCompatibilityDependency HarmonyInitDependency (described later)
-
detectMode
Check if there is use Strict and use ASM at the beginning, to make sure that the use strict written at the beginning of our code is still at the beginning
-
prewalkStatements
Iterates through all variable definitions in the current scope. Import {test} from ‘./b.js’ is also in the current scope, so import is handled here (see javasjavascript -parser for details). For the import will be adding additional ConstDependency and HarmonyImportSideEffectDependency
-
blockPrewalk
Handles let/const (var only in prewalk), class name, export, and export default in the current scope
-
walkStatements
Start digging into each node for processing. Here will find all using the test code, and then add HarmonyImportSpecifierDependency
After the manager goes through this, he will join the demo above
HarmonyCompatibilityDependency
HarmonyInitDependency
ConstDependency
HarmonyImportSideEffectDependency
HarmonyImportSpecifierDependency
These fall into two categories:
-
ModuleDependency: there is a corresponding denpendencyFactory, in the process of processModuleDependencies will to deal with the dependency, get the corresponding module
HarmonyImportSideEffectDependency --> NormalModuleFactory
HarmonyImportSpecifierDependency --> NormalModuleFactory
Both refer to the same module(./b.js), so they will be de-duplicated. Then Webpack along the dependency, handles b.js… Until all moduleDependency is handled
-
Used to generate code only when files are generated
module.source
Get the source code first, then work on each dependency
-
HarmonyCompatibilityDependency
Insert __webpack_require__.r(__webpack_exports__) at the beginning; Identifies this as an esModule
-
HarmonyInitDependency
Iterating through all dependencies, responsible for generating code to introduce the ‘./b.js’ module for import {test} from ‘./b.js’
/* harmony import */ var _b_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(0);
-
ConstDependency
In the HarmonyInitDependency phase, the import statement was inserted, so the import {test} from ‘./b.js’ needs to be removed from the source code. ConstDependency replaces this with empty, delete
-
HarmonyImportSideEffectDependency
The action phase is in HarmonyInitDependency
-
HarmonyImportSpecifierDependency
Dependencies generated by test() in the code. It replaces test in the code
-
Gets the variable name _b_js__WEBPACK_IMPORTED_MODULE_0__ corresponding to the ‘./b.js’ module
-
Get the name of the property that test corresponds to in B.js. (Because of webpack compilation, to simplify the code, export test in B.js may be changed to export a = test.)
Object(_b_js__WEBPACK_IMPORTED_MODULE_0__[/* test */ "a"])
If it is called, it will follow a logic.
if (isCall) { if (callContext === false && asiSafe) { return `(0,${access})`; } else if (callContext === false) { return `Object(${access})`; }}Copy the code
-
Then replace test in the code
-
After all the dependency:
Once we know this process, if we need to make some simple changes to the source code, we can use the various hooks of the Parser stage to do so. One advantage of making changes here is that you don’t have to worry about corrupting sourcemap and affecting hash generation.
- A demo of the code inserted into the Parser
For example, when we use a plug-in, we need to write this
import MainFunction from './a.js'
import { test } from './b.js'
MainFunction.use(test)
Copy the code
In practice, the Webpack plug-in is used to automatically insert the test when it detects an introduction
import MainFunction from './a.js'
MainFunction.use(test)
Copy the code
The key that the above mentioned HarmonyImportSideEffectDependency, HarmonyImportSpecifierDependency and ConstDependency
The following code
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");
// The path to introduce a.js. This path is followed by Webpack's resolve
const externalJSPath = `${path.join(__dirname, './a.js')}`
class ProvidePlugin {
constructor(){}apply(compiler) {
compiler.hooks.compilation.tap(
"InjectPlugin".(compilation, { normalModuleFactory }) = > {
const handler = (parser, parserOptions) = > {
// When parser processes import statements
parser.hooks.import.tap('InjectPlugin'.(statement, source) = > {
parser.state.lastHarmonyImportOrder = (parser.state.lastHarmonyImportOrder || 0) + 1;
// Create a dependency of './a.js'
const sideEffectDep = new HarmonyImportSideEffectDependency(
externalJSPath,
parser.state.module,
parser.state.lastHarmonyImportOrder,
parser.state.harmonyParserScope
);
// Set a location for dependency. This is set to the same location as import {test} from './b.js', where the modification will be inserted when the code inserts.
sideEffectDep.loc = {
start: statement.start,
end: statement.end
}
// Set renames to indicate that mainFunction is imported from outside
parser.scope.renames.set('mainFunction'."imported var");
// Add this dependency to module dependencies
parser.state.module.addDependency(sideEffectDep);
/ / -- -- -- -- -- -- -- -- -- -- -- -- - deal with insert 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
})
// For mainFunction in mainfunction. use
const mainFunction = new HarmonyImportSpecifierDependency(
externalJSPath,
parser.state.module,
-1,
parser.state.harmonyParserScope,
'default'.'mainFunction'[-1, -1].// Insert it at the beginning of the code
false
)
parser.state.module.addDependency(mainFunction)
// Insert the code snippet. Use (
const constDep1 = new ConstDependency(
'.use(',
-1.true
)
parser.state.module.addDependency(constDep1)
// Insert code snippet test
const useArgument = new HarmonyImportSpecifierDependency(
source,
parser.state.module,
-1,
parser.state.harmonyParserScope,
'test'.'test'[-1, -1].false
)
parser.state.module.addDependency(useArgument)
// Insert code snippet)
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;
Copy the code
The generated code is as follows
/ * 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") ()/ * * * / })
Copy the code
-
DefinePlugin
DefinePlugin introduction
You can use this plugin to replace constants at compile time, for example:
- Commonly used in js code according to
process.env.NODE_ENV
To distinguish between dev and Production environments. So as to realize different branch logic in different environment.
new DefinePlugin({ 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV) }) Copy the code
-
You can configure the API URL
new DefinePlugin({ API_DOMAIN: process.env.NODE_ENV === 'dev' ? '" / / 10.96.95.200 "' : '"//api.didi.cn"' }) Copy the code
Implementation of dev and Production API request domain name switch.
A brief introduction to some principles: a simple example
new DefinePlugin({ 'TEST': "'test'" }) Copy the code
The const a = TEST is used in the code. Parser iterates to the right of the = sign, triggering the expression parsing hook
/ / the key is the TEST parser.hooks.expression.for(key).tap("DefinePlugin".expr= > { const strCode = toCode(code, parser); // Result is set to 'test' if (/__webpack_require__/.test(strCode)) { // If __webpack_require__ is used, the generated ConstantDependency requireWebpackRequire=true Function (module, exports){} __webpack_require__, function(module, exports){} As a function module, exports, __webpack_require__) {} return ParserHelpers.toConstantDependencyWithWebpackRequire( parser, strCode )(expr); } else { / / ParserHelpers toConstantDependency generates a ConstDependency, and added to the current module / / ConstDependency expression = "" test", "where is the position that corresponds to the test in our code returnParserHelpers.toConstantDependency( parser, strCode )(expr); }});Copy the code
As mentioned earlier, ConstDependency replaces the source code counterpart. So do the following in the later code generation phase
ConstDependency.Template = class ConstDependencyTemplate { apply(dep, source) { // if range is a number, insert; If it is an interval, it is a replacement 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); }};Copy the code
So this is the replacement of TEST in the source code.
- Commonly used in js code according to
conclusion
I believe that through the above detailed process analysis and some corresponding demo practice, for Webpack is how to generate static files in the whole process have been understood. Hopefully, in the future, you’ll be able to do it yourself if you encounter a similar scenario where the existing eco-plugin doesn’t meet your needs.
One of the biggest motivations for us to dig into this detail was our need to use many of these static file generation applications in our open source applets framework MPX. If you are interested, you are welcome to know, to use, to build.
In addition, the team number of Didi front-end technology team has also been online, and we have synchronized certain recruitment information. We will continue to add more positions, so interested students can chat with us.