從 0 到 1 實現一款簡易版 Webpack

1、前言

這款簡易版 webpack 主要實現的功能以下:javascript

  • 異步加載代碼塊
  • 提取公共代碼塊(commons)/第三方庫(vendors)
  • loader 編譯

2、Webpack 工做流程

Webpack 的運行流程是一個串行的過程,從啓動到結束會依次執行如下流程:java

  • 初始化參數從配置文件和 Shell 語句中讀取與合併參數,得出最終的參數;
  • 開始編譯:用上一步獲得的參數初始化 Compiler 對象,加載全部配置的插件,執行對象的 run 方法開始執行編譯;
  • 肯定入口:根據配置中的 entry 找出全部的入口文件;
  • 編譯模塊:從入口文件出發,調用全部配置的 Loader 對模塊進行編譯,再找出該模塊依賴的模塊,再遞歸本步驟直到全部入口依賴的文件都通過了本步驟的處理;
  • 完成模塊編譯:在通過使用 Loader 編譯完全部模塊後,獲得了每一個模塊被編譯後的最終內容以及它們之間的依賴關係;
  • 輸出資源:根據入口和模塊之間的依賴關係,組裝成一個個包含多個模塊的 Chunk,再把每一個 Chunk 轉換成一個單獨的文件加入到輸出列表,這步是能夠修改輸出內容的最後機會;
  • 輸出完成:在肯定好輸出內容後,根據配置肯定輸出的路徑和文件名,把文件內容寫入到文件系統。
  • 在以上過程當中,Webpack 會在特定的時間點廣播出特定的事件,插件在監聽到感興趣的事件後會執行特定的邏輯,而且插件能夠調用 Webpack 提供的 API 改變 Webpack 的運行結果。


3、Webpack 之 Tapable

  • Webpack 本質上是一種事件流的機制,它的工做流程就是將各個插件串聯起來,而實現這一切的核心就是 Tapable ,Webpack 中最核心的負責編譯的 Compiler 和負責建立 Bundle 的 Compilation 都是 Tapable 的實例
  • Webpack 內部有各類各樣的鉤子,插件將本身的方法註冊到對應的鉤子上,這樣 Webpack 編譯的時候,會觸發這些鉤子,所以也就觸發了插件的方法

1. Tapable 分類

  • Tapable 提供了不少類型的 Hook,分爲同步(Sync)和異步(Async)兩大類(異步中又區分異步並行和異步串行),而根據事件執行的終止條件的不一樣,又衍生出 Basic/Bail/Waterfall/Loop 類型


類型

如何辨別node

使用要點
Basic

hook 中不包含如下三個類型關鍵字的linux

不關心監聽函數是否有返回值
Bail

hook 中包含 Bailwebpack

保險式: 只要監聽函數中有返回值(不爲 undefined ),則跳過以後的監聽函數
Waterfall

hook 中包含 Waterfallgit

瀑布式: 上一步的返回值交給下一步使用
Loop

hook 中包含 Loopes6

循環類型: 若是該監聽函數返回 true,則這個監聽函數會反覆執行,若是返回undefined 則退出循環

2. 全部 Hook 的注意事項

  • 全部的 Hook 實例化時,都接收一個可選參數,參數是一個參數名的字符串數組
  • 參數的名字能夠任意填寫,可是參數數組的長數必需要跟實際接受的參數個數一致
  • 若是回調函數不接受參數,能夠傳入空數組
  • 在實例化的時候傳入的數組長度長度有用,值沒有用途
  • 每一個 Hook 的實例就是一個相似於發佈訂閱的事件管理器,用 tap 註冊事件,第一個參數能夠任意填寫,哪怕用中文寫註釋均可以,由於調用 call 時,不用傳遞事件名,會執行全部註冊的事件
  • 執行 call 時,參數個數和實例化時的數組長度有關

4、Compiler 和 Compilation

  • Compiler 和 Compilation 都繼承自 Tapable,這樣就能夠訂閱和發射事件。
  • Compiler:Webpack 執行構建的時候,都會先讀取 Webpack 配置文件實例化一個 Compiler 對象,而後調用它的 run 方法來開啓一次完整的編譯,Compiler 對象表明了完整的 Webpack 環境配置。這個對象在啓動 Webpack 時被一次性創建,並配置好全部可操做的設置,包括 options,loader 和 plugin。當在 Webpack 環境中應用一個插件時,插件將收到此 Compiler 對象的引用。可使用它來訪問 Webpack 的主環境。
  • Compilation:對象表明一次資源版本的構建。當運行 Webpack 開發環境中間件時,每當檢測到一個文件變化,就會建立一個新的 Compilation ,從而生成一組新的編譯資源。一個 Compilation 對象表現了當前的模塊資源、編譯生成資源、變化的文件、以及被跟蹤依賴的狀態信息。Compilation 對象也提供了不少關鍵時機的回調,以供插件作自定義處理時選擇使用。

