
简介面向Vue3与TypeScript技术栈学习者这份前端UI框架项目源码包以完整可运行的工程形式呈现了从组件封装到样式组织的实际开发流程。包内整体包含1035个文件其中384个Vue组件、244个TypeScript模块与83个Less样式文件构成主体覆盖组件逻辑、类型定义与主题定制等环节另有242个SVG图标、14个JSON配置文件及60个Markdown文档分别辅助图标管理、工程配置与配套说明查阅。压缩包仅2.53MB轻量却体系完整适合在校学生、刚入行的前端开发人员以及希望对Vue3工程化查漏补缺的技术爱好者。资源附带详细代码说明与使用教程支持按目录逐模块学习组件设计、脚本编写与样式规范也可直接运行项目观察交互效果快速内化一套可复用的UI框架搭建思路。当前已有231人学习下载适合作为从零搭建前端UI框架的实践参考。1. 为什么我建议每个前端都亲手写一套UI框架前段时间接手了一个后台管理系统的重构团队里几个前端对Element Plus依赖很重但一旦遇到定制化需求——比如要改主题、要封装一个带权限控制的表格、要实现一个业务联动的表单就开始在各种论坛里翻答案。我当时的判断是与其四处抄不如自己搭一套组件库。于是我用 Vue3 TypeScript Less 从零实现了一整套前端UI框架包含按钮、表单、弹窗、表格等常用组件同时配套了教程和详细的代码说明。这套项目的价值和市面上任何开源组件库都不同它不是为了生产环境能不能直接用而是为了让你在写每个组件的过程中彻底搞清楚现代前端框架的底层逻辑。你会发现平时组件库里那些看起来理所当然的能力——props校验、插槽分发、事件透传、样式覆盖、按需引入——背后每一环都是可以拆解和复现的。这套资源最适合三类人第一类是Vue3学了一段时间、能写页面但还没写过通用组件的开发者第二类是准备面试、尤其正在刷Vue3面试题的前端工程师因为组件库开发几乎能把响应式原理、computed机制、组件通信、TypeScript类型推导全部串起来第三类是需要给团队沉淀一套统一UI规范、但又不满足于直接套用开源库的资深开发者。下面我把这套项目的设计思路和完整实现过程拆开讲清楚。2. 技术选型Vue3、TypeScript、Less为什么是黄金组合2.1 Vue3组件库开发的基础设施进化Vue3对组件库开发最大的推动不是性能提升多少倍而是组合式APIComposition API让组件逻辑的组织方式发生了根本变化。在Vue2里写一个复杂组件data、computed、methods、watch被强制切成了不同的块逻辑被拆得七零八落。而Vue3的setup函数允许你按功能而不是选项来组织代码比如一个表格组件的分页逻辑、排序逻辑、列配置逻辑可以分别封装成独立的函数再组合。此外Vue3的响应式系统基于Proxy重写比Vue2的Object.defineProperty更完整地支持数组索引、动态属性添加等操作。对组件库来说这意味着封装表格、树形控件这类数据密集型组件时行为更符合直觉不用再靠hack手段去触发更新。再加上Teleport弹窗组件挂载body、Suspense异步组件这些内建能力很多在Vue2里需要自己折腾的边界情况在Vue3里直接被框架解决了。2.2 TypeScript给组件API加上说明书组件库本质上是在定义一套别人要使用的公共API。如果用JavaScript写用户拿到的只有运行时行为代码写错了要运行起来才能发现。换成TypeScript后组件的props、事件、插槽类型在编译期就形成约束。用户在使用组件库时编辑器里能看到完整的类型提示传错参数会被红色波浪线提前拦下。我在这套项目里还做了一件很划算的事把所有组件的Props、Emits都抽成独立的interface导出。这样使用方可以import type { ButtonProps } from 组件库来继承或扩展组件类型本质上是在类型层面给了用户一个二次开发接口。这是纯JavaScript组件库给不了的。2.3 Less让样式层变得可编程很多人会问都有CSS变量了为什么还要Less我的理解是两者解决的问题不同CSS变量是运行时的浏览器加载后还能动态改适合做主题切换Less是编译时的在构建阶段就把变量替换成具体值适合做设计令牌的定义和样式函数的复用。组件库的样式恰恰需要编译期的灵活性——比如通过mixin生成不同色调的按钮通过嵌套减少重复选择器通过变量统一管理间距、字号、圆角。Less的嵌套语法也天然契合BEM命名规范的组织方式让组件样式代码结构清晰。这套项目里Less承担了样式层的全部重活后面我会专门讲它的架构设计。3. 搭建工程骨架从脚手架到目录规划3.1 初始化项目推荐直接用Vite创建Vue3 TS的模板它比Webpack方案少很多配置成本npm create vitelatest ui-framework -- --template vue-ts模板装好之后先别急着写组件把目录结构规划好。一套组件库的目录不可能和普通业务项目一样我的划分方式是这样的src/ components/ # 组件源码每个组件一个子目录 button/ src/button.vue # 组件实现 index.ts # 组件出口负责命名和注册 input/ table/ styles/ variables.less # 全局设计令牌颜色/间距/字号 mixins.less # 可复用的样式函数 index.less # 样式入口 hooks/ # 跨组件复用的组合式函数 utils/ # 类型工具、DOM工具等 index.ts # 组件库统一出口 play/ # 本地调试示例页面 docs/ # 文档站点用Vitepress我特意把components下每个组件都做成独立子目录而不是把组件文件平铺。这样做的好处是后期做按需引入时每个组件都能作为独立入口被打包不会牵扯其他组件。3.2 配置文件里的几个关键细节vite.config.ts 里最容易被忽视的是Less全局变量的注入配置import { defineConfig } from vite import vue from vitejs/plugin-vue import path from path export default defineConfig({ plugins: [vue()], resolve: { alias: { : path.resolve(__dirname, src) } }, css: { preprocessorOptions: { less: { additionalData: import /styles/variables.less; } } } })additionalData的作用是让每个样式文件自动注入variables.less这样在任何一个组件里直接就能用primary-color这类全局变量。但有两点要注意第一注入的文件里不要写实际输出CSS的规则只放变量和mixin否则每个组件样式里都会被重复塞一段CSS打包体积直接失控第二如果某个样式文件自己定义了同名变量会覆盖全局注入的值这种隐性覆盖排查起来很痛苦建议团队约定组件内变量统一加--前缀。3.3 开发环境别忽略的几个易错点tsconfig.json 中组件库开发需要开启的选项和业务项目不完全一样。关键是declaration: true和保留strict: true。开发阶段单独用Vite跑demo时类型检查往往是被跳过的等到构建时再生成.d.ts文件如果代码类型不严谨那一步会全面报错。所以建议从一开始就开着严格模式写组件。目录里还有一个src/index.ts统一出口里面把每个组件注册和导出。写的时候顺手把所有组件类型也一并导出这个习惯在用户侧的价值非常大。4. 组件开发实战以Button为例走完一个组件的完整生命周期4.1 设计组件APIProps、事件、插槽的取舍很多初学者写组件上来就撸页面这是典型的顺序错误。开发通用组件的第一步一定是设计API也就是定义别人怎么用这个组件。拿Button来说我先把需求列出来需要支持不同的语义类型primary、success、warning、danger、info、default需要支持尺寸large、default、small还需要支持plain幽灵按钮、round圆角、disabled禁用、loading加载态。事件方面只保留一个click并在disabled或loading状态下阻止触发。插槽方面保留默认插槽作为按钮文本同时预留一个icon插槽随时可以扩展图标内容。API设计阶段的关键是克制——不是功能越多越好而是让每个props都语义清晰、彼此不纠缠。4.2 写组件逻辑组合式API的正确姿势Button组件的完整实现大概是这样的script setup langts import { computed } from vue export interface ButtonProps { type?: primary | success | warning | danger | info | default size?: large | default | small plain?: boolean round?: boolean disabled?: boolean loading?: boolean } const props withDefaults(definePropsButtonProps(), { type: default, size: default, plain: false, round: false, disabled: false, loading: false }) const emit defineEmits{ (e: click, ev: MouseEvent): void }() const classes computed(() { return [ vui-button, vui-button--${props.type}, vui-button--${props.size}, { is-plain: props.plain, is-round: props.round, is-disabled: props.disabled, is-loading: props.loading } ] }) function handleClick(ev: MouseEvent) { if (props.disabled || props.loading) return emit(click, ev) } /script template button classvui-button :disableddisabled clickhandleClick span v-ifloading classvui-button__spinner/span slot / /button /template这里我用了definePropsButtonProps()而不是运行时props声明是刻意的选择类型声明会在编译期生成对应的运行时校验同时保留完整的类型信息。computed用来集中管理class绑定避免模板里堆一大串三元表达式。很多人会忽略的细节是loading态按钮在loading时视觉上要变成转圈点击要失效这些行为我都集中放在了handleClick和class计算里组件外部不需要感知。4.3 样式落地Less BEM命名样式部分用Less写命名走BEM规范。块名是vui-button元素是vui-button__spinner修饰符是vui-button--primary状态用is-前缀。Less嵌套语法让这些关系在代码里一目了然prefix: vui; .{prefix}-button { display: inline-flex; align-items: center; justify-content: center; height: 32px; padding: 0 16px; font-size: font-size-base; border-radius: border-radius-base; border: 1px solid transparent; cursor: pointer; transition: all .2s; --large { height: 40px; padding: 0 20px; font-size: 16px; } --small { height: 28px; padding: 0 10px; font-size: 12px; } --primary { background-color: primary-color; border-color: primary-color; color: #fff; :hover { background-color: lighten(primary-color, 8%); } } .is-disabled { cursor: not-allowed; opacity: 0.6; } }Less的lighten()这类内建函数可以基于主色自动推导hover颜色省去手动调色板主题一致性也更好。但一个组件库里的配色不能全靠函数套核心色值仍要在variables.less里统一定义清楚。5. 类型系统让组件在编译期拦住低级错误5.1 Props类型的定义与默认值组件库的类型设计不能只停留在有个类型的层面而要追求把错误挡在编译期。Button的Props用了联合类型switch某个属性就能自动提示所有合法选项。如果用户传了sizebig编辑器会立刻标红。这种体验是运行时校验做不到的。withDefaults也很关键。它让默认值具备类型感知默认值必须匹配接口里的类型否则编译直接失败。这比Vue2时代的default: () default强太多——那个写错运行时才崩。5.2 事件与组件实例的类型defineEmits的泛型写法很多人会忽略但它对维护体验的提升是实打实的script setup langts import Button from ./button.vue // 拿到组件的真实类型 type ButtonInstance InstanceTypetypeof Button const btnRef refButtonInstance() /script通过InstanceTypetypeof Button获取组件实例类型父组件就能调用Button暴露出去的方法比如focus。这是在类型层面维护组件公共方法的正确方式而不是在事件名上靠字符串约定。5.3 类型提示带来的实际收益有一个容易被低估的收益当整个组件库的类型定义完整时使用方的编辑器提示会自动长出来。用户敲Button那一下编辑器就能列出type、size、round等所有props及其说明。这种开发体验会明显降低组件库的使用门槛也让组件库的文档压力小很多。我见过太多组件库文档写得密密麻麻结果类型是any用户还是只能靠看源码猜用法。类型即文档不是一句口号。6. Less样式架构与主题定制Less的核心工程能力6.1 全局变量与设计令牌样式架构的核心不是组件样式本身而是设计令牌Design Token。这套项目的variables.less集中管理了所有基础变量// 颜色 primary-color: #409eff; success-color: #67c23a; warning-color: #e6a23c; danger-color: #f56c6c; // 字号 font-size-base: 14px; font-size-lg: 16px; font-size-sm: 12px; // 间距 spacing-base: 8px; spacing-lg: 16px; spacing-sm: 4px; // 圆角/阴影 border-radius-base: 4px; box-shadow-base: 0 2px 12px rgba(0, 0, 0, .1);这样后续换主题时只需要改variables.less再重新构建全套组件的风格会一起变。Less的变量有一个好处因为是编译期的最终生成的CSS里可以直接输出计算后的数值运行时不消耗性能。6.2 覆盖和扩展组件样式组件库交付给用户后被问得最多的就是怎么覆盖组件样式。这里有两个层次。第一层是Vue的scoped属性组件内部样式默认只作用于自身第二层是用户要覆盖时无路可走就得硬写权重。这套项目的做法是给核心组件暴露少量CSS变量承接关键样式.vui-button { --vui-button-bg: primary-color; background-color: var(--vui-button-bg); }用户想改某个按钮的颜色时直接在元素上设置--vui-button-bg即可不需要绞尽脑汁去算选择器权重。这种Less负责编译期设计、CSS变量负责运行时个性化的配合是我强烈推荐的标准方案。6.3 Less里写deep的正确姿势Vue3里最常被搜的问题之一就是less 写deep怎么写。很多人还停留在Vue2的/deep/写法到了Vue3直接失效。Vue3对scoped样式的深度选择器是:deep().vui-select { // 正确编译为 [data-v-xxx] .vui-select-option :deep(.vui-select-option) { padding: 8px; } // 等价写法老项目里也常见 :deep(.vui-select-option) { padding: 8px; } }注意代码里我写了一样的情况这里应该区分的是:deep()是推荐写法::v-deep是兼容写法。核心原理是scoped会给每个选择器加上[data-v-xxx]属性选择器:deep()之后的子选择器不会再被追加该属性从而可以命中子组件内部的元素。写deep时还有个经验尽量只在自定义组件内部style里借助:deep()去调整子组件内部样式业务页面里不要到处用。否则选择器全挂在子组件内部项目迭代时一旦组件结构调整业务样式会大面积崩掉。7. 文档、示例与测试组件库能走多远看这三件事7.1 组件Demo最容易被低估的调试工具很多开发者的习惯是把组件写完在App.vue里随手试一下就算结束。但组件库的组件要和各种边界条件打交道——空数据、超长文本、禁用态、加载态、键盘操作只试一种正常路径根本不够。这套项目里我建了一个play目录每个组件都有独立的调试页面把能想到的状态都铺出来。做完一个组件就打开对应的demo页逐个状态过一遍比写测试用例更早发现问题。7.2 测试重点测行为而非样式测试我选用Vitest加vue/test-utils。组件测试的粒度不是截图对样式而是行为。Button的核心行为是disabled时不触发click正常态点击会emit事件。import { describe, it, expect } from vitest import { mount } from vue/test-utils import Button from ../src/button.vue describe(Button, () { it(点击后触发click事件, async () { const wrapper mount(Button) await wrapper.trigger(click) expect(wrapper.emitted(click)).toHaveLength(1) }) it(disabled时点击不触发事件, async () { const wrapper mount(Button, { props: { disabled: true } }) await wrapper.trigger(click) expect(wrapper.emitted(click)).toBeUndefined() }) })这种行为级测试在后续重构组件内部实现时有极大的安全感。只要行为没变随便改内部结构都不心虚。7.3 文档站给每个组件写使用边界文档不完全是为别人写的也是为自己三个月后的记忆写的。我用Vitepress搭建了文档站每个组件页面都包含使用示例、API表格、注意事项三块。写API表格时我直接把TypeScript类型定义抄进去保持文档和代码同步——手动另维护一份API描述基本注定会过期。8. 打包、发布与持续迭代8.1 Vite库模式构建组件库的构建和业务应用完全不同要用Vite的库模式。最关键的是把vue设为external否则打包产物会把Vue整个打进去和业务侧的Vue实例变成两份运行时会直接报错。bundle里只留组件库自身的代码// vite.config.build.ts import { defineConfig } from vite import vue from vitejs/plugin-vue export default defineConfig({ plugins: [vue()], build: { lib: { entry: src/index.ts, name: Vui, fileName: (format) vui.${format}.js }, rollupOptions: { external: [vue], output: { globals: { vue: Vue } } } } })同时用vite-plugin-dts生成完整的.d.ts类型声明文件再在package.json里配置好入口字段。一个能被正常import的组件库才算真正交付。8.2 按需引入与会话体验组件库发布到npm之后按需引入几乎是必须的。通过unplugin-vue-components配合一个自定义resolver用户写时组件库的Volar插件会做自动导入样式也能自动加载。这需要在组件源码里把组件样式按组件目录拆分好我前面规划的目录结构这时候就发挥作用了。8.3 版本管理与迭代节奏组件库发布不是一锤子买卖。我维护这套库时给自己定了几条规矩任何破坏性变更必须大版本号升级新增功能完善后再发minorCHANGELOG里每一笔都写清动机不写优化代码质量这种废话。版本管理本质上是对使用者负责——对方升级了一个小版本不应该出现组件行为悄悄变化的情况。最后说点实际的。这套项目我断断续续写了大概三个月刚开始觉得最难的不是写组件而是忍住不写一次性组件。每个组件我都逼自己先想清楚API再动手。真正写完之后最大的收获不是多了一个组件库而是再去看Element Plus之类的开源库时一眼就能看出某个组件是怎么组织的遇到bug能直接去node_modules里翻源码解决。如果你也想达到这种状态建议把这套Vue3TSLess的源码从头到尾自己敲一遍遇到看不懂的先看教程和代码说明再尝试自己加一个新组件。这个过程走完你对Vue3的响应式、插槽、事件、类型系统、样式工程的认知会明显跨一个台阶。本文还有配套的精品资源点击获取