RootComponent
RootComponent 用于构建根组件,负责声明组件是否为页面,接收属性(properties)、组件事件(customEvents),公共状态与逻辑。
高阶函数
为支持外部泛型(events 字段的子组件类型约束),RootComponent 需调用两次:
// 第一次调用:传入子组件类型列表(可省略,默认为空数组)
// 第二次调用:传入配置选项
RootComponent<[ComponentDocA, ComponentDocB]>()({/* options */});无子组件事件时,直接 RootComponent()({ /* options */ }) 即可。
函数签名
function RootComponent<
TComponentDocList extends ComponentDoc[] = [],
>(): (options: RootComponentOptions) => RootComponentDefinition;配置字段
| 字段 | 类型 | 必填 |
|---|---|---|
| isPage | boolean | 否 |
| properties | Record<string, PropertyOption> | 否 |
| data | DataConstraint | 否 |
| store | StoreConstraint | 否 |
| computed | ComputedConstraint | 否 |
| events | EventsConstraint | 否 |
| customEvents | CustomEventConstraint | 否 |
| watch | WatchOption | 否 |
| pageLifetimes | — | 否 |
| lifetimes | — | 否 |
| methods | MethodsConstraint | 否 |
| observers | — | 否 |
| 其他原生字段 | — | 否 |
内置状态
Annil 会为每个由 RootComponent 构建的组件和页面提供内置布尔状态 attached,不需要在 TS 配置中声明即可在 WXML 中使用:
<block wx:if="{{attached}}">
<view>组件已挂载</view>
</block>attached 的行为如下:
- 组件完成
attached生命周期后为true。 - 如果用户在
data中显式声明attached,Annil 尊重用户声明,不覆盖该字段。 attached是 RootComponent 的隐式有效数据,因此也属于可用于wx:if/wx:elif的布尔数据。- 该字段不是
RootComponent配置表中必须书写的字段;组件代码分析工具应将其作为框架内置字段处理。
isPage
描述 声明组件类型:页面或组件。该字段会影响组件的入口字段、生命周期函数和事件配置。
类型 boolean · 默认值 false · 是否必填 否
该字段影响:
DefineComponent中的入口字段:页面时使用path,组件时使用name。pageLifetimes的可用生命周期类型,页面时为onLoad…,组件时为show…。customEvents字段:页面时不可用。
RootComponent()({
isPage: true,
// ...
});properties
描述 定义组件/页面的接收的属性(外部传值)。
是否必填 否
特性:
任意类型:通过
as DetailedType<T>声明精确类型。必传/可选推断:简写 → 必传;对象写法 +
value→ 可选;可配合optionalTypes声明联合类型。禁用
observable:请用watch代替。类型检测:字段错误、
value与type不一致会有类型报错。
import { DetailedType } from "annil";
type Level = 0 | 1 | 2;
type User = { id: string; age: number };
RootComponent()({
properties: {
// 必传:简写
str: String, // string
num: Number, // number
bool: Boolean, // boolean
// 必传:简写 + DetailedType
gender: String as DetailedType<"male" | "female">, // "male" | "female"
level: Number as DetailedType<Level>, // Level
// 必传:对象写法 + optionalTypes
idOrAge: {
type: String,
optionalTypes: [Number],
},
// 可选:对象写法 + value
theme: { type: String, value: "light" }, // string,可选
userInfo: {
type: Object as DetailedType<User>,
value: { id: "", age: 0 },
},
// 可选:对象写法 + value + optionalTypes
mixed: {
type: String,
value: "default",
optionalTypes: [Number],
},
},
});data
描述 定义组件内部数据,与原生 data 完全一致。与 properties 存在重名时报错。
是否必填 否
store
描述 定义基于 mobx 的响应式数据映射。getter 参数为 properties 和 data 字段的当前值。getter 返回值变化时,会自动 setData 到实例。与 properties、data 存在重名时报错。组件detach时会自动取消监听。重新attach时会重新建立监听。
是否必填 否
警告
返回值为undefined时,控制台打印警告,该子字段被忽略。
getter中没有依赖响应式数据时,控制台报错,因为没有意义。
实例上提供
this.disposer,字段名对应一个IReactionDisposer,可手动取消监听。
type Gender = "male" | "female";
type User = { id: string; name: string; age: number; gender: Gender };
const storeUser = observable({
id: "goddess",
name: "annil",
gender: "female",
age: 23,
} as User);
RootComponent()({
properties: {
userId: String,
},
store: {
// 常规写法:getter 直接访问 store 中的响应式数据
userName: () => storeUser.name,
// 依赖 properties 的写法:getter 参数为 properties 初始传值
ignoreField: (data) =>
storeUser.id === data.userId ? storeUser.age : void 0,
// 错误示例:getter 中没有依赖响应式数据
errorField: () => 23,
},
lifetimes: {
attached() {
// 手动取消监听示例
this.disposer.userName();
},
},
});computed
描述 定义计算属性。函数中的 this 为实例的代理对象,this.data 被深度代理以自动收集依赖,依赖变化后更新计算属性值。实例方法等其他属性可正常访问,但对实例属性的写入会在运行时报错。与 properties、data、store 存在重名时报错。
是否必填 否
提示
计算函数中的
this为实例的代理对象:this.data被深度代理用于依赖收集,实例方法等可正常访问,但对实例属性的写入会在运行时报错。this.data中可访问properties、data、store及其他computed字段,且为只读(运行时检测)。计算属性初始化在
store之后、attached之前。依赖变化后自动setData,会触发watch和observers。系统自动处理计算属性间的依赖顺序,后面的计算属性可依赖前面的;但请勿形成循环依赖(如 A 依赖 B、B 依赖 A),否则初始化会陷入死循环。
应显式标注返回类型(避免类型推断错误)。
this.data.__computedCache__仅供调试使用,请勿直接修改。请勿在计算函数中进行复杂或异步操作,依赖变化时会频繁重算。
RootComponent()({
properties: {
p: {
type: String,
value: "hi",
},
},
data:{
d: 'miss',
},
store: {
s: () => userStore.name,
},
computed: {
// 依赖 properties、data、store 字段
greeting():string {
return this.data.p + " " + this.data.d + " " + this.data.s;
},
// 依赖其他 computed 字段
praise():string {
return this.data.greeting + ", you are so beautiful";
},
});events
描述 声明 wxml 事件处理函数(bindtap、catchtap 等)。声明的事件只能由 wxml 触发(类型约束)。当通过泛型传入子组件类型列表时,自动提供子组件冒泡/捕获事件的字段提示与参数类型推断。
是否必填 否
子组件事件后缀
当 RootComponent 传入子组件文档泛型时,子组件中标记为冒泡或捕获的事件会自动生成带后缀的事件字段,用于区分事件传递阶段:
| 后缀 | 含义 |
|---|---|
_bubbles | 子组件冒泡事件,事件从子组件向父组件传递 |
_bubbles_catch | 子组件冒泡事件(含 composed),在当前组件阻止继续向上传递 |
_capture | 子组件捕获事件,事件从父组件向子组件传递 |
_capture_catch | 子组件捕获事件(含 composed),在当前组件阻止继续向下传递 |
_bubblesCapture | 子组件同时具有冒泡与捕获阶段的事件 |
_bubblesCapture_catch | 子组件冒泡+捕获事件(含 composed),在当前组件阻止传递 |
说明:
_catch后缀仅在子组件customEvents中配置了composed: true时生成,对应 WXML 中使用catch:前缀绑定事件。
内置事件参数类型
annil 提供了以下类型工具,用于为事件参数 e 标注精确类型:
| 类型别名 | 用途 |
|---|---|
WMBaseEvent | 微信原生基础事件类型(默认) |
WMCustomEvent<D,M,C,T> | 完整自定义事件,支持泛型定义各字段 |
Detail<T> | 仅定义 e.detail 的类型 |
Mark<T> | 仅定义 e.mark 的类型 |
Dataset<C,T,D> | 定义 currentTarget.dataset 和 target.dataset |
CurrentTargetDataset<T> | 仅定义 e.currentTarget.dataset 的类型 |
TargetDataset<T> | 仅定义 e.target.dataset 的类型 |
示例
1. 无子组件时
默认参数类型为 WMBaseEvent,也可通过内置类型工具精确标注参数:
import {
CurrentTargetDataset,
Detail,
Mark,
TargetDataset,
WMCustomEvent,
} from "annil";
RootComponent()({
events: {
// 默认基础事件类型
onTap(e) {
// e 类型为 WMBaseEvent
},
// 通过 WMCustomEvent 完整定义 detail、mark、dataset
customA(
e: WMCustomEvent<
string, // detail
{ markData: { id: string } }, // mark
{ currentTargetDatasetData: string }, // currentTarget.dataset
{ targetDatasetData: number } // target.dataset
>,
) {
e.detail; // string
e.mark?.markData; // { id: string }
e.currentTarget.dataset.currentTargetDatasetData; // string
e.target.dataset.targetDatasetData; // number
},
// 仅定义 detail
subB(e: Detail<{ str: string }>) {
e.detail.str; // string
},
// 仅定义 mark
subC(e: Mark<{ id: string }>) {
e.mark.id; // string
},
// 仅定义 currentTarget.dataset
ddd(e: CurrentTargetDataset<{ str: string }>) {
e.currentTarget.dataset.str; // string
},
// 仅定义 target.dataset
eee(e: TargetDataset<{ str: string }>) {
e.target.dataset.str; // string
},
},
});2. 传入子组件泛型时
子组件文档中,事件的 detail 类型通过联合 Bubbles / Capture 等标记冒泡或捕获行为。RootComponent 会自动为这些事件生成带后缀的字段,并提供 detail 类型推导。
import { Bubbles, BubblesComposed, Capture, CaptureComposed } from "annil";
import type { CreateComponentDoc } from "annil";
// 子组件 A:bubbles 为冒泡事件,bubblesComposed 为含 composed 的冒泡事件
type $SubDocA = CreateComponentDoc<"subA", {
events: {
str: string;
bubbles: number | Bubbles;
bubblesComposed: number | BubblesComposed;
};
}>;
// 子组件 B:capture 为捕获事件,captureComposed 为含 composed 的捕获事件
type $SubDocB = CreateComponentDoc<"subB", {
events: {
str: string;
capture: number | Capture;
captureComposed: number | CaptureComposed;
};
}>;
RootComponent<[$SubDocA, $SubDocB]>()({
events: {
// 根组件自身的基础事件(与子组件无关)
xxx(e) {
// e 类型为 WMBaseEvent
},
// 与子组件普通事件(非 Bubbles/Capture)同名时,仍当作根组件自身事件处理
subA_str(e) {
// e 类型为 WMBaseEvent
},
// 冒泡事件:后缀 _bubbles
subA_bubbles_bubbles(e) {
e.detail; // number
},
// 含 composed 的冒泡事件:后缀 _bubbles(也可用 _bubbles_catch 阻止传递)
subA_bubblesComposed_bubbles(e) {
e.detail; // number
},
subA_bubblesComposed_bubbles_catch(e) {
e.detail; // number
},
// 捕获事件:后缀 _capture
subB_capture_capture(e) {
e.detail; // number
},
// 含 composed 的捕获事件:后缀 _capture(也可用 _capture_catch 阻止传递)
subB_captureComposed_capture(e) {
e.detail; // number
},
subB_captureComposed_capture_catch(e) {
e.detail; // number
},
},
});customEvents
类型 CustomEventConstraint · 是否必填 否 · 可用条件 isPage: false(组件时可用)
定义组件对外触发的自定义事件。声明后可通过 this.事件名(detail) 在 methods 中调用触发。
三种配置形式
customEvents 支持三种配置形式,用以描述事件参数 detail 的类型:
| 形式 | 写法 | 说明 |
|---|---|---|
| 简写 | 事件名: 构造函数 | detail 为该构造函数对应的基础类型 |
| 联合 | 事件名: [构造函数1, 构造函数2, ...] | detail 为所有构造函数的联合类型 |
| 完整 | 事件名: { detail, options?, debounce?, throttle? } | 可配置事件选项和/或节流防抖(至少一项);debounce 与 throttle 互斥 |
简写形式
直接使用 String、Number、Boolean、Object、null、undefined 等构造函数声明 detail 类型。配合 as DetailedType<T> 可指定精确字面量类型。
RootComponent()({
customEvents: {
str: String, // detail: string
num: Number, // detail: number
bool: Boolean, // detail: boolean
nothing: undefined, // detail: undefined
nil: null, // detail: null
unionStr: String as DetailedType<"male" | "female">, // detail: "male" | "female"
obj: Object as DetailedType<{ id: string; age: number }>, // detail: { id: string; age: number }
},
methods: {
onTrigger() {
this.str("hello");
this.num(42);
this.unionStr("female");
this.obj({ id: "001", age: 23 });
},
},
});联合形式
传入数组,detail 类型为所有构造函数类型的联合。
RootComponent()({
customEvents: {
union: [String, Number as DetailedType<0 | 1 | 2>, null],
// detail: string | 0 | 1 | 2 | null
},
methods: {
onTrigger() {
this.union("text");
this.union(1);
this.union(null);
},
},
});完整形式
通过对象配置 detail 类型、事件选项(bubbles / capturePhase / composed)及 debounce / throttle。
RootComponent()({
customEvents: {
// 仅配置 options
bubbles: {
detail: String,
options: { bubbles: true },
},
// 配置 options + debounce
withDebounce: {
detail: Number,
options: { bubbles: true, composed: true },
debounce: 300,
},
// 配置 options + throttle
withThrottle: {
detail: String as DetailedType<"male" | "female">,
options: { capturePhase: true },
throttle: 200,
},
// 仅配置 debounce(无 options)
onlyDebounce: {
detail: undefined,
debounce: 300,
},
// 仅配置 throttle(无 options)
onlyThrottle: {
detail: undefined,
throttle: 200,
},
},
});options 配置
options 字段控制微信原生 triggerEvent 的行为,支持以下字段的任意合法组合:
| 字段 | 类型 | 说明 |
|---|---|---|
bubbles | true | 事件是否冒泡 |
capturePhase | true | 事件是否拥有捕获阶段 |
composed | true | 事件是否可跨 Shadow DOM 边界(需配合上两项之一) |
注意
options中的字段值只能为true,不可设为false(默认即为false)。composed不可单独使用,必须配合bubbles和/或capturePhase为true。options不可为空对象。- 完整形式中至少需配置
options、debounce或throttle中的一项;debounce与throttle互斥,不可同时使用。 - 若无需
options、debounce或throttle,应改用简写形式。
合法组合示例:
// 每种组合对应一个类型标签,用于事件文档类型推断
{ bubbles: true } // → Bubbles
{ capturePhase: true } // → Capture
{ bubbles: true, capturePhase: true } // → BubblesCapture
{ bubbles: true, composed: true } // → BubblesComposed
{ capturePhase: true, composed: true } // → CaptureComposed
{ bubbles: true, capturePhase: true, composed: true } // → BubblesCaptureComposed调用方式
在 methods 中通过 this.事件名(detail) 触发自定义事件。detail 参数的类型由配置自动推导:
RootComponent()({
customEvents: {
confirm: Number,
select: [String as DetailedType<"male" | "female">, Number],
close: {
detail: String,
options: { bubbles: true, composed: true },
},
},
methods: {
onConfirm() {
this.confirm(42); // ✓ 参数类型为 number
this.select("male"); // ✓ 参数类型为 "male" | "female" | number
this.close("closed"); // ✓ 参数类型为 string
},
},
});类型错误检测
与 events 字段重名
customEvents 与 events 字段存在同名时会报类型错误:
RootComponent()({
events: {
onTap(e) {},
},
customEvents: {
// @ts-expect-error "⚠️与events字段重复⚠️"
onTap: String,
},
});watch
类型 WatchOption · 是否必填 否
监听所有数据字段(含 properties、data、store、computed)的变化。其中 store 包括组件自身定义的 store 和通过 instanceConfig.setInjectInfo() 全局注入的 store 字段。key的规则与observers相同,不同点在于watch 只在数据真正变化时触发,且回调参数(newVal, oldVal)加入了旧值。
源码:WatchOption
示例
RootComponent()({
properties: {
count: Number,
},
data: {
title: "hello",
},
store: {
userName: () => userStore.name,
},
computed: {
greeting(): string {
return `Hi, ${this.data.userName}`;
},
},
watch: {
// 监听 properties
count(newVal, oldVal) {
console.log("count changed:", newVal, oldVal);
},
// 监听 data
title(newVal, oldVal) {
console.log("title changed:", newVal);
},
// 监听 store 字段
userName(newVal, oldVal) {
console.log("userName changed:", newVal);
},
// 监听 computed 字段(需手动标注类型)
greeting(newVal: string, oldVal: string) {
console.log("greeting changed:", newVal);
},
},
});监听对象子字段
对于对象类型的字段,支持监听其一级子字段或全部子字段的变化:
RootComponent()({
properties: {
user: {
type: Object as DetailedType<{ name: string; age: number }>,
value: { name: "", age: 0 },
},
},
watch: {
// 仅监听user对象(setData中key为user且值发生变化时)。
user(newVal, oldVal) {
console.log("user changed", newVal);
},
// 仅监听 user.name 子字段(setData中key为user.name且值发生变化时)
"user.name"(newVal: string, oldVal: string) {
console.log("user.name changed", newVal);
},
// 监听 user 对象及其所有子字段的变化
"user.**"(newVal, oldVal) {
console.log("user.** changed", newVal);
},
},
});监听注入的 store 字段
通过 instanceConfig.setInjectInfo() 全局注入的 store 字段同样可被 watch 监听:
import { instanceConfig } from "annil";
import { observable } from "mobx";
// 全局注入 store
const themeStore = observable({ theme: "light" as "dark" | "light" });
instanceConfig.setInjectInfo({
store: {
injectTheme: () => themeStore.theme,
},
});
RootComponent()({
watch: {
// 监听注入的 store 字段
injectTheme(newVal: "dark" | "light", oldVal: "dark" | "light") {
console.log("theme changed:", newVal, oldVal);
},
},
});错误检测
- 无可监控字段时,
watch约束为EmptyObject,不可写入任何字段。 - 字段名不在可监控范围内时报错。
- 对象联合
null的类型不可监听子字段(因可能为null无法安全访问)。
RootComponent()({
// ❌ 无可监控字段时报错
watch: {
// @ts-expect-error 无可监控字段
xxx() {},
},
});
RootComponent()({
properties: {
num: Number,
},
data: {
str: "123",
},
watch: {
// @ts-expect-error otherFields 不在可监控字段中
otherFields() {},
},
});
// ❌ 对象联合 null 的类型不可监听子字段
RootComponent()({
properties: {
user: Object as DetailedType<{ name: string; age: number } | null>,
},
watch: {
// @ts-expect-error user 可能为 null,不能监听子字段
"user.name"(newVal, oldVal) {},
},
});注意
当某个字段变化时 watch触发比computed更早,如果watch的回调中使用到了该计算属性,则computed的值为变化前的值,如果需要使用变化后的值,应监控计算属性key。
RootComponent()({
data: {
firstName: "伟",
lastName: "张",
},
computed: {
fullName(): string {
return `${this.data.firstName} ${this.data.lastName}`;
},
},
watch: {
firstName(newVal: string, oldVal: string) {
console.log(newVal, oldVal); // 涛, 伟
console.log(this.data.fullName); // 伟 张,计算属性变化前的值
wx.nextTick(() => {
console.log(this.data.fullName); // 涛 张,计算属性变化后的值
});
},
},
lifetimes: {
attached() {
this.setData({ firstName: "涛" });
},
},
});pageLifetimes
类型 PageLifetimesOption · 是否必填 否
根据 isPage 的值有两种形态:
组件时(isPage: false / 未设置)
对应原生 pageLifetimes,监听组件所在页面的生命周期。仅允许 show、hide、resize。
RootComponent()({
pageLifetimes: {
show() {},
hide() {},
resize(size: WechatMiniprogram.Page.IResizeOption) {},
},
});页面时(isPage: true)
对应页面生命周期。onLoad 的参数类型为 properties 字段类型,返回值支持 void 或 Promise<void>。
可用生命周期:onLoad、onShow、onReady、onHide、onUnload、onPullDownRefresh、onReachBottom、onPageScroll、onShareAppMessage、onShareTimeline、onAddToFavorites、onTabItemTap、onResize。
说明:微信原生要求将页面生命周期写在
methods中,annil 将其统一移至pageLifetimes字段下。
RootComponent()({
isPage: true,
properties: {
id: String,
title: {
type: String,
value: "default",
},
},
pageLifetimes: {
// props 类型为 Required<PropertiesDoc>,返回值支持 void | Promise<void>
onLoad(props) {
props.id; // string
props.title; // string
},
onShow() {},
onReady() {},
onHide() {},
onUnload() {},
onPullDownRefresh() {},
onReachBottom() {},
onPageScroll(e) {},
onShareAppMessage() {},
onShareTimeline() {},
onAddToFavorites() {},
onTabItemTap() {},
onResize() {},
},
});错误检测
- 组件时仅允许
show、hide、resize。 - 页面时仅允许页面生命周期(
onLoad、onShow等)。 - 非法生命周期字段名会报类型错误。
RootComponent()({
pageLifetimes: {
// @ts-expect-error 组件时不存在 xxx 生命周期
xxx() {},
},
});
RootComponent()({
isPage: true,
pageLifetimes: {
// @ts-expect-error 页面时不存在 xxx 生命周期
xxx() {},
},
});lifetimes
新增 beforeCreate 周期用于调试。
类型 LifetimesConstraint · 是否必填 否
| 周期 | 说明 |
|---|---|
beforeCreate(options) | annil 扩展,在配置对象进入原生 Component() 前触发 |
created | 组件实例刚创建 |
attached | 组件进入页面节点树 |
ready | 组件布局完成 |
moved | 组件在树中移动 |
detached | 组件离开页面节点树 |
error(err) | 组件方法抛出错误 |
beforeCreate的this为undefined(此时实例尚未创建)。beforeCreate的参数options为最终进入原生Component()的配置对象,便于调试。
RootComponent()({
lifetimes: {
beforeCreate(options) {
// this 为 undefined
// options 为最终配置对象,可在此时查看和调试。
console.log(options);
},
created() {
console.log("组件创建");
},
attached() {
console.log("组件挂载");
},
// ...
},
});错误检测
RootComponent()({
lifetimes: {
// @ts-expect-error 非法的生命周期字段
xxx() {},
},
});methods
描述 定义实例方法。this 指向完整实例类型,包含 data、setData、自定义事件触发方法、disposer 等所有实例属性。
是否必填 否
特性:
- 方法与
events和customEvents字段不可重名,否则会报类型错误。 - 返回类型会体现在
RootComponent的返回类型中。
RootComponent()({
data: {
count: 0,
},
customEvents: {
customField: String,
},
methods: {
handler() {
this.setData({ count: this.data.count + 1 });
this.customField("hello"); // 调用 customEvents 中的事件方法
},
},
});错误检测
RootComponent()({
events: {
onTap() {},
},
customEvents: {
onCustom: String,
},
methods: {
handler() {
this.setData({ count: this.data.count + 1 });
this.onCustom("hello"); // 调用 customEvents 中的事件方法
},
// @ts-expect-error "⚠️与events字段重复⚠️"
onTap() {},
// @ts-expect-error "⚠️与customEvents字段重复⚠️"
onCustom() {},
},
});observers
原生字段,加入了完整类型。
类型 ObserversOption
是否必填 否
其他原生字段
微信原生的其他字段(behaviors、relations、externalClasses、options、export...)会直接透传至原生 Component() 构造器.
参考
- 源码:src/api/RootComponent
- 相关 API:DefineComponent · CustomComponent