面試官:webpack原理都不會?

引言

前一段時間我把webpack源碼大概讀了一遍,webpack4.x版本後,其源碼已經比較龐大,對各類開發場景進行了高度抽象,閱讀成本也愈發昂貴。html

過分分析源碼對於你們並無太大的幫助。本文主要是想經過分析webpack的構建流程以及實現一個簡單的webpack來讓你們對webpack的內部原理有一個大概的瞭解。(保證能看懂,不懂你打我 🙈)前端

webpack 構建流程分析

首先,無須多言,上圖~node

webpack 的運行流程是一個串行的過程,從啓動到結束會依次執行如下流程:首先會從配置文件和 Shell 語句中讀取與合併參數,並初始化須要使用的插件和配置插件等執行環境所須要的參數;初始化完成後會調用Compilerrun來真正啓動webpack編譯構建過程,webpack的構建流程包括compilemakebuildsealemit階段,執行完這些階段就完成了構建過程。webpack

初始化

entry-options 啓動

從配置文件和 Shell 語句中讀取與合併參數,得出最終的參數。es6

run 實例化

compiler:用上一步獲得的參數初始化 Compiler 對象,加載全部配置的插件,執行對象的 run 方法開始執行編譯web

編譯構建

entry 肯定入口

根據配置中的 entry 找出全部的入口文件json

make 編譯模塊

從入口文件出發,調用全部配置的 Loader 對模塊進行翻譯,再找出該模塊依賴的模塊,再遞歸本步驟直到全部入口依賴的文件都通過了本步驟的處理數組

build module 完成模塊編譯

通過上面一步使用 Loader 翻譯完全部模塊後,獲得了每一個模塊被翻譯後的最終內容以及它們之間的依賴關係瀏覽器

seal 輸出資源

根據入口和模塊之間的依賴關係,組裝成一個個包含多個模塊的 Chunk,再把每一個 Chunk 轉換成一個單獨的文件加入到輸出列表,這步是能夠修改輸出內容的最後機會微信

emit 輸出完成

在肯定好輸出內容後,根據配置肯定輸出的路徑和文件名,把文件內容寫入到文件系統

分析完構建流程,下面讓咱們本身動手實現一個簡易的webpack吧~

實現一個簡易的 webpack

準備工做

目錄結構

咱們先來初始化一個項目,結構以下:

|-- forestpack
    |-- dist
    |   |-- bundle.js
    |   |-- index.html
    |-- lib
    |   |-- compiler.js
    |   |-- index.js
    |   |-- parser.js
    |   |-- test.js
    |-- src
    |   |-- greeting.js
    |   |-- index.js
    |-- forstpack.config.js
    |-- package.json

