9

Compilation product analysis

 (() => {
   // 模块依赖
 var __webpack_modules__ = ({

     "./src/index.js":
         ((module, __unused_webpack_exports, __webpack_require__) => {
       // 执行模块代码其中 同时执行__webpack_require__ 引用代码
             eval(`const str = __webpack_require__("./src/a.js");

console.log(str);`);
         }),

     "./src/a.js":
         ((module, __unused_webpack_exports, __webpack_require__) => {
             eval(`const b = __webpack_require__("./src/base/b.js");

module.exports = 'a' + b;`);
         }),

     "./src/base/b.js":
         ((module, __unused_webpack_exports, __webpack_require__) => {
             eval(`module.exports = 'b';`);
         }),

 });
 var __webpack_module_cache__ = {};
 function __webpack_require__(moduleId) {
   // 获取_webpack_module_cache__ 是否有exports值 
     var cachedModule = __webpack_module_cache__[moduleId];
   // 如果已经有了,不用再执行模块代码
     if (cachedModule !== undefined) {
         return cachedModule.exports;
     }
     var module = __webpack_module_cache__[moduleId] = {
         exports: {}
     };
   // 根据moduleId 模块文件路径,找到模块代码并执行传入 module, module.exports, __webpack_require__
     __webpack_modules__[moduleId](module, module.exports, __webpack_require__);

     return module.exports;
 }
   // 执行入口文件代码
 var __webpack_exports__ = __webpack_require__("./src/index.js");
 })()

The above code is simplified, you can see the following tool functions

  • __webpack_modules__: is an object, its value is the code of all modules, and the key value corresponds to the module file path
  • __webpack_module_cache__: cache the value of exports
  • __webpack_require__: Load the module code, according to the module file path
  • __webpack_exports__: Module external exposure method

Through the above tools and methods, you can run in the browser; from the source code es6, es7 new feature and new writing, all need to be converted into the code that can be recognized by the browser;

like:

// es6
import 

// es5 
__webpack_require__

Webpack realizes multiple module code packaging by customizing __webpack_require__, __webpack_exports__...

Next, we will build a simple version of webpack according to the above logic, and go through the following stages

  1. Configuration information
  2. Dependent build
  3. Generate template code
  4. Generate file

Configuration information

