本文將帶領你們解析babel-plugin-import 實現按需加載的完整流程,解開業界所承認 babel 插件的面紗。css
首先供上babel-plugin-import插件node
1、初見萌芽linux
首先 babel-plugin-import
是爲了解決在打包過程當中把項目中引用到的外部組件或功能庫全量打包,從而致使編譯結束後包容量過大的問題,以下圖所示:express
babel-plugin-import
插件源碼由兩個文件構成windows
先來到插件的入口文件 Index :數組
import Plugin from './Plugin'; export default function({ types }) { let plugins = null; /** * Program 入口初始化插件 options 的數據結構 */ const Program = { enter(path, { opts = {} }) { assert(opts.libraryName, 'libraryName should be provided'); plugins = [ new Plugin( opts.libraryName, opts.libraryDirectory, opts.style, opts.styleLibraryDirectory, opts.customStyleName, opts.camel2DashComponentName, opts.camel2UnderlineComponentName, opts.fileName, opts.customName, opts.transformToDefaultImport, types, ), ]; applyInstance('ProgramEnter', arguments, this); }, exit() { applyInstance('ProgramExit', arguments, this); }, }; const ret = { visitor: { Program }, // 對整棵AST樹的入口進行初始化操做 }; return ret; }
首先 Index 文件導入了 Plugin ,而且有一個默認導出函數,函數的參數是被解構出的名叫 types 的參數,它是從 babel 對象中被解構出來的,types 的全稱是 @babel/types
,用於處理 AST 節點的方法集。以這種方式引入後,咱們不須要手動引入 @babel/types
。 進入函數後能夠看見觀察者( visitor )
中初始化了一個 AST 節點 Program
,這裏對 Program
節點的處理使用完整插件結構,有進入( enter )與離開( exit )事件,且需注意:babel
通常咱們縮寫的 Identifier() { ... } 是 Identifier: { enter() { ... } } 的簡寫形式。
這裏可能有同窗會問 Program
節點是什麼?見下方 const a = 1 對應的 AST 樹 ( 簡略部分參數 )antd
{ "type": "File", "loc": { "start":... , "end": ... }, "program": { "type": "Program", // Program 所在位置 "sourceType": "module", "body": [ { "type": "VariableDeclaration", "declarations": [ { "type": "VariableDeclarator", "id": { "type": "Identifier", "name": "a" }, "init": { "type": "NumericLiteral", "value": 1 } } ], "kind": "const" } ], "directives": [] }, "comments": [], "tokens": [ ... ] }
Program 至關於一個根節點,一個完整的源代碼樹。通常在進入該節點的時候進行初始化數據之類的操做,也可理解爲該節點先於其餘節點執行,同時也是最晚執行 exit 的節點,在 exit 時也能夠作一些」善後「的工做。 既然 babel-plugin-import
的 Program
節點處寫了完整的結構,必然在 exit 時也有很是必要的事情須要處理,關於 exit 具體是作什麼的咱們稍後進行討論。 咱們先看 enter ,這裏首先用 enter 形參 state 結構出用戶制定的插件參數,驗證必填的 libraryName
[庫名稱] 是否存在。Index 文件引入的 Plugin 是一個 class 結構,所以須要對 Plugin 進行實例化,並把插件的全部參數與 @babel/types
所有傳進去,關於 Plugin 類會在下文中進行闡述。 接着調用了 applyInstance
函數:數據結構
export default function({ types }) { let plugins = null; /** * 從類中繼承方法並利用 apply 改變 this 指向,並傳遞 path , state 參數 */ function applyInstance(method, args, context) { for (const plugin of plugins) { if (plugin[method]) { plugin[method].apply(plugin, [...args, context]); } } } const Program = { enter(path, { opts = {} }) { ... applyInstance('ProgramEnter', arguments, this); }, ... } }
此函數的主要目的是繼承 Plugin 類中的方法,且須要三個參數閉包
主要的目的是讓 Program
的 enter 繼承 Plugin 類的 ProgramEnter
方法,而且傳遞 path 與 state 形參至 ProgramEnter
。Program
的 exit 同理,繼承的是 ProgramExit
方法。
如今進入 Plugin 類:
export default class Plugin { constructor( libraryName, libraryDirectory, style, styleLibraryDirectory, customStyleName, camel2DashComponentName, camel2UnderlineComponentName, fileName, customName, transformToDefaultImport, types, // babel-types index = 0, // 標記符 ) { this.libraryName = libraryName; // 庫名 this.libraryDirectory = typeof libraryDirectory === 'undefined' ? 'lib' : libraryDirectory; // 包路徑 this.style = style || false; // 是否加載 style this.styleLibraryDirectory = styleLibraryDirectory; // style 包路徑 this.camel2DashComponentName = camel2DashComponentName || true; // 組件名是否轉換以「-」連接的形式 this.transformToDefaultImport = transformToDefaultImport || true; // 處理默認導入 this.customName = normalizeCustomName(customName); // 處理轉換結果的函數或路徑 this.customStyleName = normalizeCustomName(customStyleName); // 處理轉換結果的函數或路徑 this.camel2UnderlineComponentName = camel2UnderlineComponentName; // 處理成相似 time_picker 的形式 this.fileName = fileName || ''; // 連接到具體的文件,例如 antd/lib/button/[abc.js] this.types = types; // babel-types this.pluginStateKey = `importPluginState${index}`; } ... }
在入口文件實例化 Plugin 已經把插件的參數經過 constructor
後被初始化完畢啦,除了 libraryName
之外其餘全部的值均有相應默認值,值得注意的是參數列表中的 customeName 與 customStyleName 能夠接收一個函數或者一個引入的路徑,所以須要經過 normalizeCustomName
函數進行統一化處理。
function normalizeCustomName(originCustomName) { if (typeof originCustomName === 'string') { const customeNameExports = require(originCustomName); return typeof customeNameExports === 'function' ? customeNameExports : customeNameExports.default;// 若是customeNameExports不是函數就導入{default:func()} } return originCustomName; }
此函數就是用來處理當參數是路徑時,進行轉換並取出相應的函數。若是處理後 customeNameExports
仍然不是函數就導入 customeNameExports.default
,這裏牽扯到 export default 是語法糖的一個小知識點。
export default something() {} // 等效於 function something() {} export ( something as default )
迴歸代碼,Step1 中入口文件 Program
的 Enter 繼承了 Plugin 的 ProgramEnter
方法
export default class Plugin { constructor(...) {...} getPluginState(state) { if (!state[this.pluginStateKey]) { // eslint-disable-next-line no-param-reassign state[this.pluginStateKey] = {}; // 初始化標示 } return state[this.pluginStateKey]; // 返回標示 } ProgramEnter(_, state) { const pluginState = this.getPluginState(state); pluginState.specified = Object.create(null); // 導入對象集合 pluginState.libraryObjs = Object.create(null); // 庫對象集合 (非 module 導入的內容) pluginState.selectedMethods = Object.create(null); // 存放通過 importMethod 以後的節點 pluginState.pathsToRemove = []; // 存儲須要刪除的節點 /** * 初始化以後的 state * state:{ * importPluginState「Number」: { * specified:{}, * libraryObjs:{}, * select:{}, * pathToRemovw:[] * }, * opts:{ * ... * }, * ... * } */ } ... }
ProgramEnter
中經過 getPluginState
**初始化 state 結構中的 importPluginState
對象,getPluginState
函數在後續操做中出現很是頻繁,讀者在此須要留意此函數的做用,後文再也不對此進行贅述。 可是爲何須要初始化這麼一個結構呢?這就牽扯到插件的思路。正像開篇流程圖所述的那樣 ,babel-plugin-import
具體實現按需加載思路以下:通過 import 節點後收集節點數據,而後從全部可能引用到 import 綁定的節點處執行按需加載轉換方法。state 是一個引用類型,對其進行操做會影響到後續節點的 state 初始值,所以用 Program 節點,在 enter 的時候就初始化這個收集依賴的對象,方便後續操做。負責初始化 state 節點結構與取數據的方法正是 getPluginState
。 這個思路很重要,而且貫穿後面全部的代碼與目的,請讀者務必理解再往下閱讀。
2、唯恍唯惚
藉由 Step1,如今已經瞭解到插件以 Program
爲出發點繼承了 ProgramEnter
而且初始化了 Plugin 依賴,若是讀者還有還沒有梳理清楚的部分,請回到 Step1 仔細消化下內容再繼續閱讀。 首先,咱們再回到外圍的 Index 文件,以前只在觀察者模式中註冊了 Program
的節點,沒有其餘 AST 節點入口,所以至少還需注入 import 語句的 AST 節點類型 ImportDeclaration
export default function({ types }) { let plugins = null; function applyInstance(method, args, context) { ... } const Program = { ... } const methods = [ // 註冊 AST type 的數組 'ImportDeclaration' ] const ret = { visitor: { Program }, }; // 遍歷數組,利用 applyInstance 繼承相應方法 for (const method of methods) { ret.visitor[method] = function() { applyInstance(method, arguments, ret.visitor); }; } }
建立一個數組並將 ImportDeclaration
置入,通過遍歷調用 applyInstance
_ _和 Step1 介紹同理,執行完畢後 visitor 會變成以下結構
visitor: { Program: { enter: [Function: enter], exit: [Function: exit] }, ImportDeclaration: [Function], }
如今迴歸 Plugin,進入 ImportDeclaration
export default class Plugin { constructor(...) {...} ProgramEnter(_, state) { ... } /** * 主目標,收集依賴 */ ImportDeclaration(path, state) { const { node } = path; // path 有可能被前一個實例刪除 if (!node) return; const { source: { value }, // 獲取 AST 中引入的庫名 } = node; const { libraryName, types } = this; const pluginState = this.getPluginState(state); // 獲取在 Program 處初始化的結構 if (value === libraryName) { // AST 庫名與插件參數名是否一致,一致就進行依賴收集 node.specifiers.forEach(spec => { if (types.isImportSpecifier(spec)) { // 不知足條件說明 import 是名稱空間引入或默認引入 pluginState.specified[spec.local.name] = spec.imported.name; // 保存爲:{ 別名 : 組件名 } 結構 } else { pluginState.libraryObjs[spec.local.name] = true;// 名稱空間引入或默認引入的值設置爲 true } }); pluginState.pathsToRemove.push(path); // 取值完畢的節點添加進預刪除數組 } } ... }
ImportDeclaration
會對 import 中的依賴字段進行收集,若是是名稱空間引入或者是默認引入就設置爲 { 別名 :true },解構導入就設置爲 { 別名 :組件名 } 。getPluginState
方法在 Step1 中已經進行過說明。關於 import 的 AST 節點結構 用 babel-plugin 實現按需加載 中有詳細說明,本文再也不贅述。執行完畢後 pluginState 結構以下
// 例: import { Input, Button as Btn } from 'antd' { ... importPluginState0: { specified: { Btn : 'Button', Input : 'Input' }, pathToRemove: { [NodePath] } ... } ... }
這下 state.importPluginState
結構已經收集到了後續幫助節點進行轉換的全部依賴信息。 目前已經萬事俱備,只欠東風。東風是啥?是能讓轉換 import 工做開始的 action。在 用 babel-plugin 實現按需加載 中收集到依賴的同時也進行了節點轉換與刪除舊節點。一切工做都在 ImportDeclaration
節點中發生。而 babel-plugin-import
的思路是尋找一切可能引用到 Import 的 AST 節點,對他們所有進行處理。有部分讀者也許會直接想到去轉換引用了 import 綁定的 JSX 節點,可是轉換 JSX 節點的意義不大,由於可能引用到 import 綁定的 AST 節點類型 ( type ) 已經夠多了,全部應儘量的縮小須要轉換的 AST 節點類型範圍。並且 babel 的其餘插件會將咱們的 JSX 節點進行轉換成其餘 AST type,所以能不考慮 JSX 類型的 AST 樹,能夠等其餘 babel 插件轉換後再進行替換工做。其實下一步能夠開始的入口有不少,但仍是從咱最熟悉的 React.createElement 開始。
class Hello extends React.Component { render() { return <div>Hello</div> } } // 轉換後 class Hello extends React.Component { render(){ return React.createElement("div",null,"Hello") } }
JSX 轉換後 AST 類型爲 CallExpression
(函數執行表達式),結構以下所示,熟悉結構後能方便各位同窗對以後步驟有更深刻的理解。
{ "type": "File", "program": { "type": "Program", "body": [ { "type": "ClassDeclaration", "body": { "type": "ClassBody", "body": [ { "type": "ClassMethod", "body": { "type": "BlockStatement", "body": [ { "type": "ReturnStatement", "argument": { "type": "CallExpression", // 這裏是處理的起點 "callee": { "type": "MemberExpression", "object": { "type": "Identifier", "identifierName": "React" }, "name": "React" }, "property": { "type": "Identifier", "loc": { "identifierName": "createElement" }, "name": "createElement" } }, "arguments": [ { "type": "StringLiteral", "extra": { "rawValue": "div", "raw": "\"div\"" }, "value": "div" }, { "type": "NullLiteral" }, { "type": "StringLiteral", "extra": { "rawValue": "Hello", "raw": "\"Hello\"" }, "value": "Hello" } ] } ], "directives": [] } } ] } } ] } }
所以咱們進入 CallExpression 節點處,繼續轉換流程。
export default class Plugin { constructor(...) {...} ProgramEnter(_, state) { ... } ImportDeclaration(path, state) { ... } CallExpression(path, state) { const { node } = path; const file = path?.hub?.file || state?.file; const { name } = node.callee; const { types } = this; const pluginState = this.getPluginState(state); // 處理通常的調用表達式 if (types.isIdentifier(node.callee)) { if (pluginState.specified[name]) { node.callee = this.importMethod(pluginState.specified[name], file, pluginState); } } // 處理React.createElement node.arguments = node.arguments.map(arg => { const { name: argName } = arg; // 判斷做用域的綁定是否爲import if ( pluginState.specified[argName] && path.scope.hasBinding(argName) && types.isImportSpecifier(path.scope.getBinding(argName).path) ) { return this.importMethod(pluginState.specified[argName], file, pluginState); // 替換了引用,help/import插件返回節點類型與名稱 } return arg; }); } ... }
能夠看見源碼調用了importMethod
兩次,此函數的做用是觸發 import 轉換成按需加載模式的 action,並返回一個全新的 AST 節點。由於 import 被轉換後,以前咱們人工引入的組件名稱會和轉換後的名稱不同,所以 importMethod
須要把轉換後的新名字(一個 AST 結構)返回到咱們對應 AST 節點的對應位置上,替換掉老組件名。函數源碼稍後會進行詳細分析。 回到一開始的問題,爲何 CallExpression
須要調用 importMethod
函數?由於這兩處表示的意義是不一樣的,CallExpression
節點的狀況有兩種:
CallExpression
類型,例如:import lodash from 'lodash' lodash(some values)
所以在 CallExpression
中首先會判斷 node.callee 值是不是 Identifier
,若是正確則是所述的第二種狀況,直接進行轉換。若否,則是 React.createElement 形式,遍歷 React.createElement 的三個參數取出 name,再判斷 name 是不是先前 state.pluginState 收集的 import 的 name,最後檢查 name 的做用域狀況,以及追溯 name 的綁定是不是一個 import 語句。這些判斷條件都是爲了不錯誤的修改函數本來的語義,防止錯誤修改因閉包等特性的塊級做用域中有相同名稱的變量。若是上述條件均知足那它確定是須要處理的 import 引用了。讓其繼續進入importMethod
轉換函數,importMethod
須要傳遞三個參數:組件名,File(path.sub.file),pluginState
import { join } from 'path'; import { addSideEffect, addDefault, addNamed } from '@babel/helper-module-imports'; export default class Plugin { constructor(...) {...} ProgramEnter(_, state) { ... } ImportDeclaration(path, state) { ... } CallExpression(path, state) { ... } // 組件原始名稱 , sub.file , 導入依賴項 importMethod(methodName, file, pluginState) { if (!pluginState.selectedMethods[methodName]) { const { style, libraryDirectory } = this; const transformedMethodName = this.camel2UnderlineComponentName // 根據參數轉換組件名稱 ? transCamel(methodName, '_') : this.camel2DashComponentName ? transCamel(methodName, '-') : methodName; /** * 轉換路徑,優先按照用戶定義的customName進行轉換,若是沒有提供就按照常規拼接路徑 */ const path = winPath( this.customName ? this.customName(transformedMethodName, file) : join(this.libraryName, libraryDirectory, transformedMethodName, this.fileName), // eslint-disable-line ); /** * 根據是不是默認引入對最終路徑作處理,並無對namespace作處理 */ pluginState.selectedMethods[methodName] = this.transformToDefaultImport // eslint-disable-line ? addDefault(file.path, path, { nameHint: methodName }) : addNamed(file.path, methodName, path); if (this.customStyleName) { // 根據用戶指定的路徑引入樣式文件 const stylePath = winPath(this.customStyleName(transformedMethodName)); addSideEffect(file.path, `${stylePath}`); } else if (this.styleLibraryDirectory) { // 根據用戶指定的樣式目錄引入樣式文件 const stylePath = winPath( join(this.libraryName, this.styleLibraryDirectory, transformedMethodName, this.fileName), ); addSideEffect(file.path, `${stylePath}`); } else if (style === true) { // 引入 scss/less addSideEffect(file.path, `${path}/style`); } else if (style === 'css') { // 引入 css addSideEffect(file.path, `${path}/style/css`); } else if (typeof style === 'function') { // 如果函數,根據返回值生成引入 const stylePath = style(path, file); if (stylePath) { addSideEffect(file.path, stylePath); } } } return { ...pluginState.selectedMethods[methodName] }; } ... }
進入函數後,先彆着急看代碼,注意這裏引入了兩個包:path.join 和 @babel/helper-module-imports ,引入 join 是爲了處理按需加載路徑快捷拼接的需求,至於 import 語句轉換,確定須要產生全新的 import AST 節點實現按需加載,最後再把老的 import 語句刪除。而新的 import 節點使用 babel 官方維護的 @babel/helper-module-imports
生成。如今繼續流程,首先無視一開始的 if 條件語句,稍後會作說明。再捋一捋 import 處理函數中須要處理的幾個環節:
function transCamel(_str, symbol) { const str = _str[0].toLowerCase() + _str.substr(1); // 先轉換成小駝峯,以便正則獲取完整單詞 return str.replace(/([A-Z])/g, $1 => `${symbol}${$1.toLowerCase()}`); // 例 datePicker,正則抓取到P後,在它前面加上指定的symbol符號 }
轉換到組件所在的具體路徑,若是插件用戶給定了自定義路徑就使用 customName 進行處理,babel-plugin-import
爲何不提供對象的形式做爲參數?由於 customName 修改是以 transformedMethodName 值做爲基礎並將其傳遞給插件使用者,如此設計就能夠更精確的匹配到須要按需加載的路徑。處理這些動做的函數是 withPath,withPath 主要兼容 Linux 操做系統,將 Windows 文件系統支持的 '\' 統一轉換爲 '/'。
function winPath(path) { return path.replace(/\\/g, '/'); // 兼容路徑: windows默認使用‘\’,也支持‘/’,但linux不支持‘\’,遂統一轉換成‘/’ }
對 transformToDefaultImport 進行判斷,此選項默認爲 true,轉換後的 AST 節點是默認導出的形式,若是不想要默認導出能夠將 transformToDefaultImport 設置爲 false,以後便利用 @babel/helper-module-imports
生成新的 import 節點,最後**函數的返回值就是新 import 節點的 default Identifier,替換掉調用 importMethod 函數的節點,從而把全部引用舊 import 綁定的節點替換成最新生成的 import AST 的節點。
最後,根據用戶是否開啓 style 按需引入與 customStyleName 是否有 style 路徑額外處理,以及 styleLibraryDirectory(style 包路徑)等參數處理或生成對應的 css 按需加載節點。
到目前爲止一條最基本的轉換線路已經轉換完畢了,相信你們也已經瞭解了按需加載的基本轉換流程,回到 importMethod 函數一開始的if 判斷語句,這與咱們將在 step3 中的任務息息相關。如今就讓咱們一塊兒進入 step3。
3、瞭如指掌
在 step3 中會進行按需加載轉換最後的兩個步驟:
ImportDeclaration
收集信息的時候咱們只是對其進行了依賴收集工做,並無刪除節點。而且咱們還沒有補充 Program 節點 exit 所作的 action接下來將以此列舉須要處理的全部 AST 節點,而且會給每個節點對應的接口(Interface)與例子(不關注語義):
MemberExpression(path, state) { const { node } = path; const file = (path && path.hub && path.hub.file) || (state && state.file); const pluginState = this.getPluginState(state); if (!node.object || !node.object.name) return; if (pluginState.libraryObjs[node.object.name]) { // antd.Button -> _Button path.replaceWith(this.importMethod(node.property.name, file, pluginState)); } else if (pluginState.specified[node.object.name] && path.scope.hasBinding(node.object.name)) { const { scope } = path.scope.getBinding(node.object.name); // 全局變量處理 if (scope.path.parent.type === 'File') { node.object = this.importMethod(pluginState.specified[node.object.name], file, pluginState); } } }
MemberExpression(屬性成員表達式),接口以下
interface MemberExpression { type: 'MemberExpression'; computed: boolean; object: Expression; property: Expression; } /** * 處理相似: * console.log(lodash.fill()) * antd.Button */
若是插件的選項中沒有關閉 transformToDefaultImport ,這裏會調用 importMethod 方法並返回@babel/helper-module-imports
給予的新節點值。不然會判斷當前值是不是收集到 import 信息中的一部分以及是不是文件做用域下的全局變量,經過獲取做用域查看其父節點的類型是不是 File,便可避免錯誤的替換其餘同名變量,好比閉包場景。
VariableDeclarator(path, state) { const { node } = path; this.buildDeclaratorHandler(node, 'init', path, state); }
VariableDeclarator(變量聲明),很是方便理解處理場景,主要處理 const/let/var 聲明語句
interface VariableDeclaration : Declaration { type: "VariableDeclaration"; declarations: [ VariableDeclarator ]; kind: "var" | "let" | "const"; } /** * 處理相似: * const foo = antd */
本例中出現 buildDeclaratorHandler 方法,主要確保傳遞的屬性是基礎的 Identifier 類型且是 import 綁定的引用後便進入 importMethod 進行轉換後返回新節點覆蓋原屬性。
buildDeclaratorHandler(node, prop, path, state) { const file = (path && path.hub && path.hub.file) || (state && state.file); const { types } = this; const pluginState = this.getPluginState(state); if (!types.isIdentifier(node[prop])) return; if ( pluginState.specified[node[prop].name] && path.scope.hasBinding(node[prop].name) && path.scope.getBinding(node[prop].name).path.type === 'ImportSpecifier' ) { node[prop] = this.importMethod(pluginState.specified[node[prop].name], file, pluginState); } }
ArrayExpression(path, state) { const { node } = path; const props = node.elements.map((_, index) => index); this.buildExpressionHandler(node.elements, props, path, state); }
ArrayExpression(數組表達式),接口以下所示
interface ArrayExpression { type: 'ArrayExpression'; elements: ArrayExpressionElement[]; } /** * 處理相似: * [Button, Select, Input] */
本例的處理和剛纔的其餘節點不太同樣,由於數組的 Element 自己就是一個數組形式,而且咱們須要轉換的引用都是數組元素,所以這裏傳遞的 props 就是相似 [0, 1, 2, 3] 的純數組,方便後續從 elements 中進行取數據。這裏進行具體轉換的方法是 buildExpressionHandler,在後續的 AST 節點處理中將會頻繁出現
buildExpressionHandler(node, props, path, state) { const file = (path && path.hub && path.hub.file) || (state && state.file); const { types } = this; const pluginState = this.getPluginState(state); props.forEach(prop => { if (!types.isIdentifier(node[prop])) return; if ( pluginState.specified[node[prop].name] && types.isImportSpecifier(path.scope.getBinding(node[prop].name).path) ) { node[prop] = this.importMethod(pluginState.specified[node[prop].name], file, pluginState); } }); }
首先對 props 進行遍歷,一樣確保傳遞的屬性是基礎的 Identifier
類型且是 import 綁定的引用後便進入 importMethod 進行轉換,和以前的 buildDeclaratorHandler 方法差很少,只是 props 是數組形式
LogicalExpression(path, state) { const { node } = path; this.buildExpressionHandler(node, ['left', 'right'], path, state); }
LogicalExpression(邏輯運算符表達式)
interface LogicalExpression { type: 'LogicalExpression'; operator: '||' | '&&'; left: Expression; right: Expression; } /** * 處理相似: * antd && 1 */
主要取出邏輯運算符表達式的左右兩邊的變量,並使用 buildExpressionHandler 方法進行轉換
ConditionalExpression(path, state) { const { node } = path; this.buildExpressionHandler(node, ['test', 'consequent', 'alternate'], path, state); }
ConditionalExpression(條件運算符)
interface ConditionalExpression { type: 'ConditionalExpression'; test: Expression; consequent: Expression; alternate: Expression; } /** * 處理相似: * antd ? antd.Button : antd.Select; */
主要取出相似三元表達式的元素,同用 buildExpressionHandler 方法進行轉換。
IfStatement(path, state) { const { node } = path; this.buildExpressionHandler(node, ['test'], path, state); this.buildExpressionHandler(node.test, ['left', 'right'], path, state); }
IfStatement(if 語句)
interface IfStatement { type: 'IfStatement'; test: Expression; consequent: Statement; alternate?: Statement; } /** * 處理相似: * if(antd){ } */
這個節點相對比較特殊,但筆者不明白爲何要調用兩次 buildExpressionHandler ,由於筆者所想到的可能性,都有其餘的 AST 入口能夠處理。望知曉的讀者可進行科普。
ExpressionStatement(path, state) { const { node } = path; const { types } = this; if (types.isAssignmentExpression(node.expression)) { this.buildExpressionHandler(node.expression, ['right'], path, state); } }
ExpressionStatement(表達式語句)
interface ExpressionStatement { type: 'ExpressionStatement'; expression: Expression; directive?: string; } /** * 處理相似: * module.export = antd */
ReturnStatement(path, state) { const { node } = path; this.buildExpressionHandler(node, ['argument'], path, state); }
ReturnStatement(return 語句)
interface ReturnStatement { type: 'ReturnStatement'; argument: Expression | null; } /** * 處理相似: * return lodash */
ExportDefaultDeclaration(path, state) { const { node } = path; this.buildExpressionHandler(node, ['declaration'], path, state); }
ExportDefaultDeclaration(導出默認模塊)
interface ExportDefaultDeclaration { type: 'ExportDefaultDeclaration'; declaration: Identifier | BindingPattern | ClassDeclaration | Expression | FunctionDeclaration; } /** * 處理相似: * return lodash */
BinaryExpression(path, state) { const { node } = path; this.buildExpressionHandler(node, ['left', 'right'], path, state); }
BinaryExpression(二元操做符表達式)
interface BinaryExpression { type: 'BinaryExpression'; operator: BinaryOperator; left: Expression; right: Expression; } /** * 處理相似: * antd > 1 */
NewExpression(path, state) { const { node } = path; this.buildExpressionHandler(node, ['callee', 'arguments'], path, state); }
NewExpression(new 表達式)
interface NewExpression { type: 'NewExpression'; callee: Expression; arguments: ArgumentListElement[]; } /** * 處理相似: * new Antd() */
ClassDeclaration(path, state) { const { node } = path; this.buildExpressionHandler(node, ['superClass'], path, state); }
ClassDeclaration(類聲明)
interface ClassDeclaration { type: 'ClassDeclaration'; id: Identifier | null; superClass: Identifier | null; body: ClassBody; } /** * 處理相似: * class emaple extends Antd {...} */
Property(path, state) { const { node } = path; this.buildDeclaratorHandler(node, ['value'], path, state); }
Property(對象的屬性值)
/** * 處理相似: * const a={ * button:antd.Button * } */
處理完 AST 節點後,刪除掉本來的 import 導入,因爲咱們已經把舊 import 的 path 保存在 pluginState.pathsToRemove 中,最佳的刪除的時機即是 ProgramExit
,使用 path.remove() 刪除。
ProgramExit(path, state) { this.getPluginState(state).pathsToRemove.forEach(p => !p.removed && p.remove()); }
恭喜各位堅持看到如今的讀者,已經到最後一步啦,把咱們所處理的全部 AST 節點類型註冊到觀察者中
export default function({ types }) { let plugins = null; function applyInstance(method, args, context) { ... } const Program = { ... } // 補充註冊 AST type 的數組 const methods = [ 'ImportDeclaration' 'CallExpression', 'MemberExpression', 'Property', 'VariableDeclarator', 'ArrayExpression', 'LogicalExpression', 'ConditionalExpression', 'IfStatement', 'ExpressionStatement', 'ReturnStatement', 'ExportDefaultDeclaration', 'BinaryExpression', 'NewExpression', 'ClassDeclaration', ] const ret = { visitor: { Program }, }; for (const method of methods) { ... } }
到此已經完整分析完 babel-plugin-import
的整個流程,讀者能夠從新捋一捋處理按需加載的整個處理思路,其實拋去細節,主體邏輯仍是比較簡單明瞭的。
4、一些思考
筆者在進行源碼與單元測試的閱讀後,發現插件並無對 Switch 節點進行轉換,遂向官方倉庫提了 PR,目前已經被合入 master 分支,讀者有任何想法,歡迎在評論區暢所欲言。 筆者主要補了 SwitchStatement
,SwitchCase
與兩個 AST 節點處理。
SwitchStatement
SwitchStatement(path, state) { const { node } = path; this.buildExpressionHandler(node, ['discriminant'], path, state); }
SwitchCase
SwitchCase(path, state) { const { node } = path; this.buildExpressionHandler(node, ['test'], path, state); }
5、小小總結
這是筆者第一次寫源碼解析的文章,也因筆者能力有限,若是有些邏輯闡述的不夠清晰,或者在解讀過程當中有錯誤的,歡迎讀者在評論區給出建議或進行糾錯。
如今 babel 其實也出了一些 API 能夠更加簡化 babel-plugin-import 的代碼或者邏輯,例如:path.replaceWithMultiple ,但源碼中一些看似多餘的邏輯必定是有對應的場景,因此纔會被加以保留。
此插件經受住了時間的考驗,同時對有須要開發 babel-plugin 的讀者來講,也是一個很是好的事例。不只如此,對於功能的邊緣化處理以及操做系統的兼容等細節都有作完善的處理。
若是僅僅須要使用babel-plugin-import ,此文展現了一些在 babel-plugin-import 文檔中未暴露的API,也能夠幫助插件使用者實現更多擴展功能,所以筆者推出了此文,但願能幫助到各位同窗。