從element-ui按需引入去探索
阿新 • • 發佈:2020-07-27
element-ui的按需引入的配置:[文件地址](https://element.eleme.cn/#/zh-CN/component/quickstart#an-xu-yin-ru)
```
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地址](https://github.com/blank-x/kv/tree/master/babel-plugin-component2)。
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等
```javascript
import { Button, Select } from 'element-ui'
```
將變數儲存到specified中,這個specified會作為後面處理AST的判斷條件
```javascript
specified[spec.local.name] = spec.imported.name
```
#### 第三步
在CallExpression中,根據是否使用到Button等會在AST新增節點,這些節點會轉換為下面的程式碼:
```javascript
import button form "element-ui/lib/button"
```
新增節點這個環節使用到`@babel/helper-module-imports`中的helper方法addSideEffect,addDefault,簡化了手動操作。
簡單介紹一下helper-module-imports:[文件連結](https://babeljs.io/docs/en/babel-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`結果將是下面的樣子:
```javascript
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來處理模組打包,所以這個方案更加合適。
### 結束
檢視有哪些鉤子 :[地址](https://www.babeljs.cn/docs/babel-types)
babel中外掛的執行順序:[外掛執行順序](https://www.babeljs.cn/docs/plugins#%E6%8F%92%E4%BB%B6%E9%A1%BA%E5%BA%8F):
本文只介紹了四個鉤子,原外掛還使用了IfStatement,ConditionalExpression,LogicalExpression,VariableDeclarator,Property,ArrayExpression,AssignmentExpression七個鉤子,這幾個鉤子主要是處理特殊的情況,暫時還未遇到。
最後如有錯誤之處,