preface

MyWebpack is mainly implemented for a better understanding of the workflow in WebPack, everything is the simplest implementation, does not contain details and boundary processing, involves the AST abstract syntax tree and compiled code part, it is best to print out and observe, easy to understand later.

Webapck in the React project

To better understand the implementation process of Webpack, we can first observe Webpack in the React project structure. Of course, we need to have a new project first, which can be obtained through the following steps:

  • Use the create-react-app project name command to create a project
  • Enter the corresponding project directory and runnpm run ejectCommand pull,reactProject andwebapckRelated Configurations

Compared to the normal React project, there are two extra directories called config and scripts

The config directory mainly stores webPack-related configuration content:

The scripts directory contains the three default scripts stored in pakage.json:

In the build.js file in the scripts directory, webpack is introduced and the compiler object is obtained by calling webpack(config). Finally, compiler.run(callback) is used to package the compiler object. The specific location in the code is as follows:

Webpack executes the process

Through the combination of the directory structure in the React project and the Node interface content in Webpack, the following stages can be obtained:

  • Initialize Compiler — webpack(config) to get the Compiler object
  • Start compiling – The Compiler object run() method is called to start compiling
  • Identify entry – Locate all entry files based on entry in the configuration
  • Compile module – triggered from the entry file, call all configured Laoder to compile the module, find the modules that the module depends on, and recursively specify that all modules are loaded in
  • Complete module compilation – After compiling all modules using Loader during module compilation, the final output of each module is obtained
  • Output resourcesAssemble multiple modules one by one according to the relationship between entry and moduleChunkAnd then put eachChunkConvert to a separate file to add to the output list

    PS: Output resources this step is the last chance to modify the output

  • Output complete: After determining the output content, determine the output path and file name according to the configuration, and write the file content to the file system

Implement myWebpack

The preparatory work

A simple my-webpac project structure can be derived from the directory structure in the React project:

  • configDirectory — where iswebpack.config.jsThe related configuration
  • scriptDirectory — where isscriptWhat the script needs to executejsfile
  • libDirectory — that’s where you put itmyWebpackLibrary (need to implement yourself)
  • srcCatalog — that’s itwebpack.config.jsIn the default entry file directory, whichindex.jsFor entry files, othersjsFiles belong to the test packagejsThe module

The directory structure may be modified a little later in order to better achieve modularity. The initial file structure is as follows:

My - webpack ├ ─ config │ └ ─ webpack. Config. Js ├ ─ lib │ └ ─ myWebpack │ └ ─ index. The js ├ ─ package - lock. Json ├ ─ package. The json ├ ─ Script │ └ ─ build. Js └ ─ SRC ├ ─ the add. Js ├ ─ desc. Js └ ─ index, jsCopy the code

Begin to implement

Simple configurationconfigWebpack.config.js in the directory

// config/webpack.config.js

module.exports = {
    entry: "./src/index.js".output: {
        path: 'dist'.filename: 'index.js'}};Copy the code

implementationscriptIn the directorybuild.js

Js and webapck.config.js files, passing config into myWebpack() and executing the compiler object. Finally, execution of the packaged handler begins with Compiler.run ()

// script/build.js

const myWebpack  = require('.. /lib/myWebpack'); // require('.. /lib/myWebpack/index.js')
const config  = require('.. /config/webpack.config.js');

// Get the Compiler object
const compiler = myWebpack(config);

// Start packing
compiler.run();
Copy the code

implementationlibIn the directorymyWebpackThe specific content of (that is, under its directoryindex.js)

From the way myWebpack is used in build.js, it can be seen that myWebpack must be a function and its return value must be an instance object of the Compiler class, which is called a Compiler object. In order to better modularity, we created a compiler.js file in the lib/myWebpack directory, which specifically implemented the Compiler class related logic.

In lib/myWebpack/index.js, implement myWebpack, introduce Compiler class, and return the result of new Compiler(config) :

// lib/myWebpack/index.js

const Compiler = require('./Compiler.js')

function myWebpack(config) {
  return new Compiler(config)
}

module.exports = myWebpack
Copy the code

implementationlibdirectorymyWebpackIn thecompiler.jscontent

Based on how compiler objects are used in build.js, compiler classes must exist in compiler.js, and the run() method must exist, and a few things need to be done in the run() method can be summarized as follows:

  • According to theentryPath in the configuration to parse the file toastAbstract syntax tree
  • According to theastCollect dependencies to store customizationsdepsOn the object
  • According to theastCompiled to run properly in the browsercodecontent
  • And get it compiledcodethroughoutputThe configuration of thepathfilenameWrite to the file system

Among them, the first three steps belong to the content of compilation and parsing, so the specific logic can be implemented in lib/myWebpack/parser.js and exposed to the corresponding content, and processed by the build() method in the Compiler class. As a final step, the output resources can be pulled away from the Generate () method in the Compiler class.

// lib/myWebpack/compiler.js

const { getAst, getDeps, getCode } = require('./parser.js')
const fs = require('fs')
const path = require('path')

class Compiler {
  constructor(options = {}) {
    // Webpack configures the object
    this.options = options
    // All dependent containers
    this.modules = []
  }

