在最近的開發過程當中,不一樣的項目、不一樣的頁面都須要用到某種UI控件,因而很天然的將這些UI控件拆出來,單獨創建了一個代碼庫進行維護。下面是個人組件庫大體的目錄結構以下:javascript
... - lib - components - componentA - index.vue - componentB - index.vue - componentC - index.vue - index.js ...
整個組件庫的出口在index.js
,裏面的內容差很少是下面這樣的:vue
import A from './lib/componentA'; import B from './lib/componentB'; import C from './lib/componentC'; export { A, B, C }
個人代碼庫的name爲:kb-bi-vue-component
。在項目中引用這個組件庫的時候,代碼以下:java
import { A, B } from 'kb-bi-vue-component'; ....
這個時候,問題出現了,我在頁面中,僅僅使用了A
、B
兩個組件,可是頁面打包後,整個組件庫的代碼都會被打進來,增長了產出的體積,包括了很多的冗餘代碼。很容易想到的一個解決方案是按照如下的方式引用組件。node
import A from 'kb-bi-vue-component/lib/componentA'; import B from 'kb-bi-vue-component/lib/componentB';
這種方法雖然解決了問題,我想引用哪一個組件,就引用哪一個組件,不會有多餘的代碼。可是我總以爲這種寫法看起來不太舒服。有沒有還能像第一種寫法同樣引用組件庫,而且只引用須要的組件呢?寫一個babel-plugin好了,自動將第一種寫法轉換成第二種寫法。git
本文只是簡單介紹。想要深刻理解代碼編譯,請學習<<編譯原理>>這裏有一個不錯的Babel教程:https://github.com/jamiebuild...github
Babel是Javascript編譯器,更確切地說是源碼到源碼的編譯器,一般也叫作『轉換編譯器』。也就是說,你給Babel提供一些Javascript代碼,Babel更改這下代碼,而後返回給你新生成的代碼。數組
在這整個過程當中,都是圍繞着抽象語法樹(AST)來進行的。在Javascritp中,AST,簡單來講,就是一個記錄着代碼語法結構的Object。好比下面的代碼:babel
function square(n) { return n * n; }
轉換成AST後以下,函數
{ type: "FunctionDeclaration", id: { type: "Identifier", name: "square" }, params: [{ type: "Identifier", name: "n" }], body: { type: "BlockStatement", body: [{ type: "ReturnStatement", argument: { type: "BinaryExpression", operator: "*", left: { type: "Identifier", name: "n" }, right: { type: "Identifier", name: "n" } } }] } }
AST是分層的,由一個一個的 節點(Node) 組成。如:工具
{ ... type: "FunctionDeclaration", id: { type: "Identifier", name: "square" }, ... }
{ type: "Identifier", name: ... }
每個節點都有一個必需的 type 字段表示節點的類型。如上面的FunctionDeclaration
、Identifier
等等。每種類型的節點都會有本身的屬性。
Babel的處理過程主要爲3個:解析(parse)、轉換(transform)、生成(generate)。
解析主要包含兩個過程:詞法分析和語法分析,輸入是代碼字符串,輸出是AST。
處理AST。處理工具、插件等就是在這個過程當中介入,將代碼按照需求進行轉換。
遍歷AST,輸出代碼字符串。
解析和生成過程,都有Babel都爲咱們處理得很好了,咱們要作的就是在 轉換 過程當中搞事情,進行個性化的定製開發。
這裏有詳細的介紹: https://github.com/jamiebuild...
首先,須要大體瞭解一下babel-plugin的開發方法。
babel使用一種 訪問者模式 來遍歷整棵語法樹,即遍歷進入到每個Node節點時,能夠說咱們在「訪問」這個節點。訪問者就是一個對象,定義了在一個樹狀結構中獲取具體節點的方法。簡單來講,咱們能夠在訪問者中,使用Node的type來定義一個hook函數,每一次遍歷到對應type的Node時,hook函數就會被觸發,咱們能夠在這個hook函數中,修改、查看、替換、刪除這個節點。提及來很抽象,直接看下面的內容吧。
.babelrc
來配置babel的,以下:{ "presets": [ ["es2015"], ["stage-0"] ] }
上面的配置中,只有兩個預設,並無使用插件。首先加上插件的配置。因爲是在本地開發,插件直接寫的本地的相對地址。
{ "presets": [ ["es2015"], ["stage-0"] ], "plugins":["./my-import-babel-plugin"] }
僅僅像上面這樣是有問題的,由於需求是須要針對具體的library,因此確定是須要傳入參數的。改爲下面這樣:
{ "presets": [ ["es2015"], ["stage-0"] ], "plugins":[ ["./my-import-babel-plugin", { "libraryName": "kb-bi-vue-component", "alias": "kb-bi-vue-component/lib/components"}] ] }
咱們給plugin傳了一個參數,libraryName表示須要處理的library,alias表示組件在組件庫內部的路徑。
./my-import-babel-plugin.js
module.exports = function ({ types: t }) { return { visitor: { ImportDeclaration(path, source){ const { opts: { libraryName, alias } } = source; if (!t.isStringLiteral(path.node.source, { value: libraryName })) { return; } console.log(path.node); // todo } } } }
函數的參數爲babel對象,對象中的types是一個用於 AST 節點的 Lodash 式工具庫,它包含了構造、驗證以及變換 AST 節點的方法。 該工具庫包含考慮周到的工具方法,對編寫處理AST邏輯很是有用。咱們單獨把這個types拿出來。返回的visitor就是咱們上文提到的訪問者對象。此次的需求是對 import 語句的修改,因此咱們在visitor中定義了import
的type:ImportDeclaration
。這樣,當babel處理到代碼裏的import
語句時,就會走到這個ImportDeclaration
函數裏面來。
這裏查看Babel定義的全部的AST Node: https://github.com/babel/babe...
ImportDeclaration
接受兩個參數,
path
表示當前訪問的路徑,path.node
就能取到當前訪問的Node.source
表示PluginPass
,即傳遞給當前plugin的其餘信息,包括當前編譯的文件、代碼字符串以及咱們在.babelrc
中傳入的參數等。在插件的代碼中,咱們首先取到了傳入插件的參數。接着,判斷若是不是咱們須要處理的library,就直接返回了。
這裏能夠查看babel.types的使用方法: https://babeljs.io/docs/en/ba...
... import { A, B } from 'kb-bi-vue-component' ...
咱們運行一下打包工具,輸出一下path.node
,能夠看到,當前訪問的Node以下:
Node { type: 'ImportDeclaration', start: 9, end: 51, loc: SourceLocation { start: Position { line: 10, column: 0 }, end: Position { line: 10, column: 42 } }, specifiers: [Node { type: 'ImportSpecifier', start: 18, end: 19, loc: [Object], imported: [Object], local: [Object] }, Node { type: 'ImportSpecifier', start: 21, end: 22, loc: [Object], imported: [Object], local: [Object] } ], source: Node { type: 'StringLiteral', start: 30, end: 51, loc: SourceLocation { start: [Object], end: [Object] }, extra: { rawValue: 'kb-bi-vue-component', raw: '\'kb-bi-vue-component\'' }, value: 'kb-bi-vue-component' } }
稍微解釋一下這個Node. specifiers
是一個數組,包含兩個Node,對應的是代碼import
後面的兩個參數A
和B
。這兩個Node的local
值都是Identifier
類型的Node。source
表示的是代碼from
後面的library。
接下來,按照需求把這個ImportDeclaration
類型的Node替換掉,換成咱們想要的。使用path.replaceWithMultiple
這個方法來替換一個Node。此方法接受一個Node數組。因此咱們首先須要構造出Node,裝進一個數組裏,而後扔給這個path.replaceWithMultiple
方法。
查閱文檔,
t.importDeclaration(specifiers, source) specifiers: Array<ImportSpecifier | ImportDefaultSpecifier | ImportNamespaceSpecifier> (required) source: StringLiteral (required)
能夠經過t.importDeclaration
來構造import
Node,參數如上所示。構造import
Node,須要先構造其參數須要的Node。最終,修改插件的代碼以下:
module.exports = function ({ types: t }) { return { visitor: { ImportDeclaration(path, source) { const { opts: { libraryName, alias } } = source; if (!t.isStringLiteral(path.node.source, { value: libraryName })) { return; } const newImports = path.node.specifiers.map( item => { return t.importDeclaration([t.importDefaultSpecifier(item.local)], t.stringLiteral(`${alias}/${item.local.name}`)) }); path.replaceWithMultiple(newImports); } } } }
好了,一個babel-plugin開發完成了。咱們成功的實現瞭如下的編譯:
import { A, B } from 'kb-bi-vue-component'; ↓ ↓ ↓ ↓ ↓ ↓ import A from 'kb-bi-vue-component/lib/components/A'; import B from 'kb-bi-vue-component/lib/components/B';
babel在工做時,會優先執行.babelrc
中的plugins
,接着纔會執行presets
。咱們優先將源代碼進行了轉換,再使用babel去轉換爲es5的代碼,整個過程是沒有問題的。
固然,這是最簡單的babel-plugin,還有不少其餘狀況沒有處理,好比下面這種,轉換後就不符合預期。
import { A as aaa, B } from 'kb-bi-vue-component'; ↓ ↓ ↓ ↓ ↓ ↓ import aaa from 'kb-bi-vue-component/lib/components/aaa'; import B from 'kb-bi-vue-component/lib/components/B';
要完成一個高質量的babel-plugin,還有不少的工做要作。
附:阿里已經開源了一個成熟的babel-plugin-import
參考連接:
一、https://github.com/jamiebuild...
二、https://babeljs.io/docs/en/ba...