Hello, 🙎🏻♀️🙋🏻♀ 🙆🏻♀

I love knowledge transfer and am learning to write, ClyingDeng stool!

Long time no see!

Today I will bring you a handwritten Webpack packing function!

🏳🌈 ahead hardcore, attention please!

Easy to use with WebPack

Webpack must be pretty familiar with it, let’s finish a simple example of Webpack packaging.

Initialize an empty project NPM init-y, install dependency webpack, webpack-CLI.

Create a SRC directory to store your code. Internally create a new index.js file as the entry file. In the SRC directory, index.js introduces the add and minus methods. Contents of entry file:

import add from './add.js'
import minus from './minus.js'

console.log(add(1.2))
console.log(minus(1.2))
Copy the code

Create a webpack.config.js file as a Webpack configuration. Currently webPack 5 can be packaged by specifying its environment mode. The default entry is the index.js file in SRC and the exit is the main.js file in dist. The configuration is as follows:

const path = require('path')
module.exports = {
  entry: './src/index.js'.output: {
    filename: 'main.js'.path: path.resolve(__dirname, './dist'),},mode: 'production',}Copy the code

The specific directory structure is as follows:

Finally, run the webopack command to pack the Webpack.

inindex.htmlThe packaged file is introduced in the file, run in the browser, and if the console can print the packaged results (the results of 1 and 2 plus and minus), then the webPack is successful.

Webpack workflow

  • Initialize Compiler: New Compiler(config) to get the Compiler object
  • Start compiling: The Compiler object run method is called to start compiling
  • Identify entry: Locate all entry files according to the entry in the configuration
  • Compiling module: Starting from the entry file, call all configured Loader to compile the module, and then find out the modules that the module depends on, and recursively know that all modules are loaded in
  • Complete module compilation: After compiling all modules using Loader in Step 4, the compiled final contents of each module and their dependencies are obtained
  • Output resource: According to the dependency relationship between the entry modules, it is assembled into chunks containing multiple modules. Each Chunk is then converted into a separate file and added to the output list. (Note: This step is the last chance to modify the output.)
  • Output complete: After the output content is determined, the file is written to the file system based on the specified path and file name

Handwritten webpack

According to the workflow of Webpack, there is no Loader and plugin involved this time. But there are file dependencies, so we need to compile the file module when we need to carry out a module recursive traversal collection process.

Ok, so that’s what we’ll do today: get the Webpack configuration, execute the Compiler.run() method,

Layer 1 entry file processing: According to the entry path of the incoming configuration, read files and read resources are converted to UTF-8 format, and resources are converted to AST abstract syntax tree by @babel/ Parser. Use @babel/traverse to traverse the PROGRAM. Body node in the AST to get the dependency path and collect it into the DEPS; Compile the AST into js that the browser recognizes with @babel/core. It goes through a series of processes and then outputs to the directory and folder specified by the configuration.

After the first layer is handled, define a modules general dependency collector that pushes the first layer file dependencies into modules. Iterate through modules to process the dePS dependencies of the entry file, converting each dependency to the format of the first-level entry file. Push to modules, and if there are still dependencies in the dependency, continue recursively until each DEPS is empty.

Work with modules and turn it into a diagram like this:

 {
      'index.js': {code:'xx'.deps: {'add.js':"xxx"
        },
        'add.js': {code:'xxx'.deps: {}}}}Copy the code

Finally, the bundle is generated and the resources are exported.

Initial environment setup

Based on the above environment, we have the webPack configuration, so we need to write our own WebPack method to receive configuration information.

First we know that there is a Compiler class with the run method inside to perform the packaging. Create the Compiler. Js file in the lib folder.

class Compiler{
    constructor(options){
        this.options = options
    }
    run(){
        console.log('Execute the run method'.this.options)
    }
}
module.exports = Compiler
Copy the code

Create the index.js file in the lib folder. Receive the Config configuration and pass it into the Compiler class via new Compiler.

