組件庫實現按需引入的原理

前言

已經有好長時間不在掘金冒泡了,固然也是事出有因,3月份動了一次手術,請了3個月的病假。作完手術就一直躺在牀上玩switch,一直玩到如今,什麼塞爾達傳說,火焰紋章也在這段時間打通關了(其實我很想玩ps4,可是作起來費勁只能躺着玩掌機)。css

WechatIMG237.jpeg

發生意外以前,在公司正着手準備作一個內部的ui庫,因而就研究了一下一些開源的ui庫的方案,這裏作一個簡單總結和分享。node

各個組件庫是怎麼作的?

它們一般都會使用 webpack 或者 rollup 打包生成一個入口的js文件,這個文件一般都是在你不須要按需引入組件庫時使用。react

好比 iview 組件庫中的 dist 目錄下的 iview.js 文件。webpack

import ViewUI from 'view-design';

// 引入了整個js文件,裏面可能包含了一些你不須要的組件的代碼
Vue.use(ViewUI);
複製代碼

再好比 rsuite 組件庫中 lib 目錄下的 index.js 文件。git

// 即便你沒有使用其餘組件,也會引入一整個js文件
import { Button } from 'rsuite';

function App() {
  return <Button>Hello World</Button>;
}
複製代碼

若是咱們不須要引入所有的組件,咱們首先就不能將組件的代碼,打包到一個js文件中。咱們能夠直接使用 babel 或者藉助 gulp 對組件庫源碼的 src 目錄中各個文件直接進行編譯, 並寫入到目標的 lib 目錄。github

// 使用`gulp-babel`對源代碼的目錄進行編譯
function buildLib() {
  return gulp
    .src(源碼目錄)
    .pipe(babel(babelrc()))
    .pipe(gulp.dest(目標lib目錄));
}
複製代碼

WX20200512-223522@2x.png

WX20200512-223544@2x.png

編譯後的目錄結構和源碼的目錄結構是徹底一致的,可是組件的代碼已是通過babel處理過的了。web

這個時候,咱們就能夠實現按需引入組件庫。可是業務代碼中的 import 代碼得修改一下。咱們以 rsuite 組件庫爲例。若是咱們只想使用 Button 組件,咱們就須要指明,只引入 lib\Button 目錄下 index.js 文件。gulp

// 只引入 node_modules/rsuite/lib/Button/index.js 文件
import Button from 'rsuite/lib/Button';
複製代碼

這樣作實在是頗爲麻煩,好在已經有了現成的解決方案,babel-plugin-import 插件。假設咱們打包後的目錄結構以下圖。babel

WX20200512-225547@2x.png

咱們只須要在 .babelrc 中作以下的設置。antd

// .babelrc
{
  "plugins": [
    [
      "import", {
        "libraryName": "react-ui-components-library",
        "libraryDirectory": "lib/components",
        "camel2DashComponentName": false
      }
    ]
  ]
}
複製代碼

babel插件就會自動將 import { Button } from '組件庫' 轉換爲 import Button from '組件庫/lib/components/Button'

那麼 babel-plugin-import 是如何作到的呢?

babel-plugin-import的實現機制

babel-plugin-import 源碼我並無仔細研究,只是大概看了下,不少細節並非很瞭解,若有錯誤還請多多包含。

在瞭解babel-plugin-import源碼前,咱們還須要瞭解ast,訪問者的概念,這些概念推薦你們閱讀下這篇手冊,Babel 插件手冊

babel-plugin-import 的源碼中定義了 import 節點的訪問者(babel在處理源碼時,若是遇到了import語句,會使用import訪問者對import代碼節點進行處理)

咱們先看看,import代碼節點在babel眼中張什麼樣子

image.png

圖片右邊的樹,就是訪問者函數中的 path參數

ImportDeclaration(path, { opts }) {
    const { node } = path;

    if (!node) return;

    const { value } = node.source;
    const libraryName = this.libraryName;
    const types = this.types;
    // 若是value等於咱們在插件中設置的庫的名稱
    if (value === libraryName) {
      node.specifiers.forEach(spec => {
        // 記錄引入的模塊
        if (types.isImportSpecifier(spec)) {
          this.specified[spec.local.name] = spec.imported.name;
        } else {
          this.libraryObjs[spec.local.name] = true;
        }
      });
      // 刪除原有的節點,就是刪除以前的import代碼
      path.remove();
    }
  }
複製代碼

在適當的時刻,會插入被修改引入路徑的 import 節點

importMethod(methodName, file, opts) {
    if (!this.selectedMethods[methodName]) {
      const libraryDirectory = this.libraryDirectory;
      const style = this.style;
      // 修改模塊的引入路徑,好比 antd -> antd/lib/Button
      const path = `${this.libraryName}/${libraryDirectory}/${camel2Dash(methodName)}`;
      // 插入被修改引入路徑的 import 節點
      this.selectedMethods[methodName] = file.addImport(path, 'default');
      if (style === true) {
        file.addImport(`${path}/style`);
      } else if(style === 'css') {
        file.addImport(`${path}/style/css`);
      }
    }
    return this.selectedMethods[methodName];
}
複製代碼
相關文章
相關標籤/搜索