In this paper, starting from mp.weixin.qq.com/s/nuSmPllrX…

For more information, please follow our official account: Front-end Architect Notes

Hand to hand implementation of a Webpack

In the usual work and learning process, Webpack is an important knowledge point, this paper through the analysis of the packaging principle of Webpack, finally take you to achieve a simple version of Webpack.

Webpack module loading mechanism

Before we get to the module loading mechanism of WebPack, let’s take a look at what a simple project looks like when packaged with WebPack.

Create an example directory, and create three files a.js, B.js and index.js in this directory. In order to study the specific packaging results, all modules are loaded by commonJS module, and the principle of es6 module is similar.

a.js:

module.exports = 'I am module a';
Copy the code

b.js:

module.exports = 'I am module b';
Copy the code

index.js:

const a = require('./a');
const b = require('./b');

function main() {
  console.log('I am entry module');
  console.log(a);
  console.log(b);
}

main();

module.exports = main;
Copy the code

Install webpack and webpack- CLI:

⚠️ Note: We chose the Webpack 4.x version for the demo.

$yarn add [email protected] [email protected] -dCopy the code

Add a WebPack configuration file

The Webpack packaging mode mode is set to Development mode, which makes the packaged JS code easy to view.

Webpack. Config. Js:

const path = require('path');

module.exports = {
  entry: './src/index.js'.mode: 'development'.output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'bundle.js'}};Copy the code

Add a Webpack build command to your project’s package.json and specify the webPack-packed configuration file.

package.json:

{
  "scripts": {
    "build": "webpack --config webpack.config.js"}}Copy the code

Executing $YARN Run build now generates the packaged result in the bundle.js file in the dist directory.

By removing comments and temporarily unavailable code, the entire bundle.js can be simplified as follows:

(function(modules) {
  // The module cache
  var installedModules = {};

  // The require function
  function __webpack_require__(moduleId) {
    // Check if module is in cache
    if(installedModules[moduleId]) {
        return installedModules[moduleId].exports;
    }
    // Create a new module (and put it into the cache)
    var module = installedModules[moduleId] = {
        i: moduleId,
        l: false.exports: {}};// Execute the module function
    modules[moduleId].call(module.exports, module.module.exports, __webpack_require__);

    // Flag the module as loaded
    module.l = true;

    // Return the exports of the module
    return module.exports;
  }

  return __webpack_require__(__webpack_require__.s = "./src/index.js"); ({})"./src/a.js": (function(module.exports) {
    eval("module.exports = 'I am module a'; \n\n//# sourceURL=webpack:///./src/a.js?");
  }),
  "./src/b.js": (function(module.exports) {
    eval("module.exports = 'I am module b'; \n\n//# sourceURL=webpack:///./src/b.js?");
  }),
  "./src/index.js": (function(module.exports, __webpack_require__) {
    eval("const a = __webpack_require__(/*! ./a */ \"./src/a.js\"); \nconst b = __webpack_require__(/*! ./b */ \"./src/b.js\"); \n\nfunction main() {\n console.log('I am entry module'); \n console.log(a); \n console.log(b); \n}\n\nmain(); \n\nmodule.exports = main; \n\n\n//# sourceURL=webpack:///./src/index.js?"); })});Copy the code

The structure is clear. All the logic is in a function called WebPack Bootstrap. The structure is as follows:

(function(modules){
  // body({})"a.js": (function(){}),
  "b.js": (function(){}),
  // ...
});
Copy the code

The immediate function argument is an object, the key of the object is the file path (key is actually a globally unique identifier, not necessarily the file path in production mode), and the value is the specific content of the file.

Next, look at the body of the function that executes the function immediately. The whole function body forms a closure inside and defines a closure variable installedModules, which is used to cache all modules that have been loaded.

var installedModules = {};
Copy the code

Define a __webpack_require__ function to aid in loading modules. The function takes a module ID as an input parameter. Let’s see what __webpack_require__ does.

  • checkinstalledModulesObject if there is already a cache, and if there is, return the cached module directly.
// Check if module is in cache
if(installedModules[moduleId]) {
  return installedModules[moduleId].exports;
}
Copy the code
  • If the cache does not exist, define an object to mount toinstalledModulesObject,keyIs the id of the module,valueIs an object that containsi,lexportsThree values that record the id of the module, the flag bit to mark whether the module has been loaded, and the return of the module after execution.
// Create a new module (and put it into the cache)
var module = installedModules[moduleId] = {
    i: moduleId,
    l: false.exports: {}};Copy the code
  • modules[moduleId].call()Execution module. The first parametermodule.exportsSpecifies the context in which to execute the module, and passes in the default argument, the result of the execution will hangmodule.exportsOn.
// Execute the module function
modules[moduleId].call(module.exports, module.module.exports, __webpack_require__);
Copy the code
  • Mark the module as loaded
// Flag the module as loaded
module.l = true;
Copy the code