const Compiler = require('./Compiler')

const myWebpack = (config) = > {
  return new Compiler(config)
}
module.exports = myWebpack
Copy the code

Create a new packaged script file: scripts folder –build.js file. Based on the above principle, it is not hard to see that webPack packaging executes the run method in it.

const config = require('.. /webpack.config')
const myWebpack = require('.. /lib/index')
myWebpack(config).run()
Copy the code

Configure the package command in package.json file: “build”: “node./scripts/build.js”. Run the NPM run build command and you can see that the run method has obtained the configuration information.

Process the level 1 entry file

The next steps are mainly to improve the Compiler class.

To process the first-layer entry files, you need to read file resources and convert them into ast, traverse the AST to collect information about the first-layer file dependent files, and finally compile the JS language that can be recognized by the browser according to the AST.

To obtain the ast

Read file resources through fs.readFileSync and convert them into ast through @babel/parser.

const fs = require('fs')
const path = require('path')
const babelPaser = require('@babel/parser')

 const filePath = this.options.entry
 const file = fs.readFileSync(filePath, 'utf-8')
 / / to ast
 const ast = babelPaser.parse(file, {
   sourceType: 'module',})return ast
Copy the code

Browser Debugging methods

In most cases, we are used to output the result directly at the end, but when we come across an object structure like the AST, it is often shown to be a very long section, and the hierarchy is so deeply nested that it is difficult to tell which parent the child belongs to. Using browser debugging, we can see the ast hierarchy very clearly here. Type in the debugger where we get the AST and configure package.json command: “debug”: “node –inspect-brk./scripts/build.js”. Execute the command. If you open the console on any page in the browser, you will find an extra Node.js icon:

Clicking on the icon will bring up a new console window.

Clicking on the next breakpoint will jump to the code file at our point of interruption.

By listening to the AST from the console, we can clearly see the structure in the AST. Of course, you can also use the AST tool: astexplorer.net/

Compile the ast

Once we have the AST we need to compile the AST syntax tree into js that the browser can recognize. You can just do this with @babel/core method transformFromAst. Js if advanced syntax is present, add @babel/preset-env to parse JS.

const {transformFromAst} = require('@babel/core')
getCode(ast){
   const { code } = transformFromAst(ast, null, {
    presets: ['@babel/preset-env'],})return code
 }
Copy the code

Traverse the AST to collect dependencies

We need to collect the dependencies of the entry file, so we need to go through the node in the PROGRAM. body of the AST and find the node of typeImportDeclarationThe introduction of the declaration node.

Then find source.vlaue under the node (relative path of import file import dependency) and generate absolute path according to relative path.

const traverse = require('@babel/traverse').default
  // Get dependencies
getDeps(ast, filePath) {
  // Get the folder path
  const dirname = path.dirname(filePath)
  / / collect ast syntax tree depends on: by means of the program. The ast body. Source. Rely on the value collection
  // Define a container to collect storage dependencies
  const deps = {}
  traverse(ast, {
    // Internally iterate through the program. Body in the AST to determine the statement type inside
    // If type: ImportDeclaration fires the current function
    ImportDeclaration({ node }) {
      // Import declarations
      // code.node.source.value Relative path to import file './add.js'
      const relativePath = node.source.value
      // Generate an absolute path based on the entry file
      const absolutePath = path.resolve(dirname, relativePath)
      // Add dependencies
      deps[relativePath] = absolutePath
    },
  })
  return deps
}
Copy the code

Collect dependent DEPs that map relative paths to absolute paths.

Recursively collect all dependencies

Collect rely on

Extract and encapsulate the three methods for the first layer entry file processing into a common method. As the base method build.

  // Start building
  build(filePath) {
    // Generate the AST syntax tree
    const ast = getAst(filePath)
    // Collect dependencies
    const deps = getDeps(ast, filePath)
    / / compile
    const code = getCode(ast)
    return {
      // File path
      filePath,
      // Current dependency
      deps,
      // Parsed code
      code,
    }
  }
