Vue3 业务组件库按需加载的实现原理(上)

前言

简单来说,有两个需求,第一个是实现业务组件库的按需加载,第二,因为业务组件库里面有引用了类似 Element Plus 的第三方组件库,所以在实现业务组件库按需加载的同时,业务组件库里面的引用的第三方组件库也要实现按需加载。

一般在项目中如果没有进行组件库按需加载配置,都是一开始就全量加载进行全局组件注册,这样就等于整个组件库在初始化的时候就全部加载了,如果在追求性能的项目中,这是不可接受的。这时我们就要实现组件库的按需加载,来提高性能。

按需加载的基本实现原理

首先什么是按需加载?

所谓按需加载,顾名思义就是有需要就加载,不需要就不加载,比如 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'

上述部分的代码,希望自动加载,而不需要手动设置。整个所谓按需加载所需要实现的就是上述的功能。

那么怎么实现呢?

首先上述模板代码的编译结果如下:

编译之后的效果https://template-explorer.vuejs.org/#eyJzcmMiOiI8dGVtcGxhdGU+XG4gIDxlbC1idXR0b24+5oyJ6ZKuPC9lbC1idXR0b24+XG48L3RlbXBsYXRlPiIsIm9wdGlvbnMiOnt9fQ==

复制代码
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 打包组件库

为了更好还原实际场景,我们快速创建一个组件库项目并且通过 Vite 进行打包。 首先创建一个vite-ui 的组件库目录,在根目录下初始化 Node 项目,执行 pnpm init, 会自动生成 package.json 文件,内容如下:

注意添加 "type": "module",

复制代码
{
  "name": "vite-ui",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "type": "module",
  "keywords": [],
  "author": "",
  "license": "ISC",
  "packageManager": "pnpm@10.13.1",
  "dependencies": {
    "vue": "^3.5.28"
  }
}

在根目录新建 pnpm-workspace.yaml 文件进行 Monorepo 项目配置:

复制代码
packages:
  - packages/*
  - play

总的目录结构如下:

复制代码
├── packages
│   ├── components
│   ├── hooks
│   └── utils
├── package.json
└── pnpm-workspace.yaml

接着我们安装一些必要的依赖:

复制代码
 pnpm add vite  @vitejs/plugin-vue sass  -D -w

接着我们安装一下 vue 依赖:

复制代码
pnpm add vue -w

接着我们就在 packages/components 目录下创建一个测试按钮组件

目录路径:packages/components/button/button.vue,内容如下:

复制代码
<template>
  <button>测试按钮</button>
</template>
<script setup lang="ts">
defineOptions({
  name: "my-button",
});
</script>
<style lang="scss" scoped>
button {
  color: red;
}
</style>

目录路径:packages/components/button/index.js,内容如下:

复制代码
import button from "./button.vue";
export const MyButton = button;
export default MyButton;

目录路径:packages/components/components.js,内容如下:

复制代码
import { MyButton } from "./button";
export default [MyButton];
复制代码
将所有组件集中在一个数组中统一导出,方便批量管理和使用。

目录路径:packages/components/defaults.js,内容如下:

复制代码
import components from "./components";

const install = function (app) {
  components.forEach((component) => {
    app.component(component.name, component);
  });
};

export default {
  install,
};

目录路径:packages/components/index.js,内容如下:

复制代码
export * from "./button";

import install from "./defaults";
export default install;

我们再配置一个测试文件,目录路径:packages/utils/index.js,内容如下:

复制代码
export function testUtils() {
    console.log('testUtils');
}

如果大家对创建组件库比较有经验的话,就知道上述步骤,是 Vue3 组件库的基础设置,各大组件库的实现虽然差异很大,但最核心机制都可以简单归纳为上述设置内容。

接着我们就到了我们最核心的组件库打包的环节了,我们在根本目录创建一个 vite.config.js,设置内容如下:

复制代码
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.js"),

  // 工具入口
  utils: resolve(__dirname, "./packages/utils/index.js"),
};

// 为每个组件创建独立入口
modules.forEach((name) => {
  entryArr[`components/${name}/index`] = resolve(
    __dirname,
    `./packages/components/${name}/index.js`,
  );
});

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, // 保持模块结构
          entryFileNames: `[name].mjs`,
        },
      },
    },
  };
});

设置完 Vite 配置文件后,我们还要设置 packages.json 中的打包命令脚本配置,设置如下:

复制代码
  "scripts": {
    "build": "vite build"
  },

这样我们就可以在根目录运行打包命令了:pnpm build

运行结果如下,我们成功打包了我们的组件库。

通过 pnpm 安装本地 npm 包

接着我们在根目录下创建一个测试项目:

复制代码
pnpm create vite play --template vue-js

上述 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.js 内容如下:

复制代码
import { createApp } from "vue";
import App from "./App.vue";
import viteUi from "vite-ui";
import "vite-ui/dist/style.css";

const app = createApp(App);
app.use(viteUi);
app.mount("#app");

接着修改 ./play/App.vue 内容如下:

复制代码
<template>
  <my-button></my-button>
</template>
<script></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')

即便这样我们同样可以在测试项目中使用我们的测试组件。

相关推荐
mCell7 小时前
如何零成本搭建个人站点
前端·程序员·github
mCell8 小时前
为什么 Memo Code 先做 CLI:以及终端输入框到底有多难搞
前端·设计模式·agent
恋猫de小郭8 小时前
AI 在提高你工作效率的同时,也一直在增加你的疲惫和焦虑
前端·人工智能·ai编程
少云清8 小时前
【安全测试】2_客户端脚本安全测试 _XSS和CSRF
前端·xss·csrf
萧曵 丶8 小时前
Vue 中父子组件之间最常用的业务交互场景
javascript·vue.js·交互
银烛木8 小时前
黑马程序员前端h5+css3
前端·css·css3
m0_607076608 小时前
CSS3 转换,快手前端面试经验,隔壁都馋哭了
前端·面试·css3
听海边涛声8 小时前
CSS3 图片模糊处理
前端·css·css3
IT、木易8 小时前
css3 backdrop-filter 在移动端 Safari 上导致渲染性能急剧下降的优化方案有哪些?
前端·css3·safari
0思必得09 小时前
[Web自动化] Selenium无头模式
前端·爬虫·selenium·自动化·web自动化