Vite实现Vue组件按需打包与远程加载

作者:技术拾光

时间:2026-06-27

浏览:396

Vite按需打包与远程加载Vue组件的完整实现方案 业务场景与架构设计 一个桌面端内容展示项目,技术栈是 Electron + Vue3。里面有个核心需求:内容由多个独立开发的卡片组件构成,这些组件需要能够独立更新,而不需要重新打包整个桌面应用。 组件要能热更新,随时替换而不影响主程序运行。 基于这

文章目录

Vite按需打包与远程加载Vue组件的完整实现方案

业务场景与架构设计

一个桌面端内容展示项目,技术栈是 Electron + Vue3。里面有个核心需求:内容由多个独立开发的卡片组件构成,这些组件需要能够独立更新,而不需要重新打包整个桌面应用。

Vite实现Vue组件按需打包与远程加载

组件要能热更新,随时替换而不影响主程序运行。

基于这个需求,设计了一套四层架构:

text

 复制代码┌─────────────────────────────────────────────────────────────┐
│  Layer 1: Electron桌面应用                                 │
│  职责:展示内容,提供运行容器,渲染卡片                   │
├─────────────────────────────────────────────────────────────┤
│  Layer 2: 内容编排系统                                     │
│  职责:上传组件、编排布局、配置跳转、发布配置              │
├─────────────────────────────────────────────────────────────┤
│  Layer 3: 组件开发项目(本文重点)                         │
│  职责:开发卡片组件 → 打包UMD → 生成ZIP → 上传组件仓库   │
├─────────────────────────────────────────────────────────────┤
│  Layer 4: 详情落地页                                      │
│  职责:卡片点击后的跳转详情页面                           │
└─────────────────────────────────────────────────────────────┘

这个架构的核心在于 Layer 3:每个组件独立开发、独立打包、独立部署,互不依赖。

核心问题分析

整个方案需要解决三个问题:

  1. 如何打包? 一个项目里多个组件,每个组件要能单独打包成一个 JS 文件。
  2. 如何加载? 桌面应用在运行的时候,动态加载这些 JS 文件并渲染。
  3. 如何编排? 后台系统能灵活配置,哪些组件显示在什么位置。

这篇文章主要阐述前两个问题的技术实现。

组件打包:基于Vite Lib模式的按需打包方案

设计目标

目标是实现这样的效果:在组件开发项目里,执行一条命令就能打包指定的组件。

bash

 复制代码npm run component abc        # 打包abc组件
npm run component newsList    # 打包newsList组件
npm run component chart       # 打包chart组件

每个组件独立打包,彼此不干扰。打包产物是一个能直接在浏览器里运行的 UMD 文件。

项目结构

组件开发项目的目录结构如下:

text

 复制代码component-project/
├── src/
│   └── components/
│       ├── abc/
│       │   ├── index.js       # 组件入口
│       │   ├── abc.vue        # 组件代码
│       │   └── config.json    # 组件元信息
│       ├── newsList/
│       │   ├── index.js
│       │   ├── newsList.vue
│       │   └── config.json
│       └── chart/
│           ├── index.js
│           ├── chart.vue
│           └── config.json
├── scripts/
│   └── build-component.js     # 打包脚本
├── vite.component.config.js   # Vite打包配置
└── package.json

组件导出方式

每个组件目录下都必须有一个 index.js 作为入口文件:

ja vascript

 复制代码// src/components/abc/index.js
import abc from './abc.vue'export default {
  // install方法:支持 app.use() 方式注册
  install(app) {
    app.component('abc', abc)
  },
  // 直接导出组件:支持按需引入
  abc
}

为什么要有 install 方法?因为要支持两种使用场景:

  • 消费者用 app.use() 一次性注册所有组件
  • 消费者单独引入某个组件

核心配置方案:一个配置服务所有组件

关键点在于一个配置文件动态处理所有组件,无需为每个组件单独编写配置。

ja vascript

 复制代码// vite.component.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import cssInjectedByJsPlugin from 'vite-plugin-css-injected-by-js'const componentName = process.env.COMPONENT_NAME
