Skip to content

构建你自己的 Block ​

这份指南教你如何从 Base Kit 组件沉淀一个可本地维护、可安装、可贡献的业务区块(block)。示例使用匿名的筛选区块,不包含真实 API、凭证、私有 URL 或内部需求编号。

当前可安装 Blocks ​

1. 先分清分层 ​

层负责不负责
Primitives状态、事件、parts、跨端交互语义视觉 token、业务文案
Base Kit / UI可复制的低层组件源码与默认样式远程数据、权限、领域模型
业务 wrapper接口、权限、字段映射、产品文案通用交互状态机
Blocks可复用页面切片与本地组合私有后端细节、一次性页面胶水

经验法则:block 应该像“可搬运的页面切片”,不是“绑死某业务接口的页面”。

2. 定义 block 契约 ​

在写代码前先写清合同:

ts
// 示例:本地筛选 block 的契约
export interface StatusOption {
  label: string
  value: string
  disabled?: boolean
}

export interface FilterBarProps {
  /** 可选状态列表,由业务层注入 */
  options: StatusOption[]
  /** 受控选中值 */
  modelValue?: string[]
  /** 是否禁用 */
  disabled?: boolean
}

export interface FilterBarEmits {
  (e: 'update:modelValue', value: string[]): void
  (e: 'change', value: string[]): void
}

契约里要写明:

  • 输入:options、默认值、disabled
  • 输出:update:modelValue / change
  • 本地状态:仅 UI 需要的临时状态
  • 依赖:Base Kit 组件名(如 select)
  • 排除项:远程字典、鉴权、埋点 SDK、私有域名

3. 选择 Base Kit 依赖 ​

优先依赖业务项目中的:

