jsliang 求職系列 - 32 - Webpack 簡單實現

一 目錄

不折騰的前端,和鹹魚有什麼區別前端

目錄
一 目錄
二 前言
三 第一步 轉換代碼、生成依賴
四 第二步 生成依賴圖譜
五 第三步 生成代碼字符串

二 前言

返回目錄

參考文章:實現一個簡單的Webpacknode

Webpack 的本質就是一個模塊打包器,工做就是將每一個模塊打包成相應的 bundlegit

首先,咱們須要準備目錄:github

+ 項目根路徑 || 文件夾
  - index.js      - 主入口
  - message.js    - 主入口依賴文件
  - word.js       - 主入口依賴文件的依賴文件
  - bundler.js    - 打包器
  - bundle.js     - 打包後存放代碼的文件

最終的項目地址:all-for-one - 031-手寫 Webpackshell

若是小夥伴懶得敲,那能夠看上面倉庫的最終代碼。npm

而後,咱們 index.jsmessage.jsword.js 內容以下:數組

index.js
// index.js
import message from "./message.js";
console.log(message);
message.js
// message.js
import { word } from "./word.js";
const message = `say ${word}`;
export default message;
word.js
// word.js
export const word = "hello";

最後,咱們實現一個 bundler.js 文件,將 index.js 當成入口,將裏面牽扯的文件都轉義並執行便可!babel

實現思路:閉包

  1. 利用 babel 完成代碼轉換,並生成單個文件的依賴
  2. 生成依賴圖譜
  3. 生成最後打包代碼

下面分 3 章嘗試這個內容。函數

三 第一步 轉換代碼、生成依賴

返回目錄

這一步須要利用 babel 幫助咱們進行轉換,因此先裝包:

npm i @babel/parser @babel/traverse @babel/core @babel/preset-env -D

轉換代碼須要:

  1. 利用 @babel/parser 生成 AST 抽象語法樹
  2. 利用 @babel/traverse 進行 AST 遍歷,記錄依賴關係
  3. 經過 @babel/core@babel/preset-env 進行代碼的轉換

而後添加內容:

bundler.js
const fs = require("fs");
const path = require("path");
const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;
const babel = require("@babel/core");

// 第一步:轉換代碼、生成依賴
function stepOne(filename) {
  // 讀入文件
  const content = fs.readFileSync(filename, "utf-8");
  const ast = parser.parse(content, {
    sourceType: "module", // babel 官方規定必須加這個參數,否則沒法識別 ES Module
  });
  const dependencies = {};
  // 遍歷 AST 抽象語法樹
  traverse(ast, {
    // 獲取經過 import 引入的模塊
    ImportDeclaration({ node }) {
      const dirname = path.dirname(filename);
      const newFile = "./" + path.join(dirname, node.source.value);
      // 保存所依賴的模塊
      dependencies[node.source.value] = newFile;
    },
  });
  //經過 @babel/core 和 @babel/preset-env 進行代碼的轉換
  const { code } = babel.transformFromAst(ast, null, {
    presets: ["@babel/preset-env"],
  });
  return {
    filename, // 該文件名
    dependencies, // 該文件所依賴的模塊集合(鍵值對存儲)
    code, // 轉換後的代碼
  };
}

console.log('--- step one ---');
const one = stepOne('./index.js');
console.log(one);

fs.writeFile('bundle.js', one.code, () => {
  console.log('寫入成功');
});

經過 Node 的方式運行這段代碼:node bundler.js

--- step one ---
{
  filename: './index.js',
  dependencies: { './message.js': './message.js' },
  code:`
    "use strict";

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

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

    // index.js

    console.log(_message["default"]);
`,
}
  1. 入口 filenameindex.js
  2. 依賴 message.js
  3. 轉義代碼 code

因此 jsliangcode 提取到 bundle.js 中進行查看:

bundler.js
// ...代碼省略

fs.writeFile('bundle.js', one.code, () => {
  console.log('寫入成功');
});
bundle.js
"use strict";

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

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

// index.js
console.log(_message["default"]);

解讀下這個文件內容:

  • use strict:使用嚴格模式
  • _interopRequireDefault:對不符合 babel 標準的模塊添加 default 屬性,並指向自身對象以免 exports.default 出錯

因此如今這份文件的內容是能夠運行的了,可是你運行的時候會報錯,報錯內容以下:

import { word } from "./word.js";
       ^

