OpenTiny NEXT 从入门到精通·第 2 篇:组件篇------TinyVue 核心组件库深度实战
组件库是前端应用的"乐高积木"。TinyVue 作为 OpenTiny 生态的核心 UI 组件库,拥有 130+ 企业级组件,覆盖中后台开发的绝大部分场景。但会用组件只是第一步,理解其 Renderless 无渲染架构 、掌握 主题定制 、学会 封装自定义组件,才是从普通使用者到进阶开发者的关键一跃。本篇将带你深度实战 TinyVue,解锁它的真正威力。
在日常开发中,你是否遇到过这些问题:
- 同一个项目里,既要支持 Vue 2,又要支持 Vue 3,组件库需要维护两套代码,API 还不一致?
- 想封装一个业务组件,但为了适配不同框架(Vue / React),要写好几套实现?
- 组件样式需要根据品牌色动态切换,但传统 CSS 方案改起来非常痛苦?
TinyVue 的 Renderless 架构 正是为了解决这些问题而生的。它将组件的逻辑 、模板 和样式三者分离,让核心逻辑与框架解耦,一套代码可以同时运行在 Vue 2 / Vue 3 / React 等多个框架中。
本篇文章将从以下几个方面展开:
- TinyVue 组件库全景概览与特色组件介绍
- Renderless 无渲染架构深度解析
- 主题定制与样式系统
- 从零封装一个自定义组件(含 Vue 2/3 适配)
一、TinyVue 组件库概览
1.1 130+ 组件的全景介绍
TinyVue 的组件按照功能可以分为以下几大类:
| 分类 | 组件示例 | 使用场景 |
|---|---|---|
| 基础组件 | Button、Icon、Link、Typography | 页面基础元素 |
| 布局组件 | Grid、Layout、Divider、Space | 页面结构搭建 |
| 表单组件 | Input、Select、Checkbox、Radio、DatePicker、Upload、Form | 数据录入与校验 |
| 数据展示 | Table、Tree、Pagination、Tag、Badge、Card、Carousel | 数据列表与详情展示 |
| 反馈组件 | Modal、Message、Notification、Popconfirm、Progress | 用户交互反馈 |
| 导航组件 | Menu、Tabs、Breadcrumb、Steps、Dropdown | 页面导航与路由 |
| 高阶组件 | Split、IpAddress、CalendarView、Crop、RichText | 特殊业务场景 |
每个组件都遵循 OpenTiny Design 设计规范,保证视觉一致性和交互体验。
1.2 高频组件特性速览
Table 表格组件
- 支持固定表头、固定列、多级表头
- 内置虚拟滚动,轻松处理 10 万+ 行数据
- 行内编辑、行拖拽排序、列拖拽调整宽度
- 树形表格、可展开行
Tree 树形控件
- 支持懒加载、拖拽节点、复选框
- 自定义节点内容、节点过滤
- 异步加载大数据量时自动启用虚拟滚动
Select 选择器
- 支持远程搜索、分组、创建新条目
- 虚拟滚动支持上千条选项不卡顿
- 多选、可清空、可禁用选项
Form 表单
- 支持表单校验(内置多种校验规则,可自定义)
- 动态表单项、表单联动
- 支持布局模式(inline、block)
1.3 OpenTiny 特色组件详解
除了常规组件,TinyVue 还提供了一些业界少见的特色组件,这些组件来源于华为内部业务的实际需求,经过大量生产验证。
Split 面板分隔器
可拖拽调整左右或上下两个面板大小的组件,常用于后台管理系统的左右布局。
vue
<template>
<tiny-split v-model="splitSize" mode="horizontal">
<template #left>左侧面板(可拖拽调整)</template>
<template #right>右侧面板</template>
</tiny-split>
</template>
<script setup>
import { ref } from 'vue'
import { TinySplit } from '@opentiny/vue'
const splitSize = ref(0.3) // 左侧占比 30%
</script>
IpAddress IP 地址输入框
专门用于 IPv4 地址输入的组件,自动分隔四段,支持键盘快速跳转。
vue
<tiny-ip-address v-model="ip" />
CalendarView 日历视图
比常规的 DatePicker 更强大的日历组件,支持月/周/日视图切换,可自定义渲染内容(如日程安排)。
vue
<tiny-calendar-view v-model="currentDate" :events="calendarEvents" />
Crop 图片裁切
提供可视化裁剪框,支持固定比例、自由裁剪,输出 base64 或 Blob,便于上传。
vue
<tiny-crop :src="imageUrl" :aspectRatio="16/9" @crop="handleCrop" />
二、Renderless 无渲染架构深度解析
2.1 组件与框架分离的设计理念
传统组件库的代码组织方式通常是"框架 + 组件逻辑"强耦合的。以 Vue 组件为例,模板(template)、逻辑(script)、样式(style)都写在同一个 .vue 文件中,这种结构直接绑定了 Vue 框架。
TinyVue 采用的 Renderless 架构 则将组件拆分为三个独立层:
┌─────────────────────────────────────┐
│ 框架适配层 │
│ (Vue2 适配器 / Vue3 适配器 / React适配器) │
├─────────────────────────────────────┤
│ 无渲染逻辑层 (Renderless) │
│ (状态管理、事件处理、生命周期、API) │
├─────────────────────────────────────┤
│ 样式层 (Theme) │
│ (CSS Variables / 主题变量) │
└─────────────────────────────────────┘
- 逻辑层(Renderless):纯 TypeScript 实现,包含组件的状态、计算属性、方法、生命周期等,完全不依赖任何 UI 框架。这部分代码可以跨框架复用。
- 模板层(Template):框架特定的模板代码,负责将逻辑层的数据和事件绑定到 DOM 上。不同框架有不同的模板语法,但调用的都是同一套逻辑层 API。
- 样式层(Style):CSS 样式,使用 CSS Variables 实现主题变量,与框架无关。
2.2 一套代码同时支持 Vue 2 和 Vue 3------版本适配器如何抹平差异
Vue 2 和 Vue 3 在响应式原理、生命周期、API 等方面存在差异。TinyVue 通过 版本适配器(Version Adapter) 来抹平这些差异。
适配器核心思路:
- 将 Vue 2 的 Options API 和 Vue 3 的 Composition API 封装成统一调用接口。
- 逻辑层代码只调用适配器提供的统一方法(如
useReactive、useMounted),不直接依赖 Vue。 - 适配器根据当前 Vue 版本动态选择底层实现。
简化示例(Button 组件):
typescript
// renderless/button.ts ------ 纯逻辑层,无框架依赖
export function useButton(props, { emit }) {
const handleClick = (event) => {
if (!props.disabled) {
emit('click', event);
}
};
return { handleClick };
}
vue
<!-- vue3/Button.vue ------ Vue 3 模板层 -->
<template>
<button :class="classes" @click="handleClick">
<slot />
</button>
</template>
<script setup>
import { useButton } from '../renderless/button'
const props = defineProps(['disabled'])
const emit = defineEmits(['click'])
const { handleClick } = useButton(props, { emit })
</script>
vue
<!-- vue2/Button.vue ------ Vue 2 模板层 -->
<template>
<button :class="classes" @click="handleClick">
<slot />
</button>
</template>
<script>
import { useButton } from '../renderless/button'
export default {
props: ['disabled'],
setup(props, { emit }) {
return useButton(props, { emit })
}
}
</script>
通过这种方式,Button 的核心逻辑(useButton)只写一次,Vue 2 和 Vue 3 的模板各自引用同一份逻辑,保证了 API 和行为的完全一致。
2.3 一套代码同时支持 PC 和移动端------多端同源的技术实现
传统组件库往往分 PC 版和移动版(如 Ant Design 和 Ant Design Mobile),需要维护两套代码。TinyVue 通过 响应式断点 + 适配器 实现了多端同源。
实现原理:
- 组件内部通过
useBreakpoint检测当前屏幕宽度,返回'pc'或'mobile'。 - 模板层根据端类型渲染不同的 DOM 结构和交互模式(如 PC 端是弹窗选择器,移动端是底部抽屉)。
- 样式层通过媒体查询自动适配不同屏幕。
示例:DatePicker 组件的多端适配
- PC 端:弹出浮层日历面板,鼠标交互。
- 移动端:底部弹出滚动选择器,触摸优化。
开发者只需引入同一个组件,无需关心端差异,TinyVue 会自动适配。
三、主题定制与样式系统
3.1 基于 CSS Variables 的动态主题系统
TinyVue 的主题系统基于 CSS Variables(自定义属性) 构建,而不是传统的 SCSS 变量。CSS Variables 的一大优势是 运行时动态切换,无需重新编译。
主题变量定义示例:
css
/* 亮色主题 */
:root {
--ti-button-primary-bg: #1890ff;
--ti-button-primary-hover-bg: #40a9ff;
--ti-button-primary-active-bg: #096dd9;
--ti-button-border-radius: 4px;
}
/* 暗色主题 */
[data-theme="dark"] {
--ti-button-primary-bg: #1f3a5f;
--ti-button-primary-hover-bg: #2a4b7a;
--ti-button-primary-active-bg: #0f2a4a;
}
组件样式使用变量:
css
.tiny-button--primary {
background-color: var(--ti-button-primary-bg);
border-radius: var(--ti-button-border-radius);
}
.tiny-button--primary:hover {
background-color: var(--ti-button-primary-hover-bg);
}
动态切换主题:
javascript
// 切换到暗色主题
document.documentElement.setAttribute('data-theme', 'dark');
// 切回亮色主题
document.documentElement.removeAttribute('data-theme');
3.2 主题生成器与自定义主题包导出
TinyVue 提供了 在线主题生成器,开发者可以可视化调整颜色、圆角、间距等设计 token,实时预览效果,最后导出自定义主题 CSS 文件,在项目中引入即可。
自定义主题包导入:
javascript
import '@opentiny/vue/theme/index.css' // 默认主题
import './my-custom-theme.css' // 自定义主题覆盖
3.3 Shadow DOM 样式隔离机制------微前端场景下的天然优势
TinyVue 组件可以选择性地启用 Shadow DOM 封装。启用后,组件的样式会被隔离在 shadow root 内部,不会影响外部页面,也不会被外部样式污染。
这对于微前端场景尤为重要:主应用和子应用可能使用不同的 UI 库或主题,传统 CSS 容易发生样式冲突。而 TinyVue 的 Shadow DOM 方案从根本上解决了这个问题。
启用 Shadow DOM:
vue
<template>
<tiny-button shadow-dom>隔离样式</tiny-button>
</template>
💡 资深提示:Shadow DOM 虽然解决了样式冲突,但也会带来一些限制(如全局弹窗需要挂载到 body)。TinyVue 内部已做了处理,Modal、Message 等浮层组件会自动挂载到外部,保证功能正常。
四、组件开发实战------从零封装一个自定义组件
下面我们以一个 评分组件(Rate) 为例,演示如何基于 TinyVue 的 Renderless 架构,开发一个支持 Vue 2 和 Vue 3 的跨框架组件。
4.1 理解组件 API 规范与设计模式
首先定义组件的 Props 和 Events:
typescript
// types.ts
export interface RateProps {
value: number; // 当前评分值
max?: number; // 最大分数,默认5
disabled?: boolean; // 是否禁用
allowHalf?: boolean; // 是否允许半星
}
export interface RateEmits {
(e: 'change', value: number): void;
(e: 'update:value', value: number): void;
}
4.2 编写组件逻辑层(Renderless 核心)
typescript
// renderless/rate.ts
import { ref, computed } from '../adapter' // 统一适配器
export function useRate(props: RateProps, { emit }) {
const currentValue = ref(props.value);
const max = computed(() => props.max ?? 5);
const stars = computed(() => {
return Array.from({ length: max.value }, (_, i) => i + 1);
});
const getStarClass = (index: number) => {
const starValue = index + 1;
const isFull = currentValue.value >= starValue;
const isHalf = props.allowHalf &&
currentValue.value > starValue - 1 &&
currentValue.value < starValue;
return {
'full': isFull,
'half': isHalf,
'active': isFull || isHalf
};
};
const handleClick = (index: number) => {
if (props.disabled) return;
const newValue = index + 1;
currentValue.value = newValue;
emit('update:value', newValue);
emit('change', newValue);
};
const handleMouseMove = (event: MouseEvent, index: number) => {
if (!props.allowHalf || props.disabled) return;
const rect = (event.target as HTMLElement).getBoundingClientRect();
const half = event.clientX - rect.left < rect.width / 2;
const starValue = index + (half ? 0.5 : 1);
currentValue.value = starValue;
};
return {
stars,
currentValue,
getStarClass,
handleClick,
handleMouseMove
};
}
4.3 适配 Vue 2 和 Vue 3 的版本适配层
Vue 3 模板 (vue3/Rate.vue):
vue
<template>
<div class="tiny-rate" :class="{ 'is-disabled': disabled }">
<span
v-for="(star, index) in stars"
:key="index"
class="tiny-rate__star"
:class="getStarClass(index)"
@click="handleClick(index)"
@mousemove="(e) => handleMouseMove(e, index)"
>
<i class="tiny-icon-star"></i>
</span>
</div>
</template>
<script setup lang="ts">
import { useRate } from '../renderless/rate'
const props = defineProps(['value', 'max', 'disabled', 'allowHalf'])
const emit = defineEmits(['change', 'update:value'])
const { stars, getStarClass, handleClick, handleMouseMove } = useRate(props, { emit })
</script>
<style scoped>
/* 样式略,使用 CSS Variables */
</style>
Vue 2 模板 (vue2/Rate.vue):
vue
<template>
<div class="tiny-rate" :class="{ 'is-disabled': disabled }">
<span
v-for="(star, index) in stars"
:key="index"
class="tiny-rate__star"
:class="getStarClass(index)"
@click="handleClick(index)"
@mousemove="(e) => handleMouseMove(e, index)"
>
<i class="tiny-icon-star"></i>
</span>
</div>
</template>
<script>
import { useRate } from '../renderless/rate'
export default {
props: ['value', 'max', 'disabled', 'allowHalf'],
setup(props, { emit }) {
return useRate(props, { emit })
}
}
</script>
4.4 组件文档与示例编写
组件开发完成后,需要编写使用文档和示例,方便团队成员使用。TinyVue 的文档基于 Markdown + Vue 组件演示,示例代码可直接运行。
文档示例:
markdown
# Rate 评分组件
用于快速评级操作。
## 基础用法
```vue
<tiny-rate v-model="score" />
半星支持
vue
<tiny-rate v-model="score" allow-half />
禁用状态
vue
<tiny-rate v-model="score" disabled />
API
Props
| 参数 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| value | 当前评分 | number | 0 |
| max | 最大分数 | number | 5 |
| disabled | 是否禁用 | boolean | false |
| allowHalf | 是否允许半星 | boolean | false |
至此,一个完整的跨框架自定义组件就开发完成了。整个过程中,核心逻辑只编写了一次,Vue 2 和 Vue 3 的适配成本极低。
## 总结
本篇我们深入剖析了 TinyVue 组件库的核心能力:
1. **组件全景**:130+ 组件覆盖中后台全场景,特色组件(Split、IpAddress、CalendarView 等)解决特定业务痛点。
2. **Renderless 架构**:逻辑、模板、样式三层分离,一套代码同时支持 Vue 2 / Vue 3,甚至可扩展至 React。
3. **主题系统**:基于 CSS Variables 的运行时主题切换,支持 Shadow DOM 样式隔离,微前端场景天然友好。
4. **组件开发实战**:从 API 设计到逻辑层实现,再到 Vue 2/3 适配,完整演示了跨框架组件的开发流程。
掌握这些内容,你不仅能够熟练使用 TinyVue 开发企业级应用,还能根据业务需求扩展自定义组件,甚至为 OpenTiny 社区贡献代码。
**下篇预告:** 《数据篇------表格、表单与图表的高级应用》将聚焦企业级中后台最核心的数据处理场景,带你深度掌握 TinyGrid、TinyForm、TinyChart 组件的高阶用法与性能优化技巧,敬请期待!
**如果觉得本文对你有帮助,欢迎点赞、收藏、评论,你的支持是我持续创作的动力!**