The body of the function finally returns an execution result of __webpack_require__(“./ SRC /index.js”), where./ SRC /index.js specifies the entry file to load the entire module.

Implement a simplified version of WebPack

Now that we understand the module loading mechanism above, let’s implement a simple version of WebPack ourselves.

The initial idea is to provide a mini-Webpack command line, like WebPack, with the –confg parameter to get the specified WebPack configuration file and package it.

Initialization engineering

Create a project mini-Webpack and initialize the project.

$ mkdir mini-webpack
$ cd mini-webpack
$ yarn init -y
Copy the code

Add the SRC directory, create a mini-webpack file in the SRC directory, and define a Compiler class that provides a constructor and a run method.

export default class Compiler {
  constructor() {}

  run() {
    console.log('webpack running... ')}}Copy the code

To be able to write our code in typescript, we can compile our code using TSC.

$ yarn add typescript @types/node -D
Copy the code

Create the tsconfig.json configuration file in the root directory.

{
  "compilerOptions": {
    "target": "ES2015"."noImplicitAny": false."strictNullChecks": true."noUnusedLocals": true."noUnusedParameters": true."allowSyntheticDefaultImports": true."experimentalDecorators": true."moduleResolution": "node"."resolveJsonModule": true."removeComments": false."baseUrl": "."."outDir": "dist"."rootDir": "./src"."module": "commonjs"."sourceMap": true."skipLibCheck": true
  },
  "include": [
    "./src"]}Copy the code

Json, $NPM run build to compile our TS code and export it to dist.

{
  "scripts": {
    "build": "tsc"}},Copy the code

You can now provide the Mini-Webpack command line.

Create a bin directory in the root directory, create a mini-webpack file in the bin directory, reference the mini-Webpack file in the dist directory, instantiate the mini-webpack, and run the run method.

#! /usr/bin/env node

const MiniWebpack = require('.. /dist/mini-webpack').default

new MiniWebpack().run();
Copy the code

Add a bin field to the package.json file pointing to bin/mini-webpack.

{
  "bin": {
    "mini-webpack": "bin/mini-webpack"}},Copy the code

$./bin/mini-webpack in the mini-webpack directory to call mini-webpack’s run method and print the log.

If you want to use the mini-webpack command line to compile the Example project, you can run NPM link under the mini-webpack directory, then run NPM link mini-webpack under the example directory. Add a compile instruction to package.json in the example directory.

{
  "scripts": {
    "build": "mini-webpack --config webpack.config.js"}},Copy the code

Thus, $NPM run build is executed in the example directory to begin compiling.

Core packaging module implementation

First we need to parse the parameters passed from the command line to get the webPack configuration file.

Define a parseArgs method and call it in the constructor to resolve the command line arguments through minimist and get the packaged entry, output directory, and other configuration items.

import * as minimist from 'minimist';
import * as path from 'path';

export default class Compiler {
  private config;
  private cwd;
  private entry;
  private outputDir;
  private outputFilename;
  constructor() {
    this.cwd = process.cwd();
    this.config = this.parseArgs();
    this.entry = this.config.entry;
    this.outputDir = this.config.output.path;
    this.outputFilename = this.config.output.filename || 'bundle.js';
  }

  parseArgs() {
    const args = minimist(process.argv.slice(2))
    const { config = 'webpack.config.js' } = args;
    const configPath =  path.resolve(this.cwd, config);
    return require(configPath);
  }

  run() {
    console.log('webpack running... ')}}Copy the code

As described in the webpack module loading mechanism in the previous section, the bundled bundle.js structure looks something like this:

(function(modules){
  var installedModules = {};

  function __webpack_require__(moduleId) {
    // ...
  }

  return __webpack_require__("./src/index.js"); ({})"a.js": (function(){}),
  "b.js": (function(){}),
  // ...
});
Copy the code

So, the next step is to find a way to generate such a string and output it to the directory specified by Output. Two parts of this string are dynamically generated: an entry to the immediately executed function, which is a list of resources, and a webpack entry. To make it easier to generate formatted strings, I’ve chosen to use Handlebars to generate templates here.

Define a generateCode method that takes a resource list and a package entry and generates an output string.

Install the handlebars:

$ yarn add handlebars
Copy the code

The input parameter sourceList is an array of the following structure:

[{path: "./src/a.js".code: "module.exports = 'I am module a';"}, {path: "./src/b.js".code: "module.exports = 'I am module b';"}, {path: "./src/index.js".code: "const a = __webpack_require__(/*! ./a */ \"./src/a.js\"); \nconst b = __webpack_require__(/*! ./b */ \"./src/b.js\"); \n\nfunction main() {\n console.log('I am entry module'); \n console.log(a); \n console.log(b); \n}\n\nmain(); \n\nmodule.exports = main;",},]Copy the code

GenerateCode method:

import * as path from 'path';
import * as fs from 'fs';
import * as Handlebars from 'handlebars';

