從element-ui按需引入去探索

刨根發表於2020-07-26

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

最後如有錯誤之處,望指正

相關文章