编程知识 cdmana.com

Handwritten a simple webpack compiler code

One 、webpack The main process of package compilation

compiler The process of :
  1. take webpack.config.js Pass in as a parameter Compiler class (entry-options)
  2. establish Compiler example
  3. call Compiler.run Start compilation (make)
  4. establish Compilation( compiler Internal creation compilation object , And will this Pass in ,compilation That's right compiler References to )
  5. Start creating... Based on configuration Chunk ( Read the file , Turn into AST )
  6. Use Parser from Chunk Start parsing dependencies ( Find dependencies )
  7. Use Module and Dependency Managing code module interdependencies (build-module)
  8. Use Template be based on Compilation Data generation result code
  • It can be divided into three stages
step

Two 、 preparation

Let's start with a project , Directory as follows :

  selfWebpack
    - src
      - data.js
      - index.js
      - random.js
 Copy code 
// index.js
import data from './data.js'
import random from './random.js'

console.log(' I'm a data file --->', data)
console.log(' I'm a random number --->', random)
console.log(' I am a index.js')
 Copy code 
// data.js
const result = ' I'm the data in the file '

export default result
 Copy code 
// random.js
const random = Math.random()

export default random

 Copy code 

Then we use it first webpack Make a package , Look at the What we need to do

//  Basic installation 
npm init -y
npm install webpack@4.44.2 webpack-cli@4.2.0 --save-dev
 Copy code 
// package.json
//  modify 
"scripts": {
  "test": "echo \"Error: no test specified\" && exit 1",
  "build": "webpack --mode development"
},
 Copy code 

Sort out the packaged code

(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;
  }
  // Load entry module and return exports
  return __webpack_require__(__webpack_require__.s = "./src/index.js");
})({
  "./src/data.js": function(module, __webpack_exports__, __webpack_require__) {
    "use strict";
    const result = ' I'm the data in the file '
    __webpack_exports__["default"] = (result);

  },
  "./src/index.js": function(module, __webpack_exports__, __webpack_require__) {
    "use strict";
    var _random_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__("./src/random.js");
    console.log(' I'm a data file --->', _data_js__WEBPACK_IMPORTED_MODULE_0__["default"])
    console.log(' I'm a random number --->', _random_js__WEBPACK_IMPORTED_MODULE_1__["default"])
    console.log(' I am a index.js')
  },
  "./src/random.js": function(module, __webpack_exports__, __webpack_require__) {
    "use strict";
    const random = Math.random()
    __webpack_exports__["default"] = (random);
  }
});
 Copy code 

The outermost layer is an immediate function , Participation is all modules( modular ) list. Incoming modules Parameter is an object .

  • The format of the object is , file name : Method .
  • key yes index.js The relative path of the file ,value Is an anonymous function , In the function body, we write in index.js Code in .( This is it. webpack How to load modules )
If we can achieve two functions
  1. import become __webpack_require__
  2. Read all dependencies in the module , Generate a Template

3、 ... and 、 Start building your own selfpack

  • Realization Package compiled code , Put it in src Of the same rank selfpack Catalog , Add another configuration file (selfpack.config.js), as follows :
  selfWbpack
    + src
    //  newly added 
    - selfpack
      - compilation.js
      - compiler.js
      - index.js
      - Parser.js
    - selfpack.config.js
 Copy code 
// selfpack.config.js
const { join } = require('path')
module.exports = {
  entry: join(__dirname, './src/index.js'),
  output: {
    path: join(__dirname, './dist'),
    filename: 'main.js'
  }
}
 Copy code 

Four 、 Implement transformation AST

  • Why should it be converted to ast ? Because there is import , We're going to replace it with webpack_require .
  • How do you do it? ? Traverse AST , Put... In it import The file path introduced by the statement is collected .
  1. First step , Realize to find the entry file and get the file content through the parameters
  2. The second step , Turn into AST
  3. The third step , Analysis of main module file dependency
  4. Step four , take AST Convert back to JS Code
  5. Step five , Analyze dependencies between modules , take import Replace with webpack_require
4.1 Get the entry file
// selfpack/index.js
const Compiler = require('./Compiler')
const options = require('../selfpack.config.js')
const compiler = new Compiler(options)
compiler.run()
 Copy code 
// selfpack/compilation.js
const fs = require('fs')

class Compilation {
  constructor(compiler) {
    const { options } = compiler
    this.options = options
  }compiler
  static ast(path){
    const content = fs.readFileSync(path, 'utf-8') //  Read the file 
    console.log(' get files ', content)
  }
  buildModule(absolutePath, isEntry) {
    this.ast(absolutePath)
  }
}
module.exports = Compilation
 Copy code 

npm install tapable

// selfpack/compiler.js
const { SyncHook } = require('tapable')
const Compilation = require('./Compilation')

class Compiler {
  constructor(options) {
    this.options = options
    this.hooks = {
      run: new SyncHook()
    }
  }
  run() {
    this.compile()
  }
  compile() {
    const compilation = new Compilation(this)
     // adopt entry Find the entry file 
     const entryModule = compilation.buildModule(this.options.entry, true)
  }
}
module.exports = Compiler
 Copy code 

Static methods MDN

// selfpack/Parser.js
const fs = require('fs')
class Parser{
  static ast(path) {
    const content = fs.readFileSync(path, 'utf-8') //  Read the file 
    console.log(' Read the file ', content)
  }
}
module.exports = Parser
 Copy code 

take selfpack.config.js Pass in as a parameter Compiler class , perform run Method . adopt new One Compilation example , call buildModule()

  • buildModule( absolutePath, isEntry )
    • absolutePath: The absolute path of the entry file
    • isEntry: Is it the main module

Get the results of the entry file :

getEntryFile

The first step is successful , The next step is to turn into AST

4.2 Turn it into AST

This step requires the use of @babel/parser , Turn the code into AST Grammar tree .
npm install @babel/parser sourceType What we want to analyze is ES modular

  • call Parser.ast()
  • adopt readFileSync Read file contents , Pass to parser.parse() obtain AST.
// selfpack/Parser.js
const fs = require('fs')
const parser = require('@babel/parser')

class Parser{
  static ast(path) {
    const content = fs.readFileSync(path, 'utf-8') //  Read the file 
    console.log(' Read the file ', content)
    const _ast = parser.parse(content, {
      sourceType: 'module' // What we want to analyze is ES modular 
    })
    console.log(_ast)
    console.log(' I am a body Content ', _ast.program.body)
    return _ast
  }
}
module.exports = Parser
 Copy code 
getAST

At this point, we are very smooth ! This is the information for the entire file , And we need the file content in its properties program Inside body in . to glance at body The content of

getASTBody

This is a src/index.js One of the import Of Node attribute , Its type is ImportDeclaration.

4.3 Analysis of main module file dependency

Next , Analyze the main module .

Traverse AST Want to use @babel/traverse
npm install @babel/traverse
traverse() Usage of : The first parameter is AST , The second parameter is the configuration object

// selfpack/Parser.js
const traverse  = require('@babel-traverse').default
const fs = require('fs')
const parser = require('@babel/parser')
const path = require('path')

class Parser{
  static ast(path){
    const content = fs.readFileSync(path, 'utf-8') //  Read the file 
    const _ast = parser.parse(content, {
      sourceType: 'module' // What we want to analyze is ES modular 
    })
    console.log(_ast)
    console.log(' I am a body Content ', _ast.program.body)
    return _ast
  }
  static getDependecy(ast, file) {
    const dependecies = {}
    traverse(ast, {
      ImportDeclaration: ({node}) => {
        const oldValue = node.source.value
        const dirname = path.dirname(file)
        const relativepath = "./" + path.join(dirname, oldValue) 
        dependecies[oldValue] = relativepath
        node.source.value = relativepath //  take  ./data.js  Turn it into  ./src/data.js
      }
    })
    return dependecies
  }
}
module.exports = Parser
 Copy code 
  • call Parser.getDependecy Method , Get the dependency path of the main module , Modify source code .
  • getDependecy(): Static methods , It's right type by ImportDeclaration The processing of the node of .
  • node.source.value: Namely import Value .
  • Because our packaged code , In the reference section key Turned into ./src/data.js, So there's a need to change that as well

import data from './data.js' ==> require('./data.js') ==> require('./src/data.js')

relativepath: What we get here is the dependent file path
dependecies: Is a collection of dependent objects ,key by node.source.value ,value For the converted path .

import data from './data.js'
import random from './random.js'
 Copy code 

node.source.value: refer to from hinder './data.js' 、'./random.js'

path.relative(from, to): Method returns... Based on the current working directory ( from ) To ( to ) Of ( Relative paths )

process.cwd(): return Node.js Current working directory of the process (path.resolve())

// selfpack/compilation.js
const Parser = require('./Parser')
const path = require('path')

class Compilation {
  constructor(compiler) {
    const { options } = compiler
    this.options = options
    this.entryId
    //  increase 
    this.root = process.cwd() //  The current directory where the command is executed 
  }
  buildModule(absolutePath, isEntry) {
    let ast = ''
    ast = Parser.ast(absolutePath)
    const relativePath = './' + path.relative(this.root, absolutePath)
    if(isEntry){
      this.entryId = relativePath
    }
    const dependecies = Parser.getDependecy(ast, relativePath)
    console.log(" Dependencies ", dependecies)
  }
}
module.exports = Compilation
 Copy code 

After traversing stay ast Find the node type in it , adopt index.js Of ast Get index.js File dependency ( That is to say data.js、random.js)

getDependecies

The dependency path of the main module has been found ! Go to this step , It's not far from success .

4.4 Convert code

The next step is to convert the code , That is to change the AST convert to JS Code .
Yes @babel/core Of transformFromAst and @babel/preset-env.
Install it. npm install @babel/core @babel/preset-env

  • transformFromAst: That's what brought us in AST It turns into our third parameter (@babel/preset-env) The type of module configured in , Will return the converted code

@babel/preset-env It's what we use JS New features converted into compatible code .

here Parser.js Long like this

// selfpack/Parser.js  complete 
const traverse  = require('@babel-traverse').default
const fs = require('fs')
const parser = require('@babel/parser')
const path = require('path')
//  increase 
const { transformFromAst } = require('@babel/core')

class Parser{
  static ast(path){
    const content = fs.readFileSync(path, 'utf-8') //  Read the file 
    const _ast = parser.parse(content, {
      sourceType: 'module' // What we want to analyze is ES modular 
    })
    console.log(_ast)
    console.log(' I am a body Content ', _ast.program.body)
    return _ast
  }
  static getDependecy(ast, file) {
    const dependecies = {}
    traverse(ast, {
      ImportDeclaration: ({node}) => {
        const oldValue = node.source.value
        const dirname = path.dirname(file)
        const relativepath = "./" + path.join(dirname, oldValue) 
        dependecies[oldValue] = relativepath
        node.source.value = relativepath //  take  ./data.js  Turn it into  ./src/data.js
      }
    })
    return dependecies
  }
  //  increase 
  static transform(ast) {
    const { code } = transformFromAst(ast, null, {
        presets: ['@babel/preset-env']
    })
    return code
  }
}
module.exports = Parser
 Copy code 
// selfpack/compilation.js
  ...
  buildModule(absolutePath, isEntry) {
    let ast = ''
    ast = Parser.ast(absolutePath)
    const relativePath = './' + path.relative(this.root, absolutePath)
    if(isEntry){
      this.entryId = relativePath  //  Save the file path of the main entry 
    }
    const dependecies = Parser.getDependecy(ast, relativePath)
    //  increase 
    const transformCode = Parser.transform(ast)
    console.log(" The converted code  ", transformCode)
    return {
      relativePath,
      dependecies,
      transformCode 
    }
  }
}
  ...
 Copy code 

Let's look at the results first :

getTransformFromAst

You can see const Successfully converted to var, however require("./data.js") The path of the reference has not been linked to modules Of key bring into correspondence with .

4.5 Recursively collect dependencies

How can we determine what information a module should contain ?
First, make sure that the file is unique , So we need the file path that we want , Because this is the only one .
And then analyze the contents of the file :

  • Whether other documents have been introduced
  • The main content of their own

So the module information we need is as follows :

  • The path of the module
  • The module depends on
  • The code after the module conversion

Here we get the converted code , And in buildModule Return an object , The structure of the return value is as follows :

//  Module information obtained 
  {
    relativePath: './src/xxx',
    dependecies: {
      './data.js': './src/data.js',
      './random.js': './src/random.js'
    },
    transformCode: {
      ...
    }
  }
 Copy code 

however buildModule Only one module dependency can be collected , And our ultimate goal is to collect all the dependencies , So we're going to do a recursive process . Revise it compiler.js

// selfpack/compiler.js
  ...
  compile() {
    const compilation = new Compilation(this)
     // adopt entry Find the entry file 
    const entryModule = compilation.buildModule(this.options.entry, true)

    //   increase 
    this.modules.push(entryModule)
    this.modules.map((_module) => {
      const deps = _module.dependecies
      for (const key in deps){
        if (deps.hasOwnProperty(key)){
          this.modules.push(compilation.buildModule(deps[key], false))
        }
      }
    })
    console.log(' The final  modules', this.modules)
  }
  ...
 Copy code 

Let's take a look first compile The recursive method in :

  1. Pass the main entry file into buildModule , Access to the main module
  2. The outer layer traverses the main entry file module
  3. Then get all the dependent modules of the main module
  4. Put the dependent module push To this.modules in

Take a look at the final modules

getModules

We have successfully got the : route 、 rely on 、 The converted code .

5、 ... and 、 Generate webpack Template file

The last step in compiling is Generate template file , And on the output Catalog .
We just borrowed the first paragraph of the article and packed it out dist/main.js The content of the document , And then make some changes .
Take a look at the revised compilation.js

// selfpack/compilation.js  complete 
const path = require('path')
const Parser = require('./Parser')
const fs = require('fs')


class Compilation {
  constructor(compiler) {
    //  modify 
    const { options, modules } = compiler
    this.options = options
    this.root = process.cwd() //  The current directory where the command is executed 
    this.entryId
    //  increase 
    this.modules = modules
  }
  buildModule(absolutePath, isEntry) {
    let ast = ''
    ast = Parser.ast(absolutePath)
    const relativePath = './' + path.relative(this.root, absolutePath)
    if(isEntry){
      this.entryId = relativePath
    }
    const dependecies = Parser.getDependecy(ast, relativePath)
    const transformCode = Parser.transform(ast)
    // console.log(" Dependencies ", dependecies)
    // console.log(" The converted code  ", transformCode)
    return {
      relativePath,
      dependecies,
      transformCode 
    }
  }
  //  increase 
  emitFiles(){
    let _modules = ''
    const outputPath = path.join(
      this.options.output.path,
      this.options.output.filename
    )
    this.modules.map((_module) => {
      //  Remember to quote 
      _modules += `'${_module.relativePath}': function(module, exports, require){
        ${_module.transformCode}
      },`
    })
    const template = `
    (function(modules) {
      var installedModules = {};
      function __webpack_require__(moduleId) {
        // Check if module is in cache
        if(installedModules[moduleId]) {
          return installedModules[moduleId].exports;
        }
        var module = installedModules[moduleId] = {
          exports: {}
        };
        modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
        
        return module.exports;
      }
       //  The entry function to execute 
      return __webpack_require__('${this.entryId}');
    })({
      ${_modules}
    })
    `
    const dist = path.dirname(outputPath)
    fs.mkdirSync(dist)
    fs.writeFileSync(outputPath, template, 'utf-8')
  }
}
module.exports = Compilation
 Copy code 

The contents of the packaged file , In general, it looks like this , There's a little flaw .
look down emitFiles Function function

  1. obtain selfpack.config.js Medium output Object's path,filename
  2. Traverse all modules And put it in the parameter input position of the template
  3. Create a new file , Write the compiled code to

complete compiler

// selfpack/compiler.js  complete 
const { SyncHook } = require('tapable')
const Compilation = require('./Compilation')

class Compiler {
  constructor(options) {
    this.modules = []
    this.options = options
    this.hooks = {
      run: new SyncHook()
    }
  }
  run() {
    this.compile()
  }
  compile() {
    const compilation = new Compilation(this)
    const entryModule = compilation.buildModule(this.options.entry, true)
    this.modules.push(entryModule)
    this.modules.map((_module) => {
      const deps = _module.dependecies
      for (const key in deps){
        if (deps.hasOwnProperty(key)){
          this.modules.push(compilation.buildModule(deps[key], false))
        }
      }
    })
    //  increase 
    compilation.emitFiles()
  }
}
module.exports = Compiler
 Copy code 

The compiled code is as follows :

// dist/main.js
(function (modules) {
  var installedModules = {};
  function __webpack_require__(moduleId) {
    // Check if module is in cache
    if (installedModules[moduleId]) {
      return installedModules[moduleId].exports;
    }
    var module = installedModules[moduleId] = {
      exports: {}
    };
    modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
    return module.exports;
  }
  //  The entry function to execute 
  return __webpack_require__('./src/index.js');
})({
  './src/index.js': function (module, exports, require) {
    "use strict";

    var _data = _interopRequireDefault(require("./src/data.js"));

    var _random = _interopRequireDefault(require("./src/random.js"));

    function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }

    console.log(' I'm a data file --->', _data["default"]);
    console.log(' I'm a random number --->', _random["default"]);
    console.log(' I am a index.js');
  }, './src/data.js': function (module, exports, require) {
    "use strict";

    Object.defineProperty(exports, "__esModule", {
      value: true
    });
    exports["default"] = void 0;
    var result = ' I'm the data in the file ';
    var _default = result;
    exports["default"] = _default;
  }, './src/random.js': function (module, exports, require) {
    "use strict";

    Object.defineProperty(exports, "__esModule", {
      value: true
    });
    exports["default"] = void 0;
    var random = Math.random();
    var _default = random;
    exports["default"] = _default;
  },
})
 Copy code 

Here's a simple one webpack The compilation process code is finished .
Copy the code to the browser and test it

6、 ... and 、 Realization webpack Of Plugins function

How to develop a custom plugins?
webpack In the internal implementation of their own set of life cycle , and plugins Just use apply To call webpack The life cycle that it provides .
and webpack Its life cycle is mainly tapable To achieve .
It's just... Here SyncHook, For more information, please refer to this article Tapable Detailed explanation .

Let's revise the official website ConsoleLogOnBuildWebpackPlugin.js Example .
stay src Create a new plugins

  + src
  - plugins
    - ConsoleLogOnBuildWebpackPlugin.js
 Copy code 

Write a simple one plugins

// ConsoleLogOnBuildWebpackPlugin.js
const pluginName = 'ConsoleLogOnBuildWebpackPlugin';

class ConsoleLogOnBuildWebpackPlugin {
  apply(compiler) {
    compiler.hooks.run.tap(pluginName, compilation => {
      console.log('The webpack build process is starting!!!');
    });
    //  Execute at the end of the file packaging 
    compiler.hooks.done.tap(pluginName,(compilation)=> {
      console.log(" Whole webpack End of packing ")
    })
    //  stay webpack When the file is output 
    compiler.hooks.emit.tap(pluginName,(compilation)=> {
        console.log(" The files start to fire ")
    })
  }
}
module.exports = ConsoleLogOnBuildWebpackPlugin;
 Copy code 

And then the configuration file introduces this plugins

// selfpack.config.js
const { join } = require('path')
const ConsoleLogOnBuildWebpackPlugin = require('./plugins/ConsoleLogOnBuildWebpackPlugin')

module.exports = {
  entry: join(__dirname, './src/index.js'),
  output: {
    path: join(__dirname, './dist'),
    filename: 'main.js'
  },
  plugins: [new ConsoleLogOnBuildWebpackPlugin()],
}
 Copy code 

Let's make our selfwebpack Support plugins , There are some changes to be made .

// selfpack/index.js
const Compiler = require('./Compiler')
const options = require('../selfpack.config.js')
const compiler = new Compiler(options)
const plugins = options.plugins
for (let plugin of plugins) {
    plugin.apply(compiler)
}
compiler.run()
 Copy code 
// selfpack/compiler.js
const { SyncHook } = require('tapable')
const Compilation = require('./Compilation')

class Compiler {
  constructor(options) {
    this.modules = []
    this.options = options
    this.hooks = {
      run: new SyncHook(),
      //  increase 
      emit: new SyncHook(),
      done: new SyncHook()
    }
  }
  run() {
    this.compile()
  }
  compile() {
    const compilation = new Compilation(this)
    //  increase 
    this.hooks.run.call()
     // adopt entry Find the entry file 
    const entryModule = compilation.buildModule(this.options.entry, true)
    this.modules.push(entryModule)
    this.modules.map((_module) => {
      const deps = _module.dependecies
      for (const key in deps){
        if (deps.hasOwnProperty(key)){
          this.modules.push(compilation.buildModule(deps[key], false))
        }
      }
    })
    // console.log(' The final  modules', this.modules)
    compilation.emitFiles()
    //  increase 
    this.hooks.emit.call()
    this.hooks.done.call()

  }
}
module.exports = Compiler
 Copy code 

stay compiler As soon as a function is initialized, it defines its own webpack Life cycle of , And in run During the corresponding call , In this way, we achieve our own life cycle .

The results are as follows :
runPlugins

This paper only implements the simple compiler principle , For more implementations, see webapck-github

The corresponding code is put here github

Reference article : Handwriting webpack The core principle

版权声明
本文为[Fisherman drying net every day]所创,转载请带上原文链接,感谢
https://cdmana.com/2020/12/20201224152607756V.html

Scroll to Top