Copy the code

In the Compiler class, you create an all-dependent Modules array that collects multiple layers of files. After executing the run method:

To collect the dependencies of all files, we need to recursively traverse the DEPS in the entry file, find the corresponding files according to the dependencies in DEPS, and then transfer the corresponding files to AST, compile and collect the dependencies.

   // 1. Read the contents of the import file
    const filePath = this.options.entry
    const fileInfo = this.build(filePath)

    this.modules.push(fileInfo)
    // Iterate over all dependencies
    this.modules.forEach((_) = > {
      const deps = _.deps
      for (const path in deps) {
        // Get the absolute path and collect the dependencies
        const absolutePath = deps[path]
        // The dependency file is processed and added to modules
        this.modules.push(this.build(absolutePath))
      }
    })
Copy the code

Finishing rely on

Further processing dependencies, each imported relative path file has its own dependency file path, compile code. Organize the dependencies into the following dependency diagram:

 {
   'index.js': {code:'xx'.deps: {'add.js':"xxx"
     },
     'add.js': {code:'xxx'.deps: {}}}}Copy the code

At this point, we need an empty object to collect processed dependencies. Traverse modules dependencies, organized according to the above structure.

const depsGraph = this.modules.reduce((graph, module) = > {
     return {
        ...graph,
       [module.filePath]: {
         code: module.code,
         deps: module.deps,
       },
     }
   }, {})
Copy the code

Package output resources

With the processed dependency diagram, we can generate bundles and package the output resources.

Generate the bundle

The bundle packaged with WebPack is an immediate execution function, which internally makes another request to the corresponding dependency file by executing the code in the dependency diagram. The bundle contents are as follows:

(function (depsGraph) {
function require(modulePath){
  const module= {id:modulePath,
    exports:{}
  }
  // rely on file require
  // relativePath:./add.js
  function localRequire(relativePath){
const absolutePath = depsGraph[modulePath].deps[relativePath]
return require(absolutePath)
  }
// Execute layer 1 code
(function (exports,code,require) {
eval(code)
})(module.exports,depsGraph[modulePath].code,localRequire)

  return module.exports
}
require('${this.options.entry}') (${})JSON.stringify(depsGraph)})
Copy the code

Immediately execute the function to pass in the diagram, require the first level entry file. In the first entry file we need to execute the internal code, so we need to add an immediate function to execute the code. When the first entry file is executed, the dependent Add.js file is encountered. So we also need to write a method that the request internal file relies on, localRequire, recursively calling require.

For those of you who don’t understand eval(code), it looks like this:

  • Exports: exposed modules
  • Code: file content to be executed
  • Require: The require function is triggered when a dependency requests it again, so the require name of the input parameter cannot be changed. Otherwise, the localRequire method in the immediate execution function will not be triggered, and the outer require function will be directly executed. As a result, the request will pass in the relative path of the dependent file, and the relevant file content cannot be obtained, and the execution will be incorrect.

Output resources

Get the absolute path to the output file using options:

const filePath = path.resolve(this.options.output.path, this.options.output.filename)
Copy the code

Create folders and file resources with the output packaged. Since we are only implementing a simple version of Webpack, only one output file under the package folder is considered.

// Check whether the same folder name exists and delete the folder name before writing
if (fs.existsSync(this.options.output.path)) {
  fs.unlinkSync(filePath);
  fs.rmdirSync(this.options.output.path)
}
fs.mkdirSync(this.options.output.path)
fs.writeFileSync(filePath, bundle, 'utf-8')
Copy the code

Validation functions

After executing the package command, import it into an HTML file. Run the HTML file:

You can see that the output is consistent with using WebPack packaging.

This completes a simple Webpack packaging process function 🎉🎉🎉