class Compiler {
  constructor(config) {
    // 获取配置信息
    this.config = config;
    // 保存入口路径
    this.entryId;
    // 模块依赖关系
    this.modules = {};
    // 入口路径
    this.entry = config.entry;
    // 工作路径
    this.root = process.cwd();
  }

Build dependency

getSource(modulePath) {
    const rules = this.config.module.rules;
    let content = fs.readFileSync(modulePath, 'utf8');
    return content;
  }
buildModule(modulePath, isEntry) {
    // 拿到模块内容
    const source = this.getSource(modulePath);
    // 模块id
    const moduleName = './' + path.relative(this.root, modulePath);
    if (isEntry) {
      this.entryId = moduleName;
    }
    // 解析源码需要把source 源码进行改造,返回一个依赖列表
    const {sourceCode, dependencies} = this.parse(source, path.dirname(moduleName)); // ./src
    // 把相对路径和模块中的内容,对应起来
    this.modules[moduleName] = sourceCode;
    dependencies.forEach((dep) => { // 递归加载模块
      this.buildModule(path.join(this.root, dep), false)
    })
  }

Analyze the source code through buildModule this.modules[moduleName ;

  • Find the module source code this.getSource(modulePath);
  • Parse source code, convert ast, return source code and module dependency path this.parse(source, path.dirname(moduleName))
  • Generate path and module code object: this.modules[moduleName] = sourceCode
  • For the dependent files in the module, form an iterative call this.buildModule(path.join(this.root, dep), false) re-execute the above method

parsing source code

  parse(source, parentPatch) { // AST 解析语法树
    const ast = babylon.parse(source);
    let dependencies = []; // 依赖数组
    traverse(ast, {
      CallExpression(p) {
        const node = p.node;
        if (node.callee.name == 'require') {
          node.callee.name = '__webpack_require__';
          let moduleName = node.arguments[0].value; // 模块名字
          moduleName = moduleName + (path.extname(moduleName) ? '' : '.js'); // ./a.js
          moduleName = './' + path.join(parentPatch, moduleName); // src/a.js
          dependencies.push(moduleName);
          node.arguments = [t.stringLiteral(moduleName)];
        }
      }
    });
    const sourceCode = generator(ast).code;
    return {
      sourceCode, dependencies
    }

  }

Analyze the module source code, replace the require method with __webpack_require__ , and also convert the file path

Code generation template

// ejs  模版代码
(() => {
var __webpack_modules__ = ({
<%for(let key in modules){%>
    "<%-key%>":
    ((module, __unused_webpack_exports, __webpack_require__) => {
      eval(`<%-modules[key]%>`);
     }),
<%}%>
});
var __webpack_module_cache__ = {};
function __webpack_require__(moduleId) {
var cachedModule = __webpack_module_cache__[moduleId];
if (cachedModule !== undefined) {
return cachedModule.exports;
}
var module = __webpack_module_cache__[moduleId] = {
exports: {}
};

__webpack_modules__[moduleId](module, module.exports, __webpack_require__);

return module.exports;
}
var __webpack_exports__ = __webpack_require__("<%-entryId%>");
})()
;

this.modules and this.entryId will be transferred into this template to generate executable code

Generate file

  emitFile() {
    const {output} = this.config;
    const main = path.join(output.path, output.filename);
    // 模块字符串
    let templateStr = this.getSource(path.join(__dirname, 'main.ejs'));
    // 生成代码
    const code = ejs.render(templateStr, {entryId: this.entryId, modules: this.modules});
    this.assets = {};
    this.assets[main] = code;
    // 将代码写入output文件夹/文件
    fs.writeFileSync(main, this.assets[main])
  }

loader

将引用资源,转换成模块
 getSource(modulePath) {
    const rules = this.config.module.rules;
    let content = fs.readFileSync(modulePath, 'utf8');
    for (let i = 0; i < rules.length; i++) {
        const rule = rules[i];
        const {test,use} = rule;
        let len = use.length -1
        if(test.test(modulePath)) {
          function normalLoader() {
            const loader = require(use[len--]);
            content = loader(content);
            if(len >= 0) {
              normalLoader();
            }
          }
          normalLoader();
        }
    }
    return content;
  }

Obtain the source code according to the path, and judge whether the current path can match the loader file test.test(modulePath) ,

If it can be matched, pass in the module source code, in the loader method, do other conversions content = loader(content); and form a recursive call;

// 自定义loader

// less-loader
const {render} = require('less')
function loader(source) {
  let css = '';

  render(source,(err,c) => {
    css = c;
  })
  css = css.replace(/\n/g,'\\n')
  return css;
}

module.exports = loader;

// style-loader

function loader(source) {
 let style = `
  let style = document.createElement('style')
  style.innerHTML = ${JSON.stringify(source)}
  document.head.appendChild(style);
 `;

 return style;
}
module.exports = loader;

Configuration file

const path = require('path');
module.exports = {
  mode: 'development',
  entry: './src/index.js',
  output: {
    filename: 'bundle2.js',
    path: path.resolve(__dirname, 'dist')
  },
  module: {
    rules: [
      {
        test: /\.less$/,
        use:[
          path.resolve(__dirname,'loader','style-loader'), // 后执行
          path.resolve(__dirname,'loader','less-loader') // 先执行
        ]
      }
    ]
  },
}

plugin

From a morphological point of view, a plug-in is usually a class with an apply function:

class SomePlugin {
    apply(compiler) {
    }
}

The apply function will get the parameter compiler when it runs. From this as a starting point, you can call the hook object to register various hook callbacks.

For example: compiler.hooks.make.tapAsync, where make is the name of the hook, and tapAsync defines how the hook is called.

The plug-in architecture of webpack is built on this model. Plug-in developers can use this model to insert specific code in the hook callback.

Configuration file

const path = require('path');

class P {
  constructor() {

  }
  apply(compiler) {
    // 获取compiler上方法,注册各个阶段回调
    compiler.hooks.emit.tap('emit',function () {
      console.log('emit')
    })
  }
}

module.exports = {
  mode: 'development',
  entry: './src/index.js',
  output: {
    filename: 'bundle2.js',
    path: path.resolve(__dirname, 'dist')
  },
  plugins: [
    new P()
  ]
}

compiler.js

const {SyncHook} = require('tapable');
class Compiler {
  constructor(config) {
    this.config = config;
    // 保存入口路径
    this.entryId;
    // 模块依赖关系
    this.modules = {};
    // 入口路径
    this.entry = config.entry;
    // 工作路径
    this.root = process.cwd();
    // 开始注册同步发布订阅
    this.hooks = {
      entryOption:new SyncHook(),
      compile:new  SyncHook(),
      afterCompile:new SyncHook(),
      afterPlugins:new SyncHook(),
      run:new SyncHook(),
      emit:new SyncHook(),
      done:new SyncHook()
    };

    const plugins = this.config.plugins;
    // 拿到配置项里的plugin 
    if(Array.isArray(plugins)) {
      plugins.forEach((plugin) => {
        // 调用plugin 中实例方法 apply,并传入整个Compiler 类
        plugin.apply(this);
      })
    }
    this.hooks.afterPlugins.call();
  }

The core of the plugin is that tapable adopts the publish/subscribe model. First collect/subscribe the callbacks needed in the plug-in, and execute it in the webpack life cycle, so that the plug-in can obtain the desired context at the time of use, so as to intervene and perform other operations. .

The above is the key core code part of each stage

Complete code

const path = require('path');
const fs = require('fs');
const babylon = require('babylon');
const traverse = require('@babel/traverse').default;
const t = require('@babel/types');
const generator = require('@babel/generator').default;
const ejs = require('ejs');
const {SyncHook} = require('tapable');
// babylon 解析 js 转换 ast
// https://www.astexplorer.net/
// @babel/travers
// @babel/types
// @babel/generator
class Compiler {
  constructor(config) {
    this.config = config;
    // 保存入口路径
    this.entryId;
    // 模块依赖关系
    this.modules = {};
    // 入口路径
    this.entry = config.entry;
    // 工作路径
    this.root = process.cwd();

    this.hooks = {
      entryOption:new SyncHook(),
      compile:new  SyncHook(),
      afterCompile:new SyncHook(),
      afterPlugins:new SyncHook(),
      run:new SyncHook(),
      emit:new SyncHook(),
      done:new SyncHook()
    };

    const plugins = this.config.plugins;
    if(Array.isArray(plugins)) {
      plugins.forEach((plugin) => {
        plugin.apply(this);
      })
    }
    this.hooks.afterPlugins.call();
  }

  getSource(modulePath) {
    const rules = this.config.module.rules;
    let content = fs.readFileSync(modulePath, 'utf8');
    for (let i = 0; i < rules.length; i++) {
        const rule = rules[i];
        const {test,use} = rule;
        let len = use.length -1
        if(test.test(modulePath)) {
          function normalLoader() {
            const loader = require(use[len--]);
            content = loader(content);
            if(len >= 0) {
              normalLoader();
            }
          }
          normalLoader();
        }
    }
    return content;
  }

  parse(source, parentPatch) { // AST 解析语法树
    const ast = babylon.parse(source);
    let dependencies = []; // 依赖数组
    traverse(ast, {
      CallExpression(p) {
        const node = p.node;
        if (node.callee.name == 'require') {
          node.callee.name = '__webpack_require__';
          let moduleName = node.arguments[0].value; // 模块名字
          moduleName = moduleName + (path.extname(moduleName) ? '' : '.js'); // ./a.js
          moduleName = './' + path.join(parentPatch, moduleName); // src/a.js
          dependencies.push(moduleName);
          node.arguments = [t.stringLiteral(moduleName)];
        }
      }
    });
    const sourceCode = generator(ast).code;
    return {
      sourceCode, dependencies
    }

  }

  buildModule(modulePath, isEntry) {
    // 拿到模块内容
    const source = this.getSource(modulePath);
    // 模块id
    const moduleName = './' + path.relative(this.root, modulePath);
    if (isEntry) {
      this.entryId = moduleName;
    }
    // 解析源码需要把source 源码进行改造,返回一个依赖列表
    const {sourceCode, dependencies} = this.parse(source, path.dirname(moduleName)); // ./src
    // 把相对路径和模块中的内容,对应起来
    this.modules[moduleName] = sourceCode;
    dependencies.forEach((dep) => { // 递归加载模块
      this.buildModule(path.join(this.root, dep), false)
    })
  }

  emitFile() {
    const {output} = this.config;
    const main = path.join(output.path, output.filename);
    let templateStr = this.getSource(path.join(__dirname, 'main.ejs'));
    const code = ejs.render(templateStr, {entryId: this.entryId, modules: this.modules});
    this.assets = {};
    this.assets[main] = code;
    fs.writeFileSync(main, this.assets[main])
  }

  run() {
    this.hooks.run.call();
    this.hooks.compile.call();
    // 执行,并且创建模块的依赖关系
    this.buildModule(path.resolve(this.root, this.entry), true);
    this.hooks.afterCompile.call();
    // 发射一个文件,打包后的文件
    this.emitFile();
    this.hooks.emit.call();
    this.hooks.done.call();
  }
}

module.exports = Compiler;

github link:
https://github.com/NoahsDante/webpack-learn-dev
If it helps you, click start


散一群逗逼
554 声望508 粉丝

做一位有逼格的前端工程师