這裏我先解釋下每一個文件/文件夾對應的含義:

  • dist:打包目錄
  • lib:核心文件,主要包括 compilerparser
    • compiler.js:編譯相關。 Compiler爲一個類, 而且有 run方法去開啓編譯,還有構建 modulebuildModule)和輸出文件( emitFiles
    • parser.js:解析相關。包含解析 ASTgetAST)、收集依賴( getDependencies)、轉換( es6轉es5
    • index.js:實例化 Compiler類,並將配置參數(對應 forstpack.config.js)傳入
    • test.js:測試文件,用於測試方法函數打 console使用
  • src:源代碼。也就對應咱們的業務代碼
  • forstpack.config.js:配置文件。相似 webpack.config.js
  • package.json:這個就不用我多說了~~~(什麼,你不知道??)

先完成「造輪子」前 30%的代碼

項目搞起來了,但彷佛還少點東西~~

對了!基礎的文件咱們須要先完善下:forstpack.config.jssrc

首先是forstpack.config.js

const path = require("path");

module.exports = {
  entry: path.join(__dirname, "./src/index.js"),
  output: {
    path: path.join(__dirname, "./dist"),
    filename"bundle.js",
  },
};

內容很簡單,定義一下入口、出口(你這也太簡單了吧!!別急,慢慢來嘛)

其次是src,這裏在src目錄下定義了兩個文件:

  • greeting.js
// greeting.js
export function greeting(name{
  return "你好" + name;
}

  • index.js
import { greeting } from "./greeting.js";

document.write(greeting("森林"));

ok,到這裏咱們已經把須要準備的工做都完成了。(問:爲何這麼基礎?答:固然要基礎了,咱們的核心是「造輪子」!!)

梳理下邏輯

短暫的停留一下,咱們梳理下邏輯:

Q: 咱們要作什麼?

A: 作一個比webpack更強的super webpack(很差意思,失態了,一不當心說出了個人心聲)。仍是低調點(防止一會被瘋狂打臉)

Q: 怎麼去作?

A: 看下文(23333)

Q: 整個的流程是什麼?

A: 哎嘿,大概流程就是:

  • 讀取入口文件
  • 分析入口文件,遞歸的去讀取模塊所依賴的文件內容,生成 AST語法樹。
  • 根據 AST語法樹,生成瀏覽器可以運行的代碼

正式開工

compile.js 編寫

const path = require("path");
const fs = require("fs");

module.exports = class Compiler {
  // 接收經過lib/index.js new Compiler(options).run()傳入的參數,對應`forestpack.config.js`的配置
  constructor(options) {
    const { entry, output } = options;
    this.entry = entry;
    this.output = output;
    this.modules = [];
  }
  // 開啓編譯
  run() {

  }
  // 構建模塊相關
  buildModule(filename, isEntry) {
    // filename: 文件名稱
    // isEntry: 是不是入口文件
  }
  // 輸出文件
  emitFiles() {

  }
};

compile.js主要作了幾個事情:

  • 接收 forestpack.config.js配置參數,並初始化 entryoutput
  • 開啓編譯 run方法。處理構建模塊、收集依賴、輸出文件等。
  • buildModule方法。主要用於構建模塊(被 run方法調用)
  • emitFiles方法。輸出文件(一樣被 run方法調用)

到這裏,compiler.js的大體結構已經出來了,可是獲得模塊的源碼後, 須要去解析,替換源碼和獲取模塊的依賴項, 也就對應咱們下面須要完善的parser.js

parser.js 編寫

const fs = require("fs");
// const babylon = require("babylon");
const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;
const { transformFromAst } = require("babel-core");
module.exports = {
  // 解析咱們的代碼生成AST抽象語法樹
  getAST: (path) => {
    const source = fs.readFileSync(path, "utf-8");

    return parser.parse(source, {
      sourceType"module",  //表示咱們要解析的是ES模塊
    });
  },
  // 對AST節點進行遞歸遍歷
  getDependencies: (ast) => {
    const dependencies = [];
    traverse(ast, {
      ImportDeclaration({ node }) => {
        dependencies.push(node.source.value);
      },
    });
    return dependencies;
  },
  // 將得到的ES6的AST轉化成ES5
  transform: (ast) => {
    const { code } = transformFromAst(ast, null, {
      presets: ["env"],
    });
    return code;
  },
};

看完這代碼是否是有點懵(說好的保證讓看懂的 😤)

彆着急,你聽我辯解!!😷

這裏要先着重說下用到的幾個babel包:

  • @babel/parser:用於將源碼生成 AST
  • @babel/traverse:對 AST節點進行遞歸遍歷
  • babel-core/ @babel/preset-env:將得到的 ES6AST轉化成 ES5

parser.js中主要就三個方法:

  • getAST:將獲取到的模塊內容 解析成 AST語法樹
  • getDependencies:遍歷 AST,將用到的依賴收集起來
  • transform:把得到的 ES6AST轉化成 ES5

完善 compiler.js

在上面咱們已經將compiler.js中會用到的函數佔好位置,下面咱們須要完善一下compiler.js,固然會用到parser.js中的一些方法(廢話,否則我上面幹嗎要先把parser.js寫完~~)

直接上代碼:

const { getAST, getDependencies, transform } = require("./parser");
const path = require("path");
const fs = require("fs");

module.exports = class Compiler {
  constructor(options) {
    const { entry, output } = options;
    this.entry = entry;
    this.output = output;
    this.modules = [];
  }
  // 開啓編譯
  run() {
    const entryModule = this.buildModule(this.entry, true);
    this.modules.push(entryModule);
    this.modules.map((_module) => {
      _module.dependencies.map((dependency) => {
        this.modules.push(this.buildModule(dependency));
      });
    });
    // console.log(this.modules);
    this.emitFiles();
  }
  // 構建模塊相關
  buildModule(filename, isEntry) {
    let ast;
    if (isEntry) {
      ast = getAST(filename);
    } else {
      const absolutePath = path.join(process.cwd(), "./src", filename);
      ast = getAST(absolutePath);
    }

    return {
      filename,  // 文件名稱
      dependencies: getDependencies(ast),  // 依賴列表
      transformCode: transform(ast),  // 轉化後的代碼
    };
  }
  // 輸出文件
  emitFiles() {
    const outputPath = path.join(this.output.path, this.output.filename);
    let modules = "";
    this.modules.map((_module) => {
      modules += `'${_module.filename}' : function(require, module, exports) {${_module.transformCode}},`;
    });

    const bundle = `
        (function(modules) {
          function require(fileName) {
            const fn = modules[fileName];
            const module = { exports:{}};
            fn(require, module, module.exports)
            return module.exports
          }
          require('${this.entry}')
        })({${modules}})
    `
;

    fs.writeFileSync(outputPath, bundle, "utf-8");
  }
};

關於compiler.js的內部函數,上面我說過一遍,這裏主要來看下emitFiles

emitFiles() {
    const outputPath = path.join(this.output.path, this.output.filename);
    let modules = "";
    this.modules.map((_module) => {
      modules += `'${_module.filename}' : function(require, module, exports) {${_module.transformCode}},`;
    });

    const bundle = `
        (function(modules) {
          function require(fileName) {
            const fn = modules[fileName];
            const module = { exports:{}};
            fn(require, module, module.exports)
            return module.exports
          }
          require('${this.entry}')
        })({${modules}})
    `
;

    fs.writeFileSync(outputPath, bundle, "utf-8");
  }

這裏的bundle一大坨,什麼鬼?

咱們先來了解下webpack的文件 📦 機制。下面一段代碼是通過webpack打包精簡事後的代碼:

// dist/index.xxxx.js
(function(modules{
  // 已經加載過的模塊
  var installedModules = {};

  // 模塊加載函數
  function __webpack_require__(moduleId{
    if(installedModules[moduleId]) {
      return installedModules[moduleId].exports;
    }
    var module = installedModules[moduleId] = {
      i: moduleId,
      lfalse,
      exports: {}
    };
    modules[moduleId].call(module.exports, modulemodule.exports, __webpack_require__);
    module.l = true;
    return module.exports;
  }
  __webpack_require__(0);
})([
/* 0 module */
(function(module, exports, __webpack_require__{
  ...
}),
/* 1 module */
(function(module, exports, __webpack_require__{
  ...
}),
/* n module */
(function(module, exports, __webpack_require__{
  ...
})]);

簡單分析下:

  • webpack 將全部模塊(能夠簡單理解成文件)包裹於一個函數中,並傳入默認參數,將全部模塊放入一個數組中,取名爲 modules,並經過數組的下標來做爲 moduleId
  • modules 傳入一個自執行函數中,自執行函數中包含一個 installedModules 已經加載過的模塊和一個模塊加載函數,最後加載入口模塊並返回。
  • __webpack_require__ 模塊加載,先判斷 installedModules 是否已加載,加載過了就直接返回 exports 數據,沒有加載過該模塊就經過 modules[moduleId].call(module.exports, module, module.exports, __webpack_require__) 執行模塊而且將 module.exports 給返回。

(你上面說的這一坨又是什麼鬼?我聽不懂啊啊啊啊!!!)

那我換個說法吧:

  • 通過 webpack打包出來的是一個匿名閉包函數( IIFE
  • modules是一個數組,每一項是一個模塊初始化函數
  • __webpack_require__用來加載模塊,返回 module.exports
  • 經過 WEBPACK_REQUIRE_METHOD(0)啓動程序

(小聲 bb:怎麼樣,這樣聽懂了吧)

lib/index.js 入口文件編寫

到這裏,就剩最後一步了(彷佛見到了勝利的曙光)。在lib目錄建立index.js

const Compiler = require("./compiler");
const options = require("../forestpack.config");

new Compiler(options).run();

這裏邏輯就比較簡單了:實例化Compiler類,並將配置參數(對應forstpack.config.js)傳入。

運行node lib/index.js就會在dist目錄下生成bundle.js文件。

(function (modules{
  function require(fileName{
    const fn = modules[fileName];
    const module = { exports: {} };
    fn(requiremodulemodule.exports);
    return module.exports;
  }
  require("/Users/fengshuan/Desktop/workspace/forestpack/src/index.js");
})({
  "/Users/fengshuan/Desktop/workspace/forestpack/src/index.js"function (
    require,
    module,
    exports
  
{
    "use strict";

    var _greeting = require("./greeting.js");

    document.write((0, _greeting.greeting)("森林"));
  },
  "./greeting.js"function (require, module, exports{
    "use strict";

    Object.defineProperty(exports, "__esModule", {
      valuetrue,
    });
    exports.greeting = greeting;

    function greeting(name{
      return "你好" + name;
    }
  },
});

和上面用webpack打包生成的js文件做下對比,是否是很類似呢?

來吧!展現

咱們在dist目錄下建立index.html文件,引入打包生成的bundle.js文件:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <script src="./bundle.js"></script>
  </body>
</html>

此時打開瀏覽器:

如你所願,獲得了咱們預期的結果~

總結

經過對webpack構建流程的分析以及實現了一個簡易的forestpack,相信你對webpack的構建原理已經有了一個清晰的認知!(固然,這裏的forestpackwebpack相比還很弱很弱,,,,)

參考

本文是看過極客時間程柳鋒老師的「玩轉 webpack」課程後整理的。這裏也十分推薦你們去學習這門課程~

❤️ 愛心三連擊

1.若是以爲這篇文章還不錯,來個分享、點贊、在看三連吧,讓更多的人也看到~

2.關注公衆號前端森林,按期爲你推送新鮮乾貨好文。

3.特殊階段,帶好口罩,作好我的防禦。

4.添加微信fs1263215592,拉你進技術交流羣一塊兒學習 🍻

- END -


本文分享自微信公衆號 - 全棧大佬的修煉之路(gh_7795af32a259)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。

相關文章
相關標籤/搜索