實現一個簡易版Webpack

linjiajun發表於2020-04-06

原理

  • 1、解析一個檔案及其依賴
  • 2、構建一個依賴關係圖
  • 3、將所有東西打包成一個單檔案

程式碼實現

檔案結構

1、解析檔案及其依賴

通過babylon將檔案解析成AST 線上解析器

image

程式碼實現: bundle.js

const fs = require("fs");
const babylon = require("babylon");
const traverse = require("babel-traverse").default;

let ID = 0;

function createAsset(filename) {
  const content = fs.readFileSync(filename, "utf-8");
  // 解析檔案成AST
  const ast = babylon.parse(content, {
    sourceType: "module",
  });

  const dependencies = [];
  // 根據AST獲取相關依賴
  traverse(ast, {
    ImportDeclaration: ({ node }) => {
      dependencies.push(node.source.value);
    },
  });

  const id = ID++;

  return {
    id,
    filename,
    dependencies,
  };
}

const mainAssets = createAsset("./example/entry.js");

console.log(mainAssets)

複製程式碼

輸出結果:

image

2、構建一個依賴關係圖

// 構建一個依賴關係圖
function createGraph(entry) {
  const mainAssets = createAsset(entry);

  const queue = [mainAssets];

  for (const asset of queue) {
    const dirname = path.dirname(asset.filename);

    asset.mapping = {};

    asset.dependencies.forEach((relativePath) => {
      const absolutePath = path.join(dirname, relativePath);

      const child = createAsset(absolutePath);

      asset.mapping[relativePath] = child.id;

      queue.push(child);
    });
  }
  return queue;
}

const graph = createGraph("./example/entry.js");
console.log(graph);
複製程式碼

輸出結果:

image

3、將所有東西打包成一個單檔案

在解析檔案時,使用babel對程式碼進行轉譯

// 解析一個檔案及其依賴
function createAsset(filename) {
  const content = fs.readFileSync(filename, "utf-8");
  const ast = babylon.parse(content, {
    sourceType: "module",
  });

  const dependencies = [];
  traverse(ast, {
    ImportDeclaration: ({ node }) => {
      dependencies.push(node.source.value);
    },
  });

  const id = ID++;
  // 使用babel對程式碼進行轉譯
  const { code } = babel.transformFromAst(ast, null, {
    presets: ["env"],
  });

  return {
    id,
    filename,
    dependencies,
    code,
  };
}
複製程式碼
// 將所有東西打包成一個單檔案
function bundle(graph) {
  let modules = "";

  graph.forEach((mod) => {
    modules += `${mod.id}:[
      function(require,module,exports){
        ${mod.code}
      },
      ${JSON.stringify(mod.mapping)}
    ],`;
  });
  const result = `
   (function(modules){
     function require(id){
       const [fn, mapping] = modules[id];
        
        // 因為程式碼引入檔案時根據相對路徑,所以需要把相對路徑跟id進行一個對映
       function localRequire(relativePath){
         return require(mapping[relativePath])
       }

       const module = {exports:{}};

       fn(localRequire,module,module.exports)

       return module.exports;
     }
     // 執行入口模組
     require(0);
   })({${modules}})
   `;

  return result;
}

const graph = createGraph("./example/entry.js");
const result = bundle(graph);
console.log(result);

複製程式碼

輸出結果:

(function(modules) {
    function require(id) {
        const [fn, mapping] = modules[id];

        function localRequire(relativePath) {
            return require(mapping[relativePath])
        }

        const module = {
            exports: {}
        };

        fn(localRequire, module, module.exports)

        return module.exports;
    }
    require(0);
})({
    0: [
        function(require, module, exports) {
            "use strict";

            var _message = require("./message.js");

            var _message2 = _interopRequireDefault(_message);

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

            console.log(_message2.default);
        },
        {
            "./message.js": 1
        }
    ],
    1: [
        function(require, module, exports) {
            "use strict";

            Object.defineProperty(exports, "__esModule", {
                value: true
            });

            var _name = require("./name.js");

            exports.default = "hello " + _name.name + "!";
        },
        {
            "./name.js": 2
        }
    ],
    2: [
        function(require, module, exports) {
            "use strict";

            Object.defineProperty(exports, "__esModule", {
                value: true
            });
            var name = exports.name = 'Aaron';
        },
        {}
    ],
})
複製程式碼

把程式碼複製到瀏覽器執行,執行成功!

image

一個簡易版的Webapck完成了。

相關連結

例子原始碼 視訊教程 babylon babel-traverse docs

相關文章