簡(jiǎn)單實(shí)現(xiàn) babel-plugin-import 插件
前言
平時(shí)在使用?antd、element?等組件庫(kù)的時(shí)候,都會(huì)使用到一個(gè)?Babel?插件:babel-plugin-import,這篇文章通過(guò)例子和分析源碼簡(jiǎn)單說(shuō)一下這個(gè)插件做了一些什么事情,并且實(shí)現(xiàn)一個(gè)最小可用版本。
插件地址:https://github.com/ant-design/babel-plugin-import[1]
babel-plugin-import 介紹
Why:為什么需要這個(gè)插件
antd?和?element?這兩個(gè)組件庫(kù),看它的源碼,?index.js?分別是這樣的:
//?antd
export?{?default?as?Button?}?from?'./button';
export?{?default?as?Table?}?from?'./table';
//?element
import?Button?from?'../packages/button/index.js';
import?Table?from?'../packages/table/index.js';
export?default?{
??Button,
??Table,
};
antd?和?element?都是通過(guò)?ES6 Module?的?export?來(lái)導(dǎo)出帶有命名的各個(gè)組件。
所以,我們可以通過(guò)?ES6?的?import { } from?的語(yǔ)法來(lái)導(dǎo)入單組件的?JS?文件。但是,我們還需要手動(dòng)引入組件的樣式:
//?antd
import?'antd/dist/antd.css';
//?element
import?'element-ui/lib/theme-chalk/index.css';
如果僅僅是只需要一個(gè)?Button?組件,卻把所有的樣式都引入了,這明顯是不合理的。
當(dāng)然,你說(shuō)也可以只使用單個(gè)組件啊,還可以減少代碼體積:
import?Button?from?'antd/lib/button';
import?'antd/lib/button/style';
PS:類似?antd?的組件庫(kù)提供了?ES Module?的構(gòu)建產(chǎn)物,直接通過(guò)?import {} from?的形式也可以?tree-shaking,這個(gè)不在今天的話題之內(nèi),就不展開(kāi)說(shuō)了~
對(duì),這沒(méi)毛病。但是,看一下如果我們需要多個(gè)組件的時(shí)候:
import?{?Affix,?Avatar,?Button,?Rate?}?from?'antd';
import?'antd/lib/affix/style';
import?'antd/lib/avatar/style';
import?'antd/lib/button/style';
import?'antd/lib/rate/style';
會(huì)不會(huì)覺(jué)得這樣的代碼不夠優(yōu)雅?如果是我,甚至想打人。
這時(shí)候就應(yīng)該思考一下,如何在引入?Button?的時(shí)候自動(dòng)引入它的樣式文件。
What:這個(gè)插件做了什么
簡(jiǎn)單來(lái)說(shuō),babel-plugin-import?就是解決了上面的問(wèn)題,為組件庫(kù)實(shí)現(xiàn)單組件按需加載并且自動(dòng)引入其樣式,如:
import?{?Button?}?from?'antd';
??????↓?↓?↓?↓?↓?↓
var?_button?=?require('antd/lib/button');
require('antd/lib/button/style');
只需關(guān)心需要引入哪些組件即可,內(nèi)部樣式我并不需要關(guān)心,你幫我自動(dòng)引入就 ok。
How:這個(gè)插件怎么用
簡(jiǎn)單來(lái)說(shuō)就需要關(guān)心三個(gè)參數(shù)即可:
{
??"libraryName":?"antd",?????//?包名
??"libraryDirectory":?"lib",?//?目錄,默認(rèn)?lib
??"style":?true,?????????????//?是否引入?style
}
其它的看文檔:https://github.com/ant-design/babel-plugin-import#usage[2]
babel-plugin-import 源碼分析
主要來(lái)看一下?babel-plugin-import?如何加載?JavaScript?代碼和樣式的。
以下面這段代碼為例:
import?{?Button,?Rate?}?from?'antd';
ReactDOM.render(<Button>xxxxButton>);
第一步 依賴收集
babel-plubin-import?會(huì)在?ImportDeclaration?里將所有的?specifier?收集起來(lái)。
先看一下?ast?吧:

可以從這個(gè)?ImportDeclaration?語(yǔ)句中提取幾個(gè)關(guān)鍵點(diǎn):
source.value: antd specifier.local.name: Button specifier.local.name: Rate
需要做的事情也很簡(jiǎn)單:
import?的包是不是?antd,也就是?libraryName把? Button?和?Rate?收集起來(lái)
來(lái)看代碼:
ImportDeclaration(path,?state)?{
??const?{?node?}?=?path;
??if?(!node)?return;
??//?代碼里?import?的包名
??const?{?value?}?=?node.source;
??//?配在插件?options?的包名
??const?{?libraryName?}?=?this;
??//?babel-type?工具函數(shù)
??const?{?types?}?=?this;
??//?內(nèi)部狀態(tài)
??const?pluginState?=?this.getPluginState(state);
??//?判斷是不是需要使用該插件的包
??if?(value?===?libraryName)?{
????//?node.specifiers?表示?import?了什么
????node.specifiers.forEach(spec?=>?{
??????//?判斷是不是?ImportSpecifier?類型的節(jié)點(diǎn),也就是是否是大括號(hào)的
??????if?(types.isImportSpecifier(spec))?{
????????//?收集依賴
????????//?也就是?pluginState.specified.Button?=?Button
????????//?local.name?是導(dǎo)入進(jìn)來(lái)的別名,比如?import?{?Button?as?MyButton?}?from?'antd'?的?MyButton
????????//?imported.name?是真實(shí)導(dǎo)出的變量名
????????pluginState.specified[spec.local.name]?=?spec.imported.name;
??????}?else?{
????????//?ImportDefaultSpecifier?和?ImportNamespaceSpecifier
????????pluginState.libraryObjs[spec.local.name]?=?true;
??????}
????});
????pluginState.pathsToRemove.push(path);
??}
}
待?babel?遍歷了所有的?ImportDeclaration?類型的節(jié)點(diǎn)之后,就收集好了依賴關(guān)系,下一步就是如何加載它們了。
第二步 判斷是否使用
收集了依賴關(guān)系之后,得要判斷一下這些?import?的變量是否被使用到了,我們這里說(shuō)一種情況。
我們知道,JSX?最終是變成?React.createElement()?執(zhí)行的:
ReactDOM.render(<Button>HelloButton>);
??????↓?↓?↓?↓?↓?↓
React.createElement(Button,?null,?"Hello");
沒(méi)錯(cuò),createElement?的第一個(gè)參數(shù)就是我們要找的東西,我們需要判斷收集的依賴中是否有被?createElement?使用。
分析一下這行代碼的?ast,很容易就找到這個(gè)節(jié)點(diǎn):

