Vue 3 + TypeScript + Vite 组件库搭建,自助式生成相应组件文档

前言

随着Vue 3的普及,组件库开发也迎来了新的最佳实践。无论是企业内部的业务组件库,还是开源的UI库,开发者都希望能够在多个项目中复用组件,避免重复开发。但组件从诞生到被他人使用,中间涉及许多工程化环节:打包配置、类型支持、文档演示、版本管理,以及在微前端架构下的运行时共享。本文将使用Vue 3 + TypeScript + Vite,完整记录一套组件库的构建流程,并探讨如何利用Webpack 5的模块联邦(Module Federation)在另一个Vue 3应用中动态加载组件,实现跨项目实时共享。

实现目标

  1. 开发一组基础Vue 3组件 (以Button、Input为例),使用TypeScript + <script setup>语法,支持按需引入和类型提示。
  2. 搭建组件文档站点,方便团队查看组件用法和API。
  3. 将组件库发布到npm ,供其他项目通过npm install安装使用。
  4. 通过模块联邦,在另一个独立的Vue 3项目中直接引用组件库中的组件,无需安装npm包,实现运行时共享。

最终我们将得到一个完整的组件库工程,既能作为传统npm包使用,也能在微前端场景下作为远程组件动态加载。

整体思路

采用以下技术栈:

  • 构建工具:Vite(开发体验好,对库模式支持完善)
  • 框架:Vue 3 + TypeScript
  • 组件开发 :使用Vue单文件组件(SFC) + <script setup> + CSS Modules或普通CSS
  • 文档工具:Storybook for Vue 3(交互式组件演示)或 VitePress(更轻量)。本文选择Storybook,因为它专为组件设计,且支持Vue 3。
  • 打包发布 :Vite的库模式打包,输出ES模块和CommonJS格式,并生成.d.ts类型声明文件。
  • 模块联邦 :由于Vite原生不支持模块联邦,我们将使用@originjs/vite-plugin-federation插件,在消费者项目(Vite + Vue 3)中远程加载组件库的暴露模块。同时,组件库本身也需要作为联邦暴露方进行配置。

整体流程:初始化项目 -> 开发组件 -> 配置Storybook -> 打包 -> 发布npm -> 创建消费者应用 -> 配置模块联邦引用组件。

创建项目

创建一个新目录并初始化:

bash 复制代码
npm create vue@latest my-ui-lib
# 选择 TypeScript、JSX 支持(可选),不需要Router/Pinia等
cd my-ui-lib

Vue官方脚手架会生成一个基础Vue 3 + Vite项目。我们需要调整项目结构,准备作为组件库发布。

调整目录结构

我们将组件源码放在src/components下,每个组件一个文件夹。同时创建src/index.ts作为库的入口文件。

css 复制代码
my-ui-lib/
├── src/
│   ├── components/
│   │   ├── Button/
│   │   │   ├── Button.vue
│   │   │   ├── index.ts
│   │   │   └── style.css
│   │   └── Input/
│   │       ├── Input.vue
│   │       ├── index.ts
│   │       └── style.css
│   └── index.ts
├── package.json
├── vite.config.ts
└── tsconfig.json

安装必要依赖

除了项目初始依赖,我们还需要安装一些开发依赖:

bash 复制代码
npm install -D vite-plugin-dts  # 生成类型声明文件

vite-plugin-dts 会在打包时生成.d.ts文件。

修改 vite.config.ts

我们需要配置Vite以库模式构建:

typescript 复制代码
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import dts from 'vite-plugin-dts'
import path from 'path'

export default defineConfig({
  plugins: [
    vue(),
    dts({
      insertTypesEntry: true,
      outDir: 'dist/types',
      include: ['src/**/*.ts', 'src/**/*.vue'],
      // 跳过故事文件
      exclude: ['**/*.stories.ts', '**/*.stories.vue'],
    }),
  ],
  build: {
    lib: {
      entry: path.resolve(__dirname, 'src/index.ts'),
      name: 'MyUiLib',
      formats: ['es', 'cjs'],
      fileName: (format) => `my-ui-lib.${format}.js`,
    },
    rollupOptions: {
      // 将vue作为外部依赖,不打包进库
      external: ['vue'],
      output: {
        globals: {
          vue: 'Vue',
        },
      },
    },
  },
})

配置 package.json

设置入口文件和类型定义,并将vue移到peerDependencies:

