“This is the first day of my participation in the First Challenge 2022. For details: First Challenge 2022”
preface
Why should we learn about the WebPack execution process?
First: Webpack is often used and necessary for development. It contains many interesting functions, such as hot update, start server, Babel parsing and so on. It is simply a contractor, making our development simple and efficient
Second: from the perspective of architecture, we can learn the implementation process, further in-depth source code, to see the framework design of Webpack, learn some advantages
Learning is not aimless, not after reading the article feel that they have mastered him, to ask more why? , more hands-on, more interactive
Github address please refer to github.com/Y-wson/Dail…
What can we learn from this passage?
-
- Loader and plugin principle, and simple loader and plugin writing
-
- Simple use of Tapable
-
- How are Babel and AST resolved
-
- Webpack executes the process
-
- How are files packaged by WebPack executed
Execute the process
Let’s talk about the execution flow of Webpack in words first
- Initialization parameters: from the configuration file and
Shell
Statement read and merge parameters, get the final parameter; - Start compiling: Initialize with the parameters obtained in the previous step
Compiler
Class to load all configured plug-ins that execute objectsrun
Method starts compiling; Determine the entry: according to the configurationentry
Find all the entry files - Compile module: from the entry file, call all configured
Loader
Compile the module, then find the module that the module depends on, and then recurse this step until all the entry dependent files have been processed by this step; - Complete module compilation: use after step 3
Loader
After all modules are translated, the final content of each module is translated and the dependencies between them are obtained - Output resources: Assembled into modules based on the dependencies between entry and module
Chunk
And then put eachChunk
Add it to the output list as a separate file. This step is the last chance to modify the output - Output complete: After determining the output content, determine the output path and file name based on the configuration, and write the file content to the file system
Code implementation
Next, we will explain it point by point according to the above implementation process
Before we write the code, let’s talk about the library that the project will use
@babel/core Babel/presets -env is used to convert ES6 code to ES5 code EJS template file, support JAVASCRIPT tapable in HTML, and preset to release and subscribe mode. Webpack implements the core of wiring plug-ins togetherCopy the code
Yarn Add install it
The directory structure is as follows
So let’s just write webpack.config.js, webpack.config.js, we all know what it does, is the webpack configuration item
// webpack.config.js
const path = require("path");
module.exports = {
context: process.cwd(), // The current root directory
mode: "development".// Work mode
entry: path.join(__dirname, "src/index.js"), // Import file
output: { // Export file
filename: "bundle.js".path: path.join(__dirname, "./dist"),},module: {// To load the module conversion loader
rules: [{test: /\.js$/,
use: [
{
loader: path.join(__dirname, "./loaders/babel-loader.js"),
options: {
presets: ["@babel/preset-env"],},},],},plugins: [new RunPlugin(), new DonePlugin()], / / the plugin
};
Copy the code
Step 1: Initialize parameters: from the configuration file andShell
Statement read and merge parameters, get the final parameter;
This step is simple to implement
// lib/Compiler.js
class Compiler {}let options = require(".. /webpack.config");
Copy the code
Step 2: Start compiling: initialize with the parameters obtained in the previous stepCompiler
Class,
Here we write a Compiler class that stands for Compiler and initializes options as configured in webpack.config.js
//lib/Compiler
class Compiler {
constructor(options){
this.options = options; }}let options = require(".. /webpack.config");
Copy the code
Load all configured plugins, and then execute the corresponding parameters at the corresponding stage. For example, at the start of compilation, we execute the runPlugin, and output the start of compilation textrun
Method starts compiling;
Plugins are classes. That’s why plugins are classes in webpack.config.js, and they need to be new because plugins are classes
plugins: [new RunPlugin(), new DonePlugin()], / / the plugin
Copy the code
We have loaded two classes, new RunPlugin, to start compiling and new DonePlugin to finish executing
Each plug-in must have an apply method that registers the plug-in, and then pass an instance of the Compiler to the plug-in. The plug-in can then listen for the RUN hook
Does this sound familiar? Yes, this is the publishing subscriber model. How does the publishing subscriber model work in Webpack?
// plugins/RunPlugin
module.exports = class RunPlugin {
// Register the plug-in
apply(compiler) {
compiler.hooks.run.tap("RunPlugin".() = > {
console.log("RunPlugin"); }); }};Copy the code
We’re going to talk about a library, Tapable, which is very important, because tapable is the reason webPack is able to string plug-ins together,
Here is a simple way to use the following, small partners if very interested, recommended to see juejin.cn/post/693979… Miss Jiang’s article, more usage methods of tapable
let { SyncHook } = require("tapable");
let hook = new SyncHook();
/ / to monitor
hook.tap("some name".() = > {
console.log("some name");
});
/ / triggers
hook.call();
Copy the code
Determine the entry: according to the configurationentry
Find all the entry files
Remember that in React, hooks are defined from the hooks that start with run and end with done. Remember that in React, plugins are defined from the hooks that start with run and end with done. As long as WebPack executes the corresponding lifecycle function, the code block in the lifecycle function in the plug-in executes
// Use synchronous hooks
let { SyncHook } = require("tapable");
class Compiler {
constructor(options) {
this.options = options;
// Define two hooks
this.hooks = {
run: new SyncHook(),
done: new SyncHook(),
};
}
run() {
this.hooks.run.call(); // Triggers the run hook to execute
let entry = path.join(this.options.context, this.options.entry); }}let options = require(".. /webpack.config");
let compiler = new Compiler(options);
if (options.plugins && Array.isArray(options.plugins)) {
for (const plugin of options.plugins) {
// Invoke the plug-in's registration methodplugin.apply(compiler); }}Copy the code
We can put the new Compiler section in a separate folder, webpack.js
// bin/webpack.js
const Compiler = require(".. /lib/Compiler");
// 1. Obtain the package configuration
const config = require(".. /webpack.config");
// 2. Create a Compiler instance
const createCompiler = function () {
// Create the Compiler instance
const compiler = new Compiler(config);
// Load the plug-in
if (Array.isArray(config.plugins)) {
for (const plugin ofconfig.plugins) { plugin.apply(compiler); }}return compiler;
};
const compiler = createCompiler();
// 3. Enable compilation
compiler.run();
Copy the code
Package. json configures build to point to webpack.js
"scripts": {
"build": "node bin/webpack.js"
},
Copy the code
From the entry file, invoke all configuredLoader
To compile the module,
run(){
this.hooks.run.call(); // Triggers the run hook to execute
let entry = path.join(this.options.context, this.options.entry);
// Building blocks
this.buildModule(entry, true);
}
Copy the code
We know that Webpack has module, chunk and file concepts
file>chunk>module
For time reasons, we will only demonstrate this through Module
In the constructor we initialize modules to hold the module
constructor(options) {
this.options = options;
this.modules = [];
this.hooks = {
run: new SyncHook(),
done: new SyncHook(),
};
}
Copy the code
Build the module and perform breadth-first traversal of all dependent submodules
Read the source code
buildModule(modulePath, isEntry) {
// Module source code
const source = this.getSource(modulePath);
}
Copy the code
Module source code we need to compile by loader
So write a loader and configure it in webpack.config.js
module: {
rules: [{test: /\.js$/,
use: [
{
loader: path.join(__dirname, "./loaders/babel-loader.js"),
options: {
presets: ["@babel/preset-env"],},},],},Copy the code
Babel-loader. js is a very representative loader, so we’ll write this loader
Select presets from babel-loader to load presets from babel-loader
loader/babel-loader.js
const babel = require("@babel/core");
const loader = function (source, options) {
let result = babel.transform(source, {
presets: options.presets,
});
return result.code;
};
module.exports = loader;
Copy the code
Then our getSource will be written, returning the compiled source code
getSource(modulePath) {
// Read the contents of the file
let content = fs.readFileSync(modulePath, "utf-8");
const rules = this.options.module.rules;
for (let rule of rules) {
const { test, use } = rule;
if (test.test(modulePath)) {
// Recurse all loaders
// use is treated as an array, executed from right to left
let length = use.length - 1;
function loopLoader() {
// Execute from right to left
const { loader, options } = use[length--];
let loaderFunc = require(loader);
// loader is a function
content = loaderFunc(content, options);
if (length >= 0) { loopLoader(); }}if (length >= 0) { loopLoader(); }}}return content;
}
Copy the code
Find the module that the module depends on, and then recurse this step until all the entry dependent files have been processed by this step;
How do we find out the module dependent module, we will use the ast (abstract syntax tree), can depend upon with convenient access to the module, this is the first point we have to do, and one more thing to do is we load module inside the path is a relative path, so we also want to hand in the path, how to get the relative path parsing module
// Build the module and do breadth-first traversal of all dependent submodules
buildModule(modulePath, isEntry) {
// Module source code
const source = this.getSource(modulePath);
// Replace is compatible with Windows
modulePath =
". /" + path.relative(this.root, modulePath).replace(/\\/g."/");
const { sourceCode, dependencies } = this.parse(source, modulePath);
// Here is a complete module, save to modules
this.modules[modulePath] = JSON.stringify(sourceCode);
// Get all module dependencies recursively and save all paths and dependent modules
dependencies.forEach((d) = > {
this.buildModule(path.join(this.root, d));
}, false);
}
Copy the code
The corresponding parsing code is
// Parse according to module source code
parse(source, moduleName) {
let dependencies = [];
const dirname = path.dirname(moduleName);
const requirePlugin = {
visitor: {
// replace require with __webpack_require__
CallExpression(p) {
const node = p.node;
if (node.callee.name === "require") {
node.callee.name = "__webpack_require__";
// Path replacement
let modulePath = node.arguments[0].value;
modulePath =
". /" + path.join(dirname, modulePath).replace(/\\/g."/"); node.arguments = [t.stringLiteral(modulePath)]; dependencies.push(modulePath); ,}}}};let result = babel.transform(source, {
plugins: [requirePlugin],
});
return {
sourceCode: result.code,
dependencies,
};
}
Copy the code
Output resources: Assembled into modules based on the dependencies between entry and moduleChunk
And then put eachChunk
Add it to the output list as a separate file. This step is the last chance to modify the output
Because of time constraints, we are not going to put together chunks files.
Now that we’ve done 90% of our work, we’re ready to package up the files, refine our run method,
Output complete: After determining the output content, determine the output path and file name based on the configuration, and write the file content to the file system
run() {
this.hooks.run.call();
const entry = this.options.entry;
this.buildModule(entry, true);
const outputPath = path.resolve(this.root, this.options.output.path);
const filePath = path.resolve(outputPath, this.options.output.filename);
// Output file
this.mkdirp(outputPath, filePath);
}
Copy the code
Let me write mkdirp
mkdirp(outputPath, filePath) {
console.log("Simple-webpack ------------------> File output");
const { modules, entryPath } = this;
// Create a folder
if(! fs.existsSync(outputPath)) { fs.mkdirSync(outputPath); } ejs .renderFile(path.join(__dirname,"Template.ejs"), { modules, entryPath })
.then((code) = > {
fs.writeFileSync(filePath, code);
console.log("Simple-webpack ------------------> Package complete");
});
}
Copy the code
Ejs templates for
(function (modules) {
var installedModules = {};
function __webpack_require__(moduleId) {
if (installedModules[moduleId]) {
return installedModules[moduleId].exports;
}
var module = installedModules[moduleId] = {
i: moduleId,
l: false.exports: {}}; modules[moduleId].call(module.exports, module.module.exports, __webpack_require__);
module.l = true;
return module.exports;
}
return __webpack_require__("<%-entryPath%>"); < % ({})for (const key in modules) {%>
"<%-key%>":
(function (module.exports, __webpack_require__) {
eval(<%-modules[key]%>); }}), < % % >});Copy the code
Ok, finally done, execute the package command, NPM run build
This is not similar to the require source code we wrote before, about the require source code can refer to the article I wrote before the require source code (small white level tutorial), from here we can see that Webpack implements its own modular loading mode, To solve the problem of communication between modules
(function (modules) {
var installedModules = {};
function __webpack_require__(moduleId) {
if (installedModules[moduleId]) {
return installedModules[moduleId].exports;
}
var module = (installedModules[moduleId] = {
i: moduleId,
l: false.exports: {},}); modules[moduleId].call(module.exports,
module.module.exports,
__webpack_require__
);
module.l = true;
return module.exports;
}
return __webpack_require__("./src/index.js"); ({})"./src/index.js": function (module.exports, __webpack_require__) {
eval(
'"use strict"; \n\nvar _app = __webpack_require__("./src/app.js"); \n\nconsole.log(_app.a); '
);
},
"./src/app.js": function (module.exports, __webpack_require__) {
eval(
'"use strict"; \n\nObject.defineProperty(exports, "__esModule", {\n value: true\n}); \nexports.a = void 0; \nvar a = "app"; \nexports.a = a; '); }});Copy the code
Then we reference the bundle.js file we packaged with index.html to see if the index.html file fails
<! DOCTYPEhtml>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="Width = device - width, initial - scale = 1.0" />
<title>Document</title>
</head>
<body></body>
<script src="./dist/bundle.js"></script>
</html>
Copy the code
When we open the browser, we see exactly what we want, and we’re done
If you have any questions, please leave a message in the comments section.
conclusion
According to the beginning of the Webpack implementation process, we handwritten a Webpack source code, through handwritten source code, we deepened the understanding of the implementation process of Webpack, you friends must be more hands-on writing
Reference:
Write a webpack by hand and see how the AST works
simple-webpack