Read the advice

  • Recommended audience: Want to further understand webPack principles
  • Objectives of this paper:
    • Learn webPack packaging and implement your own Bundler.js

    • Implement a loader yourself

    • Implement a plugin yourself

Webpack packaging principles

Get the configuration Start the WebPack and perform the build based on the configuration information

Packaging process for WebPack

1. Start with the analysis of the entry module

- What dependencies are there - conversion codeCopy the code

2, recursive analysis of other dependent modules

- What dependencies are there - conversion codeCopy the code

3. Generate a bundle file that can be executed in the browser

Analysis entry file

Start by creating the files you want to package in the root SRC folder: index.js, add.js, say.js

// index.js
import { say } from "./sya.js";
document.write("hello" + say("webpack")); //hello webpack
Copy the code
// add.js
export function add() {
  return "add";
}
Copy the code
// say.js
import { add } from "./add.js";
export function say(str) {
  return str + add();
}

Copy the code

The files to be packaged have been created and exported and imported from each other. Next, create a lib folder to store your own bundller.js files to implement packaging.

The realization of the bundler. Js

How do I start Webpack

// start webpack node webpack.js const options = require('./webpack.config.js') // Get webpack configuration cosnt bundler = // run(); require('./lib/bundler.js') new bundler (options).run()Copy the code

This can be seen from the above code:

  • bundler.jsIt’s going to export abundlerClass.
  • And will receive the WebPack configuration passed inoptionsParameters.
  • The last executionrun()The function performs the build.

Obtaining file contents

To analyze the contents of the file, we need to get the contents of the file first, so we need to import the core module of Node fs.readfilesync () to read the contents of the file