  // Start packing
  run() {
    // Get the path in options
    const filePath = this.options.entry

    // First build, get entry file information
    const fileInfo = this.build(filePath)

    // Save the file information
    this.modules.push(fileInfo)

    // Iterate over all dependencies
    this.modules.forEach((fileInfo) = > {

      {relativePath: absolutePath}
      const deps = fileInfo.deps
      for (const relativePath in deps) {
        // Get the corresponding absolute path
        const absolutePath = deps[relativePath]
        // Package the dependent files
        const fileInfo = this.build(absolutePath)
        // Save the packaged result to modules for later processing
        this.modules.push(fileInfo)
      }
      
    })

    // Organize the Modules array into a better dependency graph
    /* { 'index.js': { 'code': 'xxx', 'deps': { [relativePath]: [absolutePath] } } } */
    const depsGraph = this.modules.reduce(function (graph, module) {
      return {
        ...graph,
        [module.filePath]: {
          code: module.code,
          deps: module.deps,
        },
      }
    }, {})

    // Build the output from the dependency graph
    this.generate(depsGraph)
  }

  // Start building
  build(filePath) {
    // Parse the file into an AST abstract syntax tree
    const ast = getAst(filePath)

    {relativePath: absolutePath}
    const deps = getDeps(ast, filePath)

    // Compile into code according to the AST
    const code = getCode(ast)

    return {
      filePath, // Current file path
      deps, // All dependencies of the current file
      code, // The parsed code of the current file}}// Generate output resources
  generate(depsGraph) {
    const bundle = '(function(depsGraph){function require(module){var exports = {}; Function localRequire(relativePath){function localRequire(relativePath){function localRequire(relativePath){ Return require(depsGraph[module]. Deps [relativePath]); } (function(require, exports, code){ eval(code); })(localRequire, exports, depsGraph[module].code); Return exports; // return exports; // return exports; } require('The ${this.options.entry}'); }) (The ${JSON.stringify(depsGraph)});
      `
    const { output } = this.options
    const dirPath = path.resolve(output.path)
    const filePath = path.join(dirPath, output.filename)

    // Create a directory if the specified directory does not exist
    if(! fs.existsSync(dirPath)) { fs.mkdirSync(dirPath) }// Write to the file
    fs.writeFileSync(filePath, bundle.trim(), 'utf-8')}}module.exports = Compiler
Copy the code

Explanation of the contents of the bundle variable in the generate() method

  • The main purpose of this implementation is to generate an independent scopejsThe modular
  • One of therequire()The way is throughevalFunction to execute, compiledcodeBecause the compiledcodeIt’s a stringjscode
  • require()In the methodlocalRequire ()The method actually performs orrequire()Method itself, but does some processing to the current module path, which is actually recursion
  • require()The method also has an immediate anonymous function that takes three arguments:require, exports, code, includingcodeParameters are easy to understand, but why do we need to passrequire, exportsParameters?
    • We can do that by looking at what’s compiledcodeFor example, import filesindex.jsAnd the introduction into itadd.jsCompiling result ofcodeAs follows:
// the result of compiling the index.js content => The require method is required here, so the external must be passed in
"'use strict';
var _add = _interopRequireDefault(require('./add.js'));
var _desc = _interopRequireDefault(require('./desc.js'));
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }
console.log('add = ', (0, _add['default'])(1, 2));
console.log('desc = ', (0, _desc['default'])(3, 1));"

// Add.js content compiler result => Here we need to use exports object, so external must be passed in
"'use strict'; Object.defineProperty(exports, '__esModule', { value: true}); exports['default'] = void 0; function add(x, y) { return x + y; } var _default = add; exports['default'] = _default;"
Copy the code

implementationlibdirectorymyWebpackIn theparser.jscontent

Here are three things you need to do:

  • According to theentryPath in the configuration to parse the file toastAbstract syntax tree, need to help@babel/parserIn theparsemethods
  • According to theastCollect dependencies to store customizationsdepsObject, with help@babel/traversetraverseastIn theprogram.bodyTo facilitate collecting dependencies at specific times
  • According to theastCompiled to run properly in the browsercodeContent, need help@babel/coreIn thetransformFromAstmethods
// lib/myWebpack/parser.js

const babelParser = require('@babel/parser')
const { transformFromAst } = require('@babel/core')
const babelTraverse = require('@babel/traverse').default
const path = require('path')
const fs = require('fs')

const parser = {
  getAst(filePath) {
    // Read the entry file through options.entry
    const file = fs.readFileSync(filePath, 'utf-8')

    // Parse the entry file contents into ast -- abstract syntax tree
    const ast = babelParser.parse(file, {
      sourceType: 'module'.// Process the ES module in the parsed file
    })

    return ast
  },

  getDeps(ast, filePath) {
    // Get the path of the file folder
    const dirname = path.dirname(filePath)
    // A container for storing dependencies
    const deps = {}

    Collect dependencies based on the AST
    babelTraverse(ast, {
      // The body in the AST is iterated internally, executing according to the corresponding statement type
      // The ImportDeclaration(code) method is triggered when type === "ImportDeclaration"
      ImportDeclaration({ node }) {
        // Get the relative path of the current file
        const relativePath = node.source.value
        // Add dependencies: {relativePath: absolutePath}
        deps[relativePath] = path.resolve(dirname, relativePath)
      },
    })
    return deps
  },

  getCode(ast) {
    // Compile code: Compile syntax that is not recognized by the browser
    const { code } = transformFromAst(ast, null, {
      presets: ['@babel/preset-env'],})return code
  },
}

module.exports = parser
Copy the code

Final directory structure

My - webpack ├ ─ config │ └ ─ webpack. Config. Js ├ ─ lib │ └ ─ myWebpack │ ├ ─ compiler. Js │ ├ ─ index. The js │ └ ─ parser. Js ├ ─ Package - lock. Json ├ ─ package. Json ├ ─ README. Md ├ ─ script │ └ ─ build. Js └ ─ SRC ├ ─ the add. Js ├ ─ desc. Js └ ─ index, jsCopy the code