CustomComponent
CustomComponent 用于在 DefineComponent 的 subComponents 中声明"组件类型驱动"的子组件配置。它将已有组件类型文档(如 $TopNav)接入父组件,并提供 inherit、data、store、computed、events、watch 等字段的组合能力。
高阶函数
与 RootComponent 相同,CustomComponent 也需调用两次:
// 第一次调用:传入泛型参数
// 第二次调用:传入配置选项
CustomComponent<RootDoc, CompDoc, "Prefix">()({/* options */});函数签名
function CustomComponent<
TRootComponentDefinition extends RootComponentDefinition,
TCompDoc extends ComponentDoc,
Prefix extends string = "",
>(): (options: Options) => InferredCustomComponentDefinition;Options 与 InferredCustomComponentDefinition 用于说明输入、输出形状;它们不是公开导出的类型。实际返回类型会根据子组件的 composed 事件及 _catch 配置推导。
泛型参数
| 参数 | 说明 |
|---|---|
TRootComponentDefinition | 父组件类型(typeof rootComponent),提供数据与方法联动 |
TCompDoc | 子组件文档类型,定义子组件的 properties 和 events |
Prefix | 可选,额外补充的前缀,用于解决多实例时的字段冲突 |
配置字段
| 字段 | 类型 | 必填 |
|---|---|---|
| inherit | CustomInheritConstraint | 否 |
| data | CustomDataConstraint | 否 |
| store | CustomStoreConstraint | 否 |
| computed | CustomComputedConstraint | 否 |
| events | CustomEventsConstraint | 否 |
| methods | CustomMethodsConstraint | 否 |
| watch | CustomWatchOption | 否 |
| observers | CustomObserversOption | 否 |
| pageLifetimes | CustomPageLifetimesOption | 否 |
| lifetimes | CustomLifetimesOption | 否 |
| 其他原生字段 | WMCompOtherOption 的部分字段 | 否 |
WXML 约定
当自定义组件需要使用 wx:if、wx:for 等 WXML 控制属性时,建议将控制属性放在外层 block,再由 block 包裹自定义组件:
<block wx:for="{{items}}" wx:key="id">
<top-nav item="{{item}}" />
</block>组件属性引用循环变量时,变量必须来自包裹它的 block wx:for 作用域。对应的 CustomComponent 配置可使用 inherit: "wxml",表示该属性由 WXML 模板传值:
CustomComponent<Root, $TopNav>()({
inherit: {
topNav_item: "wxml",
},
});这种写法也便于 vscode-annil 根据明确的模板作用域进行字段检查;Annil 运行时不读取 WXML 的 id 来建立该关系。
inherit
描述 声明子组件数据从何而来,可以来自根组件可用数据(properties、data、store、computed)也可以来自 wxml 模板。被 inherit 声明的字段不允许在 data、store、computed 中重复声明。
类型 CustomInheritConstraint · 是否必填 否
每个 key 为子组件文档 properties 中的字段名,value 为以下之一:
| value 写法 | 含义 |
|---|---|
"字段名" | 从根组件数据中获取同名/同类型的字段值 |
"字段名"[] | 从根组件数据中获取多个字段的值(取类型并集) |
"wxml" | 数据来自 .wxml 模板(如 wx:for 产生的子数据),不做类型映射 |
类型约束:
- 对应的根数据字段类型必须是子组件对应
properties字段类型的子类型。 - 与根组件数据字段重名时不合法。
import { CustomComponent } from "annil";
import type { $TopNav } from "path/to/top-nav";
type Root = typeof rootComponent;
CustomComponent<Root, $TopNav>()({
inherit: {
topNav_title: "title", // 从根组件 data.title 继承
topNav_item: "wxml", // 数据来自 wxml 模板
},
});data
描述 声明子组件内部数据。可写范围为子组件文档中 inherit 未覆盖的剩余字段,以及 _前缀_ 开头的内部字段。
类型 CustomDataConstraint · 是否必填 否
特性:
- 字段值类型应与子组件文档中定义的类型一致。
this.data和setData的类型会被替换为文档中定义的类型(而非配置中的字面量类型)。
import { type CreateComponentDoc, CustomComponent } from "annil";
type CompDoc = CreateComponentDoc<"aaa", {
properties: {
str: "a" | "b";
num?: number;
obj?: { id: string } | null;
};
}>;
CustomComponent<{}, CompDoc>()({
inherit: { aaa_obj: "wxml" },
data: {
aaa_str: "a", // ✓ 剩余字段
aaa_num: 123, // ✓ 剩余字段
_aaa_template: "internal", // ✓ 内部字段
},
});错误检测:
CustomComponent<{}, CompDoc>()({
inherit: { aaa_str: "wxml" },
data: {
// @ts-expect-error "⚠️ 与Inherit字段重复 ⚠️"
aaa_str: "123",
// @ts-expect-error "⚠️ 组件文档无需字段 ⚠️"
aaa_xxx: "xxx",
_aaa_str: "内部字段", // ✓
},
});store
描述 定义基于 mobx 的响应式数据映射,将全局 store 数据映射到子组件字段。
类型 CustomStoreConstraint · 是否必填 否
可写范围为 inherit 和 data 均未覆盖的剩余字段,以及内部字段。getter 参数为父组件 properties 的初始传值。
CustomComponent<Root, CompDoc>()({
inherit: { aaa_str: "wxml" },
data: { aaa_num: 123 },
store: {
aaa_obj: () => userStore.user,
_aaa_internal: () => storeX.val,
},
});computed
描述 定义子组件计算属性。
类型 CustomComputedConstraint · 是否必填 否
可写范围为 inherit、data、store 均未覆盖的剩余字段,以及内部字段。
计算函数应显式标注返回类型;该返回类型会参与子组件字段和 watch 的类型推导。
CustomComponent<Root, CompDoc>()({
computed: {
aaa_display(): string {
return this.data.aaa_str + " " + this.data.aaa_num;
},
},
});events
描述 声明子组件 wxml 事件处理函数。
类型 CustomEventsConstraint · 是否必填 否
特性:
- key 由子组件文档
events中的字段名推导。 - 事件参数
e.detail类型自动推导,自动去除Bubbles、Capture等标记。 - 冒泡/捕获事件额外支持
_catch后缀表示阻止传递。 - 基础组件(
Wm.View等)事件默认为WMBaseEvent。 - 可使用
Detail<T>、Mark<T>等类型工具覆盖参数类型。
import {
type Bubbles,
type BubblesComposed,
type CreateComponentDoc,
CustomComponent,
} from "annil";
type CompDoc = CreateComponentDoc<"aaa", {
events: {
str: string;
bubbles: string | Bubbles;
Composed: number | BubblesComposed;
};
}>;
CustomComponent<{}, CompDoc>()({
events: {
aaa_str(e) {
e.detail;
}, // string
aaa_bubbles(e) {
e.detail;
}, // string
aaa_Composed_catch(e) {
e.detail;
}, // number,阻止穿透
},
});基础组件事件:
import { CustomComponent, type Wm } from "annil";
CustomComponent<{}, Wm.View>()({
events: {
view_tap(e) {/* e: WMBaseEvent */},
view_longtap(e) {/* e: WMBaseEvent */},
},
});
CustomComponent<{}, Wm.ScrollView>()({
events: {
scrollView_bindscroll(e) {
e.detail; // { scrollLeft, scrollTop, scrollHeight, scrollWidth, deltaX, deltaY }
},
},
});自定义参数类型:
import { CustomComponent, type Detail, type Mark, type Wm } from "annil";
CustomComponent<{}, Wm.View>()({
events: {
view_tap(e: Detail<number>) {
e.detail;
},
view_longtap(e: Mark<{ id: string }>) {
e.mark.id;
},
},
});错误检测:
CustomComponent<{}, CompDoc>()({
events: {
aaa_str(e) {}, // ✓
// @ts-expect-error 非法字段
aaa_other() {},
},
});methods
描述 定义子组件方法。
类型 CustomMethodsConstraint · 是否必填 否
特性:
- 方法名必须以
前缀_开头(前缀从子组件文档自动推导)。 - 不可与
events字段重名。 this指向CustomInstance,可访问根组件methods和子组件数据。
CustomComponent<Root, Wm.View>()({
methods: {
view_xxx() {
return 123;
},
},
events: {
view_tap(e) {
this.view_xxx(); // ✓ 自身方法
this.rootMethod(); // ✓ 根组件方法(Root 有定义时)
},
},
});错误检测:
CustomComponent<Root, CompDoc>()({
methods: {
// @ts-expect-error 前缀错误(应为 aaa_ 开头)
xxx_yyy() {},
// @ts-expect-error 与 events 字段重复
aaa_str() {},
},
});watch
描述 监听子组件数据变化。与 RootComponent 的 watch 一致,支持 (newVal, oldVal) 和深度比较。
类型 CustomWatchOption · 是否必填 否
可监听范围包括:子组件 data、store、computed 字段,根组件的 properties、data、store、computed 字段,以及通过 instanceConfig 注入的 store 字段。
CustomComponent<Root, CompDoc>()({
data: { aaa_str: "123", _aaa_other: 0 },
store: { aaa_num: () => 123 },
watch: {
aaa_str(newVal, oldVal) {
console.log(newVal, oldVal);
},
aaa_num(newVal, oldVal) {
console.log("store changed", newVal);
},
_aaa_other(newVal, oldVal) {
console.log("internal", newVal);
},
title(newVal, oldVal) {
console.log("root title", newVal);
},
},
});observers
描述 原生字段,与微信原生 observers 一致。
类型 CustomObserversOption · 是否必填 否
优先使用 watch
watch 支持 (newVal, oldVal) 和深度比较。
pageLifetimes
描述 页面生命周期。与 RootComponent 的 pageLifetimes 行为一致。
类型 CustomPageLifetimesOption · 是否必填 否
lifetimes
描述 组件生命周期,扩展 beforeCreate。
类型 CustomLifetimesOption · 是否必填 否
前缀机制
子组件所有字段名需遵循 前缀_xxx 命名规范。前缀由组件文档字段名推导:优先取 properties 的前缀;没有 properties 时取 events 的前缀。
当同一组件类型多次出现时,通过第三个泛型参数 Prefix 在原前缀末尾追加其首字母大写形式:
const topNav = CustomComponent<Root, $TopNav>()({/* ... */});
const topNav2 = CustomComponent<Root, $TopNav, "Secondary">()({
// 若原前缀为 topNav,字段前缀变为 topNavSecondary_xxx
data: { topNavSecondary_title: "次级标题" },
});内部字段
以 _前缀_ 开头的字段为内部字段,用于存储临时公共数据,不受组件文档约束:
CustomComponent<{}, CompDoc>()({
data: {
_aaa_isReady: false,
_aaa_template: "temp",
},
});必传字段检测
自动检测子组件文档中无默认值的必传字段是否被 inherit、data、store、computed 完整覆盖。若有遗漏,返回类型为错误字符串:
import type { CreateComponentDoc } from "annil";
type CompDoc = CreateComponentDoc<"aaa", {
properties: {
str: string; // 必传
bool: boolean; // 必传
num?: number; // 可选
};
}>;
// 类型: "缺少必传的字段aaa_str、aaa_bool"
const SubDoc = CustomComponent<{}, CompDoc>()({});返回类型
返回类型由配置推导;当存在未被阻止的 composed 事件时,返回对象包含:
composedEvents:子组件文档中标记了Composed的事件(未被_catch阻止的部分),去除前缀后暴露给上层组件。
import {
type BubblesComposed,
type CreateComponentDoc,
CustomComponent,
} from "annil";
type CompDoc = CreateComponentDoc<"aaa", {
events: {
Composed: number | BubblesComposed;
};
}>;
const sub = CustomComponent<{}, CompDoc>()({});
// sub.composedEvents: { Composed: number | BubblesComposed }
// 所有 Composed 事件被 _catch 阻止 → 返回 never
const blocked = CustomComponent<{}, CompDoc>()({
events: { aaa_Composed_catch() {} },
});
// blocked: never完整示例
import { CustomComponent, RootComponent } from "annil";
import type { $BottomBar } from "path/to/bottom-bar";
import type { $TopNav } from "path/to/top-nav";
const rootComponent = RootComponent()({
data: { title: "首页", count: 0, activeTab: "home" },
methods: {
onHeaderTap() {
console.log("header tapped");
},
increment() {
this.setData({ count: this.data.count + 1 });
},
},
});
type Root = typeof rootComponent;
// 顶部导航
const topNav = CustomComponent<Root, $TopNav>()({
inherit: { topNav_title: "title" },
events: {
topNav_tap() {
this.onHeaderTap();
},
},
});
// 底部栏(带额外前缀)
const bottomBar = CustomComponent<Root, $BottomBar, "Secondary">()({
inherit: { bottomBarSecondary_active: "activeTab" },
data: { _bottomBarSecondary_loading: false },
methods: {
bottomBarSecondary_onTap() {
this.increment();
},
},
});