1. 程式人生 > >從element-ui按需引入去探索

從element-ui按需引入去探索

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七個鉤子,這幾個鉤子主要是處理特殊的情況,暫時還未遇到。 最後如有錯誤之處,