5、Webpack 源碼閱讀技巧

1. 找到關鍵文件

1.1 bin/webpack.js

node_modules\webpack\bin\webpack.jsgithub

  • 打開項目中的 packge.json 文件,找到 webpack,Ctrl + 鼠標點擊 ==> 就能夠快速找到 webpack 的位置
// 找到這裏的代碼
// webpack 有兩種命令行工具: webpack-cli 和 webpack-command 
// 由於 webpack-cli 功能更強大,通常都是用 webpack-cli,因此會執行下面的語句
else if (installedClis.length === 1) {
    const path = require("path");
    const pkgPath = require.resolve(`${installedClis[0].package}/package.json`);
    const pkg = require(pkgPath);
    require(path.resolve(
        path.dirname(pkgPath),
        pkg.bin[installedClis[0].binName]
    ));
} 複製代碼

1.2 lib/webpack.js

node_modules\webpack\lib\webpack.jsweb

  • webpack 的入口文件,能夠從這裏開始閱讀源碼

1.3 webpack\declarations

node_modules\webpack\declarationstypescript

  • 這個目錄下,放置了用 typescript 寫的 webpack 配置項/插件的申明文件

1.4 Compiler.js / Compilation.js

node_modules\webpack\lib

  • 在 webpack 的 Compiler.js / Compilation.js 文件中輸入如下代碼:能夠獲取到 webpack 的全部鉤子

2. debug 代碼

2.1 閱讀思路

  • 先摺疊無關的分支的邏輯,只看主體流程代碼
  • 尋找關鍵路徑,根據變量名和方法名猜想意圖,而後經過閱讀源碼來驗證想法
  • debugger 關鍵路徑,理解整個執行過程

2.2 第一種調試方法

  • 將上面在 bin/webpack.js 裏找到的代碼,複製到一個單獨的文件 debugger.js
// debugger.js 和項目中的 packge.json 同級
// 右鍵運行 debugger.js ,至關於使用 npx webpack 運行 webpack
// npx webpack 其實就是用 node 執行 bin 下面的 cli.js 
// npx webpack = node ./node_modules/webpack-cli/bin/cli.js
// 找到 webpack-cli/bin/cli.js ,設置斷點,就能夠開始調試了(這是第一種方法)
const path = require("path");
const pkgPath = require.resolve(`webpack-cli/package.json`);
const pkg = require(pkgPath);
require(path.resolve(
        path.dirname(pkgPath),
        './bin/cli.js'
        //pkg.bin['webpack-cli']
));複製代碼

2.3 第二種調試方法

  • 新建一個 cli.js 文件,在 webstorm 裏面設置斷點,而後右鍵運行 cli.js ,開始調試代碼
let webpack = require("webpack");
let webpackOptions = require("./webpack.config");
const compiler = webpack(webpackOptions);

compiler.run((err, stat) => {
  console.log(err);
  console.log(stat)
});複製代碼

6、Webpack 構建後的代碼分析

1. webpack 4

1.1 webpack.config.js

const path = require('path');
module.exports = {
    // 用開發模式打包代碼 !!!!!!
    mode:'development',
    devtool:'none',
    entry:'./src/index.js',
    output:{
        path:path.resolve(__dirname,'dist'),
        filename:'bundle.js'
    },
};複製代碼

1.2 源碼

// src/index.js
import {logMsg}  from './sync-module';
console.log(logMsg);
let button = document.createElement('button');
button.innerHTML = '請點我';
button.addEventListener('click',()=>{
    import(/*webpackChunkName: 'async-module'*/'./async-module.js').then(result=>{
        console.log(result.default);
    });
});
document.body.appendChild(button);


// src/async-module.js
module.exports = "我是異步模塊";


// src/sync-module.js
export function logMsg() {
    console.log('我是同步模塊');
}複製代碼

1.3 bundle.js

