贡献指南:新增组件 / 文档 / 调试
这篇文档完整覆盖在 Skyler UI 中新增一个组件的全流程,包括:源码结构、文档撰写、playground 调试和发版。下面用一个具体例子 —— 新增 SkySwitch 开关组件 —— 一步步演示。
如果你只想看清单,跳到末尾 📋 速查 Checklist。
0. 仓库结构速览
skyler-ui/
├── packages/
│ └── skyler-ui/ ← 组件库源码(发包到 npm)
│ ├── src/
│ │ ├── components/<name>/ ← 每个组件一个文件夹
│ │ │ ├── <name>.vue ← 组件实现
│ │ │ ├── types.ts ← props / emits 类型
│ │ │ └── index.ts ← 导出 + withInstall
│ │ ├── styles/
│ │ │ ├── _vars.scss ← 设计 token(CSS 变量)
│ │ │ ├── _mixins.scss ← SCSS mixin
│ │ │ ├── components/_<name>.scss ← 组件样式
│ │ │ └── index.scss ← 入口(@use 所有组件样式)
│ │ ├── utils/ ← BEM、install、addUnit
│ │ └── index.ts ← 库主入口(导出 + Vue 插件)
│ └── package.json
├── docs/ ← VitePress 文档站
│ ├── components/<name>.md ← 每个组件一份文档
│ └── .vitepress/config.ts ← 侧边栏配置
└── playground/ ← Vite + Vue 3 调试环境
└── src/App.vue ← 在这里写 demo 试组件新增一个组件 = 5 个源码文件 + 1 个文档 + 改 2 个 barrel 文件 + 改 1 个侧边栏配置。
1. 写组件源码
1.1 建目录
cd packages/skyler-ui/src/components
mkdir switch1.2 switch/types.ts —— 定义 props/emits
把 props/emits 抽到独立文件,便于在外部引用类型,也是文档表格的"事实源"。
// packages/skyler-ui/src/components/switch/types.ts
import type { ExtractPropTypes, PropType } from 'vue'
export type SwitchSize = 'small' | 'medium' | 'large'
export const switchProps = {
/** v-model 绑定值 */
modelValue: { type: Boolean, default: false },
/** 尺寸 */
size: { type: String as PropType<SwitchSize>, default: 'medium' },
/** 激活态颜色 */
activeColor: { type: String, default: '' },
/** 是否禁用 */
disabled: { type: Boolean, default: false },
/** 加载中 */
loading: { type: Boolean, default: false },
} as const
export type SwitchProps = ExtractPropTypes<typeof switchProps>
export const switchEmits = {
'update:modelValue': (_v: boolean) => true,
change: (_v: boolean) => true,
}
export type SwitchEmits = typeof switchEmits约定:
- 所有 props 都写 JSDoc,文档表格的"说明"列就是它。
- 复杂联合类型抽出
export type Xxx方便用户引用。 - emits 用对象形式(带运行时校验),不用数组。
1.3 switch/switch.vue —— 组件实现
⚠️ 关键纪律: 用 <view> / <text>,不要用 <div> / <span>,否则在小程序里会编译失败。
<!-- packages/skyler-ui/src/components/switch/switch.vue -->
<template>
<view :class="classes" :style="rootStyle" @click="onClick">
<view class="sky-switch__node">
<view v-if="loading" class="sky-switch__loading" />
</view>
</view>
</template>
<script lang="ts">
import { computed, defineComponent } from 'vue'
import { createBEM } from '../../utils/bem'
import { switchEmits, switchProps } from './types'
export default defineComponent({
name: 'SkySwitch',
props: switchProps,
emits: switchEmits,
setup(props, { emit }) {
const bem = createBEM('switch')
const classes = computed(() => [
bem.b(),
bem.m(props.size),
bem.is('on', props.modelValue),
bem.is('disabled', props.disabled),
bem.is('loading', props.loading),
])
const rootStyle = computed(() => ({
backgroundColor: props.modelValue && props.activeColor
? props.activeColor
: undefined,
}))
const onClick = () => {
if (props.disabled || props.loading) return
const next = !props.modelValue
emit('update:modelValue', next)
emit('change', next)
}
return { classes, rootStyle, onClick }
},
})
</script>约定:
name: 'SkyXxx'大驼峰统一前缀Sky,渲染出来就是<sky-xxx>。- 使用
createBEM('switch')生成类名 →.sky-switch、.sky-switch--small、.is-on。 - 不要用
document/window/navigator这种 DOM API,小程序里不存在。需要计时器/动画用 Vue 的watch+ setTimeout 即可。
1.4 switch/index.ts —— 导出
// packages/skyler-ui/src/components/switch/index.ts
import { withInstall } from '../../utils/install'
import Switch from './switch.vue'
export const SkySwitch = withInstall(Switch)
export default SkySwitch
export * from './types'withInstall 给组件挂上 .install 方法,这样既能 app.use(SkySwitch) 单独注册,也能被库总入口的 app.use(SkylerUI) 统一安装。
1.5 styles/components/_switch.scss —— 样式
// packages/skyler-ui/src/styles/components/_switch.scss
.sky-switch {
position: relative;
display: inline-block;
box-sizing: border-box;
width: 80rpx;
height: 44rpx;
background: var(--sky-border-color-dark);
border-radius: var(--sky-radius-round);
cursor: pointer;
transition: background var(--sky-transition);
&--small { width: 64rpx; height: 36rpx; }
&--large { width: 96rpx; height: 52rpx; }
&.is-on { background: var(--sky-color-primary); }
&.is-disabled,
&.is-loading { opacity: var(--sky-disabled-opacity); cursor: not-allowed; }
&__node {
position: absolute;
top: 4rpx;
left: 4rpx;
width: 36rpx;
height: 36rpx;
background: #fff;
border-radius: 50%;
box-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.2);
transition: left var(--sky-transition);
}
&--small &__node { width: 28rpx; height: 28rpx; }
&--large &__node { width: 44rpx; height: 44rpx; }
&.is-on &__node { left: calc(100% - 40rpx); }
&.is-on.sky-switch--small &__node { left: calc(100% - 32rpx); }
&.is-on.sky-switch--large &__node { left: calc(100% - 48rpx); }
&__loading {
position: absolute;
inset: 4rpx;
border: 2rpx solid currentColor;
border-top-color: transparent;
border-radius: 50%;
animation: sky-switch-spin 0.8s linear infinite;
}
}
@keyframes sky-switch-spin {
to { transform: rotate(360deg); }
}约定:
- 单位统一用
rpx(750 设计稿基准,uniapp 标准),颜色用var(--sky-*)CSS 变量。 - 浏览器(playground / docs)通过 vite postcss 自动把
rpx按 0.5 转成px,无须手写 px。 - 库不预编译,发包形态是源码 → 消费方的 uniapp 编译器在每个目标平台原样处理 .vue + .scss。
- BEM:块
.sky-switch、元素.sky-switch__node、修饰.sky-switch--small、状态.is-on。
2. 接到库的 barrel 入口
2.1 让 SCSS 入口包含新组件
// packages/skyler-ui/src/styles/index.scss
@use './vars';
@use './components/button';
@use './components/icon';
@use './components/cell';
@use './components/tag';
@use './components/divider';
@use './components/loading';
@use './components/toast';
@use './components/switch'; // ← 新增2.2 让 JS 入口导出并自动注册
// packages/skyler-ui/src/index.ts
import SkySwitch from './components/switch' // ← 新增
const components = [
SkyButton,
SkyIcon,
SkyCell,
SkyTag,
SkyDivider,
SkyLoading,
SkyToast,
SkySwitch, // ← 新增
] as const
export {
SkyButton, SkyIcon, SkyCell, SkyTag,
SkyDivider, SkyLoading, SkyToast,
SkySwitch, // ← 新增
}
export * from './components/switch/types' // ← 新增到这里,组件就能在 import { SkySwitch } from '@whskyler/skyler-ui' 里用了。
3. 在 playground 里调试
playground 是一个独立的 Vite + Vue 3 项目,路径别名直接指向 packages/skyler-ui/src,改源码立刻热更新,不需要先 build。
3.1 启动 playground
pnpm play # 或 pnpm dev,等价
# → http://localhost:40003.2 在 playground/src/App.vue 加一段 demo
<section class="play__section">
<h2>Switch</h2>
<view class="row">
<sky-switch v-model="state.s1" />
<sky-switch v-model="state.s2" size="small" />
<sky-switch v-model="state.s3" size="large" active-color="#19be6b" />
<sky-switch v-model="state.s4" disabled />
<sky-switch v-model="state.s5" loading />
</view>
<text>当前值:{{ JSON.stringify(state) }}</text>
</section>// 在 <script setup> 里加:
const state = reactive({ s1: false, s2: true, s3: true, s4: false, s5: false })保存就会热更新,浏览器立即看到效果。
3.3 playground 怎么"代理" uniapp 标签
playground 是浏览器环境,没有 uniapp 运行时。view / text 是通过 playground/src/uni-shim.ts 注册成 Vue 组件实现的——它们渲染为普通 <div> / <span>:
// playground/src/uni-shim.ts (节选)
app.component('view', makeProxy('View', 'div'))
app.component('text', makeProxy('Text', 'span'))所以你写组件时视觉和小程序里基本一致(源码 rpx = 浏览器 0.5 × rpx = 小程序原值,由 playground 的 postcss 在 dev 时实时换算)。如果要 100% 还原小程序效果,仍需要在 HBuilderX/CLI 里跑真机调试。
3.4 调试技巧
- 看真实 DOM:打开浏览器 DevTools → Elements,能看到
<sky-switch>渲染出的真实结构。 - 组件状态:装 Vue DevTools 浏览器插件,能直接看每个
<sky-switch>的 props 和 setup 状态。 - 样式联调:DevTools 里改 CSS 变量
--sky-color-primary,整个页面立即变色。 - HMR 不刷新? 类型
.ts文件偶尔需要重启 vite,组件.vue一律热更。
4. 写文档
文档放在 docs/components/<name>.md。每个组件文档结构是固定的:标题 → 简介 → N 个 Demo → API 表格。
4.1 新建 docs/components/switch.md
# Switch 开关
布尔值切换。
## 基础用法
<Demo title="基础" description="使用 v-model 双向绑定。">
<sky-switch v-model="basic" />
<template #code>
\`\`\`vue
<sky-switch v-model="value" />
\`\`\`
</template>
</Demo>
## 尺寸
<Demo>
<view class="demo-row">
<sky-switch v-model="s1" size="small" />
<sky-switch v-model="s2" />
<sky-switch v-model="s3" size="large" />
</view>
<template #code>
\`\`\`vue
<sky-switch v-model="v" size="small" />
<sky-switch v-model="v" />
<sky-switch v-model="v" size="large" />
\`\`\`
</template>
</Demo>
## 自定义颜色
<Demo>
<sky-switch v-model="green" active-color="#19be6b" />
<template #code>
\`\`\`vue
<sky-switch v-model="v" active-color="#19be6b" />
\`\`\`
</template>
</Demo>
## 禁用 / 加载
<Demo>
<view class="demo-row">
<sky-switch v-model="d1" disabled />
<sky-switch v-model="d2" loading />
</view>
<template #code>
\`\`\`vue
<sky-switch v-model="v" disabled />
<sky-switch v-model="v" loading />
\`\`\`
</template>
</Demo>
<script setup>
import { ref } from 'vue'
const basic = ref(false)
const s1 = ref(true)
const s2 = ref(true)
const s3 = ref(false)
const green = ref(true)
const d1 = ref(true)
const d2 = ref(false)
</script>
## API
### Props
| 属性 | 说明 | 类型 | 默认值 |
| --- | --- | --- | --- |
| v-model | 绑定值 | `boolean` | `false` |
| size | 尺寸 | `'small' \| 'medium' \| 'large'` | `'medium'` |
| active-color | 激活态背景色 | `string` | — |
| disabled | 禁用 | `boolean` | `false` |
| loading | 加载中 | `boolean` | `false` |
### Events
| 事件 | 说明 | 回调参数 |
| --- | --- | --- |
| update:modelValue | v-model 更新 | `(v: boolean)` |
| change | 值变化 | `(v: boolean)` |4.2 <Demo> 是什么
它是 docs/.vitepress/theme/components/Demo.vue 注册的全局组件,长这样:
<Demo>默认插槽:放实时预览的组件(直接写<sky-xxx>,因为 docs 已经全局注册了组件库)。#code插槽:放代码块(用\``vue` 围栏),点击「查看代码」会展开。- 可选 props:
title/description显示在 demo 上方。
4.3 文档里的 <script setup>
VitePress 的 markdown 文件支持 Vue SFC 的 <script setup>,用来准备 demo 需要的 ref。注意:
- 不能写在文件顶部(会被 frontmatter 解析),最好放到第一个 demo 之后或文件中部。
- 不要 import
vue之外的东西(除非你想增加构建依赖)。
4.4 把组件加到侧边栏
// docs/.vitepress/config.ts
sidebar: {
'/components/': [
{
text: '基础',
items: [
{ text: 'Button 按钮', link: '/components/button' },
{ text: 'Icon 图标', link: '/components/icon' },
{ text: 'Divider 分割线', link: '/components/divider' },
],
},
{
text: '展示',
items: [
{ text: 'Cell 单元格', link: '/components/cell' },
{ text: 'Tag 标签', link: '/components/tag' },
],
},
{
text: '反馈',
items: [
{ text: 'Loading 加载', link: '/components/loading' },
{ text: 'Toast 轻提示', link: '/components/toast' },
],
},
{
text: '表单', // ← 新分组
items: [
{ text: 'Switch 开关', link: '/components/switch' },
],
},
],
},4.5 启动文档站点验收
pnpm docs:dev
# → http://localhost:5173打开 /components/switch,应该看到完整文档 + 可交互 demo + API 表格。
5. 发新版本
确认 playground 和文档都正常后:
5.1 先把功能改动推上去
git add .
git commit -m "feat: SkySwitch 开关组件"
git push origin main5.2 用一键脚本 bump + push tag
# 改 bug
pnpm release:patch # 0.2.0 → 0.2.1
# 加新组件 / 加新 props(向后兼容)
pnpm release:minor # 0.2.0 → 0.3.0
# 破坏性变更(删 prop、改默认值等)
pnpm release:major # 0.2.0 → 1.0.0scripts/release.sh 会按顺序做:
| 步骤 | 行为 | 失败会发生什么 |
|---|---|---|
| 工作区干净检查 | git diff-index --quiet HEAD + 没有 untracked 文件 | 直接 exit,提示先 commit/stash |
| 分支检查 | 必须在 main | exit,提示 checkout |
| 远端同步检查 | git fetch && rev-parse 对比 | exit,提示 pull/push |
| typecheck | pnpm -F @whskyler/skyler-ui typecheck | exit |
| 二次确认 | 显示当前版本 + 即将发布版本,y/N | 输 N 退出 |
| npm registry 占用查询 | npm view <name>@<new-version> 是否返回 404 | 占用直接 exit |
| 改版本 + commit + tag | npm version <level> -m "chore: release v%s" | npm 报错,全部回滚 |
| push | git push origin main --follow-tags | 网络问题,本地 commit/tag 仍保留,可手动 push |
5.3 触发云效 publish-npm 流水线
去云效页面 → publish-npm 流水线 → 「运行」。流水线会再做一遍 typecheck + 版本占用检查 + npm publish + smoke test。
想 push tag 自动触发?在云效流水线 → 触发设置 → 加「Tag 触发,包含
v*.*.*」即可,下次pnpm release:patch推完 tag 自动跑。
📋 速查 Checklist
在 PR 合并 / 发版前对照检查:
源码
- [ ]
packages/skyler-ui/src/components/<name>/下三个文件齐全(vue / types / index) - [ ] 组件
name: 'SkyXxx',类名.sky-xxx,标签<sky-xxx> - [ ] 模板里只用
view/text/image/scroll-view等 uniapp 通用标签,不用div/span - [ ] 没有
document/window/localStorage等 DOM API - [ ] 尺寸用
rpx(750 基准),颜色用var(--sky-*)CSS 变量 - [ ] props/emits 都有 JSDoc 注释
接入
- [ ]
src/styles/index.scss加了@use './components/<name>'; - [ ]
src/index.ts导出了组件 + 加到components数组 +export * from types
文档
- [ ]
docs/components/<name>.md用<Demo>包了所有示例 - [ ] 文档末尾有 Props / Events / Slots 表格
- [ ]
docs/.vitepress/config.ts侧边栏加了入口
验证
- [ ]
pnpm play看到组件能正常交互 - [ ]
pnpm docs:dev看到文档页能正常渲染、demo 能交互 - [ ]
pnpm typecheck类型检查无报错 - [ ]
pnpm docs:build构建无报错
发版
- [ ] 功能改动已 push 到 main
- [ ] 跑
pnpm release:<patch|minor|major>自动 bump + tag + push - [ ] 云效 publish-npm 流水线运行成功
- [ ]
npm view @whskyler/skyler-ui能看到新版本
❓ 常见坑
| 现象 | 原因 | 解决 |
|---|---|---|
playground 里 view / text 报「Failed to resolve component」 | uni-shim 没注册或 vite 配置漏了 isCustomElement | 检查 playground/src/main.ts 调了 installUniShim,vite.config.ts 里 compilerOptions.isCustomElement 包含该 tag |
| 组件在文档里没样式 | styles/index.scss 漏了 @use | 在入口加上对应的 @use './components/<name>'; |
npm publish 403 / 2FA 错 | token 不能绕过 2FA | 重新生成 Granular Access Token,勾「Bypass 2FA」 |
docs:build 失败说找不到 <view> | docs/.vitepress/config.ts 的 vue.template.compilerOptions.isCustomElement 没覆盖该标签 | 在那个 Set 里加上对应 tag 名 |
| 在小程序里组件不显示 / 样式错乱 | 用了 div / position: fixed 等小程序限制特性 | 改用 view,弹层用 cover-view 或 z-index 实测 |