使用Vite实现Vue组件的按需打包和远程加载

 更新时间:2026年06月25日 08:32:08   作者:锋行天下  
本文介绍如何使用ElectronVue3构建支持热更新的桌面端应用架构,重点解决组件独立打包、动态加载及样式处理问题,采用Vite-plugin-css-injected-by-js实现组件热更新及样式自动注入,需要的朋友可以参考下

一、背景与架构

1.1 业务场景

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

简单说就是:组件要能热更新,随时替换,不影响主程序。

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

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

这个架构的核心是 Layer 3:如何让每个组件独立开发、独立打包、独立部署。

1.2 核心问题

我们要解决三个问题:

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

本文重点讲前两个问题。

二、组件打包:Vite Lib模式

2.1 设计目标

我们想要这样的效果:在组件开发项目中,执行命令就能打包指定组件。

npm run component abc        # 打包abc组件
npm run component newsList    # 打包newsList组件
npm run component chart       # 打包chart组件

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

2.2 项目结构

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

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

2.3 组件如何导出

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

// 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() 一次性注册所有组件
  • 消费者单独引入某个组件

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

关键点来了:我们不想为每个组件写一个配置文件,而是希望一个配置文件动态处理所有组件

// 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_ENTRY

export 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中

2.5 打包脚本

// 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
})

2.6 package.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,消费者通过这个全局变量获取组件。

2.7 样式处理

Vite 默认会将 CSS 提取为独立文件。但我们的目标是一个JS文件包含所有内容,用户不需要关心CSS文件。

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

npm install --save-dev vite-plugin-css-injected-by-js

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

三、组件加载:动态注册

3.1 整体流程

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

3.2 加载单个组件

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)
  })
}

3.3 加载所有组件并启动应用

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

// 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')
})

3.4 使用组件

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

<template>
  <div>
    <!-- 就像使用本地组件一样 -->
    <abc title="标题" content="内容" />
    <news-list :data="newsData" />
  </div>
</template>

<script>
export default {
  data() {
    return {
      newsData: ['新闻1', '新闻2']
    }
  }
}
</script>

四、遇到的问题与解决方案

4.1 问题一:CSS没有被打包进JS

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

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

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

import cssInjectedByJsPlugin from 'vite-plugin-css-injected-by-js'

export default defineConfig({
  plugins: [
    vue(),
    cssInjectedByJsPlugin()
  ]
})

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

4.2 问题二:刷新页面组件消失

现象:首次加载页面正常,刷新后组件变成空标签,如 <abc></abc>

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

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

// ❌ 错误写法
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')
  })

4.3 问题三:ref报错

现象:组件使用了 refreactive 等Vue API,报错:

Uncaught TypeError: Cannot read properties of undefined (reading 'ref')

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

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

// main.js
import * as Vue from 'vue'

window.Vue = Vue

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

五、总结

5.1 技术栈

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

5.2 核心机制

打包阶段

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

加载阶段

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

5.3 关键决策

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

5.4 适用场景

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

以上就是使用Vite实现Vue组件的按需打包和远程加载的详细内容,更多关于Vite Vue组件按需打包和远程加载的资料请关注脚本之家其它相关文章!

相关文章

  • Vue双向绑定详解

    Vue双向绑定详解

    这篇文章主要介绍了Vue 实现双向绑定的四种方法,非常不错,具有参考借鉴价值,需要的朋友参考下吧,希望能够给你带来帮助
    2021-11-11
  • vue3点击不同的菜单页切换局部页面实现方法

    vue3点击不同的菜单页切换局部页面实现方法

    这篇文章主要给大家介绍了关于vue3点击不同的菜单页切换局部页面实现的相关资料,文中示例代码介绍的非常详细,对大家学习或者使用vue3具有一定的参考价值,需要的朋友可以参考下
    2023-08-08
  • vue-cli3+typescript新建一个项目的思路分析

    vue-cli3+typescript新建一个项目的思路分析

    这篇文章主要介绍了vue-cli3+typescript新建一个项目的思路,本文通过实例代码给大家介绍的非常详细,具有一定的参考借鉴价值,需要的朋友可以参考下
    2019-08-08
  • 一起来学习Vue的生命周期

    一起来学习Vue的生命周期

    这篇文章主要为大家详细介绍了Vue的生命周期,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下,希望能够给你带来帮助
    2022-02-02
  • Vue3使用axios实现Ajax请求过程

    Vue3使用axios实现Ajax请求过程

    这段文章详细介绍了axios在Vue.js项目中的应用,包括如何引入axios、发送GET和POST请求、处理响应数据等,通过实际示例展示了如何使用axios进行Ajax请求,适用于Vue.js开发者进行数据交互
    2026-05-05
  • vue使用codemirror的两种用法

    vue使用codemirror的两种用法

    这篇文章主要介绍了在vue里使用codemirror的两种用法,每种方法通过实例代码给大家介绍的非常详细,具有一定的参考借鉴价值,需要的朋友可以参考下
    2019-08-08
  • Vue/React子组件实例暴露方法(TypeScript)

    Vue/React子组件实例暴露方法(TypeScript)

    最近几个月都在用TS开发各种项目,框架有涉及到Vue3,React18等,记录一下Vue/React组件暴露出变量/函数的方法的写法,对vue react组件暴露方法相关知识感兴趣的朋友跟随小编一起看看吧
    2022-11-11
  • Vue中Class和Style实现v-bind绑定的几种用法

    Vue中Class和Style实现v-bind绑定的几种用法

    项目开发中给元素添加/删除 class 是非常常见的行为之一, 例如网站导航都会给选中项添加一个 active 类用来区别选与未选中的样式,那么在 vue 中 我们如何处理这类的效果呢?下面我们就一起来了解一下
    2021-05-05
  • vue前端信息详情页模板梳理详解

    vue前端信息详情页模板梳理详解

    这篇文章主要为大家详细介绍了vue前端信息详情页模板梳理,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2022-04-04
  • Vue中data数据初始化方法详解

    Vue中data数据初始化方法详解

    这篇文章主要介绍了Vue中data数据初始化方法,数据初始化是在组件实例化时发生的,在组件中,可以通过data选项来定义组件的初始数据,需要详细了解可以参考下文
    2023-05-05

最新评论