/* src/ index.js sync-module.js async-module.js */
// webpack 打包後,會把引用模塊的相對路徑變成相對於 webpack.config.js 的相對路徑
// 在 index.js 中 引入 "./sync-module.js" => 最終會變成 "./src/sync-module.js"

// webpack 啓動代碼的自執行函數
(function(modules) { // webpackBootstrap

  // install a JSONP callback for chunk loading
  function webpackJsonpCallback(data) {
    // data => [
      // [chunkName],
      // { chunkID : chunk 內容}
    // ]
    var chunkIds = data[0];
    var moreModules = data[1];

    // add "moreModules" to the modules object,
    // then flag all "chunkIds" as loaded and fire callback
    var moduleId, chunkId, i = 0, resolves = [];
    for(;i < chunkIds.length; i++) {
      chunkId = chunkIds[i];
      if(Object.prototype.hasOwnProperty.call(installedChunks, chunkId) && installedChunks[chunkId]) {
        // installedChunks[chunkId] => [resolve, reject, Promise]
        // 將 resolve 存到 resolves 數組中,先不着急執行 resolve()
        resolves.push(installedChunks[chunkId][0]);
      }
      // 設置爲 0 ,表示已經加載成功
      installedChunks[chunkId] = 0;
    }
    for(moduleId in moreModules) {
      if(Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
        // 異步 chunk 加載完成後,將異步 chunk 的代碼合併到 modules 中
        // 這樣以後加載該 chunk 時,能夠直接從 modules 中獲取到
        // key 是模塊 ID ,value 是模塊內容
        modules[moduleId] = moreModules[moduleId];
      }
    }
    if(parentJsonpFunction){
      parentJsonpFunction(data);
    }

    while(resolves.length) {
      // 執行 resolve
      resolves.shift()();
    }
  }


  // The module cache
  // 普通模塊的緩存:只要加載初始化過一次的模塊都放到這,以後再使用這個模塊時
  // 直接從這裏獲取,不須要再初始化一遍
  var installedModules = {};


  // object to store loaded and loading chunks
  // !!!!!!!!!!!!!!!!!!!!!!
  // 存儲已加載的或者加載中的 chunk (這裏的 chunk 包含: 入口 chunk 和異步加載的 chunk)
  // !!!!!!!!!!!!!!!!!!!!!!
  // installedChunks 對象中,每一個 key 對應的 value 值的意思
  // undefined = chunk not loaded, null = chunk preloaded/prefetched
  // undefined 表示 chunk 還未加載,null 表示 chunk 會預加載
  // Promise = chunk loading, 0 = chunk loaded
  // Promise 表示 chunk 正在加載中,0 表示 chunk 加載完成
  var installedChunks = {
    // 若是是單入口,key 的默認值是 main
    "main": 0
  };


  // script path function
  // 設置異步 chunk 的請求 url
  function jsonpScriptSrc(chunkId) {
    return __webpack_require__.p + "" + chunkId + ".bundle.js"
  }

  // The require function
  // webpack 本身實現的一個 require 方法,能夠直接在瀏覽器中運行
  function __webpack_require__(moduleId) {

    // Check if module is in cache
    // 加載模塊前,先從緩存列表中查找,是否已經加載過
    if(installedModules[moduleId]) {
      // 若是有,說明模塊已經緩存過,直接返回該模塊的導出對象 exports
      return installedModules[moduleId].exports;
    }
    // Create a new module (and put it into the cache)
    // 建立一個新的模塊對象,而且放到緩存列表中
    var module = installedModules[moduleId] = {
      // 模塊 ID
      i: moduleId,
      // 是否已經加載 loaded:false
      l: false,
      // 模塊導出對象,默認是一個空對象
      exports: {}
    };

    // Execute the module function
    // 加載模塊
    // modules 是自執行函數接收的參數——一個包含模塊信息的對象
    // key 是模塊路徑, value 是一個函數,裏面包含了模塊的內容
    // {
    // "./src/a.js":
    // (function(module, __webpack_exports__, __webpack_require__) {
    // 模塊內容:xxx
    // 模塊內容:xxx
    // 模塊內容:xxx
    // },
    // }
    // 從 modules 對象中找到對應的 key,執行函數(value)並將內部的 this 指向上面新建 module 的 exports 對象
    // 目的是將函數內部的內容都放置到 module.exports 中
    modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);

    // Flag the module as loaded
    // 設置爲已加載
    module.l = true;

    // Return the exports of the module
    // 最終返回當前模塊的內容
    return module.exports;
  }

  // This file contains only the entry chunk.
  // The chunk loading function for additional chunks
  // 加載異步 chunk
  __webpack_require__.e = function requireEnsure(chunkId) {
    var promises = [];

    // JSONP chunk loading for javascript
    // 用 jsonp 來請求加載異步 chunk
    var installedChunkData = installedChunks[chunkId];

    // 0 means "already installed".
    // 若是要加載的 chunk 沒有初始化過
    if(installedChunkData !== 0) {

      // a Promise means "currently loading".
      // 排除了 0,緩存 chunk 列表裏的值就剩下 undefined/null/Promise
      // 當模塊正在加載中時
      if(installedChunkData) {
        promises.push(installedChunkData[2]);
      }
      // 當模塊還未加載過
      else {
        // setup Promise in chunk cache
        var promise = new Promise(function(resolve, reject) {
          // 新建一個 promise 時,會當即執行它的函數體
          // 將當前 chunk 的狀態設置爲 Promise,表示正在加載中
          installedChunkData = installedChunks[chunkId] = [resolve, reject];
        });
        // 給當前的 installedChunkData 添加一個值
        // 而後將 installedChunkData 添加到 promises 數組中
        promises.push(installedChunkData[2] = promise);

        // start chunk loading
        // 用 jsonp 來請求加載異步 chunk
        var script = document.createElement('script');
        var onScriptComplete;

        script.charset = 'utf-8';
        script.timeout = 120;
        if (__webpack_require__.nc) {
          script.setAttribute("nonce", __webpack_require__.nc);
        }
        // 設置請求 url
        script.src = jsonpScriptSrc(chunkId);

        // create error before stack unwound to get useful stacktrace later
        var error = new Error();
        onScriptComplete = function (event) {
          // avoid mem leaks in IE.
          script.onerror = script.onload = null;
          clearTimeout(timeout);
          var chunk = installedChunks[chunkId];
          if(chunk !== 0) {
            if(chunk) {
              var errorType = event && (event.type === 'load' ? 'missing' : event.type);
              var realSrc = event && event.target && event.target.src;
              error.message = 'Loading chunk ' + chunkId + ' failed.\n(' + errorType + ': ' + realSrc + ')';
              error.name = 'ChunkLoadError';
              error.type = errorType;
              error.request = realSrc;
              chunk[1](error);
            }
            installedChunks[chunkId] = undefined;
          }
        };
        var timeout = setTimeout(function(){
          onScriptComplete({ type: 'timeout', target: script });
        }, 120000);
        script.onerror = script.onload = onScriptComplete;
        document.head.appendChild(script);
      }
    }
    // 執行完全部的 promise後再返回結果
    return Promise.all(promises);
  };

  // expose the modules object (__webpack_modules__)
  // 將模塊列表放到 __webpack_require__ 的 m 屬性上
  __webpack_require__.m = modules;

  // expose the module cache
  // 將緩存列表放到 __webpack_require__ 的 c 屬性上
  __webpack_require__.c = installedModules;

  // define getter function for harmony exports
  // 在 exports 對象上定義 name 屬性的 getter 方法
  __webpack_require__.d = function(exports, name, getter) {
    // 判斷 exports 對象上是否有 name 屬性
    if(!__webpack_require__.o(exports, name)) {
      // 在 exports 對象上添加 name 屬性,可枚舉爲 true
      // get 的值爲 getter,當訪問該屬性時,該方法會被執行
      Object.defineProperty(exports, name, { enumerable: true, get: getter });
    }
  };

  // define __esModule on exports
  // 在 exports 對象上定義一個 __esModule 屬性,用來判斷當前模塊是否爲 es6 模塊
  __webpack_require__.r = function(exports) {
    // 若是當前瀏覽器支持 Symbol
    if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
      // 設置前
      // console.log(exports.toString());// [object Object]
      // 給 exports 對象類型設置爲 Module
      Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
      // 設置後
      // console.log(exports.toString());// [object Module]
    }
    // 不然給 exports 對象添加一個表示 esm 的屬性
    Object.defineProperty(exports, '__esModule', { value: true });
  };

  // create a fake namespace object 建立一個命名空間對象
  / 爲何要建立一個命名空間對象?
  // 由於 import('xxx.js') 加載的 js,多是 esm ,也多是 cjs
  // 因此須要兼容處理
  // mode & 1: value is a module id, require it 若是值是模塊ID,加載它
  // mode & 2: merge all properties of value into the ns 把全部的屬性合併到命名空間上 ns —— nameSpace
  // mode & 4: return value when already ns object 當已是命名空間對象的話直接返回值
  // mode & 8|1: behave like require 就像 require 同樣
  // mode 爲何要用二進制來判斷? 高效。節約內存
  // linux 裏面的權限判斷也是用的二進制, 7 => 111 可讀可寫可執行
  __webpack_require__.t = function(value, mode) {
    // value 最開始是模塊 ID
    // 直接加載模塊
    if(mode & 1) value = __webpack_require__(value);
    // 不用加載模塊,直接返回模塊內容
    if(mode & 8) return value;
    // 若是 value 已是一個對象而且 __esModule 屬性爲 true 的話就直接返回 value
    if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;
    // 不然就建立一個空對象,加載這個對象,
    var ns = Object.create(null);
    // 在對象上設置 __esModule 屬性爲true
    __webpack_require__.r(ns);
    // 給 ns 對象定義一個 default 屬性
    Object.defineProperty(ns, 'default', { enumerable: true, value: value });
    // 若是 mode 爲2,而且 value 不是字符串,把值的全部屬性都定義到 ns 對象上
    if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));
    return ns;//{__esModule:true,default:'模塊內容'}
  };

  // getDefaultExport function for compatibility with non-harmony modules
  // 一個能獲取模塊內容的函數
  __webpack_require__.n = function(module) {
    // 若是是 __esModule,說明是 es6 模塊,須要返回模塊的 default 屬性
    // 若是不是,說明是 cjs 模塊,直接返回模塊自己
    var getter = module && module.__esModule ?
      function getDefault() { return module['default']; } :
      function getModuleExports() { return module; };
    //給 getter 添加一個 a 的屬性,就是 getter 方法自己
    __webpack_require__.d(getter, 'a', getter);
    return getter;
  };

  // Object.prototype.hasOwnProperty.call
  __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };

  // __webpack_public_path__
  // 公開訪問路徑
  __webpack_require__.p = "";

  // on error function for async loading
  // 加載異步 chunk 時的錯誤輸出
  __webpack_require__.oe = function(err) { console.error(err); throw err; };


  // 第一次執行的時候,window["webpackJsonp"] 會是一個空數組
  // jsonpArray 和 window["webpackJsonp"] 共同指向同一塊內存地址
  var jsonpArray = window["webpackJsonp"] = window["webpackJsonp"] || [];

  // 綁定 this,將老的數組的 push 方法始終指向 jsonpArray
  var oldJsonpFunction = jsonpArray.push.bind(jsonpArray);
  // 若是不綁定 this 的話,那麼在 webpackJsonpCallback 中執行 parentJsonpFunction(data) 的時候
  // 就至關於執行了一個 「裸的」數組原生 的 push,data 不知道該添加給誰
  // var oldJsonpFunction = jsonpArray.push;

  //重寫 jsonArray 的 push 方法,賦值爲 webpackJsonpCallback
  jsonpArray.push = webpackJsonpCallback;
  jsonpArray = jsonpArray.slice();
  for(var i = 0; i < jsonpArray.length; i++) webpackJsonpCallback(jsonpArray[i]);
  // 爲何要保留老的數組的 push 方法?
  // 避免 "重複發請求" 加載 chunk ,若是已經加載好了的,就拿來直接用
  var parentJsonpFunction = oldJsonpFunction;

  // 總結:
  // 一、window["webpackJsonp"] 的 push 方法被重寫,再也不是數組原生的方法,而是用來執行 jsonp 回調函數的
  // 二、這時候若是想要給 window["webpackJsonp"] 這個數組添加數據時,就沒法用 push 來添加了
  // 三、因此這裏多定義一個 jsonpArray 數組,它和 window["webpackJsonp"] 共同指向同一塊內存地址
  // 四、經過給 jsonpArray 添加(push)數據,那麼相應的 window["webpackJsonp"] 就能獲取到這些數據

  // Load entry module and return exports
  // 加載入口模塊而且返回導出對象
  return __webpack_require__(__webpack_require__.s = "./src/index.js");
})
/************************************************************************/
/* src/ index.js sync-module.js async-module.js */
// webpack 打包後,會把引用模塊的相對路徑變成相對於 webpack.config.js 的相對路徑
// 在 index.js 中 引入 "./sync-module.js" => 最終會變成 "./src/sync-module.js"