SyntaxError: Unexpected token {

也就是說咱們執行到 message.js,可是它裏面的內容無法運行,由於 importES6 內容嘛。

咋整,繼續看下面內容。

四 第二步 生成依賴圖譜

返回目錄

既然咱們只生成了一份轉義後的文件:

--- step one ---
{
  filename: './index.js',
  dependencies: { './message.js': './message.js' },
  code:`
    "use strict";

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

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

    // index.js

    console.log(_message["default"]);
`,
}

那麼咱們能夠根據其中的 dependencies 進行遞歸,將整個依賴圖譜都找出來:

bundler.js
// ...省略前面內容

// 第二步:生成依賴圖譜
// entry 爲入口文件
function stepTwo(entry) {
  const entryModule = stepOne(entry);
  // 這個數組是核心,雖然如今只有一個元素,日後看你就會明白
  const graphArray = [entryModule];
  for (let i = 0; i < graphArray.length; i++) {
    const item = graphArray[i];
    const { dependencies } = item; // 拿到文件所依賴的模塊集合(鍵值對存儲)
    for (let j in dependencies) {
      graphArray.push(stepOne(dependencies[j])); // 敲黑板!關鍵代碼,目的是將入口模塊及其全部相關的模塊放入數組
    }
  }
  // 接下來生成圖譜
  const graph = {};
  graphArray.forEach((item) => {
    graph[item.filename] = {
      dependencies: item.dependencies,
      code: item.code,
    };
  });
  return graph;
}

console.log('--- step two ---');
const two = stepTwo('./index.js');
console.log(two);

let word = '';
for (let i in two) {
  word = word + two[i].code + '\n\n';
}
fs.writeFile('bundle.js', word, () => {
  console.log('寫入成功');
});

因此當咱們 node bundler.js 的時候,會打印內容出來:

--- step two ---
{
  './index.js': {
    dependencies: { './message.js': './message.js' },
    code: '"use strict";\n\nvar _message = _interopRequireDefault(require("./message.js"));\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }\n\n// index.js\nconsole.log(_message["default"]);'
  },
  './message.js': {
    dependencies: { './word.js': './word.js' },
    code:
      '"use strict";\n\nObject.defineProperty(exports, "__esModule", {\n  value: true\n});\nexports["default"] = void 0;\n\nvar _word = require("./word.js");\n\n// message.js\nvar message = "say ".concat(_word.word);\nvar _default = message;\nexports["default"] = _default;'
  },
  './word.js': {
    dependencies: {},
    code:
      '"use strict";\n\nObject.defineProperty(exports, "__esModule", {\n  value: true\n});\nexports.word = void 0;\n// word.js\nvar word = "hello";\nexports.word = word;' 
    }
}

能夠看到咱們將整個依賴關係中的文件都搜索出來,並經過 babel 進行了轉換,而後 jsliang 經過 Nodefs 模塊將其寫進了 bundle.js 中:

bundler.js
let word = '';
for (let i in two) {
  word = word + two[i].code + '\n\n';
}
fs.writeFile('bundle.js', word, () => {
  console.log('寫入成功');
});

再來看 bundle.js 內容:

bundle.js
"use strict";

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

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

// index.js
console.log(_message["default"]);

"use strict";

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports["default"] = void 0;

var _word = require("./word.js");

// message.js
var message = "say ".concat(_word.word);
var _default = message;
exports["default"] = _default;

"use strict";

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.word = void 0;
// word.js
var word = "hello";
exports.word = word;

跟步驟一的解析差很少,不過這樣子的內容是無法運行的,畢竟咱們塞到同一個文件中了,因此須要步驟三咯。

五 第三步 生成代碼字符串

返回目錄

最後一步咱們實現下面代碼:

bundler.js
// 下面是生成代碼字符串的操做
function stepThree(entry){
  // 要先把對象轉換爲字符串,否則在下面的模板字符串中會默認調取對象的 toString 方法,參數變成 [Object object],顯然不行
  const graph = JSON.stringify(stepTwo(entry))
  return `(function(graph) {
  // require 函數的本質是執行一個模塊的代碼,而後將相應變量掛載到 exports 對象上
  function require(module) {
    // localRequire 的本質是拿到依賴包的 exports 變量
    function localRequire(relativePath) {
      return require(graph[module].dependencies[relativePath]);
    }
    var exports = {};
    (function(require, exports, code) {
      eval(code);
    })(localRequire, exports, graph[module].code);
    return exports; // 函數返回指向局部變量,造成閉包,exports 變量在函數執行後不會被摧毀
  }
  require('${entry}')
})(${graph})
`;
};

console.log('--- step three ---');
const three = stepThree('./index.js');
console.log(three);

fs.writeFile('bundle.js', three, () => {
  console.log('寫入成功');
});

能夠看到,stepThree 返回的是一個當即執行函數,須要傳遞 graph

(function(graph) {
  // 具體內容
})(graph)

那麼圖譜(graph)怎麼來?須要經過 stepTwo(entry) 拿到了依賴圖譜。

可是,由於步驟二返回的是對象啊,若是直接傳進去對象,那麼就會被轉義,因此須要 JSON.stringify()

const graph = JSON.stringify(stepTwo(entry));
(function(graph) {
  // 具體內容
})(graph)

那爲何這個函數(stepThree)須要傳遞 entry?緣由在於咱們須要一個主入口,就比如 Webpack 單入口形式:

轉變先後
// 轉變前
const graph = JSON.stringify(stepTwo(entry));
(function(graph) {
  function require(module) {
    // ...具體內容
  }
  require('${entry}')
})(graph)

/* --- 分界線 --- */

// 轉變後
const graph = JSON.stringify(stepTwo(entry));
(function(graph) {
  function require(module) {
    // ...具體內容
  }
  require('./index.js')
})(graph)

這樣咱們就清楚了,從 index.js 入手,而後再看裏面具體內容:

function require(module) {
  // localRequire 的本質是拿到依賴包的 exports 變量
  function localRequire(relativePath) {
    return require(graph[module].dependencies[relativePath]);
  }
  var exports = {};
  (function(require, exports, code) {
    eval(code);
  })(localRequire, exports, graph[module].code);
  return exports; // 函數返回指向局部變量,造成閉包,exports 變量在函數執行後不會被摧毀
}
require('./index.js')

eval 是指 JavaScript 能夠運行裏面的字符串代碼,eval('2 + 2') 會出來結果 4,因此 eval(code) 就跟咱們第一步的時候,node bundle.js 同樣,執行 code 裏面的代碼。

因此咱們執行 require(module) 裏面的代碼,先走:

(function(require, exports, code) {
  eval(code);
})(localRequire, exports, graph[module].code);

此刻這個代碼中,傳遞的參數有 3 個:

  • require:若是在 eval(code) 執行代碼期間,碰到 require 就調用 localRequire 方法
  • exports:若是在 eval(code) 執行代碼期間,碰到 exports 就將裏面內容設置到對象 exports
  • graph[module].code:一開始 module'./index.js',因此查找 graph'./index.js' 對應的 code,將其傳遞進 eval(code) 裏面

有的小夥伴會好奇這代碼怎麼走的,咱們能夠先看下面一段代碼:

const localRequire = (abc) => {
  console.log(abc);
};

const code = `
  console.log(456);
  doRequire(123)
`;

(function(doRequire, code) {
  eval(code);
})(localRequire, code);

這段代碼中,執行的 doRequire 其實就是傳入進來的 localRequire 方法,最終輸出 456123

如今,再回頭來看:

區塊一: bundle.js
function require(module) {
  // localRequire 的本質是拿到依賴包的 exports 變量
  function localRequire(relativePath) {
    return require(graph[module].dependencies[relativePath]);
  }
  var exports = {};
  (function (require, exports, code) {
    eval(code);
  })(localRequire, exports, graph[module].code);
  return exports; // 函數返回指向局部變量,造成閉包,exports 變量在函數執行後不會被摧毀
}
require("./index.js");

它先執行 當即執行函數 (function (require, exports, code) {})(),再到 eval(code),從而執行下面代碼:

區塊二: graph['./index.js'].code
"use strict";

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

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

// index.js
console.log(_message["default"]);

在碰到 require("./message.js") 的時候,繼續進去上面【區塊一】的代碼,由於此刻的 require 是:

function localRequire(relativePath) {
  return require(graph[module].dependencies[relativePath]);
}

因此咱們再調用本身的 require() 方法,將內容傳遞進去,變成:require('./message.js')

……以此類推,直到 './word.js' 裏面沒有 require() 方法體了,咱們再執行下面內容,將 exports 導出去。

這就是這段內容的運行流程。

至於其中細節咱們就不一一贅述了,小夥伴們若是還沒看懂能夠自行斷點調試,這裏面的代碼口頭描述的話 jsliang 講得不是清楚。

最後咱們看看輸出整理後的 bundle.js

bundle.js
(function (graph) {
  // require 函數的本質是執行一個模塊的代碼,而後將相應變量掛載到 exports 對象上
  function require(module) {
    // localRequire 的本質是拿到依賴包的 exports 變量
    function localRequire(relativePath) {
      return require(graph[module].dependencies[relativePath]);
    }
    var exports = {};
    (function (require, exports, code) {
      eval(code);
    })(localRequire, exports, graph[module].code);
    return exports; // 函數返回指向局部變量,造成閉包,exports 變量在函數執行後不會被摧毀
  }
  require("./index.js");
})({
  "./index.js": {
    dependencies: { "./message.js": "./message.js" },
    code: `
      "use strict";
      
      var _message = _interopRequireDefault(require("./message.js"));
      
      function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }
      
      // index.js
      
      console.log(_message["default"]);
    `,
  },
  "./message.js": {
    dependencies: { "./word.js": "./word.js" },
    code: `
      "use strict";
      
      Object.defineProperty(exports, "__esModule", {
        value: true
      });
      
      exports["default"] = void 0;
      
      var _word = require("./word.js");
      
      // message.js
      
      var message = "say ".concat(_word.word);
      
      var _default = message;
      
      exports["default"] = _default;
    `,
  },
  "./word.js": {
    dependencies: {},
    code: `
      "use strict";
      
      Object.defineProperty(exports, "__esModule", {
        value: true
      });
      
      exports.word = void 0;
      
      // word.js
      
      var word = "hello";

      exports.word = word;',
  },
});

此時咱們 node bundle.js,就能夠獲取到:

say hello

這樣咱們就手擼完成了單入口的 Webpack 簡單實現。


jsliang 的文檔庫由 梁峻榮 採用 知識共享 署名-非商業性使用-相同方式共享 4.0 國際 許可協議 進行許可。<br/>基於 https://github.com/LiangJunrong/document-library 上的做品創做。<br/>本許可協議受權以外的使用權限能夠從 https://creativecommons.org/licenses/by-nc-sa/2.5/cn/ 處得到。
相關文章
相關標籤/搜索