【LPieces-UI】01-从零开始搭建 Vue3 组件库

引言

你好,最近我开始打造一个 Vue3 + TypeScript 的组件库,参考了 ElementPlus 的设计理念。在这篇文章中,我将分享如何从零开始搭建一个现代化的 Vue3 组件库项目。

本文适合人群:

  • Vue3 开发者
  • 想了解组件库开发的同学
  • 对 monorepo 工程化感兴趣的前端工程师

你将学到:

  • 使用 pnpm + monorepo 管理项目
  • 配置 TypeScript + Vue3 开发环境
  • Vite 构建组件库
  • 项目目录结构设计

一、技术选型

在开始之前,先确定我们的技术栈:

技术 选择 原因
包管理器 pnpm 快,节省磁盘空间,monorepo 支持好
构建工具 Vite 快,原生支持 ESM
框架 Vue3 Composition API,TypeScript 支持好
语言 TypeScript 类型安全,更好的开发体验
测试框架 Vitest 与 Vite 生态一致,快
文档 VitePress 轻量,Vue3 原生支持
组件预览 Storybook 组件开发预览神器

二、项目初始化

2.1 创建项目目录

bash 复制代码
mkdir lpieces-ui
cd lpieces-ui

2.2 初始化项目

bash 复制代码
git init // 初始化git

mkdir packages // 创建packages目录

echo -e 'packages:\n "packages/*"' > pnpm-workspace.yaml // 创建pnpm-workspace.yaml

pnpm init // 初始化package.json

2.3 创建所有的目录

bash 复制代码
cd packages

mkdir components core docs hooks theme utils // 创建所有子目录

vim init.shell // 创建一个init.shell脚本来init所有子目录

./init.shell // 执行创建好的init.shell脚本

init.shell脚本写入以下代码:

shell 复制代码
for i in components core docs hooks theme utils; do
 cd $i
 pnpm init
 cd ..

done

确保在packages下,创建 play组件预览目录

bash 复制代码
pnpm create vite play --telmplate vue-ts

2.4 修改子包package.json的name

除了core跟我们的项目名字一样,其他的子包都是以 @lpieces-ui/ 为开头,如下:

bash 复制代码
core/package.json -> name: 'lpieces-ui'
components/package.json -> name: '@lpieces-ui/components'
hooks/package.json -> name: '@lpieces-ui/hooks'
utils/package.json -> name: '@lpieces-ui/utils'
docs/package.json -> name: '@lpieces-ui/docs'
play/package.json -> name: '@lpieces-ui/play'
theme/package.json -> name: '@lpieces-ui/theme'

2.5 安装项目依赖

确保在项目根目录下安装

bash 复制代码
pnpm add -Dw typescript vite vitest vue-tsc postcss-color-mix postcss-each postcss-each-variables postcss-for postcss-nested @types/node @types/lodash-es @vitejs/plugin-vue @vitejs/plugin-vue-jsx @vue/tconfig @popperjs/core async-validator

pnpm add -w lodash-es vue

2.6 修改根目录的package.json

把name改为 @lpieces-ui/workspace 添加子包依赖

perl 复制代码
"dependencies": {
    "lodash-es": "^4.17.23",
    "vue": "^3.5.30",
    "lpieces-ui": "workspace:*",
    "@lpieces-ui/hooks": "workspace:*",
    "@lpieces-ui/theme": "workspace:*",
    "@lpieces-ui/utils": "workspace:*"
 }

2.7 安装子包依赖

bash 复制代码
pnpm add -D @vue/test-utils @vitest/coverage-v8 jsdom --filter @lpieces-ui/components

pnpm add -D @vitepress --filter @lpieces-ui/docs

core可以看作是components的唯一出口,所以只需要把components链接进来就可以了

core/package.json 复制代码
"devDependencies": {
   "@lpieces-ui/components": "workspace:*"
},

之后打开play,删除ts配置文件,因为ts配置后续我们会写一个全局的,修改package.json 对比根目录的配置,删除多余的依赖

四、配置 TypeScript

在根目录创建 tsconfig.json

json 复制代码
{
    "extends": "@vue/tsconfig/tsconfig.dom.json",
    "compilerOptions": {
        "target": "ES2020",
        "useDefineForClassFields": true,
        "module": "ESNext",
        "lib": ["ES2020", "DOM", "DOM.Iterable"],
        "skipLibCheck": true,
        "moduleResolution": "bundler",
        "allowImportingTsExtensions": true,
        "resolveJsonModule": true,
        "isolatedModules": true,
        "noEmit": true,
        "jsx": "preserve",
        "jsxImportSource": "vue",
        "strict": true,
        "noUnusedLocals": true,
        "noUnusedParameters": true,
        "noFallthroughCasesInSwitch": true
    },
    "include": [
        "env.d.ts",
        "packages/**/*.ts",
        "packages/**/*.tsx",
        "packages/**/*.vue"
    ]
}

创建 postcss.config.cjs

javascript 复制代码
/* eslint-env node */
module.exports = {
  plugins: [
    require('postcss-nested'),
    require('postcss-each-variables'),
    require('postcss-each')({
      plugins: {
        beforeEach: [require('postcss-for'), require('postcss-color-mix')]
      }
    }),
    // require('cssnano')({ preset: 'default' })
  ]
}

之后在根目录跑一次 pnpm install