({
  // key 是模塊 ID ,value 是模塊內容
  "./src/index.js":
  /*!**********************!*\ // 入口 chunk !*** ./src/index.js ***! \**********************/
  /*! no exports provided */
    (function(module, __webpack_exports__, __webpack_require__) {
 "use strict";
      __webpack_require__.r(__webpack_exports__);
      /* harmony import */ var _sync_module__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./sync-module */ "./src/sync-module.js");

      console.log(_sync_module__WEBPACK_IMPORTED_MODULE_0__["logMsg"]);

      let button = document.createElement('button');
      button.innerHTML = '請點我';
      button.addEventListener('click',()=>{
        __webpack_require__.e(/*! import() | async-module */ "async-module")
          .then(__webpack_require__.t.bind(null, /*! ./async-module.js */ "./src/async-module.js", 7))
          .then(result=>{// result = {__esModule:true,default:'模塊內容'}
          console.log(result.default);
        });
      });
      document.body.appendChild(button);
    }),

  "./src/sync-module.js":
  /*!****************************!*\ !*** ./src/sync-module.js ***! // 入口 chunk 依賴的同步模塊 \****************************/
  /*! exports provided: logMsg */
    (function(module, __webpack_exports__, __webpack_require__) {
 "use strict";
      __webpack_require__.r(__webpack_exports__);
      /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "logMsg", function() { return logMsg; });

      function logMsg() {
        console.log('我是同步模塊');
      }
    })
});複製代碼

