最近在公司實現一個業務元件庫按需加載的需求。簡單來說,有兩個需求,第一個是實現業務元件庫的按需加載,第二,因為業務元件庫裡面有引用了類似 Element Plus 的第三方元件庫,所以在實現業務元件庫按需加載的同時,業務元件庫裡面的引用的第三方元件庫也要實現按需加載。
作為一個程式設計技術人員,即便有了AI,也需要研究底層的技術原理,甚至需要比沒有AI的時代更加深入研究。在AI時代,基礎的都通過AI實現了,只有AI解決不了的問題,最終還得靠你自己的專業知識去解決,而這將是你的核心競爭力的體現,所以在AI時代對技術人員的技術素養要求將更加高。
扯遠了,我們回到業務元件庫按需加載的實現原理的主題上來。
一般在專案中如果沒有進行元件庫按需加載配置,都是一開始就全量加載進行全域元件註冊,這樣就等於整個元件庫在初始化的時候就全部加載了,如果在追求性能的專案中,這是不可接受的。這時我們就要實現元件庫的按需加載,來提高性能。
首先什麼是按需加載?
所謂按需加載,顧名思義就是有需要就加載,不需要就不加載,比如 Element Plus 元件庫有幾十個元件,可能在我們的專案只用到了其中一個元件 <el-button>,那麼我們就希望只加載跟這個按鈕元件相關的代碼,從而達到減少打包體積的效果。
按需加載最簡單的實現方式就是手動設置,實現如下:
<template>
<el-button>按鈕</el-button>
</template>
<script>
import { ElButton } from 'element-plus/es/components/button'
import 'element-plus/es/components/button/style/index'
export default {
components: { ElButton },
}
</script>
我們像上述例子這樣手動引用第三方元件庫的話,在打包的時候就只會打包引用到的元件,因為目前的開源元件庫基本都實現了有利於 Tree Shaking 的 ESM 模組化實現。
如果每個業務元件都需要進行上述設置,其實還是挺繁瑣的,所以我們希望只在 template 中直接調用就好,其他什麼設置都不需要,就像全域註冊元件那樣使用。
<template>
<el-button>按鈕</el-button>
</template>
而剩下部分的代碼,我們希望在打包或者運行的時候自動設置上去。主要是以下部分的代碼:
import { ElButton } from 'element-plus/es/components/button'
import 'element-plus/es/components/button/style/index'
上述部分的代碼,希望自動加載,而不需要手動設置。整個所謂按需加載所需要實現的就是上述的功能。
那麼怎麼實現呢?
首先上述模板代碼的編譯結果如下:
import { createTextVNode as _createTextVNode, resolveComponent as _resolveComponent, withCtx as _withCtx, createVNode as _createVNode, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"
export function render(_ctx, _cache, $props, $setup, $data, $options) {
const _component_el_button = _resolveComponent("el-button")
return (_openBlock(), _createElementBlock("template", null, [
_createVNode(_component_el_button, null, {
default: _withCtx(() => [
_createTextVNode("按鈕")
], undefined, true),
_: 1 /* STABLE */
})
]))
}
我們只需要找到 Vue3 的內置函數 _resolveComponent("el-button") 部分,然後替換成對應的元件代碼即可。例如:
+ import { ElButton } from 'element-plus/es/components/button'
+ import 'element-plus/es/components/button/style/index'
import { createTextVNode as _createTextVNode, resolveComponent as _resolveComponent, withCtx as _withCtx, createVNode as _createVNode, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"
export function render(_ctx, _cache, $props, $setup, $data, $options) {
- const _component_el_button = _resolveComponent("el-button")
+ const _component_el_button = ElButton
return (_openBlock(), _createElementBlock("template", null, [
_createVNode(_component_el_button, null, {
default: _withCtx(() => [
_createTextVNode("按鈕")
], undefined, true),
_: 1 /* STABLE */
})
]))
}
上述就是元件庫按需加載的基本實現原理。
為了更好還原實際場景,我們快速創建一個元件庫專案並且通過 Vite 進行打包。首先創建一個 cobyte-vite-ui 的元件庫目錄,在根目錄下初始化 Node 專案,執行 pnpm init,會自動生成 package.json 文件,內容如下:
{
"name": "cobyte-vite-ui",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"packageManager": "[email protected]"
}
在根目錄新建 pnpm-workspace.yaml 文件進行 Monorepo 專案配置:
packages:
- packages/*
- play
總的目錄結構如下:
├── packages
│ ├── components
│ ├── hooks
│ └── utils
├── play
├── package.json
└── pnpm-workspace.yaml
接著我們安裝一些必要的依賴:
pnpm add vite typescript @vitejs/plugin-vue sass @types/node -D -w
接著我們安裝一下 vue 依賴:
pnpm add vue -w
基礎依賴安裝完畢,我們設置一下 TS 的配置,因為我們這個專案是一個 TS 的專案,在根目錄創建一個 tsconfig.json,配置內容可以簡單設置如下:
{
"compilerOptions": {
"target": "ESNext",
"module": "NodeNext",
"sourceMap": true, // 關鍵:啟用源映射
"outDir": "./dist", // 可選:指定輸出目錄
"esModuleInterop": true
}
}
接著我們就在 packages/components 目錄下創建一個測試按鈕元件。目錄路徑:packages/components/button/button.vue,內容如下:
<template>
<button>測試按鈕</button>
</template>
<script setup lang="ts">
defineOptions({
name: 'co-button',
});
</script>
<style lang="scss" scoped>
button {
color: red;
}
</style>
目錄路徑:packages/components/button/index.ts,內容如下:
import button from "./button.vue";
export const CoButton = button;
export default CoButton;
目錄路徑:packages/components/components.ts,內容如下:
import { CoButton } from './button';
export default [
CoButton
]
將所有元件集中在一個數組中統一導出,方便批量管理和使用。
目錄路徑:packages/components/defaults.ts,內容如下:
import { App } from 'vue';
import components from './components';
const install = function(app: App) {
components.forEach(component => {
app.component(component.name, component);
});
};
export default {
install
};
目錄路徑:packages/components/index.ts,內容如下:
export * from './button';
import install from './defaults';
export default install;
我們再配置一個測試文件,目錄路徑:packages/utils/index.ts,內容如下:
export function testUtils() {
console.log('testUtils');
}
如果大家對創建元件庫比較有經驗的話,就知道上述步驟,是 Vue3 元件庫的基礎設置,各大元件庫的實現雖然差異很大,但最核心機制都可以簡單歸納為上述設置內容。大家如果想詳細了解更多,也可以看看本欄目前面章節的內容。
接著我們就到了我們最核心的元件庫打包的環節了,我們在根本目錄創建一個 vite.config.ts,設置內容如下:
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import path, { resolve } from "path";
import fs from "fs";
// 動態獲取元件目錄列表
const componentsDir = resolve(__dirname, "./packages/components");
const modules = fs.readdirSync(componentsDir).filter((name) => {
const fullPath = path.join(componentsDir, name);
// 只獲取目錄,排除文件
return fs.statSync(fullPath).isDirectory();
});
const entryArr = {
// 主入口
index: resolve(__dirname, "./packages/components/index.ts"),
// 工具入口
utils: resolve(__dirname, "./packages/utils/index.ts"),
};
// 為每個元件創建獨立入口
modules.forEach((name) => {
entryArr[`components/${name}/index`] = resolve(__dirname, `./packages/components/${name}/index.ts`);
});
export default defineConfig(({ command, mode }) => {
// 主構建配置
return {
plugins: [
vue(),
],
build: {
lib: {
entry: entryArr,
formats: ["es"], // 只構建 ES 模組
cssFileName: "style",
},
rollupOptions: {
external: [
"vue",
],
output: {
format: "es",
preserveModules: true,
},
},
},
};
});
設置完 Vite 配置文件後,我們還要設置 packages.json 中的打包命令腳本配置,設置如下:
"scripts": {
"build": "vite build"
},
這樣我們就可以在根目錄運行打包命令了:pnpm build。
運行結果如下,我們成功打包了我們的元件庫。

接著我們在根目錄下創建一個測試專案:
pnpm create vite play --template vue-ts
上述 play 就是測試專案目錄,我們原本就建了個 play 目錄,現在這條命令會直接在 play 目錄中生成一個使用 Vite 創建的 Vue 專案。
接著我們修改根目錄的 package.json 文件:
- "main": "index.js",
+ "module": "/dist/index.mjs",
接著我們進入 play 目錄,透過 pnpm 安裝本地 npm 包,命令如下:
pnpm add ../
運行完上述命令,我們可以看到 ./play/packages.json 文件變化如下:

可以看到我們成功把我們本地的 npm 包安裝到 play 測試專案中了。
接著修改 ./play/main.ts 內容如下:
import { createApp } from 'vue'
import App from './App.vue'
import CobyteViteUI from 'cobyte-vite-ui'
import 'cobyte-vite-ui/dist/style.css'
const app = createApp(App)
app.use(CobyteViteUI)
app.mount('#app')
我們直接引用我們本地創建的 npm 包。
接著修改 ./play/App.vue 內容如下:
<template>
<co-button></co-button>
</template>
<script setup lang="ts">
</script>
最後我們運行 play 測試專案,結果如下:

我們可以看到成功運行了本地元件庫的 npm 包。
接下來我們希望不進行完整引入元件庫:
import { createApp } from 'vue'
import App from './App.vue'
- import CobyteViteUI from 'cobyte-vite-ui'
- import 'cobyte-vite-ui/dist/style.css'
const app = createApp(App)
- app.use(CobyteViteUI)
app.mount('#app')
即便這樣我們同樣可以在測試專案中使用我們的測試元件。
根據上文我們知道 App.vue 的模板內容會被編譯成:
import { resolveComponent as _resolveComponent, createVNode as _createVNode, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"
export function render(_ctx, _cache, $props, $setup, $data, $options) {
const _component_co_button = _resolveComponent("co-button")
return (_openBlock(), _createElementBlock("template", null, [
_createVNode(_component_co_button)
]))
}
那麼根據上文我們知道需要把 _resolveComponent("co-button") 部分替換成對應的元件對象,內容如下:
+ import CoButton from 'cobyte-vite-ui/dist/components/button'
+ import 'cobyte-vite-ui/dist/style.css'
import { resolveComponent as _resolveComponent, createVNode as _createVNode, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"
export function render(_ctx, _cache, $props, $setup, $data, $options) {
- const _component_co_button = _resolveComponent("co-button")
+ const _component_co_button = CoButton
return (_openBlock(), _createElementBlock("template", null, [
_createVNode(_component_co_button)
]))
}
那麼要實現上述功能,我們得透過 Vite 插件 來實現,我們在上面安裝了一個 @vitejs/plugin-vue 插件,這個 Vite 插件的主要功能就是把 .vue 文件編譯成上述的 js 內容。那麼我們這樣在它的後面繼續添加一個插件在編譯後的 js 內容中去實現上述替換功能即可。
我們在 ./packages/utils/index.ts 文件中實現這個自動加載元件的 Vite 插件,實現如下:
import MagicString from 'magic-string';
export default function VitePluginAutoComponents() {
return {
// 插件名稱,用於調試和錯誤信息
name: 'vite-plugin-auto-component',
// transform 鉤子函數,在轉換模塊時調用
// code: 文件內容,id: 文件路徑
transform(code, id) {
// 使用正則表達式檢查文件是否為.vue文件
// 如果不是.vue文件,不進行處理
if (/\.vue$/.test(id)) {
// 創建 MagicString 實例,用於高效地修改字符串並生成 source map
const s = new MagicString(code)
// 初始化結果數組,用於存儲匹配到的元件信息
const results = []
// 使用 matchAll 方法查找所有匹配的 resolveComponent 調用
// 正則表達式解釋:
// _?resolveComponent\d* - 匹配可能的函數名變體(可能帶下劃線或數字後綴)
// \("(.+?)"\) - 匹配括號內的字符串參數
// g - 全局匹配
for (const match of code.matchAll(/_?resolveComponent\d*\("(.+?)"\)/g)) {
// match[1] 是第一個捕獲組,即元件名稱字符串
const matchedName = match[1]
// 檢查匹配是否有效:
// match.index != null - 確保有匹配位置
// matchedName - 確保捕獲到元件名
// !matchedName.startsWith('_') - 確保元件名不以_開頭(可能是內部元件)
if (match.index != null && matchedName && !matchedName.startsWith('_')) {
// 計算匹配字符串的起始位置
const start = match.index
// 計算匹配字符串的結束位置
const end = start + match[0].length
// 將匹配信息存入結果數組
results.push({
rawName: matchedName, // 原始元件名稱
// 創建替換函數,使用 MagicString 的 overwrite 方法替換指定範圍的文本
replace: resolved => s.overwrite(start, end, resolved),
})
}
}
// 遍歷所有匹配結果進行處理
for (const { rawName, replace } of results) {
// 定義要替換的變量名(這裡暫時編碼為 CoButton)
const varName = `CoButton`
// 在代碼開頭添加導入語句:
// 1. 導入 CoButton 元件
// 2. 導入樣式文件
s.prepend(`import ${varName} from 'cobyte-vite-ui/dist/components/button';\nimport 'cobyte-vite-ui/dist/style.css';\n`)
// 執行替換:將 resolveComponent("xxx") 調用替換為元件變量名
replace(varName)
}
// 返回轉換後的代碼
return {
code: s.toString(), // 轉換後的代碼字符串
map: null,
}
}
},
}
}
我們在上述 Vite 插件中使用到了新工具庫 magic-string,我們需要安裝一下它的依賴:
pnpm add magic-string -D -w
magic-string 是一個專注於字符串操作,主要作用是對源代碼可以進行精準的插入、刪除、替換等操作。
上述編寫的 Vite 的插件主要是實現在 .vue 文件中查找所有形如 resolveComponent("xxx") 的函數調用,對於每一個找到的調用,它會在文件頂部添加一個固定的導入語句,例如導入 CoButton 元件和樣式。最後把找到的 resolveComponent("xxx") 替換成對應的元件,例如 CoButton。
然後我們在根目錄重新打包,接著在 play 目錄中的 vite.config.ts 文件中進行以下修改:
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
+ import AutoComponents from 'cobyte-vite-ui/dist/utils'
export default defineConfig({
plugins: [vue(), AutoComponents()],
})
接著我們再次重啟 play 測試專案,我們可以看到即便我們不導入任何我們編寫的元件庫設置,我們依然可以在 play 專案中成功使用 CoButton 元件。