export default class Compiler {
  // ...
  generateCode(sourceList, entryPath) {
    const tplPath = path.join(__dirname, '.. /templates'.'bundle.hbs');
    const tpl = fs.readFileSync(tplPath, 'utf-8');
    const template = Handlebars.compile(tpl);

    const data = {
      entryPath,
      sourceList,
    };

    const bundleContent = template(data);

    fs.writeFileSync(path.join(this.outputDir, this.outputFilename), bundleContent, { encoding: 'utf8' });
  }
  // ...
}
Copy the code

Bundle. HBS template file:

(function(modules) {
  // The module cache
  var installedModules = {};

  // The require function
  function __webpack_require__(moduleId) {
    // Check if module is in cache
    if(installedModules[moduleId]) {
        return installedModules[moduleId].exports;
    }
    // Create a new module (and put it into the cache)
    var module = installedModules[moduleId] = {
        i: moduleId,
        l: false.exports: {}};// Execute the module function
    modules[moduleId].call(module.exports, module.module.exports, __webpack_require__);

    // Flag the module as loaded
    module.l = true;

    // Return the exports of the module
    return module.exports;
  }

  return __webpack_require__(__webpack_require__.s = "{{{ entryPath }}}");
})
({
  {{#each sourceList}}
    "{{{path}}}": (function(module.exports, __webpack_require__) {
      eval(`{{{code}}}`);
    }),
  {{/each}}
});
Copy the code

With that in mind, all you need to do is get the resource list and the whole WebPack compilation and packaging process is ready to go.

Webpack packaging usually starts with an entry as a starting point to build its internal dependency graph. After reading the entry file, it will find out which files the import file depends on. After finding these dependency files, it will record them. Then find out which files the dependencies depend on, until finally all the dependencies have been found, and generate a complete dependency graph.

This process introduces a new concept: how to analyze a file and parse the require or import syntax?

The answer is Babel.

Three of Babel’s packages are used here:

  • @babel/parser: Parses the code into an AST syntax tree
  • @babel/traverse: can be used to traverse the AST syntax tree generated by updating @babel/ Parser
  • @babel/generator: Generates code according to the AST
  • @babel/types: provides some utility class methods

Install the above dependencies:

$ yarn add @babel/parser @babel/traverse @babel/generator @babel/types
Copy the code

Define a build method that reads the source file based on the module path passed in. Then call this.parseModule, passing in the source file and parent directory of the current module, and get the sourceCode sourceCode regenerated via @babel/generator. And moduleList, a list of dependencies in that file, and then push the collected data into the sourceList list, and recursively follow the moduleList list of dependencies.

The build method:

build(modulePath) {
  const code = fs.readFileSync(modulePath, 'utf-8');

  const { sourceCode, moduleList } = this.parseModule(code, path.dirname(modulePath));

  this.sourceList.push({
    path: `. /${path.relative(this.cwd, modulePath)}`.code: sourceCode,
  });

  if(moduleList.length ! = =0) {
    moduleList.forEach(m= > this.build(path.resolve(this.cwd, m))); }}Copy the code

The parseModule method mainly involves some processing of source files by Babel. Pass @babel/parser to ast syntax tree. Then pass @babel/traverse through ast syntax tree to find require statements (import statements are similar). Replace the require method name with the __webpack_require__ method name, and convert the path of function arguments to a relative path starting with./ SRC.

ParseModule method:

parseModule(code, parentPath) {
  const relativePath = path.relative(this.cwd, parentPath);

  const ast = parser.parse(code);
  let moduleList: Array<string> = [];
  traverse(ast, {
    CallExpression({ node }) {
      if (node.callee.name === 'require') {
        node.callee.name = '__webpack_require__';
        let moduleName = node.arguments[0].value;

        moduleName = path.extname(moduleName) ?  moduleName : moduleName + '.js';
        moduleName = `. /${path.join(relativePath, moduleName)}`; node.arguments = [types.stringLiteral(moduleName)]; moduleList.push(moduleName); }}});const sourceCode = generator(ast).code;
  
  return {
    sourceCode,
    moduleList,
    }
}
Copy the code

$NPM run build = $NPM run build = $NPM run build = $NPM run build The bundle.js file is generated in the dist directory.

run() {
  this.build(path.join(this.cwd, this.entryPath));
  this.generateCode(this.sourceList);
}
Copy the code

conclusion

From the introduction of the above chapter, it can be seen that the packaging principle of WebPack is not very complicated. After understanding the packaging principle, it will be natural to implement a WebPack packaging tool. Of course, here is only a minimal webpack packaging tool, the real Webpack packaging will also involve loader, plug-in system and a series of complex work, especially webpack plug-in system for understanding front-end engineering is still of great benefit, For example, there are some well-known open source frameworks in the industry, such as UMI and Taro. Interested students can read the relevant source code.

Refer to the link

The sample source code

  • How does Webpack packaging work? You will understand after reading this article!
  • How does Webpack work?
  • In-depth understanding of the Webpack file packaging mechanism