json 复制代码
{
  "name": "my-ui-lib",
  "version": "0.1.0",
  "type": "module",
  "files": ["dist"],
  "main": "dist/my-ui-lib.cjs.js",
  "module": "dist/my-ui-lib.es.js",
  "types": "dist/types/index.d.ts",
  "exports": {
    ".": {
      "import": "./dist/my-ui-lib.es.js",
      "require": "./dist/my-ui-lib.cjs.js",
      "types": "./dist/types/index.d.ts"
    }
  },
  "scripts": {
    "dev": "vite build --watch",
    "build": "vite build",
    "type-check": "vue-tsc --noEmit"
  },
  "peerDependencies": {
    "vue": ">=3.2"
  },
  "devDependencies": {
    "@vitejs/plugin-vue": "^4.0.0",
    "vite": "^4.0.0",
    "vite-plugin-dts": "^3.0.0",
    "vue-tsc": "^1.0.0"
  }
}

组件开发

以Button组件为例,编写一个简单但类型完善的组件。

组件实现

src/components/Button/Button.vue

vue 复制代码
<template>
  <button
    class="btn"
    :class="[`btn--${type}`]"
    :disabled="disabled"
    @click="onClick"
  >
    <slot />
  </button>
</template>

<script setup lang="ts">
import { defineProps, defineEmits } from 'vue'

export type ButtonType = 'primary' | 'default' | 'danger'

const props = defineProps<{
  type?: ButtonType
  disabled?: boolean
}>()

const emit = defineEmits<{
  (e: 'click', event: MouseEvent): void
}>()

const onClick = (e: MouseEvent) => {
  emit('click', e)
}
</script>

<style scoped>
.btn {
  padding: 8px 16px;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}
.btn--primary {
  background-color: #42b883;
  color: white;
}
.btn--default {
  background-color: #e5e7eb;
  color: #1f2937;
}
.btn--danger {
  background-color: #ef4444;
  color: white;
}
.btn:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}
</style>

src/components/Button/index.ts

typescript 复制代码
import Button from './Button.vue'
export default Button

统一导出

src/index.ts

typescript 复制代码
export { default as Button } from './components/Button'
export { default as Input } from './components/Input'  // Input组件类似,此处略

类型支持

由于我们使用了<script setup>,组件本身的Props类型会自动生成,但为了更好的IDE支持,我们可以在index.ts中重新导出类型:

typescript 复制代码
export type { ButtonProps } from './components/Button/Button.vue'

但需要从.vue文件导出类型,需在Button.vue中添加:

vue 复制代码
<script setup lang="ts">
// ... 
</script>

<script lang="ts">
export type ButtonProps = {
  type?: 'primary' | 'default' | 'danger'
  disabled?: boolean
}
</script>

然后就可以在index.ts中导出该类型。

文档配置

我们将使用Storybook搭建组件文档。Storybook提供了交互式演示,非常适合组件库。

初始化Storybook

在项目根目录运行:

bash 复制代码
npx storybook@latest init --type vue3

这会自动安装Storybook for Vue 3并生成配置。

配置Storybook支持Vite

Storybook 7默认支持Vite,无需额外配置。

编写组件故事

src/components/Button目录下创建Button.stories.ts

typescript 复制代码
import type { Meta, StoryObj } from '@storybook/vue3'
import Button from './Button.vue'

const meta: Meta<typeof Button> = {
  title: 'Components/Button',
  component: Button,
  tags: ['autodocs'],
  argTypes: {
    type: {
      control: { type: 'select' },
      options: ['primary', 'default', 'danger'],
    },
    disabled: { control: 'boolean' },
    onClick: { action: 'clicked' },
  },
}

export default meta
type Story = StoryObj<typeof Button>

export const Primary: Story = {
  args: {
    type: 'primary',
    default: 'Primary Button',
  },
}

export const Danger: Story = {
  args: {
    type: 'danger',
    default: 'Danger Button',
  },
}

export const Disabled: Story = {
  args: {
    disabled: true,
    default: 'Disabled Button',
  },
}

运行文档

bash 复制代码
npm run storybook

访问http://localhost:6006即可查看组件文档。

组件npm发布

组件开发完成后,我们需要将其打包并发布到npm。

打包

执行:

bash 复制代码
npm run build

检查dist目录,应该包含my-ui-lib.es.jsmy-ui-lib.cjs.js以及types文件夹。

配置 package.json 的 files 字段

确保files字段包含dist,这样发布时只上传必要文件。

登录npm并发布

bash 复制代码
npm login
npm publish --access public

如果包名已被占用,需要修改package.json中的name

发布成功后,其他项目即可通过npm install my-ui-lib安装使用。

模块联邦使用

除了作为npm包,我们还可以通过Webpack 5的模块联邦(Module Federation)在另一个Vue 3项目中直接远程引用组件库的组件,实现运行时共享。由于我们的组件库使用Vite构建,而模块联邦通常与Webpack配合更好,我们将采用@originjs/vite-plugin-federation插件来让Vite项目同时支持联邦的导出和导入。