來(lái)看代碼:
CallExpression(path,?state)?{
??const?{?node?}?=?path;
??const?file?=?(path?&&?path.hub?&&?path.hub.file)?||?(state?&&?state.file);
??//?方法調(diào)用者的?name
??const?{?name?}?=?node.callee;
??//?babel-type?工具函數(shù)
??const?{?types?}?=?this;
??//?內(nèi)部狀態(tài)
??const?pluginState?=?this.getPluginState(state);
??//?如果方法調(diào)用者是?Identifier?類型
??if?(types.isIdentifier(node.callee))?{
????if?(pluginState.specified[name])?{
??????node.callee?=?this.importMethod(pluginState.specified[name],?file,?pluginState);
????}
??}
??//?遍歷?arguments?找我們要的?specifier
??node.arguments?=?node.arguments.map(arg?=>?{
????const?{?name:?argName?}?=?arg;
????if?(
??????pluginState.specified[argName]?&&
??????path.scope.hasBinding(argName)?&&
??????path.scope.getBinding(argName).path.type?===?'ImportSpecifier'
????)?{
??????//?找到?specifier,調(diào)用?importMethod?方法
??????return?this.importMethod(pluginState.specified[argName],?file,?pluginState);
????}
????return?arg;
??});
}
除了?React.createElement(Button)?之外,還有?const btn = Button?/?[Button]?... 等多種情況會(huì)使用?Button,源碼中都有對(duì)應(yīng)的處理方法,感興趣的可以自己看一下:?https://github.com/ant-design/babel-plugin-import/blob/master/src/Plugin.js#L163-L272[3]?,這里就不多說(shuō)了。
第三步 生成引入代碼(核心)
第一步和第二步主要的工作是找到需要被插件處理的依賴關(guān)系,比如:
import?{?Button,?Rate?}?from?'antd';
ReactDOM.render(<Button>HelloButton>);
Button?組件使用到了,Rate?在代碼里未使用。所以插件要做的也只是自動(dòng)引入?Button?的代碼和樣式即可。
我們先回顧一下,當(dāng)我們?import?一個(gè)組件的時(shí)候,希望它能夠:
import?{?Button?}?from?'antd';
??????↓?↓?↓?↓?↓?↓
var?_button?=?require('antd/lib/button');
require('antd/lib/button/style');
并且再回想一下插件的配置?options[4],只需要將?libraryDirectory?以及?style?等配置用上就完事了。
小朋友,你是否有幾個(gè)問(wèn)號(hào)?這里該如何讓?babel?去修改代碼并且生成一個(gè)新的?import?以及一個(gè)樣式的?import?呢,不慌,看看代碼就知道了:
import?{?addSideEffect,?addDefault,?addNamed?}?from?'@babel/helper-module-imports';
importMethod(methodName,?file,?pluginState)?{
??if?(!pluginState.selectedMethods[methodName])?{
????// libraryDirectory:目錄,默認(rèn) lib
????// style:是否引入樣式
????const?{?style,?libraryDirectory?}?=?this;
????//?組件名轉(zhuǎn)換規(guī)則
????//?優(yōu)先級(jí)最高的是配了 camel2UnderlineComponentName:是否使用下劃線作為連接符
????//?camel2DashComponentName?為?true,會(huì)轉(zhuǎn)換成小寫字母,并且使用?-?作為連接符
????const?transformedMethodName?=?this.camel2UnderlineComponentName
????????transCamel(methodName,?'_')
??????:?this.camel2DashComponentName
????????transCamel(methodName,?'-')
??????:?methodName;
????//?兼容?windows?路徑
????//?path.join('antd/lib/button')?==?'antd/lib/button'
????const?path?=?winPath(
??????this.customName
??????????this.customName(transformedMethodName,?file)
????????:?join(this.libraryName,?libraryDirectory,?transformedMethodName,?this.fileName),
????);
????//?根據(jù)是否有導(dǎo)出?default?來(lái)判斷使用哪種方法來(lái)生成?import?語(yǔ)句,默認(rèn)為?true
????//?addDefault(path,?'antd/lib/button',?{?nameHint:?'button'?})
????//?addNamed(path,?'button',?'antd/lib/button')
????pluginState.selectedMethods[methodName]?=?this.transformToDefaultImport
????????addDefault(file.path,?path,?{?nameHint:?methodName?})
??????:?addNamed(file.path,?methodName,?path);
????//?根據(jù)不同配置?import?樣式
????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)?{
??????addSideEffect(file.path,?`${path}/style`);
????}?else?if?(style?===?'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]?};
}
addSideEffect,?addDefault?和?addNamed?是?@babel/helper-module-imports?的三個(gè)方法,作用都是創(chuàng)建一個(gè)?import?方法,具體表現(xiàn)是:
addSideEffect
addSideEffect(path,?'source');
??????↓?↓?↓?↓?↓?↓
import?"source"
addDefault
addDefault(path,?'source',?{?nameHint:?"hintedName"?})
??????↓?↓?↓?↓?↓?↓
import?hintedName?from?"source"
addNamed
addNamed(path,?'named',?'source',?{?nameHint:?"hintedName"?});
??????↓?↓?↓?↓?↓?↓
import?{?named?as?_hintedName?}?from?"source"
更多關(guān)于?@babel/helper-module-imports?見(jiàn):@babel/helper-module-imports[5]
總結(jié)
一起數(shù)個(gè) 1 2 3,babel-plugin-import?要做的事情也就做完了。
我們來(lái)總結(jié)一下,babel-plugin-import?和普遍的?babel?插件一樣,會(huì)遍歷代碼的?ast,然后在?ast上做了一些事情:
收集依賴:找到? importDeclaration,分析出包?a?和依賴?b,c,d....,假如?a?和?libraryName一致,就將?b,c,d...?在內(nèi)部收集起來(lái)判斷是否使用:在多種情況下(比如文中提到的? CallExpression)判斷 收集到的?b,c,d...?是否在代碼中被使用,如果有使用的,就調(diào)用?importMethod?生成新的?impport?語(yǔ)句生成引入代碼:根據(jù)配置項(xiàng)生成代碼和樣式的? import?語(yǔ)句
不過(guò)有一些細(xì)節(jié)這里就沒(méi)提到,比如如何刪除舊的?import?等... 感興趣的可以自行閱讀源碼哦。
看完一遍源碼,是不是有發(fā)現(xiàn),其實(shí)除了?antd?和?element?等大型組件庫(kù)之外,任意的組件庫(kù)都可以使用?babel-plugin-import?來(lái)實(shí)現(xiàn)按需加載和自動(dòng)加載樣式。
沒(méi)錯(cuò),比如我們常用的?lodash,也可以使用?babel-plugin-import?來(lái)加載它的各種方法,可以動(dòng)手試一下。
動(dòng)手實(shí)現(xiàn) babel-plugin-import
看了這么多,自己動(dòng)手實(shí)現(xiàn)一個(gè)簡(jiǎn)易版的?babel-plugin-import?吧。
如果還不了解如何實(shí)現(xiàn)一個(gè)?Babel?插件,可以閱讀?【Babel 插件入門】如何用 Babel 為代碼自動(dòng)引入依賴
最簡(jiǎn)功能實(shí)現(xiàn)
按照上文說(shuō)的,最重要的配置項(xiàng)就是三個(gè):
{
??"libraryName":?"antd",
??"libraryDirectory":?"lib",
??"style":?true,
}
所以我們也就只實(shí)現(xiàn)這三個(gè)配置項(xiàng)。
并且,上文提到,真實(shí)情況中會(huì)有多種方式來(lái)調(diào)用一個(gè)組件,這里我們也不處理這些復(fù)雜情況,只實(shí)現(xiàn)最常見(jiàn)的??調(diào)用。
入口文件
入口文件的作用是獲取用戶傳入的配置項(xiàng)并且將核心插件代碼作用到?ast?上。
import?Plugin?from?'./Plugin';
export?default?function?({?types?})?{
??let?plugins?=?null;
??//?將插件作用到節(jié)點(diǎn)上
??function?applyInstance(method,?args,?context)?{
????for?(const?plugin?of?plugins)?{
??????if?(plugin[method])?{
????????plugin[method].apply(plugin,?[...args,?context]);
??????}
????}
??}
??const?Program?=?{
????//?ast?入口
????enter(path,?{?opts?=?{}?})?{
??????//?初始化插件實(shí)例
??????if?(!plugins)?{
????????plugins?=?[
??????????new?Plugin(
????????????opts.libraryName,
????????????opts.libraryDirectory,
????????????opts.style,
????????????types,
??????????),
????????];
??????}
??????applyInstance('ProgramEnter',?arguments,?this);
????},
????//?ast?出口
????exit()?{
??????applyInstance('ProgramExit',?arguments,?this);
????},
??};
??const?ret?=?{
????visitor:?{?Program?},
??};
??//?插件只作用在?ImportDeclaration?和?CallExpression?上
??['ImportDeclaration',?'CallExpression'].forEach(method?=>?{
????ret.visitor[method]?=?function?()?{
??????applyInstance(method,?arguments,?ret.visitor);
????};
??});
??return?ret;
}
核心代碼
真正修改?ast?的代碼是在?plugin?實(shí)現(xiàn)的:
import?{?join?}?from?'path';
import?{?addSideEffect,?addDefault?}?from?'@babel/helper-module-imports';
/**
?*?轉(zhuǎn)換成小寫,添加連接符
?*?@param?{*}?_str???字符串
?*?@param?{*}?symbol?連接符
?*/
function?transCamel(_str,?symbol)?{
??const?str?=?_str[0].toLowerCase()?+?_str.substr(1);
??return?str.replace(/([A-Z])/g,?$1?=>?`${symbol}${$1.toLowerCase()}`);
}
/**
?*?兼容?Windows?路徑
?*?@param?{*}?path
?*/
function?winPath(path)?{
??return?path.replace(/\\/g,?'/');
}
export?default?class?Plugin?{
??constructor(
????libraryName,???????????????????????????????????//?需要使用按需加載的包名
????libraryDirectory?=?'lib',??????????????????????//?按需加載的目錄
????style?=?false,?????????????????????????????????//?是否加載樣式
????types,?????????????????????????????????????????//?babel-type?工具函數(shù)
??)?{
????this.libraryName?=?libraryName;
????this.libraryDirectory?=?libraryDirectory;
????this.style?=?style;
????this.types?=?types;
??}
??/**
???*?獲取內(nèi)部狀態(tài),收集依賴
???*?@param?{*}?state
???*/
??getPluginState(state)?{
????if?(!state)?{
??????state?=?{};
????}
????return?state;
??}
??/**
???*?生成?import?語(yǔ)句(核心代碼)
???*?@param?{*}?methodName
???*?@param?{*}?file
???*?@param?{*}?pluginState
???*/
??importMethod(methodName,?file,?pluginState)?{
????if?(!pluginState.selectedMethods[methodName])?{
??????// libraryDirectory:目錄,默認(rèn) lib
??????// style:是否引入樣式
??????const?{?style,?libraryDirectory?}?=?this;
??????//?組件名轉(zhuǎn)換規(guī)則
??????const?transformedMethodName?=?transCamel(methodName,?'');
??????//?兼容?windows?路徑
??????//?path.join('antd/lib/button')?==?'antd/lib/button'
??????const?path?=?winPath(join(this.libraryName,?libraryDirectory,?transformedMethodName));
??????//?生成?import?語(yǔ)句
??????//?import?Button?from?'antd/lib/button'
??????pluginState.selectedMethods[methodName]?=?addDefault(file.path,?path,?{?nameHint:?methodName?});
??????if?(style)?{
????????//?生成樣式?import?語(yǔ)句
????????//?import?'antd/lib/button/style'
????????addSideEffect(file.path,?`${path}/style`);
??????}
????}
????return?{?...pluginState.selectedMethods[methodName]?};
??}
??ProgramEnter(path,?state)?{
????const?pluginState?=?this.getPluginState(state);
????pluginState.specified?=?Object.create(null);
????pluginState.selectedMethods?=?Object.create(null);
????pluginState.pathsToRemove?=?[];
??}
??ProgramExit(path,?state)?{
????//?刪除舊的?import
????this.getPluginState(state).pathsToRemove.forEach(p?=>?!p.removed?&&?p.remove());
??}
??/**
???*?ImportDeclaration?節(jié)點(diǎn)的處理方法
???*?@param?{*}?path
???*?@param?{*}?state
???*/
??ImportDeclaration(path,?state)?{
????const?{?node?}?=?path;
????if?(!node)?return;
????//?代碼里?import?的包名
????const?{?value?}?=?node.source;
????//?配在插件?options?的包名
????const?{?libraryName?}?=?this;
????//?babel-type?工具函數(shù)
????const?{?types?}?=?this;
????//?內(nèi)部狀態(tài)
????const?pluginState?=?this.getPluginState(state);
????//?判斷是不是需要使用該插件的包
????if?(value?===?libraryName)?{
??????//?node.specifiers?表示?import?了什么
??????node.specifiers.forEach(spec?=>?{
????????//?判斷是不是?ImportSpecifier?類型的節(jié)點(diǎn),也就是是否是大括號(hào)的
????????if?(types.isImportSpecifier(spec))?{
??????????//?收集依賴
??????????//?也就是?pluginState.specified.Button?=?Button
??????????//?local.name?是導(dǎo)入進(jìn)來(lái)的別名,比如?import?{?Button?as?MyButton?}?from?'antd'?的?MyButton
??????????//?imported.name?是真實(shí)導(dǎo)出的變量名
??????????pluginState.specified[spec.local.name]?=?spec.imported.name;
????????}?else?{
??????????//?ImportDefaultSpecifier?和?ImportNamespaceSpecifier
??????????pluginState.libraryObjs[spec.local.name]?=?true;
????????}
??????});
??????//?收集舊的依賴
??????pluginState.pathsToRemove.push(path);
????}
??}
??/**
???*?React.createElement?對(duì)應(yīng)的節(jié)點(diǎn)處理方法
???*?@param?{*}?path
???*?@param?{*}?state
???*/
??CallExpression(path,?state)?{
????const?{?node?}?=?path;
????const?file?=?(path?&&?path.hub?&&?path.hub.file)?||?(state?&&?state.file);
????//?方法調(diào)用者的?name
????const?{?name?}?=?node.callee;
????//?babel-type?工具函數(shù)
????const?{?types?}?=?this;
????//?內(nèi)部狀態(tài)
????const?pluginState?=?this.getPluginState(state);
????//?如果方法調(diào)用者是?Identifier?類型
????if?(types.isIdentifier(node.callee))?{
??????if?(pluginState.specified[name])?{
????????node.callee?=?this.importMethod(pluginState.specified[name],?file,?pluginState);
??????}
????}
????//?遍歷?arguments?找我們要的?specifier
????node.arguments?=?node.arguments.map(arg?=>?{
??????const?{?name:?argName?}?=?arg;
??????if?(
????????pluginState.specified[argName]?&&
????????path.scope.hasBinding(argName)?&&
????????path.scope.getBinding(argName).path.type?===?'ImportSpecifier'
??????)?{
????????//?找到?specifier,調(diào)用?importMethod?方法
????????return?this.importMethod(pluginState.specified[argName],?file,?pluginState);
??????}
??????return?arg;
????});
??}
}
這樣就實(shí)現(xiàn)了一個(gè)最簡(jiǎn)單的?babel-plugin-import?插件,可以自動(dòng)加載單包和樣式。
完整代碼:https://github.com/axuebin/babel-plugin-import-demo[6]
總結(jié)
本文通過(guò)源碼解析和動(dòng)手實(shí)踐,深入淺出的介紹了?babel-plugin-import?插件的原理,希望大家看完這篇文章之后,都能清楚地了解這個(gè)插件做了什么事。
關(guān)于?Babel?你會(huì)用到的一些鏈接:
Babel 用戶手冊(cè)[7] Babel 插件手冊(cè)[8] ast 分析[9] 節(jié)點(diǎn)規(guī)范[10]
參考資料
https://github.com/ant-design/babel-plugin-import:?https://github.com/ant-design/babel-plugin-import
[2]?https://github.com/ant-design/babel-plugin-import#usage:?https://github.com/ant-design/babel-plugin-import#usage
[3]?https://github.com/ant-design/babel-plugin-import/blob/master/src/Plugin.js#L163-L272:?https://github.com/ant-design/babel-plugin-import/blob/master/src/Plugin.js#L163-L272
[4]?options:?https://github.com/ant-design/babel-plugin-import#options
[5]?@babel/helper-module-imports:?https://babeljs.io/docs/en/next/babel-helper-module-imports.html
[6]?https://github.com/axuebin/babel-plugin-import-demo:?https://github.com/axuebin/babel-plugin-import-demo
[7]?Babel 用戶手冊(cè):?https://github.com/jamiebuilds/babel-handbook/blob/master/translations/zh-Hans/user-handbook.md
[8]?Babel 插件手冊(cè):?https://github.com/jamiebuilds/babel-handbook/blob/master/translations/zh-Hans/plugin-handbook.md
[9]?ast 分析:?https://astexplorer.net/
[10]?節(jié)點(diǎn)規(guī)范:?https://github.com/estree/estree