五、withInstall 工具函数

packages/utils/install.ts 中创建组件安装工具:

typescript 复制代码
import type { App, Plugin } from 'vue'
import { each } from 'lodash-es'

type SFCWithInstall<T> = T & Plugin

export function makeInstaller(components: Plugin[]) {
  const installer = (app: App) => each(components, c => app.use(c))

  return installer as Plugin
}

export const withInstall = <T>(components: T) => {
    (components as SFCWithInstall<T>).install = (app: App) => {
      const name = (components as any).name
      app.component(name, components as Plugin)
    }
    return components as SFCWithInstall<T>
}

utils/index.ts 统一导出

javascript 复制代码
export * from './install'

六、第一个组件:Button

让我们创建一个简单的 Button 组件来验证项目配置。

6.1 创建 Button 组件

packages/components/Button/Button.vue

vue 复制代码
<script setup lang="ts">
import { computed } from 'vue'

interface Props {
  type?: 'primary' | 'success' | 'warning' | 'danger' | 'info' | ''
  size?: 'large' | 'default' | 'small' | ''
  disabled?: boolean
  plain?: boolean
}

const props = withDefaults(defineProps<Props>(), {
  type: '',
  size: '',
  disabled: false,
  plain: false
})

defineOptions({
  name: 'lp-button'
})

const classes = computed(() => [
  'lp-button',
  props.type ? `lp-button--${props.type}` : '',
  props.size ? `lp-button--${props.size}` : '',
  { 'is-disabled': props.disabled, 'is-plain': props.plain }
])
</script>

<template>
  <button :class="classes" :disabled="disabled">
    <slot />
  </button>
</template>

<style scoped>
.lp-button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 8px 16px;
  border-radius: 4px;
  border: 1px solid #dcdfe6;
  background: #fff;
  color: #606266;
  cursor: pointer;
  transition: all 0.3s;
}

.lp-button:hover {
  border-color: #c6e2ff;
  color: #409EFF;
}

.lp-button--primary {
  background: #409EFF;
  border-color: #409EFF;
  color: #fff;
}

.lp-button--primary:hover {
  background: #79bbff;
  border-color: #79bbff;
}

.is-disabled {
  cursor: not-allowed;
  opacity: 0.6;
}

.is-plain {
  background: transparent;
}
</style>

6.2 组件导出

packages/components/Button/index.ts

typescript 复制代码
import Button from './Button.vue'
import { withInstall } from '@lpieces-ui/utils'

export const LpButton = withInstall(Button)

七、core配置

创建core/components.ts

typescript 复制代码
import { LpButton, LpIcon, LpButtonGroup } from '@lpieces-ui/components'
import type { Plugin } from 'vue'

export default [
    LpButton,
    LpIcon,
    LpButtonGroup
] as Plugin[]

创建core/index.ts

typescript 复制代码
import { makeInstaller } from '@lpieces-ui/utils'
import components from './components'

const installer = makeInstaller(components)

export * from '@lpieces-ui/components'
export default installer

八、play预览配置

在main.ts引入我们的组件库

javascript 复制代码
import { createApp } from 'vue'
import App from './App.vue'
import LPiecesUI from 'lpieces-ui'

createApp(App).use(LPiecesUI).mount('#app')

之后再App.vue使用<lp-button>组件

九、docs文档配置

初始化

bash 复制代码
npx vitepress init

之后修改下scripts

json 复制代码
"scripts": {
    "dev": "vitepress dev",
    "build": "vitepress build",
    "preview": "vitepress preview"
 },

十、根目录 package.json

配置根目录的脚本:

json 复制代码
{
  "name": "@lpieces-ui/workspace",
  "version": "1.0.0",
  "scripts": {
    "dev": "pnpm --filter @lpieces-ui/play dev",
    "docs:dev": "pnpm --filter @lpieces-ui/docs dev",
    "docs:build": "pnpm --filter @lpieces-ui/docs build"
  }
}

到这一步项目就可以启动了

bash 复制代码
pnpm dev 启动预览

pnpm docs:dev 启动文档
相关推荐
AlienZHOU2 小时前
AI Coding 时代下,我的技术面试实践分享
前端·后端·面试
Captaincc6 小时前
AI用量v0.1.11更新发布 新增 jusage doctor 诊断指令 托盘展示token 和余额 新增 AutoClaw 支持
前端·后端·vibecoding
计算机魔术师7 小时前
德国Wiki被黑后两周,OpenAI终于把模型失控的账本摊开了
前端
kyriewen8 小时前
我让 AI 当面试官面了我一轮:第 3 个追问我就卡住了(附 10 道追问清单)
前端·面试·ai编程
IT_陈寒8 小时前
Python的GIL把我坑惨了,多线程跑得比单线程还慢
前端·人工智能·后端
前端snow8 小时前
ai agent --- 多agent框架之图编排引擎-langgraph
前端
竹林8188 小时前
OmniPic Studio v3.2.1 核心技术架构与全平台发版解析文档
前端·浏览器
JamesZhang800788 小时前
页面内存只涨不跌? 一次泄漏排查, 牵出 WeakMap 的诞生
前端
Z小明8 小时前
第 6 章 组件进阶
前端·vue.js
江华森8 小时前
HTTP请求的完整过程详解:从DNS解析到TCP挥手的微秒级实战分析
前端