一、前言
這款簡易版 webpack 主要實現的功能如下:
- 非同步載入程式碼塊
- 提取公共程式碼塊(commons)/第三方庫(vendors)
- loader 編譯
二、Webpack 工作流程
Webpack 的執行流程是一個序列的過程,從啟動到結束會依次執行以下流程:
- 初始化引數:從配置檔案和 Shell 語句中讀取與合併引數,得出最終的引數;
- 開始編譯:用上一步得到的引數初始化 Compiler 物件,載入所有配置的外掛,執行物件的 run 方法開始執行編譯;
- 確定入口:根據配置中的 entry 找出所有的入口檔案;
- 編譯模組:從入口檔案出發,呼叫所有配置的 Loader 對模組進行編譯,再找出該模組依賴的模組,再遞迴本步驟直到所有入口依賴的檔案都經過了本步驟的處理;
- 完成模組編譯:在經過使用 Loader 編譯完所有模組後,得到了每個模組被編譯後的最終內容以及它們之間的依賴關係;
- 輸出資源:根據入口和模組之間的依賴關係,組裝成一個個包含多個模組的 Chunk,再把每個 Chunk 轉換成一個單獨的檔案加入到輸出列表,這步是可以修改輸出內容的最後機會;
- 輸出完成:在確定好輸出內容後,根據配置確定輸出的路徑和檔名,把檔案內容寫入到檔案系統。
- 在以上過程中,Webpack 會在特定的時間點廣播出特定的事件,外掛在監聽到感興趣的事件後會執行特定的邏輯,並且外掛可以呼叫 Webpack 提供的 API 改變 Webpack 的執行結果。
三、Webpack 之 Tapable
- Webpack 本質上是一種事件流的機制,它的工作流程就是將各個外掛串聯起來,而實現這一切的核心就是 Tapable ,Webpack 中最核心的負責編譯的 Compiler 和負責建立 Bundle 的 Compilation 都是 Tapable 的例項
- Webpack 內部有各種各樣的鉤子,外掛將自己的方法註冊到對應的鉤子上,這樣 Webpack 編譯的時候,會觸發這些鉤子,因此也就觸發了外掛的方法
1. Tapable 分類
- Tapable 提供了很多型別的 Hook,分為同步(Sync)和非同步(Async)兩大類(非同步中又區分非同步並行和非同步序列),而根據事件執行的終止條件的不同,又衍生出 Basic/Bail/Waterfall/Loop 型別
型別 | 如何辨別 | 使用要點 |
Basic | hook 中不包含以下三個型別關鍵字的 | 不關心監聽函式是否有返回值 |
Bail | hook 中包含 Bail | 保險式: 只要監聽函式中有返回值(不為 undefined ),則跳過之後的監聽函式 |
Waterfall | hook 中包含 Waterfall | 瀑布式: 上一步的返回值交給下一步使用 |
Loop | hook 中包含 Loop | 迴圈型別: 如果該監聽函式返回 true,則這個監聽函式會反覆執行,如果返回undefined 則退出迴圈 |
2. 所有 Hook 的注意事項
- 所有的 Hook 例項化時,都接收一個可選引數,引數是一個引數名的字串陣列
- 引數的名字可以任意填寫,但是引數陣列的長數必須要跟實際接受的引數個數一致
- 如果回撥函式不接受引數,可以傳入空陣列
- 在例項化的時候傳入的陣列長度長度有用,值沒有用途
- 每個 Hook 的例項就是一個類似於釋出訂閱的事件管理器,用 tap 註冊事件,第一個引數可以任意填寫,哪怕用中文寫註釋都可以,因為呼叫 call 時,不用傳遞事件名,會執行所有註冊的事件
- 執行 call 時,引數個數和例項化時的陣列長度有關
四、Compiler 和 Compilation
- Compiler 和 Compilation 都繼承自 Tapable,這樣就可以訂閱和發射事件。
- Compiler:Webpack 執行構建的時候,都會先讀取 Webpack 配置檔案例項化一個 Compiler 物件,然後呼叫它的 run 方法來開啟一次完整的編譯,Compiler 物件代表了完整的 Webpack 環境配置。這個物件在啟動 Webpack 時被一次性建立,並配置好所有可操作的設定,包括 options,loader 和 plugin。當在 Webpack 環境中應用一個外掛時,外掛將收到此 Compiler 物件的引用。可以使用它來訪問 Webpack 的主環境。
- Compilation:物件代表一次資源版本的構建。當執行 Webpack 開發環境中介軟體時,每當檢測到一個檔案變化,就會建立一個新的 Compilation ,從而生成一組新的編譯資源。一個 Compilation 物件表現了當前的模組資源、編譯生成資源、變化的檔案、以及被跟蹤依賴的狀態資訊。Compilation 物件也提供了很多關鍵時機的回撥,以供外掛做自定義處理時選擇使用。
五、Webpack 原始碼閱讀技巧
1. 找到關鍵檔案
1.1 bin/webpack.js
node_modules\webpack\bin\webpack.js
- 開啟專案中的 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.js
- webpack 的入口檔案,可以從這裡開始閱讀原始碼
1.3 webpack\declarations
node_modules\webpack\declarations
- 這個目錄下,放置了用 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)
});複製程式碼
六、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;
// 總結:
// 1、window["webpackJsonp"] 的 push 方法被重寫,不再是陣列原生的方法,而是用來執行 jsonp 回撥函式的
// 2、這時候如果想要給 window["webpackJsonp"] 這個陣列新增資料時,就無法用 push 來新增了
// 3、所以這裡多定義一個 jsonpArray 陣列,它和 window["webpackJsonp"] 共同指向同一塊記憶體地址
// 4、通過給 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);
})
});複製程式碼
七、抽象語法樹(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 程式碼中的語法單元主要包括以下這麼幾種
- 關鍵字:
const
、let
、var
等 - 識別符號:可能是一個變數,也可能是 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. 專案中需要用到的工具
- astexplorer
- @babel/core 裡面內建了 babylon/parser,也可以用它來轉換 AST
- @babel/parser is a JavaScript parser used in Babel.
- @babel/traverse maintains the overall tree state, and is responsible for replacing, removing, and adding nodes.
- @babel/types contains methods for building ASTs manually and for checking the types of AST nodes.
- @babel/generator Turns an AST into code.
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);複製程式碼
八、後語
- 本文涉及的一些知識點,講的比較淺,有興趣的可以自行查閱相關資料深入瞭解。
- 因為文章之前是在語雀上寫的,所以一些圖片的水印就變成了我的,如有冒犯請原諒,請告訴我原文地址,我在後面加上去,謝謝。
九、參考文章
developer.mozilla.org/zh-CN/docs/…