element-ui的按需引入的配置:文件地址
npm install babel-plugin-component -D
{
"presets": [["es2015", { "modules": false }]],
"plugins": [
[
"component",
{
"libraryName": "element-ui",
"styleLibraryName": "theme-chalk"
}
]
]
}
import Vue from 'vue';
import { Button, Select } from 'element-ui';
import App from './App.vue';
Vue.component(Button.name, Button);
Vue.component(Select.name, Select);
三步下來就能方便的使用按需引入的功能了。
其中的原理是什麼?babel-plugin-component在其中做了什麼?
探究處理過程
首先新建一個demo,使用最簡化的配置,demo地址。
demo中只用了四種鉤子:
Program:第一個訪問的節點,初始化資料。
ImportDeclaration:處理import import { Button, Select } from 'element-ui';
CallExpression:函式執行會訪問到,處理Vue.component(Button.name, Button);
MemberExpression:處理物件訪問,Select.name
。
總結一下處理的過程:
第一步
在Program初始化specified等資料,在處理當前檔案的過程中這些資料作為全域性使用。
第二步
在 ImportDeclaration 裡將收集import的變數,比如Button,Select等
import { Button, Select } from 'element-ui'
將變數儲存到specified中,這個specified會作為後面處理AST的判斷條件
specified[spec.local.name] = spec.imported.name
第三步
在CallExpression中,根據是否使用到Button等會在AST新增節點,這些節點會轉換為下面的程式碼:
import button form "element-ui/lib/button"
新增節點這個環節使用到@babel/helper-module-imports
中的helper方法addSideEffect,addDefault,簡化了手動操作。
簡單介紹一下helper-module-imports:文件連結
呼叫addSideEffect方法能夠生成類似 import "source"
的程式碼,適合新增css等資源。
呼叫addDefault方法能夠生成類似import _default from "source"
的程式碼,適合新增js。
上面三步之後,想要的AST就構建完成了。以demo為例,原始碼:
import { Button } from 'element-ui';
Vue.component(Button.name,Button)
執行npm run build ,babel處理之後的程式碼是:
var _button = _interopRequireDefault(require("element-ui/lib/theme-chalk/button.css"));
require("element-ui/lib/theme-chalk/base.css");
var _button2 = _interopRequireDefault(require("element-ui/lib/button"));
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }
Vue.component(_button2["default"].name, _Button);
可以看到自動引入了css require("element-ui/lib/theme-chalk/base.css")
,引入element-ui不見了,增加了require("element-ui/lib/button")
需要解釋一下,上面的import變成了require是因為babel中presets-env的影響;同理_interopRequireDefault也是。
如果在babel.config.json設定modules:false
結果將是下面的樣子:
import _Button2 from "element-ui/lib/theme-chalk/button.css";
import "element-ui/lib/theme-chalk/base.css";
import _Button from "element-ui/lib/button";
Vue.component(_Button.name, _Button);
// 看起來順眼多了
版本問題
在自己檢查程式碼時發現第一個demo的結果Vue.component(_button2["default"].name, _Button);
中的_Button是一個錯誤,程式碼中沒有這個引用,執行起來肯定是要報錯的;仔細檢視了plugin.js並沒有發現問題。當換成直接引入babel-plugin-component的時候就沒有了問題,通過對比終於發現@babel/helper-module-imports的版本不同,
- babel-plugin-component 內部node_modules中依賴的 @babel/helper-module-imports 版本7.0.0
- 跟隨helper-module-transforms一起安裝的是7.10.4
切換到版本7.0.0就可以了。
解決方案 一
版本問題能夠通過修改plugin.js來解決麼?看下面的程式碼:
function importMethod(methodName, file, opts) {
if (!selectedMethods[methodName]) {
....
selectedMethods[methodName] = addDefault(file.path, path, { nameHint: methodName });
....
}
// ....
return selectedMethods[methodName];
}
在對Vue.component(Button.name, Button)
的訪問中需要對引數Button做兩次處理,都需要執行到importMethod方法,methodName的值就是"Button"
,按照執行邏輯兩次執行返回的是同一的物件:
{
type:"Identifier",
name:"_Button"
}
生成程式碼的時候應該是 Vue.component(_button2["default"].name, _button2["default"])
,這裡卻好像把第二個_Button給忘了,猜測難道此處的引用傳值導致的麼?
考慮到通過一個簡單的物件能生成_button2["default"]
,說明自己也可以建立一個物件生成對應的程式碼,於是就簡單的deepClone一下selectedMethods[methodName],試過之後果然可以,此處並沒有查詢到真正的原因,只作為探索,程式碼如下:
function importMethod(methodName, file, opts) {
if (!selectedMethods[methodName]) {
....
selectedMethods[methodName] = addDefault(file.path, path, { nameHint: methodName });
....
}
// ....
// 此處的t是types,帶有一個cloneDeep的方法
return t.cloneDeep(selectedMethods[methodName]);
}
解決方案二:
其實在打斷點的時候發現,最終生成生成的AST是正確的,錯在程式碼生成的階段,經過嘗試發現直接把modules:false
就可以避免問題。一般來說我們都要把babel的模組處理取消掉,由webpack來處理模組打包,所以這個方案更加合適。
結束
檢視有哪些鉤子 :地址
babel中外掛的執行順序:外掛執行順序:
本文只介紹了四個鉤子,原外掛還使用了IfStatement,ConditionalExpression,LogicalExpression,VariableDeclarator,Property,ArrayExpression,AssignmentExpression七個鉤子,這幾個鉤子主要是處理特殊的情況,暫時還未遇到。
最後如有錯誤之處,望指正