將Vue組件庫更換爲按需加載

按需加載DEMO倉庫地址css

背景

我司前端團隊擁有一套支撐公司業務系統的UI組件庫,通過屢次迭代後,組件庫體積很是龐大。前端

組件庫依賴在npm上管理,組件庫以項目根目錄的 index.js 做爲出口導出,文件中導入了項目中全部的組件,並提供組件安裝方法。vue

index.jsnode

import Button from "./button";
import Table from "./table";
import MusicPlayer from "./musicPlayer";

import utils from "../utils"
import * as directive from "../directive";
import * as filters from "../filters";

const components = {
    Button,
    Table,
    MusicPlayer
}

const install = (Vue) => {
    Object.keys(components).forEach(component => Vue.use(component));
    
    // 此處繼續完成一些服務的掛載
}

if (typeof window !== 'undefined' && window.Vue) {
  install(Vue, true);
}

export default {
    install,
    ...components
}

複製代碼

組件庫並不導出編譯完成後的依賴文件,業務系統使用時,安裝依賴並導入,就能註冊組件。webpack

import JRUI from 'jr-ui';
Vue.use(JRUI);
複製代碼

組件庫的編譯是交由業務系統的編譯服務順帶編譯的。git

即組件庫項目自己不會編譯,僅做爲組件導出。node_module 就像一個免費的雲盤,用於存儲組件庫代碼。github

由於經業務系統編譯,在業務系統中。組件庫代碼可以和本地文件同樣,直接調試。並且很是簡單粗暴,並不須要作一些依賴導出的額外配置。web

但也存在缺點vue-cli

  1. 組件庫中沒法使用更爲特殊的代碼npm

    vue-cli會靜態編譯在 node_module 引用的 .vue 文件,但不會編譯 node_module 中的其餘文件,一旦組件庫代碼存在特殊的語法擴展(JSX),或者特殊的語言(TypeScript)。此時項目啓動會運行失敗。

  2. 組件庫中使用 webpack 的特殊變量將不起效

    組件庫中的 webpack 配置不會被業務系統去執行,因此組件庫中的路徑別名等屬性沒法使用

  3. 組件庫依賴每次都是全量加載

    index.js 自己就是全量的組件導入,因此即便業務系統只使用了部分組件, index.js 也會將全部的組件文件(圖片資源,依賴)都打包進去,依賴體積老是全量大小的。

業務系統並不存在只使用一兩個組件的狀況,每一個業務系統都須要絕大部分組件。

幾乎每一個項目都會使用好比 按鈕,輸入框,下拉選項,表格 等常見基礎組件。

只有部分組件僅在少數特殊業務線使用,例如 富文本編輯器,音樂播放器

組件分類

爲了解決上述問題,及完成按需引入的效果。提供兩種組件導出方式,全量導出基礎導出

將組件導出分爲兩種類型。基礎組件按需引入組件

按需引入組件的評定標準爲:

  1. 較少業務系統使用
  2. 組件中包含體積較大或資源文件較多的第三方依賴
  3. 未被其餘組件內部引用

全量導出模式導出所有組件,基礎導出僅導出基礎組件。在須要使用按需引入組件時,須要自行引入對應組件。

調整爲按需引入

參考 element-ui 的導出方案,組件庫導出的組件依賴,要提供每一個組件單獨打包的依賴文件。

全量導出 index.js 文件無需改動,在 index.js 同級目錄增長新文件 base.js,用於導出基礎組件。

base.js

import Button from "./Button";
import Table from "./table";

const components = {
    Button,
    Table
}

const install = (Vue) => {
    Object.keys(components).forEach(component => Vue.use(component));
}

export default {
    install,
    ...components
}
複製代碼

修改組件庫腳手架工具,增長額外打包配置。用於編譯組件文件,輸出編譯後的依賴。

vue.config.js

const devConfig = require('./build/config.dev');
const buildConfig = require('./build/config.build');

