Project structures,
Create a new folder, my-WebPack, and use development tools to open the project directory.
-
Execute NPM init -y to generate package.json
-
Creating the bin directory
The bin directory represents the executable file. Under bin, create the main program execution entry my-webpack.js
#! /usr/bin/env node // The above code is used to declare the execution environment console.log("Study well, I'm sleepy."); Copy the code
-
Configure the bin field in backage.json
{ "bin": { // Declare the directive and the file to execute it "my-webpack": "./bin/my-webpack.js"}},Copy the code
-
Perform NPM link to link the current project into the global package
To use the Webpack directive globally like webPack, we must link the package globally
-
My-webpack is executed from the command line and the program is successfully executed
Analysis of the Bundle
Create a new project to do a simple package, and analyze the packaged bundles
- Build a Webpack project
- newdemoDirectory, and indemoDirectory execution
yarn init -y
- The installationwebpack
yarn add webpack webpack-cli -D
- Creating a Service Modulesrc/index.js,src/moduleA.js,src/moduleB.js
// src/index.js const moduleA = require("./moduleA.js") console.log("Index.js, imported successfully" + moduleA.content); // src/moduleA.js const moduleB = require("./moduleB.js") console.log("ModuleA module, imported successfully" + moduleB.content); module.exports = { content: "ModuleA module" } // src/moduleB.js module.exports = { content: "ModuleB module" } Copy the code
- newwebpack.config.jsConfiguring packaging Parameters
const path = require("path") module.exports = { entry: "./src/index.js".output: { filename: "bundle.js".path: path.resolve("./build")},mode: "development" } Copy the code
- newbuild-scriptScript and execute
npm run build
// package.json { "scripts": { "build": "webpack"}},Copy the code
- newdemoDirectory, and indemoDirectory execution
- Yeah, packedbuild/bundle.jsanalysis
(() = > { /** * all modules ** All modules are waiting to be loaded in __webpack_modules__ as key-value pairs with module object keys as module ID(path) values as module contents. * Other modules are loaded within the module via the webpack_require__ function wrapped in webpack */ var __webpack_modules__ = ({ "./src/index.js": (function (__unused_webpack_module, __unused_webpack_exports, __webpack_require__) { eval("const moduleA = __webpack_require__(/*! ./moduleA */ \"./src/moduleA.js\")\r\nconsole.log(\"this\", this); \r\nconsole.log(\"index.js, successfully imported \" + modulea.content); \n\n//# sourceURL=webpack://demo/./src/index.js?"); }), "./src/moduleA.js": ((module, __unused_webpack_exports, __webpack_require__) = > { eval("Const moduleB = __webpack_require__(\"./ SRC/moduleb.js \")\r\nconsole.log(\"moduleA module, successfully imported \" + moduleb.content); \ r \ nmodule exports = {\ r \ n content: \ "moduleA module \ \ r \ n} \ n \ n / / # sourceURL = webpack: / / demo /. / SRC/moduleA. Js?"); }), "./src/moduleB.js": ((module) = > { eval("Module. Exports = {\ r \ n content: \" moduleB module \ \ r \ n} \ n \ n / / # sourceURL = webpack: / / demo /. / SRC/moduleB js?"); })});/** * Module cache ** The cache will be added after each new module is loaded. The cache will be directly used before the same module is loaded next time to avoid repeated loading. * / var __webpack_module_cache__ = {}; /** * Webpack_modules__ this function loads modules based on the module ID, and checks whether there is a cache in the module cache before loading, */ if there is no cache, and loads the module from all modules (__webpack_modules__) */ function __webpack_require__(moduleId) { // Check if there is any available cache before loading var cachedModule = __webpack_module_cache__[moduleId]; if(cachedModule ! = =undefined) { return cachedModule.exports; } // Create a new empty module and add it to the cache var module = __webpack_module_cache__[moduleId] = { exports: {}};// Executes the module method, which loads the code in the module and retrieves the exported content of the module __webpack_modules__[moduleId].call(module.exports, module.module.exports, __webpack_require__); // Returns the final exported data of the module return module.exports; } /** * this is the starting point for bundle.js execution, If there is a dependency, the import continues through __webpack_require__, and then executes the code * "./ SRC /index.js" in the import file module. Import "./ SRC/modulea.js "through __webpack_exports__ and "./ SRC/moduleb.js" */ through __webpack_exports__ var __webpack_exports__ = __webpack_require__("./src/index.js"); /** * the execution process * __webpack_require__ loads a module in __webpack_modules__, whose execution method recursively calls __webpack_require__ to load the next module until all modules are loaded */}) ();Copy the code
right_webpack_require_analysis
_webpack_require_Non-critical dog utility functions of the bundle, so to speak. import all dependencies by recursively calling this function
var __webpack_modules__ = ({ "./src/index.js": (function (__unused_webpack_module, __unused_webpack_exports, __webpack_require__) { eval("const moduleA = __webpack_require__(/*! ./moduleA */ \"./src/moduleA.js\")\r\nconsole.log(\"this\", this); \r\nconsole.log(\"index.js, successfully imported \" + modulea.content); \n\n//# sourceURL=webpack://demo/./src/index.js?"); }),... })function __webpack_require__(moduleId) { // Cache processing var cachedModule = __webpack_module_cache__[moduleId]; if(cachedModule ! = =undefined) {returncachedModule.exports; }// Create a module object and add a cache var module = __webpack_module_cache__[moduleId] = {exports: {}}; /** * Webpack implements the __webpack_modules__ module based on the Node environment, passing in module, module.exports and changing this pointer __webpack_modules__[moduleId].call(module.exports, module.module.exports, __webpack_require__); // Returns the final exported data of the module return module.exports; } Copy the code
Dependency module analysis
_webpack_modules_ holds all modules, and the most complex is the dependency of all modules, which involves js, nod basics, and the concept of abstract syntax trees. The work of Webpack is module analysis, and then process the files through various plugins and loaders to generate _webpack_modules_. The biggest difficulty in implementing Webpack is module analysis.
Here is the import file “./ SRC /index.js” as an example to analyze the module dependency generated _webpack_modules_
Start parsing
Go back to the my-webpack main program execution file bin/my-webpack.js
-
Read the package configuration file, get the package configuration parameters (inlet, outlet, etc.)
// my-webpack/bin/my-webpack.js #!/usr/bin/env node const path = require("path") // 1. Import the packaging configuration file to obtain the packaging configuration // When using the my-webpack tool to package other projects, you need to obtain the absolute path of the project packaging configuration file const config = require(path.resolve("webpack.config.js")) console.log("config", config); Copy the code
-
Back in the DEMO project, enter the instruction my-webpack in the terminal to use your own packaging tool
Package configuration parameters were successfully obtained
Code parser
Use a parser to parse project code against configuration parameters
-
Create lib/Compiler.js in the tool’s my-webpack directory
Create a new Compiler class using object-oriented thinking
// lib/Compiler.js const path = require("path") const fs = require("fs") class Compiler { constructor(config) { this.config = config this.entry = config.entry // process. CWD can obtain the absolute path of the node execution file // Get the file path of the packaged project this.root = process.cwd() } // Parse the file module based on the file path passed in depAnalyse(modulePath) { const file = this.getSource(modulePath) console.log("file", file); } // Pass the file path, read the file, and return getSource(path) { // Read the file in utF-8 encoding format and return return fs.readFileSync(path, "utf-8")}// Execute the parser start() { // Pass in the absolute path of the entry file to start parsing dependencies // Note: __dirname cannot be used here. __dirname represents the exclusive path to the "my-webpack" root directory of the utility library, not the root path of the project to be packaged this.depAnalyse(path.resolve(this.root, this.entry)) } } module.exports = Compiler // bin/my-webpack.js // 2. Import the parser, create a new instance, and execute the parser const Compiler = require(".. /lib/Compiler") new Compiler(config).start() Copy the code
-
In the packaged project DEMO, re-executing my-webpack successfully reads the entry file
Abstract syntax tree
After successfully reading the module code, the module code can be converted into an abstract syntax tree (AST) and the require syntax replaced by its own wrapper loading function _webpack_require_
Code online to abstract syntax tree: astexplorer.net/
Two packages are needed to generate and traverse the abstract syntax tree in the packaging project: @babel/ Parser and @babel/traverse
-
Install NPM I @babel/ parser@babel /traverse -S
-
Generate an AST and transform the syntax
// my-webpack/lib/Compiler.js // Import the parser const parser = require("@babel/parser") // Import converter es6 export requires.defult const traverse = require("@babel/traverse").default class Compiler { depAnalyse(modulePath) { const code = this.getSource(modulePath) // Parse the code into an AST abstract syntax tree const ast = parser.parse(code) /** * traverse to convert syntax, which receives two arguments * -ast: abstract syntax tree node tree before conversion * -options: Traverse syntax tree nodes that are triggered when a node meets a hook condition * -callexpression: */ traverse(ast, { // This hook is triggered when an abstract syntax tree node type is CallExpression (expression invocation) CallExpression(p) { console.log("Name of syntax node of this type", p.node.callee.name); }})}}Copy the code
-
Go back to the DEMO project and execute my-WebPack to repackage using your own library
-
Replace keywords in code
// my-webpack/lib/Compiler.js traverse(ast, { // This hook is triggered when an abstract syntax tree node type is CallExpression (expression invocation) CallExpression(p) { console.log("Name of syntax node of this type", p.node.callee.name); if (p.node.callee.name === 'require') { / / modify the require p.node.callee.name = "__webpack_require__" // Change the path of the current module dependent module. Using Node to access resources must be in the form of "./ SRC /XX" let oldValue = p.node.arguments[0].value // change the "./ XXX "path to "./ SRC/XXX" oldValue = ". /" + path.join("src", oldValue) // Avoid window path with "\" p.node.arguments[0].value = oldValue.replace(/\\/g."/") console.log("Path", p.node.arguments[0].value); }}})Copy the code
-
Back at DEMO, execute my-Webpack to repack to see the console output
To generate the source code
After processing the AST, code can be generated with @babel/ Generator
- The installation
npm i @babel/generator -S
- After parsing the AST, build the code
// my-webpack/lib/Compiler.js // Import the generator const generator = require("@babel/generator").default class Compiler { depAnalyse(modulePath) { traverse(ast, { ...... }) // Generate code from the abstract syntax tree const sourceCode = generator(ast).code console.log("Source", sourceCode); }}Copy the code
- Go back toDEMO, the implementation of
my-webpack
To rebuild
Build dependencies recursively
In Compiler, we use depAnalyse to convert syntax in the./ SRC /index.js module and dependency module paths by passing in module paths. This only parses the “./ SRC /index.js” layer, and its dependencies don’t parse builds, so we recursively execute depAnalyse for all modules
class Compiler {
depAnalyse(modulePath) {
// The dependency array of the current module stores all the dependency paths of the current module
let dependencies = []
traverse(ast, {
CallExpression(p) {
if (p.node.callee.name === 'require') {
p.node.callee.name = "__webpack_require__"
let oldValue = p.node.arguments[0].value
oldValue = ". /" + path.join("src", oldValue)
p.node.arguments[0].value = oldValue.replace(/\\+/g."/")
// Every time you parse require, place the path of the dependent module in dependencies
dependencies.push(p.node.arguments[0].value)
}
},
})
const sourceCode = generator(ast).code
console.log("sourceCode", sourceCode);
// If the module has other dependencies it recursively calls depAnalyse and continues parsing the code until it has no dependencies
dependencies.forEach(e= > {
// The absolute path to the module passed in
this.depAnalyse(path.resolve(this.root, depPath))
})
}
}
Copy the code
Get all modules
The webpack result is that all modules are grouped together in the form of module IDS + module execution functions
class Compiler {
constructor(config){...// Store all the packaged modules
this.modules = {}
}
depAnalyse(modulePath){ traverse(ast, {..... })const sourceCode = generator(ast).code
// The ID of the module is treated as a relative path
let modulePathRelative = ". /" + path.relative(this.root, modulePath)
// Replace "\" with "/"
modulePathRelative = modulePathRelative.replace(/\\+/g."/")
// Add the current module to modules when it is parsed
this.modules[modulePathRelative] = sourceCode
dependencies.forEach(depPath= >{... })}start() {
this.depAnalyse(path.resolve(this.root, this.entry))
// Get the final parsing result
console.log("module".this.modules); }}Copy the code
Execute my-Webpack in DEMO to review the packing results
So far, we use Compiler class, recursively call depAnalyse method to parse all modules of the project, obtain modules and successfully build the whole project.
Generate _webpack_modules_
The webpack is used to generate _webPack_modules_, so the module is where the module ID+ module execution functions exist. We can use a template engine (this is an EJS example) to process module code into module execution functions
-
Install NPM I EJS-S
-
New template rendering my – webpack/template/output. The ejs
(() = > {var __webpack_modules__ = ({/ / traverse use template syntax k is the module ID < % for (modules) in the let k {% > "< % % > - k" : (function (module, exports, __webpack_require__) { eval(`<%- modules[k]%>`); }}), < % % >}); 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].call(module.exports, module, module.exports, __webpack_require__); return module.exports; } var __webpack_exports__ = __webpack_require__("<%-entry%>"); }) ();Copy the code
-
Render code using the template and write it to the export file
// my-webpack/lib/Compiler.js / / import ejs const ejs = require("ejs") class Compiler { constructor(config){... }depAnalyse(modulePath) {....} getSource(path){... }start() { this.depAnalyse(path.resolve(this.root, this.entry)) this.emitFile() } // Generate the file emitFile() { // Read the template to which the code renders const template = this.getSource(path.resolve(__dirname, ".. /template/output.ejs")) // Pass in the render template and the variables used in the template let result = ejs.render(template, { entry: this.entry, modules: this.modules }) // Get the output path let outputPath = path.join(this.config.output.path,this.config.output.filename) // Generate the bundle file fs.writeFileSync(outputPath,result) } } Copy the code
-
In the DEMO project, re-execute my-Webpack
Note that the output path of webpack.config.js in the DEMO project is DEMO/build/bundle.js. You must make sure the DEMO has a build folder, otherwise you may get an error when writing files using my-Webpack.
Custom loader
Earlier we implemented a packaging tool, my-Webpack, which now only works with JS files. If you want to process other files or operate js codes, you need to use Loader. Loader’s main function is to process a section of code matching rules to generate the final code for output.
How to use loader
- Install loader package
- In the WebPack configuration file, add the configuration to the Rules of the Module
- Some loaders also need to configure additional parameters (optional)
What is the loader
The Loader module is a function. Webpack passes resources to the Loader function, and the Loader processes the resources and returns them.
We implement a loader in the WebPack project
-
Process the exported content of SRC/moduleb.js
Replace exported content containing “moduleB” with “module-b”
-
Create loader/index.js in the DEMO project
// Loader is essentially a function module.exports = function (resource) { // The matched resource is processed and returned return resource.replace(/moduleB/g."MODULE-B")}Copy the code
-
In webpack.config.js of DEMO, configure a custom loader
// DEMO/webpack.config.js module.exports = { module: { rules: [{test: /.js$/g, use: "./loader/index.js"}}}]Copy the code
-
Execute NPM Run build, use WebPack to package the project and execute the packaged code
Loader execution sequence
There are two scenarios for using Loaders to process a resource: Single matching rule Multiple Loaders and multiple matching rules single loader
Multiple loaders for a single matching rule:
If a single matching rule has multiple Loaders, the loader execution sequence is from the right to the left
Multiple matching rules Single loader:
When multiple matching rules are applied to a single Loader, the loader is executed from bottom to top
Loader type
Loader types include front, inline, normal, and rear
The four Loaders are executed in the following sequence: Front > Inline > Normal > Rear
Pre-loader and post-Loader
The pre-loader and post-loader are controlled by The Enforce field of the Loader. The pre-loader is executed before all loaders, and the post-loader is executed after all Loaders are executed
Inline loader
Use inline loader to parse files, must be in the require, import imported resources before the loader, multiple loader to use! Separate cases:
import Styles from 'style-loader! css-loader? modules! ./styles.css'
Copy the code
Get the Options configuration
Most loaders can be used only after registration in rules. Some loaders with complex functions need to be configured with options. For example, the URL-loader that generates image resources sets a threshold for generating base64 file sizes.
In the loader,this can get the context and webpack configuration. Options can be obtained from this.getOptions
-
Incoming parameters
module: { rules: [{test: /.js$/g, use: { loader: "./loader/index.js".options: { target: /moduleB/g, replaceContent: "MD_B"}},},]},Copy the code
-
Custom loader to use parameters
// demo/loader/index.js const loaderUtils = require("loader-utils") // Loader is essentially a function module.exports = function (resource) { const { target, replaceContent } = this.getOptions() // The matched resource is processed and returned return resource.replace(target, replaceContent) } Copy the code
-
Execute NPM Run build, use WebPack to package the project and execute the packaged code
My-webpack adds loader functionality
Through the previous configuration of loader and handwritten Loader, it can be found that my-Webpack adds loader functions mainly through the following steps:
- Read the module.rules configuration item of the Webpack configuration file and iterate backwards (rules matches each matching rule in reverse order)
- Match the file type based on the res and import the Loader function in batches
- Call all Loader functions iteratively in reverse order
- Finally, the processing code is returned
Code section (my-webpack/lib/ compiler.js)
- Gets all configured in the configuration fileloader
class Compiler { constructor(config){...// Get all loaders this.rules = config.module && config.module.rules } ... } Copy the code
- The statementuseLoaderMethod, called when a dependency is resolved, passing in the parsed source code and module path
class Compiler {...depAnalyse(modulePath) { let code = this.getSource(modulePath) // Use the loader to process the source code code = this.useLoader(code, modulePath) const ast = parser.parse(code) ... } / / use the loader useLoader(code, modulePath) { // Return source code without rules configuration if (!this.rules) return code // Obtain the source code and output it to loader, traversing loader in reverse order for (let index = this.rules.length - 1; index >= 0; index--) { const { test, use } = this.rules[index] // If the current module meets the Loader re match if (test.test(modulePath)) { /** * If there is a single match rule and multiple loaders, the use field is an array. For a single loader, the use field is a string or object */ if (use instanceof Array) { // The loader passes the source code in reverse order for processing for (let i = use.length - 1; i >= 0; i--) { let loader = use[i]; /** * Loader is a string or object. * String format: * use:["loader1","loader2"] * use:[ * { * loader:"loader1", * options:.... * } * ] */ let loaderPath = typeof loader === 'string' ? loader : loader.loader // Obtain the absolute path of the Loader loaderPath = path.resolve(this.root, loaderPath) // Loader context const options = loader.options const loaderContext = { getOptions() { return options } } / / import loader loader = require(loaderPath) // The incoming context executes the loader processing source code code = loader.call(loaderContext, code) } } else { let loaderPath = typeof loader === 'string' ? use : use.loader // Obtain the absolute path of the Loader loaderPath = path.resolve(this.root, loaderPath) // Loader context const loaderContext = { getOptions() { return use.options } } / / import loader let loader = require(loaderPath) // The incoming context executes the loader processing source code code = loader.call(loaderContext, code) } } } return code } } Copy the code
- perform
my-webpack
, the use ofmy-webpackPackage the project and execute the packaged code
A custom plugin
Webpack’s plug-in interface gives the user access to the compile process by registering handler functions with different event node lifecycle hooks during the compile process, and by executing each hook, the plug-in has access to the current state of the compile.
In short, custom plug-ins can perform some functions by processing the source code in the declaration cycle hooks of the WebPack compilation process.
Plug-in lifecycle hooks
hook | role | parameter |
---|---|---|
entryOption | Called after processing entry configuration for the WebPack option | context,entry |
afterPlugins | Called after initializing the list of internal plug-ins | compiler |
beforeRun | Called before running Compiler | compiler |
run | Called when Compiler starts working | compiler |
emit | Called when asSTES is emitted to the ASSTES directory | compilation |
done | Called after compilation is complete | stats |
. | . | . |
The composition of the WebPack plug-in
- A js named function
- Define an apply method on the prototype of the plug-in
- Specify a binding towebpackIts own event hook
- Many of the event hooks within WebPack are implemented through the Tabable library, which focuses on custom event firing and handling
- Handles specific data for webPack internal instances
- The callback provided by WebPack is invoked when the functionality is complete
Implement a simple plugin
- Demo project new demo/plugins/helloWorldPlugin. Js
1. Declare a named function
module.exports = class HelloWorldPlugin {
// 2. Function prototype must have the apply method
apply(compiler) {
// 3. Register hook callbacks with hooks
// 4. Trigger the subsequent callback when the done event occurs
compiler.hooks.done.tap("HelloWorldPlugin".(stats) = > {
console.log("The whole Webpack pack is finished.");
})
// 5. Trigger the subsequent callback when the done event occurs
compiler.hooks.emit.tap("HelloWorldPlugin".(stats) = > {
console.log("File launch is over."); }}})Copy the code
- webpack.config.jsIntroduce custom plug-ins
const path = require("path") / / import HelloWorldPlugin const HelloWorldPlugin = require("./plugin/helloWorldPlugin") module.exports = { entry: "./src/index.js".output: { filename: "bundle.js".path: path.resolve("./build")},module: { rules: [{test: /.js$/g, use: [{ loader: "./loader/index.js".options: { target: /moduleB/g, replaceContent: "MD_B"}}]},]},// Configure the custom plug-in plugins: [ new HelloWorldPlugin() ], mode: "development" } Copy the code
-
NPM run Build repackage demo
-
The HTML – webpack – the plugin
The html-webpack-plugin simply copies the specified HTML template and automatically imports bundle.js
How to do that? 1. Write a custom plug-in and register the afterEmit hook. 2. Read the HTML template from the template property passed in when creating the plugin instance. 4. Walk through the list of resources generated by webPack and, if there are multiple bundle.js, import them into THE HTML one by one. 5. Output the generated HTML string to the dist directory
- demoProject constructionsrc/index.html, newplugin/HTMLPlugin.js
// demo/plugin/HTMLPlugin.js const fs = require("fs") const cheerio = require("cheerio") module.exports = class HTMLPlugin { constructor(options) { Filename: indicates the output filename of the template. * options.template: indicates the input path of the target template this.options = options } // plugins must have the apply method apply(compiler) { // 3, afterEmit at the end of the resource emit, fetch the bundle and other resources compiler.hooks.afterEmit.tap("HTMLPlugin".(compilation) = > { // 4, read the HTML target template passed in, get the DOM structure string const template = fs.readFileSync(this.options.template, "utf-8") /** * 5, 'yarn add cheerio' install cheerio and import it */ let $ = cheerio.load(template) console.log(Object.keys(compilation.assets)); // 6. Loop through all resources once into HTML Object.keys(compilation.assets).forEach(e= > $('body').append(`<script src="./${e}"></script>`)) // output the new HTML string to the dist directory fs.writeFileSync("./build/" + this.options.filename, $.html()) }) } } Copy the code
- Configure the plug-in
// demo/webpack.config.js / / import HTMLPlugin const HTMLPlugin = require("./plugin/HTMLPlugin") module.exports = { // Configure the custom plug-in plugins: [ new HTMLPlugin({ filename: "index.html".template: "./src/index.html" }), new HelloWorldPlugin(), ], } Copy the code
npm run build
Package the project to view the generatedbuild/index.html
Compiler differs from Compilation
- Compiler objects are the tools that value WebPack uses to package project files
- Compilation objects are the products that are packaged at each stage of a Webpack each time it is packaged
My-webpack adds plugin functionality
tapable
Project files are packaged and processed within WebPack through various event flow concatenation plug-ins. The core of the event flow mechanism is implemented through Tapable, which is similar to the Events library of Node. The core principle of Tapable is publish and subscribe.
Add plugin functionality
We’ll do a simple demonstration here, setting up a few hook functions at key nodes. The internal implementation of Webpack is far more complex than ours, after all, there are only dozens of hook functions.
-
Install YARN Add Tapable
-
Load plugin, declare hook, execute hook
// my-webpack/lib/Compiler.js / / import tabable const { SyncHook } = require("tapable") class Compiler { constructor(config){...// 1 this.hooks = { compile: new SyncHook(), afterCompile: new SyncHook(), emit: new SyncHook(), afterEmit: new SyncHook(['modules']), done: new SyncHook() } // get all plugin objects and execute the apply method if (Array.isArray(this.config.plugins)) { this.config.plugins.forEach(e= > { // Pass in the Compiler instance, and the plug-in can register the hooks in the Apply method e.apply(this)}}}start() { // Execute compile hook before parsing this.hooks.compile.call() this.depAnalyse(path.resolve(this.root, this.entry)) // After analysis, execute the afterCompile hook this.hooks.afterCompile.call() // Execute emit hook before resource launches this.hooks.emit.call() this.emitFile() // After the resource is fired, execute the afterEmit hook this.hooks.afterEmit.call() // When parsing is complete, execute the done hook this.hooks.done.call() } } Copy the code
-
Register the hook in helloWorldPlugin
// demo/plugin/helloWorldPlugin.js module.exports = class HelloWorldPlugin { apply(compiler) { compiler.hooks.done.tap("HelloWorldPlugin".(stats) = > { console.log("The whole Webpack pack is finished."); }) compiler.hooks.emit.tap("HelloWorldPlugin".(stats) = > { console.log("File launched."); }}})Copy the code
-
Execute my-webpack and repack
The last
The purpose of this article is to understand what WebPack “is” and how it works by implementing the basic features of webPack, each part of which and each feature can be far more complex than ours. Finally, paste the compiler code
const path = require("path")
const fs = require("fs")
// Import the parser
const parser = require("@babel/parser")
// Import converter es6 export requires.defult
const traverse = require("@babel/traverse").default
// Import the generator
const generator = require("@babel/generator").default
/ / import ejs
const ejs = require("ejs")
/ / import tabable
const { SyncHook } = require("tapable")
class Compiler {
constructor(config) {
this.config = config
this.entry = config.entry
// process. CWD can obtain the absolute path of the node execution file
// Get the file path of the packaged project
this.root = process.cwd()
// Store all the packaged modules
this.modules = {}
// Get all loaders
this.rules = config.module && config.module.rules
// 1
this.hooks = {
compile: new SyncHook(),
afterCompile: new SyncHook(),
emit: new SyncHook(),
afterEmit: new SyncHook(['modules']),
done: new SyncHook()
}
// get all plugin objects and execute the apply method
if (Array.isArray(this.config.plugins)) {
this.config.plugins.forEach(e= > {
// Pass in the Compiler instance, and the plug-in can register the hooks in the Apply method
e.apply(this)}}}// Parse the file module based on the file path passed in
depAnalyse(modulePath) {
let code = this.getSource(modulePath)
// Use the loader to process the source code
code = this.useLoader(code, modulePath)
// Parse the code into an AST abstract syntax tree
const ast = parser.parse(code)
// The dependency array of the current module stores all the dependency paths of the current module
let dependencies = []
/** * traverse to convert syntax, which receives two arguments * -ast: abstract syntax tree node tree before conversion * -options: Traverse syntax tree nodes that are triggered when a node meets a hook condition * -callexpression: */
traverse(ast, {
// This hook is triggered when an abstract syntax tree node type is CallExpression (expression invocation)
CallExpression(p) {
if (p.node.callee.name === 'require') {
/ / modify the require
p.node.callee.name = "__webpack_require__"
// Change the path of the current module dependent module. Using Node to access resources must be in the form of "./ SRC /XX"
let oldValue = p.node.arguments[0].value
// change the "./ XXX "path to "./ SRC/XXX"
oldValue = ". /" + path.join("src", oldValue)
// Avoid window path with "\"
p.node.arguments[0].value = oldValue.replace(/\\+/g."/")
// Every time you parse require, place the path of the dependent module in dependencies
dependencies.push(p.node.arguments[0].value)
}
},
})
const sourceCode = generator(ast).code
// The ID of the module is treated as a relative path
let modulePathRelative = ". /" + path.relative(this.root, modulePath)
// Replace "\" with "/"
modulePathRelative = modulePathRelative.replace(/\\+/g."/")
// Add the current module to modules when it is parsed
this.modules[modulePathRelative] = sourceCode
// If the module has other dependencies it recursively calls depAnalyse and continues parsing the code until it has no dependencies
dependencies.forEach(depPath= > {
// The absolute path to the module passed in
this.depAnalyse(path.resolve(this.root, depPath))
})
}
// Pass the file path to read the file
getSource(path) {
// Read the file in utF-8 encoding format and return
return fs.readFileSync(path, "utf-8")}// Execute the parser
start() {
// Execute compile hook before parsing
this.hooks.compile.call()
// Pass in the absolute path of the entry file to start parsing dependencies
// Note: the path cannot be __dirname. __dirname represents the absolute path of the "my-webpack" root directory of the utility library, not the root path of the project to be packaged
this.depAnalyse(path.resolve(this.root, this.entry))
// After analysis, execute the afterCompile hook
this.hooks.afterCompile.call()
// Execute emit hook before resource launches
this.hooks.emit.call()
this.emitFile()
// After the resource is fired, execute the afterEmit hook
this.hooks.afterEmit.call()
// When parsing is complete, execute the done hook
this.hooks.done.call()
}
// Generate the file
emitFile() {
// Read the template to which the code renders
const template = this.getSource(path.resolve(__dirname, ".. /template/output.ejs"))
// Pass in the render template and the variables used in the template
let result = ejs.render(template, {
entry: this.entry,
modules: this.modules
})
// Get the output path
let outputPath = path.join(this.config.output.path, this.config.output.filename)
// Generate the bundle file
fs.writeFileSync(outputPath, result)
}
/ / use the loader
useLoader(code, modulePath) {
// Return source code without rules configuration
if (!this.rules) return code
// Obtain the source code and output it to loader, traversing loader in reverse order
for (let index = this.rules.length - 1; index >= 0; index--) {
const { test, use } = this.rules[index]
// If the current module meets the Loader re match
if (test.test(modulePath)) {
/** * If there is a single match rule and multiple loaders, the use field is an array. For a single loader, the use field is a string or object */
if (use instanceof Array) {
// The loader passes the source code in reverse order for processing
for (let i = use.length - 1; i >= 0; i--) {
let loader = use[i];
/** * Loader is a string or object. * String format: * use:["loader1","loader2"] * use:[ * { * loader:"loader1", * options:.... * } * ] */
let loaderPath = typeof loader === 'string' ? loader : loader.loader
// Obtain the absolute path of the Loader
loaderPath = path.resolve(this.root, loaderPath)
// Loader context
const options = loader.options
const loaderContext = {
getOptions() {
return options
}
}
/ / import loader
loader = require(loaderPath)
// The incoming context executes the loader processing source code
code = loader.call(loaderContext, code)
}
} else {
let loaderPath = typeof loader === 'string' ? use : use.loader
// Obtain the absolute path of the Loader
loaderPath = path.resolve(this.root, loaderPath)
// Loader context
const loaderContext = {
getOptions() {
return use.options
}
}
/ / import loader
let loader = require(loaderPath)
// The incoming context executes the loader processing source code
code = loader.call(loaderContext, code)
}
}
}
return code
}
}
module.exports = Compiler
Copy the code