2. webpack 5

  • 相比 webpack 4 ,代碼稍微易讀些
(function(modules) { // webpack 的啓動代碼自執行函數
  // The module cache 模塊的緩存
  var installedModules = {};

  // The require function webpack本身實現了一個require方法
  function __webpack_require__(moduleId) {

    // Check if module is in cache 判斷一下這個模塊ID是否在緩存中
    if(installedModules[moduleId]) {
      return installedModules[moduleId].exports;//若是有,說明此模塊加載過,直接返回導出對象exports
    }
    // Create a new module (and put it into the cache)
    // 建立一個新的模塊對象而且把它放到緩存中
    var module = installedModules[moduleId] = {
      i: moduleId,// 模塊ID
      l: false,//是否已經加載loaded false
      exports: {} //導出對象,默認是一個空對象
    };

    // Execute the module function 執行此模塊對應的方法,目的是給module.exports賦值
    modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);

    // Flag the module as loaded 把模塊設置爲已加載
    module.l = true;

    // Return the exports of the module 返回模塊的導出對象
    return module.exports;
  }

  // the startup function
  function startup() {
    // Load entry module and return exports
    // 加載入口模塊而且返回導出對象
    return __webpack_require__("./src/index.js");
  }

  // run startup 執行啓動方法
  return startup();
})
({

 "./src/hello.js":
 (function(module) {
   module.exports = "hello";
 }),
 "./src/index.js":
 (function(__unusedmodule, __unusedexports, __webpack_require__) {
    let hello = __webpack_require__( "./src/hello.js");
    console.log(hello);
 })
});複製代碼

