手寫簡易webpack

散一群逗逼發表於2022-01-04

編譯產物分析

 (() => {
   // 模組依賴
 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");
 })()

以上程式碼是通過精簡過的,可以看到以下工具函式

  • __webpack_modules__:是一個物件,它的值是所有模組的程式碼,key值對應是模組檔案路徑
  • __webpack_module_cache__: 快取exports的值
  • __webpack_require__:載入模組程式碼,根據模組檔案路徑
  • __webpack_exports__:模組對外暴露方法

通過以上工具方法,就可以在瀏覽器run起來;從原始碼es6、es7 新特性新寫法,都需要轉成瀏覽器可識別的程式碼;

如:

// es6
import 

// es5 
__webpack_require__

webpack通過自定義__webpack_require__、__webpack_exports__ ...,實現多個模組程式碼打包。

接下來將按照上述邏輯,來構建簡易版的webpack,通過以下幾個階段

  1. 配置資訊
  2. 依賴構建
  3. 生成模版程式碼
  4. 生成檔案

配置資訊

class Compiler {
  constructor(config) {
    // 獲取配置資訊
    this.config = config;
    // 儲存入口路徑
    this.entryId;
    // 模組依賴關係
    this.modules = {};
    // 入口路徑
    this.entry = config.entry;
    // 工作路徑
    this.root = process.cwd();
  }

構建依賴

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)
    })
  }

通過buildModule解析原始碼,形成模組依賴this.modules[moduleName

  • 找到模組原始碼this.getSource(modulePath);
  • 解析原始碼,轉換ast,返回原始碼和模組依賴路徑this.parse(source, path.dirname(moduleName))
  • 生成路徑與模組程式碼物件:this.modules[moduleName] = sourceCode
  • 對模組中有依賴的檔案,形成迭代呼叫this.buildModule(path.join(this.root, dep), false)重新執行以上方法

解析原始碼

  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
    }

  }

解析模組原始碼,替換require方法成__webpack_require__,同時把檔案路徑也轉換掉

程式碼生成模版

// 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.modulesthis.entryId資料,傳入此模版中,生成可執行程式碼

生成檔案

  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;
  }

根據路徑獲取原始碼,判斷當前路徑是否能匹配上loader檔案test.test(modulePath)

如果可以匹配,將模組原始碼傳入,loader方法中,再做其他轉換 content = loader(content);並且形成遞迴呼叫;

// 自定義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;

配置檔案

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

從形態上看,外掛通常是一個帶有 apply 函式的類:

class SomePlugin {
    apply(compiler) {
    }
}

apply 函式執行時會得到引數 compiler ,以此為起點可以呼叫 hook 物件註冊各種鉤子回撥,

例如:compiler.hooks.make.tapAsync ,這裡面 make 是鉤子名稱,tapAsync 定義了鉤子的呼叫方式,

webpack 的外掛架構基於這種模式構建而成,外掛開發者可以使用這種模式在鉤子回撥中,插入特定程式碼

配置檔案

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();
  }

plugin 核心就是tapable採用釋出/訂閱的模式,先蒐集/訂閱外掛中所需要回撥,在webpack生命週期中去執行,這樣外掛就可以在使用的時機,獲取想要的上下文,從而進行干預以及其他操作。

以上就是各個階段關鍵核心程式碼部分

完整程式碼

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連結:
https://github.com/NoahsDante...
如果對你有幫助,點個start

相關文章