const entryFile = process.env.COMPONENT_ENTRYexport default defineConfig({
  plugins: [
    vue(),
    cssInjectedByJsPlugin()
  ],
  build: {
    lib: {
      entry: entryFile,
      name: componentName + 'Component',
      fileName: (format) => `${componentName}.${format}.js`
    },
    rollupOptions: {
      external: ['vue'],
      output: {
        globals: {
          vue: 'Vue'
        }
      }
    },
    cssCodeSplit: false
  }
})

配置项说明:

配置项作用
process.env.COMPONENT_NAME通过环境变量传入组件名
process.env.COMPONENT_ENTRY通过环境变量传入入口文件路径
external: ['vue']Vue不打包进去,由宿主环境提供
globals: { vue: 'Vue' }打包产物期望全局有 window.Vue
cssCodeSplit: false不拆分CSS
cssInjectedByJsPlugin()将CSS注入到JS中

打包脚本实现

ja vascript

 复制代码// scripts/build-component.js
import { execSync } from 'child_process'
import fs from 'fs'
import path from 'path'// 获取命令行参数
const componentName = process.argv[2]if (!componentName) {
  console.error('请指定组件名称')
  process.exit(1)
}// 检查组件的入口文件是否存在
const entryFile = path.join('src/components', componentName, 'index.js')
if (!fs.existsSync(entryFile)) {
  console.error(`组件入口文件不存在: ${entryFile}`)
  process.exit(1)
}// 设置环境变量
process.env.COMPONENT_NAME = componentName
process.env.COMPONENT_ENTRY = entryFile// 执行打包
execSync('npx vite build --config vite.component.config.js', {
  stdio: 'inherit',
  env: process.env
})

package.json脚本配置

json

 复制代码{
  "scripts": {
    "component": "node scripts/build-component.js"
  }
}

执行 npm run component abc 的完整流程:

  1. build-component.js 读取参数 abc
  2. 拼接入口路径 src/components/abc/index.js
  3. 通过环境变量传递给 Vite
  4. Vite 读取 vite.component.config.js 配置
  5. 打包输出 dist/abc/abc.umd.js 和 dist/abc/abc.es.js

打包产物会在全局暴露 window.abcComponent,消费者通过这个全局变量获取组件。

样式处理方案

Vite 默认会把 CSS 提取成独立的文件。但目标是一个 JS 文件包含所有内容,用户不用额外关心 CSS 文件。

解决方案:使用 vite-plugin-css-injected-by-js

bash

 复制代码npm install --sa ve-dev vite-plugin-css-injected-by-js

这个插件会在组件加载时自动创建一个 标签并插入到页面中。用户只需要引入 JS 文件,样式会自动生效。

组件加载:动态注册与远程加载机制

整体流程

text

 复制代码1. 应用启动 → 2. 拉取配置 → 3. 获取组件列表 → 4. 动态加载JS → 5. 注册组件 → 6. 渲染页面

单个组件加载

ja vascript

 复制代码function loadComponent(name) {
  return new Promise((resolve) => {
    const script = document.createElement('script')
    script.src = `/libs/${name}/${name}.umd.js`
    
    script.onload = () => {
      // 打包时配置的 name 是 componentName + 'Component'
      const component = window[`${name}Component`]
      resolve(component  component[name] : null)
    }
    
    script.onerror = () => {
      console.warn(`组件 ${name} 加载失败`)
      resolve(null)
    }
    
    document.head.appendChild(script)
  })
}

全量组件加载与应用启动

关键点:必须等所有组件加载完成后再挂载应用,否则会出现组件还没注册但页面已经渲染的情况。

ja vascript

 复制代码// main.js