7、抽象語法樹(Abstract Syntax Tree)

1. AST 是什麼

  • JavaScript Parser 會把代碼轉化爲一顆抽象語法樹(AST),這顆樹定義了代碼的結構,經過操縱這顆樹,咱們能夠精準的定位到聲明語句、賦值語句、運算語句等等,實現對代碼的分析、優化、變動等操做。


2. AST 用途

  • 代碼語法的檢查、代碼風格的檢查、代碼的格式化、代碼的高亮、代碼錯誤提示、代碼自動補全等等
  • 如 JSLint、JSHint 對代碼錯誤或風格的檢查,發現一些潛在的錯誤
  • IDE 的錯誤提示、格式化、高亮、自動補全等等
  • 代碼混淆壓縮
  • UglifyJS2 等
  • 優化變動代碼,改變代碼結構使達到想要的結構
  • 代碼打包工具 Webpack、Rollup 等等
  • CommonJS、AMD、CMD、UMD 等代碼規範之間的轉化
  • CoffeeScript、TypeScript、JSX 等轉化爲原生 Javascript

3. AST 執行流程

  • 解析源代碼
  • 詞法解析(Lexical Analysis)詞法解析器(Tokenizer)在這個階段將代碼字符串轉換爲語法單元數組 —— Tokens(令牌)。例如 for (const item of items) {} 詞法解析後的結果以下:

