Skip to content

贡献指南:新增组件 / 文档 / 调试

这篇文档完整覆盖在 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 建目录

bash
cd packages/skyler-ui/src/components
mkdir switch

1.2 switch/types.ts —— 定义 props/emits

把 props/emits 抽到独立文件,便于在外部引用类型,也是文档表格的"事实源"。

ts
// 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>,否则在小程序里会编译失败。

vue
<!-- 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 —— 导出

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 —— 样式

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 入口包含新组件

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 入口导出并自动注册

ts
// 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

bash
pnpm play     # 或 pnpm dev,等价
# → http://localhost:4000

3.2 在 playground/src/App.vue 加一段 demo

vue
<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>
ts
// 在 <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>

ts
// 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

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` 围栏),点击「查看代码」会展开。
  • 可选 propstitle / description 显示在 demo 上方。

4.3 文档里的 <script setup>

VitePress 的 markdown 文件支持 Vue SFC 的 <script setup>,用来准备 demo 需要的 ref。注意:

  • 不能写在文件顶部(会被 frontmatter 解析),最好放到第一个 demo 之后或文件中部。
  • 不要 import vue 之外的东西(除非你想增加构建依赖)。

4.4 把组件加到侧边栏

ts
// 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 启动文档站点验收

bash
pnpm docs:dev
# → http://localhost:5173

打开 /components/switch,应该看到完整文档 + 可交互 demo + API 表格。


5. 发新版本

确认 playground 和文档都正常后:

5.1 先把功能改动推上去

bash
git add .
git commit -m "feat: SkySwitch 开关组件"
git push origin main

5.2 用一键脚本 bump + push tag

bash
# 改 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.0

scripts/release.sh 会按顺序做:

步骤行为失败会发生什么
工作区干净检查git diff-index --quiet HEAD + 没有 untracked 文件直接 exit,提示先 commit/stash
分支检查必须在 mainexit,提示 checkout
远端同步检查git fetch && rev-parse 对比exit,提示 pull/push
typecheckpnpm -F @whskyler/skyler-ui typecheckexit
二次确认显示当前版本 + 即将发布版本,y/N输 N 退出
npm registry 占用查询npm view <name>@<new-version> 是否返回 404占用直接 exit
改版本 + commit + tagnpm version <level> -m "chore: release v%s"npm 报错,全部回滚
pushgit 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 调了 installUniShimvite.config.tscompilerOptions.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.tsvue.template.compilerOptions.isCustomElement 没覆盖该标签在那个 Set 里加上对应 tag 名
在小程序里组件不显示 / 样式错乱用了 div / position: fixed 等小程序限制特性改用 view,弹层用 cover-viewz-index 实测

Released under the MIT License.