module.exports = process.env.NODE_ENV === 'development' ? devConfig : buildConfig;
複製代碼

config.build.js

const fs = require('fs');
const path = require('path');
const join = path.join;
// 獲取基於當前路徑的目標文件
const resolve = (dir) => path.join(__dirname, '../', dir);

/** * @desc 大寫轉橫槓 * @param {*} str */
function upperCasetoLine(str) {
  let temp = str.replace(/[A-Z]/g, function (match) {
    return "-" + match.toLowerCase();
  });
  if (temp.slice(0, 1) === '-') {
    temp = temp.slice(1);
  }
  return temp;
}

/** * @desc 獲取組件入口 * @param {String} path */
function getComponentEntries(path) {
    let files = fs.readdirSync(resolve(path));

    const componentEntries = files.reduce((fileObj, item) => {
      // 文件路徑
      const itemPath = join(path, item);
      // 在文件夾中
      const isDir = fs.statSync(itemPath).isDirectory();
      const [name, suffix] = item.split('.');
    
      // 文件中的入口文件
      if (isDir) {
        fileObj[upperCasetoLine(item)] = resolve(join(itemPath, 'index.js'))
      }
      // 文件夾外的入口文件
      else if (suffix === "js") {
        fileObj[name] = resolve(`${itemPath}`);
      }
      return fileObj
    }, {});
    
    return componentEntries;
}

const buildConfig = {
  // 輸出文件目錄
  outputDir: resolve('lib'),
  // webpack配置
  configureWebpack: {
    // 入口文件
    entry: getComponentEntries('src/components'),
    // 輸出配置
    output: {
      // 文件名稱
      filename: '[name]/index.js',
      // 構建依賴類型
      libraryTarget: 'umd',
      // 庫中被導出的項
      libraryExport: 'default',
      // 引用時的依賴名
      library: 'jr-ui',
    }
  },
  css: {
    sourceMap: true,
    extract: {
      filename: '[name]/style.css'
    }
  },
  chainWebpack: config => {
    config.resolve.alias
      .set("@", resolve("src"))
      .set("@assets", resolve("src/assets"))
      .set("@images", resolve("src/assets/images"))
      .set("@themes", resolve("src/themes"))
      .set("@views", resolve("src/views"))
      .set("@utils", resolve("src/utils"))
      .set("@mixins", resolve("src/mixins"))
      .set("jr-ui", resolve("src/components/index.js"));
  }
}

module.exports = buildConfig;
複製代碼

此時咱們的 npm run build 命令,執行的即是以上這段 webpack 配置。

配置中,會尋找組件目錄的全部入口文件。對每一個入口文件根據設置進行編譯輸出到指定路徑。

configureWebpack: {
    // 入口文件
    entry: getComponentEntries('src/components'),
    // 輸出配置
    output: {
      // 文件名稱
      filename: '[name]/index.js',
      // 輸出依賴類型
      libraryTarget: 'umd',
      // 庫中被導出的項
      libraryExport: 'default',
      // 引用時的依賴名
      library: 'jr-ui',
    }
},
css: {
    sourceMap: true,
    extract: {
      filename: '[name]/style.css'
    }
}
複製代碼
function getComponentEntries(path) {
    let files = fs.readdirSync(resolve(path));

    const componentEntries = files.reduce((fileObj, item) => {
      // 文件路徑
      const itemPath = join(path, item);
      // 在文件夾中
      const isDir = fs.statSync(itemPath).isDirectory();
      const [name, suffix] = item.split('.');
    
      // 文件中的入口文件
      if (isDir) {
        fileObj[upperCasetoLine(item)] = resolve(join(itemPath, 'index.js'))
      }
      // 文件夾外的入口文件
      else if (suffix === "js") {
        fileObj[name] = resolve(`${itemPath}`);
      }
      return fileObj;
    }, {});
    
    return componentEntries;
}
複製代碼

