Compile product analysis
(() => { // Module dependency var __webpack_modules__ = ({ "./src/index.js": ((module, __unused_webpack_exports, __webpack_require__) => { // Execute the module code, where it is executed at the same time__ webpack_require__ Reference code eval(`const str = __webpack_require__("./src/a.js"); console.log(str);`); }), "./src/a.js": ((module, __unused_webpack_exports, __webpack_require__) => { eval(`const b = __webpack_require__("./src/base/b.js"); module.exports = 'a' + b;`); }), "./src/base/b.js": ((module, __unused_webpack_exports, __webpack_require__) => { eval(`module.exports = 'b';`); }), }); var __webpack_module_cache__ = {}; function __webpack_require__(moduleId) { // Get_ webpack_module_cache__ Is there an exports value var cachedModule = __webpack_module_cache__[moduleId]; // If you already have it, you don't have to execute the module code if (cachedModule !== undefined) { return cachedModule.exports; } var module = __webpack_module_cache__[moduleId] = { exports: {} }; // According to the moduleId module file path, find the module code and execute the incoming module, module exports, __ webpack_ require__ __webpack_modules__[moduleId](module, module.exports, __webpack_require__); return module.exports; } // Execute entry file code var __webpack_exports__ = __webpack_require__("./src/index.js"); })()
The above code is simplified. You can see the following tool functions
- __ webpack_modules__: Is an object, its value is the code of all modules, and the key value corresponds to the module file path
- __ webpack_module_cache__: Cache the value of exports
- __ webpack_require__: Load the module code according to the module file path
- __ webpack_exports__: Module external exposure method
Through the above tools and methods, you can run in the browser; From the source code es6 and es7, new features and new writing methods need to be transformed into code recognized by the browser;
For example:
// es6 import // es5 __webpack_require__
Webpack through customization__ webpack_require__,__ webpack_exports__ ..., Implement multiple module code packaging.
Next, we will build a simple version of webpack according to the above logic through the following stages
- configuration information
- Dependency build
- Generate template code
- Generate file
configuration information
class Compiler { constructor(config) { // Get configuration information this.config = config; // Save entry path this.entryId; // Module dependencies this.modules = {}; // Entry path this.entry = config.entry; // Working path this.root = process.cwd(); }
Build dependency
getSource(modulePath) { const rules = this.config.module.rules; let content = fs.readFileSync(modulePath, 'utf8'); return content; } buildModule(modulePath, isEntry) { // Get module content const source = this.getSource(modulePath); // Module id const moduleName = './' + path.relative(this.root, modulePath); if (isEntry) { this.entryId = moduleName; } // To parse the source code, you need to transform the source code and return a dependency list const {sourceCode, dependencies} = this.parse(source, path.dirname(moduleName)); // ./src // Match the relative path with the content in the module this.modules[moduleName] = sourceCode; dependencies.forEach((dep) => { // Recursive loading module this.buildModule(path.join(this.root, dep), false) }) }
Analyze the source code through buildModule to form a module dependent on this modules[moduleName;
- Find the module source code this getSource(modulePath);
- Parse the source code, convert the ast, return the source code and module dependency path this parse(source, path.dirname(moduleName))
- Generate path and module code object: this modules[moduleName] = sourceCode
- For the dependent files in the module, form an iterative call this Buildmodule (path. Join (this. Root, DEP), false) re executes the above method
Parsing source code
parse(source, parentPatch) { // AST parsing syntax tree const ast = babylon.parse(source); let dependencies = []; // Dependent array traverse(ast, { CallExpression(p) { const node = p.node; if (node.callee.name == 'require') { node.callee.name = '__webpack_require__'; let moduleName = node.arguments[0].value; // Module name moduleName = moduleName + (path.extname(moduleName) ? '' : '.js'); // ./a.js moduleName = './' + path.join(parentPatch, moduleName); // src/a.js dependencies.push(moduleName); node.arguments = [t.stringLiteral(moduleName)]; } } }); const sourceCode = generator(ast).code; return { sourceCode, dependencies } }
Parse the module source code and replace the require method with__ webpack_require__, At the same time, the file path is also converted
Code generation template
// ejs template code (() => { var __webpack_modules__ = ({ <%for(let key in modules){%> "<%-key%>": ((module, __unused_webpack_exports, __webpack_require__) => { eval(`<%-modules[key]%>`); }), <%}%> }); var __webpack_module_cache__ = {}; function __webpack_require__(moduleId) { var cachedModule = __webpack_module_cache__[moduleId]; if (cachedModule !== undefined) { return cachedModule.exports; } var module = __webpack_module_cache__[moduleId] = { exports: {} }; __webpack_modules__[moduleId](module, module.exports, __webpack_require__); return module.exports; } var __webpack_exports__ = __webpack_require__("<%-entryId%>"); })() ;
Will put this modules,this.entryId data is passed into this template to generate executable code
Generate file
emitFile() { const {output} = this.config; const main = path.join(output.path, output.filename); // Module string let templateStr = this.getSource(path.join(__dirname, 'main.ejs')); // Generate code const code = ejs.render(templateStr, {entryId: this.entryId, modules: this.modules}); this.assets = {}; this.assets[main] = code; // Write code to output folder / file fs.writeFileSync(main, this.assets[main]) }
loader
Convert referenced resources into modules getSource(modulePath) { const rules = this.config.module.rules; let content = fs.readFileSync(modulePath, 'utf8'); for (let i = 0; i < rules.length; i++) { const rule = rules[i]; const {test,use} = rule; let len = use.length -1 if(test.test(modulePath)) { function normalLoader() { const loader = require(use[len--]); content = loader(content); if(len >= 0) { normalLoader(); } } normalLoader(); } } return content; }
Obtain the source code according to the path and judge whether the current path can match the loader file test test(modulePath),
If it can be matched, pass the module source code into the loader method, and then make other transformations. content = loader(content); And form recursive call;
// Custom loader // less-loader const {render} = require('less') function loader(source) { let css = ''; render(source,(err,c) => { css = c; }) css = css.replace(/\n/g,'\\n') return css; } module.exports = loader; // style-loader function loader(source) { let style = ` let style = document.createElement('style') style.innerHTML = ${JSON.stringify(source)} document.head.appendChild(style); `; return style; } module.exports = loader;
configuration file
const path = require('path'); module.exports = { mode: 'development', entry: './src/index.js', output: { filename: 'bundle2.js', path: path.resolve(__dirname, 'dist') }, module: { rules: [ { test: /\.less$/, use:[ path.resolve(__dirname,'loader','style-loader'), // Post execution path.resolve(__dirname,'loader','less-loader') // Execute first ] } ] }, }
plugin
In terms of form, a plug-in is usually a class with an apply function:
class SomePlugin { apply(compiler) { } }
When the apply function runs, it will get the parameter compiler, which can be used as a starting point to call the hook object to register various hook callbacks,
For example: compiler hooks. make. tapAsync, where make is the hook name. tapAsync defines the call method of the hook,
The plug-in architecture of webpack is built based on this pattern. Plug-in developers can use this pattern to insert specific code into hook callbacks
configuration file
const path = require('path'); class P { constructor() { } apply(compiler) { // Get the method on the compiler and register callbacks at various stages compiler.hooks.emit.tap('emit',function () { console.log('emit') }) } } module.exports = { mode: 'development', entry: './src/index.js', output: { filename: 'bundle2.js', path: path.resolve(__dirname, 'dist') }, plugins: [ new P() ] }
compiler.js
const {SyncHook} = require('tapable'); class Compiler { constructor(config) { this.config = config; // Save entry path this.entryId; // Module dependencies this.modules = {}; // Entry path this.entry = config.entry; // Working path this.root = process.cwd(); // Start registering synchronous publications and subscriptions this.hooks = { entryOption:new SyncHook(), compile:new SyncHook(), afterCompile:new SyncHook(), afterPlugins:new SyncHook(), run:new SyncHook(), emit:new SyncHook(), done:new SyncHook() }; const plugins = this.config.plugins; // Get the plugin in the configuration item if(Array.isArray(plugins)) { plugins.forEach((plugin) => { // Call the instance method apply in the plugin and pass in the entire Compiler class plugin.apply(this); }) } this.hooks.afterPlugins.call(); }
The core of the plugin is that tapable adopts the publish / subscribe mode. First collect / subscribe the callbacks required in the plug-in and execute them in the webpack life cycle, so that the plug-in can obtain the desired context at the time of use, so as to intervene and other operations.
The above is the key core code of each stage
Complete code
const path = require('path'); const fs = require('fs'); const babylon = require('babylon'); const traverse = require('@babel/traverse').default; const t = require('@babel/types'); const generator = require('@babel/generator').default; const ejs = require('ejs'); const {SyncHook} = require('tapable'); // babylon parsing js transform ast // https://www.astexplorer.net/ // @babel/travers // @babel/types // @babel/generator class Compiler { constructor(config) { this.config = config; // Save entry path this.entryId; // Module dependencies this.modules = {}; // Entry path this.entry = config.entry; // Working path this.root = process.cwd(); this.hooks = { entryOption:new SyncHook(), compile:new SyncHook(), afterCompile:new SyncHook(), afterPlugins:new SyncHook(), run:new SyncHook(), emit:new SyncHook(), done:new SyncHook() }; const plugins = this.config.plugins; if(Array.isArray(plugins)) { plugins.forEach((plugin) => { plugin.apply(this); }) } this.hooks.afterPlugins.call(); } getSource(modulePath) { const rules = this.config.module.rules; let content = fs.readFileSync(modulePath, 'utf8'); for (let i = 0; i < rules.length; i++) { const rule = rules[i]; const {test,use} = rule; let len = use.length -1 if(test.test(modulePath)) { function normalLoader() { const loader = require(use[len--]); content = loader(content); if(len >= 0) { normalLoader(); } } normalLoader(); } } return content; } parse(source, parentPatch) { // AST parsing syntax tree const ast = babylon.parse(source); let dependencies = []; // Dependent array traverse(ast, { CallExpression(p) { const node = p.node; if (node.callee.name == 'require') { node.callee.name = '__webpack_require__'; let moduleName = node.arguments[0].value; // Module name moduleName = moduleName + (path.extname(moduleName) ? '' : '.js'); // ./a.js moduleName = './' + path.join(parentPatch, moduleName); // src/a.js dependencies.push(moduleName); node.arguments = [t.stringLiteral(moduleName)]; } } }); const sourceCode = generator(ast).code; return { sourceCode, dependencies } } buildModule(modulePath, isEntry) { // Get module content const source = this.getSource(modulePath); // Module id const moduleName = './' + path.relative(this.root, modulePath); if (isEntry) { this.entryId = moduleName; } // To parse the source code, you need to transform the source code and return a dependency list const {sourceCode, dependencies} = this.parse(source, path.dirname(moduleName)); // ./src // Match the relative path with the content in the module this.modules[moduleName] = sourceCode; dependencies.forEach((dep) => { // Recursive loading module this.buildModule(path.join(this.root, dep), false) }) } emitFile() { const {output} = this.config; const main = path.join(output.path, output.filename); let templateStr = this.getSource(path.join(__dirname, 'main.ejs')); const code = ejs.render(templateStr, {entryId: this.entryId, modules: this.modules}); this.assets = {}; this.assets[main] = code; fs.writeFileSync(main, this.assets[main]) } run() { this.hooks.run.call(); this.hooks.compile.call(); // Execute and create dependencies on the module this.buildModule(path.resolve(this.root, this.entry), true); this.hooks.afterCompile.call(); // Launch a file, packaged file this.emitFile(); this.hooks.emit.call(); this.hooks.done.call(); } } module.exports = Compiler;
github link:
https://github.com/NoahsDante...
If it helps you, click start