改造组件库(远程暴露方)

首先,在组件库项目中安装插件:

bash 复制代码
npm install -D @originjs/vite-plugin-federation

修改vite.config.ts,添加联邦配置:

typescript 复制代码
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import federation from '@originjs/vite-plugin-federation'

export default defineConfig({
  plugins: [
    vue(),
    federation({
      name: 'myUiLib',
      filename: 'remoteEntry.js',
      exposes: {
        './Button': './src/components/Button/Button.vue',
        './Input': './src/components/Input/Input.vue',
      },
      shared: ['vue'],
    }),
  ],
  build: {
    target: 'esnext',
    // 注意:库模式和联邦不能同时使用,我们需要单独为联邦模式构建一个输出
    // 可以创建一个专门的配置文件,如vite.federation.config.ts
  },
})

由于库模式和联邦模式在输出上有所不同,我们可能需要创建两个构建配置。简单起见,我们可以在package.json中添加一个单独的脚本用于构建联邦版本:

json 复制代码
"build:federation": "vite build --config vite.federation.config.ts"

新建vite.federation.config.ts,内容专门用于联邦构建:

typescript 复制代码
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import federation from '@originjs/vite-plugin-federation'

export default defineConfig({
  plugins: [
    vue(),
    federation({
      name: 'myUiLib',
      filename: 'remoteEntry.js',
      exposes: {
        './Button': './src/components/Button/Button.vue',
        './Input': './src/components/Input/Input.vue',
      },
      shared: ['vue'],
    }),
  ],
  build: {
    target: 'esnext',
    outDir: 'dist-federation', // 输出到单独目录,避免与npm包冲突
  },
})

然后运行npm run build:federation,生成dist-federation目录,包含remoteEntry.js和各个组件代码。我们需要将这些文件部署到静态服务器(例如使用vercelnetlify或本地serve),假设部署后的基础URL为https://my-ui-lib.com/

创建消费者项目(远程使用方)

新建一个Vue 3项目作为消费者:

bash 复制代码
npm create vue@latest my-app
cd my-app

安装联邦插件:

bash 复制代码
npm install -D @originjs/vite-plugin-federation

修改vite.config.ts

typescript 复制代码
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import federation from '@originjs/vite-plugin-federation'

export default defineConfig({
  plugins: [
    vue(),
    federation({
      name: 'myApp',
      remotes: {
        myUiLib: 'https://my-ui-lib.com/remoteEntry.js',
      },
      shared: ['vue'],
    }),
  ],
})

在消费者项目的组件中使用远程组件:

vue 复制代码
<template>
  <div>
    <h1>我的应用</h1>
    <Button type="primary" @click="handleClick">远程按钮</Button>
  </div>
</template>

<script setup lang="ts">
import { defineAsyncComponent } from 'vue'

const Button = defineAsyncComponent(() => import('myUiLib/Button'))
// 或者直接在模板中使用,需要类型声明
const handleClick = () => console.log('clicked')
</script>

为了让TypeScript识别远程模块,可以在src/shims.d.ts中添加:

typescript 复制代码
declare module 'myUiLib/Button' {
  import { DefineComponent } from 'vue'
  const component: DefineComponent<{ type?: string; disabled?: boolean }>
  export default component
}

运行npm run dev,即可看到远程加载的Button组件正常工作。

注意事项

  • 远程组件库和消费者项目必须共享相同的Vue版本(由shared配置保证)。
  • 联邦构建输出的文件需要部署到支持CORS的静态服务器。
  • 开发环境下,远程模块可能因跨域问题无法加载,可以配置代理或使用https模式。
相关推荐
SunnyJingJing2 小时前
2026 css自适应实现布局方式
前端
贾铭2 小时前
如何实现一个网页版的剪映(四)使用插件化思维创建pixi绘制画布(转场/滤镜)
前端·javascript
kgduu2 小时前
js之xml处理
xml·前端·javascript
凌览2 小时前
尤雨溪新公司官宣!Vite+ 正式开源,前端圈要变天了?
前端·javascript·后端
Highcharts.js2 小时前
在 Highcharts 中实现 Marimekko可变宽度图|示例教程
javascript·highcharts·图表开发·可视化图表库·可变宽图
We་ct2 小时前
LeetCode 22. 括号生成:DFS回溯解法详解
前端·数据结构·算法·leetcode·typescript·深度优先·回溯
Mr_Mao2 小时前
什么?我居然在 React 用 Pinia?
前端
老虎06272 小时前
ECharts 基础与折线图
前端·echarts
小雨青年3 小时前
鸿蒙 HarmonyOS 6 | 混合开发 (01) Web 组件内核——ArkWeb 加载机制与 Cookie 管理
前端·华为·harmonyos