Javascript 代碼中的語法單元主要包括如下這麼幾種

  • 關鍵字:constletvar
  • 標識符:多是一個變量,也多是 if、else 這些關鍵字,又或者是 true、false 這些常量
  • 運算符
  • 數字
  • 空格
  • 註釋
  • 語法解析(Syntactic Analysis):這個階段語法解析器 (Parser) 會把 Tokens 轉換爲抽象語法樹
  • 深度優先遍歷語法樹,修改語法樹
  • 將語法樹轉換回源代碼



4. JavaScript Parser

  • JavaScript Parser,把 js 源碼轉化爲抽象語法樹的解析器。
  • 瀏覽器會把 js 源碼經過解析器轉爲抽象語法樹,再進一步轉化爲字節碼或直接生成機器碼。
  • 通常來講每一個 js 引擎都會有本身的抽象語法樹格式,Chrome 的 v8 引擎,firefox 的SpiderMonkey 引擎等等,MDN 提供了詳細 SpiderMonkey AST format 的詳細說明,算是業界的標準。

5. 項目中須要用到的工具

6. AST 使用例子

6.1 轉換箭頭函數

const babylon = require('@babel/parser');
// @babel/core 裏面內置了 babylon/parser,也能夠用它來轉換 AST
const babel = require('@babel/core');
let types = require('@babel/types');
let generate = require('@babel/generator').default;
let traverse = require('@babel/traverse').default;

const originalSource = "const a = (a, b) => a + b;";

// 將 當前模塊 的內容轉換成 AST
const ast = babylon.parse(originalSource);
// @babel/core 裏面內置了 babylon/parser,也能夠用它來轉換 AST
// const ast = babel.parse(originalSource);

// 遍歷語法樹,尋找要修改的目標節點
traverse(ast, {
    // 若是當前節點是一個 箭頭函數 時
    ArrowFunctionExpression: (nodePath) => {
        let node = nodePath.node;
        let body = node.body;
        if(!types.isBlockStatement(node.body)){
            body = types.blockStatement([types.returnStatement(node.body)])
        }
        let newNode = types.functionExpression(null,node.params,body);
        nodePath.replaceWith(newNode);
    }
});

// 把轉換後的抽象語法樹從新生成代碼
let {code} = generate(ast);
console.log('新的 code =>', code);複製代碼

6.2 轉換 class

const babylon = require('@babel/parser');
let types = require('@babel/types');
let generate = require('@babel/generator').default;
let traverse = require('@babel/traverse').default;
const originalSource = `class Person{ constructor(name){ this.name = name; } getName(){ return this.name } }`;
// 將 當前模塊 的內容轉換成 AST
const ast = babylon.parse(originalSource);
// 遍歷語法樹,尋找要修改的目標節點
traverse(ast, {
    // 若是當前節點是一個 class 時
    ClassDeclaration: (nodePath) => {
        let node = nodePath.node;
        let bodys = node.body.body;
        let id = node.id;
        bodys = bodys.map(body => {
            if (body.kind === 'constructor') {
                return types.functionExpression(id, body.params, body.body)
            } else {
                let left = types.memberExpression(id, types.identifier('prototype'));
                left = types.memberExpression(left, body.key);
                let right = types.functionExpression(null, body.params, body.body);
                return types.assignmentExpression('=', left, right);
            }
        });
        nodePath.replaceWithMultiple(bodys);
    }
});
// 把轉換後的抽象語法樹從新生成代碼
let {code} = generate(ast);
console.log('新的 code =>', code);複製代碼

8、後語

  • 本文涉及的一些知識點,講的比較淺,有興趣的能夠自行查閱相關資料深刻了解。
  • 由於文章以前是在語雀上寫的,因此一些圖片的水印就變成了個人,若有冒犯請原諒,請告訴我原文地址,我在後面加上去,謝謝。

9、參考文章

github.com/jamiebuilds…

developer.mozilla.org/zh-CN/docs/…

juejin.im/post/5d94bf…

github.com/webpack/tap…

www.jianshu.com/p/273e1c990…

10、源碼

github.com/yjdjiayou/e…

相關文章
相關標籤/搜索