Const fs = require("fs") module.exports = class Budler {// Get webpack configuration constructor(options) {this.entry = options.entry; this.output = options.output; } run() { this.build(this.entry); } build(entryFile) { //entryFile ./src/index.js //1. Let content = fs.readFileSync(entryFile, "utF-8 "); console.log(content) }Copy the code

Get module dependencies

Now that the previous step has successfully read the contents of the file, we need to know what dependencies were introduced into the file and extracted from it. Here we need to install a Babel plugin @babel/ Parser and import it.

The plug-in provides us with a parser() method that takes two parameters, the first being the code to analyze and the second being the configuration item

Const parser = require("@babel/parser") module.exports = class Budler {// Get webpack configuration constructor(options) {this.entry = options.entry; this.output = options.output; } run() { this.build(this.entry); } build(entryFile) { //entryFile ./src/index.js //1. Let content = fs.readFileSync(entryFile, "utF-8 "); const ast = parser.parse(content, { sourceType: "module" }) console.log(ast) }Copy the code

The AST abstract syntax tree is printed above

The AST object above is too messy. All we really need is the information about which modules were introduced and their paths, so print ast.program again

As you can see in the body array, the type is ImportDeclaration and ExpressionStatement, respectively.

And our index.js does have the first line introduced, and the second line is the expression

// index.js
import { say } from "./sya.js"
document.write("hello" + say("webpack")) //hello webpack
Copy the code

So we use Babel parse method, can help us analyze AST abstract syntax tree, through the AST can get the declaration statement, declaration statement placed in the entry file corresponding dependencies, so we use the abstract syntax tree, our JS code into JS objects.

The @babel/traverse plugin will help us find the import nodes quickly. Note: use.default with traverse

Const path = require('path') const denpendcies = {}; Tarverse (ast, {ImportDeclaration({node}) {const dirName = path.dirname(entryFile); const newPath = path.join(dirname, node.source.value); denpendcies[node.source.value] = newPath; }}) console.log(denpendcies) // {'./say.js': 'SRC \\say.js'} // The dependency introduced by key, value is the pathCopy the code

The next thing you need to do is convert the code with Babel, compile it, convert it to es5 that the browser understands, install @babel/core, and introduce a tool that gives us a transformFromAst() transformation that takes three parameters. The first parameter is the AST. The second argument can be null, and the third argument is a configuration item (note that “@babel/preset-env” in the configuration item also needs to be installed manually). This method converts the AST abstract syntax tree into an object and returns an object containing a code that is generated for compilation, Code for the current module that can be run directly in a browser.

const { transformFromAst } = require('@babel/core')

 const { code } = transformFromAst(ast, null, {
      presets: ["@babel/preset-env"]
    });
    console.log(code)
Copy the code

The printable is the contents of index.js, and it is translated, so congratulations! Our analysis of the entry file code is done! Now we can make some optimizations to make the code look more elegant. Create the utils.js file in the lib folder and put all the utility class functions there

// utils.js const fs = require('fs') const path = require('path') const parser = require('@babel/parser') const tarverse // exportModule = // exportmodule = // exportmodule = // exportmodule = // exportmodule = // exportmodule = // exportmodule = // exportmodule = // exportmodule = // exportmodule = // exportmodule = // exportmodule = // exportmodule = // exportmodule = // exportmodule GetAst: fileName => {let content = fs.readfilesync (fileName, 'utF-8 ') return parser. Parse (content, {sourceType: 'module'}) dependcies: (ast, fileName) => {const dependcies = {} { ImportDeclaration({ node }) { // denpendcies.push(node.source.value); Relative path const dirname = path.dirname(fileName) const newPath = path.join(dirname, Node.source. value) dependcies[node.source.value] = newPath}}) return dependcies}, // ast => { const { code } = transformFromAst(ast, null, { presets: ['@babel/preset-env'] }) return code } }Copy the code

It is then introduced in bundler.js

// bundler.js
const { getAst, getDependcies, getCode } = require('./utils.js')
Copy the code

Transform the code and generate a file that can be executed in the browser

From the above steps: We already have the file name, dependencies, and code in the file, so what we need to do is generate code from this information that can actually be executed on the browser side

// bundler.js module.exports = class Complier { constructor(options) { this.entry = options.entry this.output = Options.output this.modules = [] // Store module} run() {const info = this.build(this.entry) this.modules. Push (info) for (let) i = 0; i < this.modules.length; i++) { const item = this.modules[i] const { dependencies } = item if (dependencies) { for (let j in dependencies) { this.modules.push(this.build(dependencies[j])) } } } console.log(this.modules) } build(fileName) { let ast = Dependcies(ast, fileName) dependencies = dependcies (ast, fileName) dependencies = dependcies (ast, fileName) dependencies = dependcies (ast, fileName) dependencies = Dependcies(ast, fileName) dependencies, code } } }Copy the code

The outputthis.modulesAfter, as we expected, is an array of 3 modules, the contents of the object also withbuild()Return returns the same value

The next step is to convert the data format to fileName:{dependencies,code}, convert the array to an object, and then return it to the file to package the code

// Convert data structure const obj = {} this.modules. ForEach (item => {obj[item.filename] = {dependencies: item.dependencies, code: item.code } }) console.log(obj)Copy the code

The last step is to generate the file to run in the browser

Exports = class Complier{file(code) {// bundler.js module.exports = class Complier{file(code) {// /dist/main.js const filePath = path.join(this.output.path, This.output.filename) const newCode = json.stringify (code) // To write to a file, so return string // all code in the page should be placed in a large closure to avoid interference with the global environment, Const bundle = '(function(graph){function require(module){function localRequire(relativePath){return require(graph[module].dependencies[relativePath]) } var exports = {}; (function(require,exports,code){ eval(code) })(localRequire,exports,graph[module].code) return exports; } require('${this.entry}') //./src/index.js })(${newCode})` fs.writeFileSync(filePath, bundle, 'utf-8') } }Copy the code

Write your own loader

The process of writing a Loader by yourself is relatively simple,

Loader is a function, declarative function, cannot use arrow function.

Get the source code, do further modification processing, and then return the source code after processing

Follow the design rules and structure formulated by Webpack, input and output are strings, and each Loader is completely independent, plug and play

Synchronous loader

By default, loader exports a function that accepts the matched file resource string and SourceMap. We can modify the file content string and return it to the next loader for processing. Implement a simple loader example: a loader that replaces a string in the source code

// index.js console.log(' I want to replace STR ') // replaceloader. js // need to use declarative function, because we want to call this in context, using this data, this function takes one argument, Module. exports = function(source) {console.log(source, this, this.query) return source.replace(' STR ',' I replaced! ')}Copy the code

Then go to the configuration file to use the loader

// webpack.config.js // require('path') module: {rules: [{test: /\.js$/, use: path.resolve(__dirname, "./loader/replaceLoader.js") } ] },Copy the code

Asynchronous loader

How do we configure and accept the loader parameters?

// replaceloader. js const loaderUtils = require("loader-utils") // module.exports = //return source.replace(" STR ", this.query.name) const options = loaderUtils.getOptions(this) const result = source.replace("str", options.name) return source.replace("str", options.name) }Copy the code

With the tool above, you can accept arguments passed in from the outside to replace the string

// webpack.config.js // require('path') module: {rules: [{test: /\.js$/, use: [{loader: path. Resolve (__dirname, ". / loader/replaceLoader. Js "), the options: {name: "I was replaced the 2"}}]}},Copy the code

If we want to return more than the processed source code, we can use this.callback

// replaceloader. js const loaderUtils = require("loader-utils") // module.exports = function(source) { const options = loaderUtils.getOptions(this) const result = source.replace("str", options.name) this.callback(null, result) }Copy the code

The detailed method for callback is as follows:

This callback ({/ / when unable to convert the original content, give Webpack returns an Error Error: Error | Null, / / the contents of the converted the content: String | Buffer, / / the converted concluded the content of the original content of the Source Map sourceMap (optional)? : SourceMap, // Generate AST syntax tree (optional) abstractSyntaxTree? : AST })Copy the code

What if the loader has an asynchronous task? We’ll use this.async to process it, which will return this.callback

const loaderUtils = require("loader-utils") module.exports = function(source) { const options = Loaderutils.getoptions (this) // Defines an asynchronous processing, Const callback = this.async() setTimeout(() => {const callback = this.async() setTimeout() => {  const result = source.replace("str", options.name) callback(null, result) }, 3000) };Copy the code

Write a plugin

What the plugin does: Start packaging, at some point, a mechanism that helps us deal with something

Plugin is slightly more complex than loader. In the source code of Webpack, the mechanism of plugin still occupies a very large scene. It can be said that plugin is the soul of Webpack

The essence of plugin is a class that contains an apply function and accepts an argument, compiler

Let’s write a simple plugin

// copy-plugin.js class CopyPlugin {constructor() {} //compiler: Webpack instance apply(Compiler) {}} module.exports = CopyPluginCopy the code

Used in configuration files

// webpack.config.js

const CopyPlugin = require("./plugin/copy-plugin")
plugins: [new CopyPlugin()]
Copy the code

How to pass parameters

We can receive a parameter, Options, from constructor, which is passed in when the configuration file is used

// copy-plugin.js class CopyPlugin {constructor(options) {console.log(options)} //compiler: Webpack instance apply(compiler) {}} module.exports = CopyPlugin // webpack.config.js const CopyPlugin = Plugins: [new CopyPlugin({title: 'parameter '})] require("./plugin/copy-plugin") plugins: [new CopyPlugin({title:' parameter '}]Copy the code

When does the config Plugin take place

Class CopyPlugin {constructor(options) {console.log(options)} // Compiler: Webpack instance apply (compiler) {/ / before creating a resource file to the output directory emit compiler. The hooks, emit. TapAsync (' CopyPlugin '(compilation, Cb) => {console.log(compilation. Assets) compilation. Assets ['copyright.txt'] = {// Function () {return 'hello copy'}, // file size: Function () {return 20}} // the compilation is done, the cb()}); Compiler.hooks.com running. Tap (' CopyPlugin 'compilation = > {the console. The log (' started')})}} module. Exports = CopyPluginCopy the code

Compiler not only has synchronous hooks, which are registered via the TAP function, but also asynchronous hooks, which are registered via tapAsync and tapPromise

compiler.hooks.emit.tapPromise("CopyPlugin", (compilation) => {
      return new Promise((resolve, reject) => {
        setTimeout(()=>{
          console.log("compilation emit")
          resolve()
        }, 1000)
      })
    })
Copy the code

There is also a compilation, which, along with the compiler objects mentioned above, acts as a bridge between Plugin and Webpack

  • The Compiler object contains all configuration information for the Webpack environment. This object is created once when webPack is started, and all the actionable Settings are configured, including options, Loader, and plugin. When a plug-in is applied in a WebPack environment, the plug-in receives a reference to this Compiler object. You can use it to access the main webPack environment

  • The compilation object contains the current module resources, compile-generated resources, changing files, and so on. When running the WebPack development environment middleware, each time a file change is detected, a new compilation is created, resulting in a new set of compilation resources. The Compilation object also provides a number of critical timing callbacks that plug-ins can choose to use when doing custom processing

Now a simple Webpack plugin is complete!

Give it a thumbs up if you think it’s good