text
src/components/ui/*

而不是在 block 里直接 import registry 源码路径。远程数据和产品策略放在业务 wrapper,不放进 block。

安装底座组件:

bash
pnpm dlx @varo-ui/cli add --target weapp components/select

4. 创建本地 block ​

建议路径:

text
src/components/blocks/status-filter.vue

最小实现示例:

vue
<script setup lang="ts">
import { computed } from 'vue'
import { VSelect } from '../ui/select'

export interface StatusOption {
  label: string
  value: string
  disabled?: boolean
}

const props = withDefaults(
  defineProps<{
    options: StatusOption[]
    modelValue?: string[]
    disabled?: boolean
    placeholder?: string
  }>(),
  {
    modelValue: () => [],
    disabled: false,
    placeholder: '选择状态'
  }
)

const emit = defineEmits<{
  'update:modelValue': [value: string[]]
  'change': [value: string[]]
}>()

const value = computed({
  get: () => props.modelValue,
  set: (next) => {
    const normalized = next ?? []
    emit('update:modelValue', normalized)
    emit('change', normalized)
  }
})
</script>

<template>
  <section class="status-filter">
    <header class="status-filter__head">
      <strong>状态筛选</strong>
      <span>{{ value.length }} 项</span>
    </header>
    <VSelect
      v-model:value="value"
      multiple
      :disabled="disabled"
      :options="options"
      :placeholder="placeholder"
    />
  </section>
</template>

要求:

  • props / emits 类型完整
  • 布局小而可读
  • 不写死业务接口

5. 测试 block ​

至少覆盖:

  • 初始渲染
  • 用户选择后的 emit 值
  • disabled 行为
  • 依赖边界(不请求网络、不读私有环境变量)
ts
import { mount } from '@vue/test-utils'
import { describe, expect, it } from 'vitest'
import StatusFilter from '../status-filter.vue'

describe('StatusFilter', () => {
  it('emits selected statuses', async () => {
    const wrapper = mount(StatusFilter, {
      props: {
        options: [
          { label: '待支付', value: 'pending' },
          { label: '已完成', value: 'done' }
        ],
        modelValue: []
      }
    })

    await wrapper.setProps({ modelValue: ['pending'] })
    expect(wrapper.props('modelValue')).toEqual(['pending'])
  })
})

6. 本地使用与维护 ​

vue
<script setup lang="ts">
import { ref } from 'vue'
import StatusFilter from '@/components/blocks/status-filter.vue'

const statuses = ref<string[]>([])
const options = [
  { label: '待支付', value: 'pending' },
  { label: '已完成', value: 'done' }
]
</script>

<template>
  <StatusFilter v-model="statuses" :options="options" />
</template>

复制进业务仓库的源码归业务团队所有,可以继续改布局、token 和交互,不必回灌到 Varo。

7. 准备 registry 元数据 ​

若要让别人通过 CLI 安装,新增:

text
registry/blocks/status-filter/registry.json
registry/blocks/status-filter/status-filter.vue

registry.json 示例:

json
{
  "name": "status-filter",
  "type": "block",
  "title": "Status Filter",
  "description": "A local filter bar for selecting multiple anonymized statuses with VSelect.",
  "targets": ["weapp"],
  "dependencies": ["vue"],
  "registryDependencies": ["components/select"],
  "files": [
    {
      "target": "weapp",
      "from": "registry/blocks/status-filter/status-filter.vue",
      "to": "src/components/blocks/status-filter.vue"
    }
  ],
  "docs": "/blocks/status-filter"
}

字段要点:

  • type:block
  • targets:如 weapp
  • registryDependencies:递归安装的底座组件
  • files:from/to 映射
  • docs:文档路由

8. 补充双语文档 ​

每个公开 block 至少说明:

  • 用途
  • 依赖
  • target
  • 安装后的文件落点
  • 可定制边界 / 不可放入的业务逻辑

现有示例:

9. 校验安装 ​

建议清单:

bash
# 类型与测试
pnpm typecheck
pnpm test

# 打包 CLI 后在临时目录验证安装
pnpm --filter @varo-ui/cli build
pnpm dlx @varo-ui/cli add --registry ./registry --target weapp blocks/status-filter

确认:

  • 依赖组件会递归安装
  • 目标文件已存在时默认不覆盖(no-clobber)
  • 需要覆盖时使用显式 force 流程
  • 安装结果不带私有域名、token、内部任务号

独立发布第三方 Registry ​

不必先贡献回 Varo。把 registry/ 目录及其所有传递依赖的 manifest 和源码部署到自己的静态站点,即可从业务项目安装:

bash
pnpm dlx @varo-ui/cli add --registry https://ui.example.com/registry/ --target weapp blocks/status-filter

CLI 从该地址下的 blocks/status-filter/registry.json 读取元数据,按 files.from 去掉 registry/ 前缀后的路径下载源码。普通和 target-specific 的 registryDependencies 都在同一个 Registry 内解析;缺失依赖会报错,不会回退到官方 Registry。也可以用 --registry ./registry 验证本地目录。

只安装信任的源码。远端地址只支持 HTTP(S),不能携带凭证、查询参数或 fragment,不跟随重定向;单个响应上限为 10 MiB、30 秒。安装保留 src/ 路径限制、符号链接检查、文件冲突检查和失败回滚,不自动安装 npm 依赖。

直接使用标准 shadcn-vue 清单 ​

也支持标准 registry.json 目录(name、homepage、items)和独立 registry-item.json,无需改写成 Varo 的 from/to/targets:

bash
pnpm dlx @varo-ui/cli add --registry ./registry.json hello-world
pnpm dlx @varo-ui/cli add --registry ./hello-world.json hello-world
pnpm dlx @varo-ui/cli add --registry https://ui.example.com/r/registry.json hello-world
pnpm dlx @varo-ui/cli export --registry ./registry.json hello-world > hello-world.json

files.path 相对清单目录读取;已包含 content 的发布条目无需源文件。组件、UI、hook、lib 默认分别进入 src/components、src/components/ui、src/composables、src/lib,保留组件子目录;显式 target 优先,但必须留在 src/。也会读取消费项目的 components.json 和 TypeScript/JSONC 别名配置,并用 AST 重写随文件移动而变化的 JS/TS/Vue script 导入。

标准清单默认 H5;Weapp 条目需明确声明 meta.varo.target: "weapp"。目录内依赖按名称解析,独立条目可读取同目录的 <name>.json,跨 Registry 依赖使用明确的 HTTP(S) 条目 URL。支持 registry:block/component/ui/hook/composable/lib/page/file/theme/style 的文件语义;page/file 文件必须提供明确目标。不支持 registry:base/font、框架转换或 npm 自动安装;css、cssVars、tailwind、envVars 与样式继承 extends 会明确报错,不会静默忽略,也不会自动回退到公共 Registry。

供 shadcn-vue 生态安装 ​

导出一个条目时会内联当前 target 的全部传递依赖和源码,生成符合 shadcn-vue Registry 协议 的 JSON:

bash
mkdir -p public/r
pnpm dlx @varo-ui/cli export --registry ./registry --target weapp blocks/status-filter > public/r/status-filter.json
# 部署 public/ 后,在已配置 components.json 和 TypeScript 路径别名的消费项目执行:
pnpm dlx shadcn-vue@latest add https://ui.example.com/r/status-filter.json

输出采用 registry:file、内联 content 和显式 ~/src/... 安装路径,可由 shadcn-vue 以及委托它安装的生态工具消费;已用 shadcn-vue 2.8.2 验证。Varo 的 add --registry 也可以直接读取导出的单项 JSON。

导出要求文件内容为有效 UTF-8;非 UTF-8 字节会明确报错,不会被替换字符静默损坏。add 仍按原始字节复制二进制资源。安装与导出都会拒绝“同一路径既是文件又是其他文件的父目录”的冲突,包括仅大小写不同的路径冲突。

H5 与 Weapp 必须分别导出。meta.varo.target 记录目标,但第三方安装器不会替你检查运行时,也不会把 Vue 转成 Wevu;消费工程仍需安装匹配的依赖并接入主题。CLI API 调用方须使用 await resolveRegistryItems(...),现在本地与远端解析均返回 Promise。

10. 贡献回 Varo ​

这是可选的上游贡献流程,不是发布第三方 Registry 的前置条件。

提交前过一遍隐私与可移植清单:

贡献物通常包括:源码、registry.json、测试、双语文档。

故障排查 ​

现象处理
依赖没装上检查 registryDependencies 拼写,如 components/select
目标文件冲突确认 to 路径;默认 no-clobber,覆盖需显式 force
block 难复用把远程数据/权限/埋点移到业务 wrapper
小程序不可用删除 H5-only API,保持双端契约
单测过但安装失败用 packed CLI + 临时 fixture 复现,检查 files 映射

安装现有 block ​

bash
pnpm dlx @varo-ui/cli add --target weapp blocks/profile-edit
pnpm dlx @varo-ui/cli add --target weapp blocks/order-filter

安装后:

text
src/components/ui/*        # 底座组件源码
src/components/blocks/*    # block 源码
src/components/biz/*       # 建议放置业务二次封装

相关文档 ​