import * as Vue from 'vue'
import { createApp } from 'vue'
import App from './App.vue'// 将Vue挂载到全局(组件打包时external了Vue)
window.Vue = Vue// 组件列表(实际项目中从配置接口获取)
const components = ['abc', 'newsList']// 加载所有组件
const loaders = components.map(name => loadComponent(name))// 等待全部加载完成
Promise.all(loaders).then(results => {
  const app = createApp(App)
  
  // 注册所有组件
  results.forEach((component, index) => {
    if (component) {
      app.component(components[index], component)
      console.log(` 已注册: ${components[index]}`)
    }
  })
  
  app.mount('#app')
})

组件使用方式

组件注册后,就可以在任何 Vue 文件里直接使用:

vue

 复制代码<template>
  <div>
    
    <abc title="标题" content="内容" />
    <news-list :data="newsData" />
  div>
template><script>
export default {
  data() {
    return {
      newsData: ['新闻1', '新闻2']
    }
  }
}
script>

常见问题与解决方案

CSS未打包进JS的问题

现象:配置了 cssCodeSplit: false,但 Vite 仍然生成了独立的 CSS 文件,组件显示时没有样式。

原因:cssCodeSplit: false 只是在内部不拆分 CSS 文件(即所有 CSS 合并到一个文件),但 Vite 仍然会输出独立的 CSS 文件。

解决方案:使用 vite-plugin-css-injected-by-js 插件。

ja vascript

 复制代码import cssInjectedByJsPlugin from 'vite-plugin-css-injected-by-js'export default defineConfig({
  plugins: [
    vue(),
    cssInjectedByJsPlugin()
  ]
})

这样样式代码会被注入到 JS 中,组件加载时自动插入 标签到页面。

刷新页面组件消失的问题

现象:首次加载页面正常,刷新后组件变成空标签,比如 。

原因:动态加载脚本是异步操作,但 app.mount('#app') 是同步执行的。刷新时脚本还没加载完,应用就已经挂载了,导致组件未注册。

解决方案:用 Promise.all 等待所有组件加载完成后再执行 app.mount()。

ja vascript

 复制代码//  错误写法
components.forEach(name => loadComponent(name))
const app = createApp(App)
app.mount('#app')//  正确写法
Promise.all(components.map(name => loadComponent(name)))
  .then(() => {
    const app = createApp(App)
    app.mount('#app')
  })

ref引用报错的问题

现象:组件使用了 ref、reactive 等 Vue API,报错:

text

 复制代码Uncaught TypeError: Cannot read properties of undefined (reading 'ref')

原因:组件打包时配置了 external: ['vue'],这意味着组件代码中的 import { ref } from 'vue' 在运行时需要在全局找到 Vue 对象。但默认情况下,Vue 应用没有把 Vue 暴露到 window 上。

解决方案:在应用入口将 Vue 挂载到全局。

ja vascript

 复制代码// main.js
import * as Vue from 'vue'window.Vue = Vue

这是一个隐式契约:组件打包时约定宿主环境必须提供 window.Vue,消费者必须遵守这个约定。

方案总结

技术栈

技术作用
Vite打包工具,lib模式支持库打包
Vue3组件框架
UMD模块格式,兼容script标签加载
vite-plugin-css-injected-by-js将CSS注入到JS中

核心机制

打包阶段:

  • 通过命令行参数传递组件名
  • 环境变量动态配置Vite入口和输出
  • UMD格式将组件暴露到全局 window

加载阶段:

  • 动态创建 script 标签加载JS文件
  • Promise.all 控制加载时序
  • 加载完成后注册为Vue全局组件

关键决策

决策理由
外部化Vue避免重复打包,减小文件体积
CSS注入JS一个文件搞定所有,降低使用成本
UMD格式script标签兼容性最好
Promise.all控制时序解决刷新页面组件消失的问题

适用场景

  • 组件需要跨项目复用的场景
  • 组件需要运行时动态加载的场景
  • 内容需要灵活编排的场景
  • 微前端架构中的组件共享场景

项目地址

1,显示组件项目
2,编写打包单个组件
这两个项目是个demo,第2个展示如何打包单个组件,打包之后把生成的文件复制到项目1的public/libs中自动注册以文件名为命名的组件,可以直接使用文件名作为标签使用

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›