項目中的組件目錄爲以下,配置將會將每一個組件打包編譯導出到 lib

components                         組件文件目錄
│                         
│— button                         
│   │— button.vue                  button組件
│   └─ index.js                    button組件導出文件
│
│— input                         
│   │— input.vue                   input組件
│   └─ index.js                    input組件導出文件
│
│— musicPlayer
│   │— musicPlayer.vue             musicPlayer組件
│   └─ index.js                    musicPlayer組件導出文件
│
│  base.js                         基礎組件的導出文件
└─ index.js                        全部組件的導出文件

lib                                編譯後的文件目錄
│                         
│— button                         
│   │— style.css                   button組件依賴樣式
│   └─ index.js                    button組件依賴文件
│
│— input                         
│   │— style.css                   input組件依賴樣式
│   └─ index.js                    input組件依賴文件
│
│— music-player
│   │— style.css                   musicPlayer組件依賴樣式
│   └─ index.js                    musicPlayer組件依賴文件
│
│— base                         
│   │— style.css                   基礎組件依賴樣式
│   └─ index.js                    基礎組件依賴文件
│
└─ index                         
    │— style.css                   全部組件依賴樣式
    └─ index.js                    全部組件依賴文件
複製代碼

獲取組件所有入口時,對入口名稱作駝峯轉橫槓處理 upperCasetoLine,是由於 babel-plugin-import 在按需引入時,如組件名稱爲駝峯命名,路徑會轉換爲橫槓分隔。

例如業務系統引入

import { MusicPlayer } from "jr-ui"

// 轉化爲
var MusicPlayer = require('jr-ui/lib/music-player');
require('jr-ui/lib/music-player/style.css');
複製代碼

由於組件庫命名約定,組件文件夾命名大小寫並不以橫槓隔開。但爲了讓 babel-plugin-import 正確運行,因此此處對每一個文件的入口文件名稱作了轉換處理。

如不通過方法轉換名稱,也能夠配置 babel.config.js 中的plugin-import配置 camel2DashComponentNamefalse,來禁用名稱轉換。

babel-plugin-import路徑命名issue

業務系統使用時

全量導出默認導出所有組件

// 全量導出
import JRUI from "jr-ui";
import "jr-ui/lib/index/index.css";

Vue.use(JRUI);
複製代碼

基礎導出僅導出基礎組件,如須要使用額外組件,須要安裝 babel-plugin-import 插件且配置 babel.config.js 來完成導入語句的轉換

npm i babel-plugin-import -D
複製代碼

業務系統——babel.config.js配置

module.exports = {
  presets: ["@vue/app", ["@babel/preset-env", { "modules": false }]],
  plugins: [
    [
      "import",
      {
        "libraryName": "jr-ui",
        "style": (name) => {
            return `${name}/style.css`;
        }
      }
    ]
  ]
}
複製代碼
// 基礎導出
import JRUI_base from "jr-ui/lib/base";
import "jr-ui/lib/base/index.css";
Vue.use(JRUI_base);

// 按需使用額外引入的組件
import { MusicPlayer } from "jr-ui";
Vue.use(MusicPlayer);
複製代碼

業務系統中調試組件庫代碼

若是仍然想調試組件庫代碼,在引入組件時,直接引入組件庫依賴內的 components 下的組件導出文件並覆蓋安裝。就能調試目標組件。

import button from "jr-ui/src/components/button";
Vue.use(button);
複製代碼

優化效果

在組件庫較大的狀況下,優化效果很是明顯。在使用基礎組件時,體積小了一兆。並且還減小了不少組件內沒必要要的第三方依賴文件資源。

案例倉庫地址,若有疑問和錯誤的地方,歡迎你們提問或指出。

祝你有個快樂的勞動節假期 :)

Have a nice day.

參考資料

vue-cli執行解析

babel-plugin-import

相關文章
相關標籤/搜索