逻辑树和业务状态
glass-easel ComponentSpace 在 service 中运行,自定义组件是完整实例,拥有 data、properties、observers、relations、lifetimes、事件回调、带类型的 dataset 和 node id。模板也在 service 求值,输出 RenderNode 虚拟树。
运行时
packages/runtime 是 Tiny 的小程序内核,不引用 window、document 或 iframe。service 线程用 glass-easel 维护逻辑树,负责 App/Page/Component、页面栈、setData、observer、relations 和模板求值;render 线程由宿主持有真实渲染树,只负责内置组件、样式、布局和 DOM。两棵树通过 tree.flow、tree.patch 和 tree.event 同步。
总览
service 执行小程序逻辑并维护 glass-easel 逻辑树;render 线程由宿主持有真实组件树。两棵树不共享内存,靠稳定的 node id、component flow 和 virtual node patch 对齐。
service thread render thread (one root per page)
+-------------------------------+ +-------------------------------+
| App / Page / Component | | host TreeRenderer |
| glass-easel logic ComponentSpace | tree | real built-in components |
| template -> virtual RenderNode | <------> | glass-easel render tree |
| data / observers / relations | flow | DOM / layout / styles |
| node ids + patches | patch | no app methods |
+-------------------------------+ +-------------------------------+glass-easel ComponentSpace 在 service 中运行,自定义组件是完整实例,拥有 data、properties、observers、relations、lifetimes、事件回调、带类型的 dataset 和 node id。模板也在 service 求值,输出 RenderNode 虚拟树。
每个页面有一个 render root。宿主的 TreeRenderer 创建真实内置组件、自定义组件容器、slot、shadow root、样式和布局。render 不执行 app methods、observer 或 relations。
service 在创建虚拟节点时分配稳定 id,render 只维护 id 到真实组件的映射。后续 patch、事件、查询和元素树检查都以这个 id 为锚点,而不是从 DOM 反推小程序语义。
当前模板更新走 virtual node patch;component flow 只负责首屏挂载(define、create-node、props、children、ready)。增量不依赖微信旧的 FLOW_UPDATE/FLOW_SET_DATA/FLOW_DATA_OBSERVER,snapshot diff 已经覆盖文本、属性、结构和重排。
service 线程
service 是“小程序应用”真正运行的地方,由 bootServiceThread 一次性装配。
按 runtimeCompatibility 写入 App/Page/Component、getApp/getCurrentPages 等全局;API 对象安装到配置的 namespace 和 aliases。未实现的 API 返回 undefined,并产生 UNSUPPORTED_API 警告。
installMiniProgramGlobals(registry, apiHost)service.js 被导入时执行 App(...),registerApp 会立即调用 onLaunch 和 onShow。此时页面还没有创建。
registerApp(options)Page() 按顺序绑定到 manifest 里的页面路径;Component() 执行 normalizeComponentDefinition,合并 behaviors、observers、relations、options 和 externalClasses。
registerComponent(options)等待 loadArtifacts,加载 templates 和 styles,构建组件 schema,创建 LogicTreeAdapter,挂载首屏逻辑树并触发 onLoad/onShow。
control('runtime', 'bootstrap')宿主的渲染树挂载完成后发送 tree.ready,service 用逻辑树快照发送 tree.flow;随后宿主发 page.ready,service 触发 onReady。
control('page', 'ready', { pageId })| 页面操作 | service 逻辑树 | render 渲染树 |
|---|---|---|
page.navigateTo | 当前页 onHide;创建 pageId;挂载新页逻辑树;onLoad → onShow | 宿主创建新的 render root,应用新页 flow |
page.navigateBack | 移除页面;倒序触发 onHide → onUnload;恢复页 onShow | 每页 render 模式保留原 root;单 render 模式重新挂载 |
page.redirectTo / reLaunch | 销毁旧页,创建目标页;执行对应生命周期 | 关闭不再使用的 render root,挂载新页 |
page.switchTab | 复用 tab 页缓存,更新 tabBar 状态 | 切换可见 root,关闭已卸载页 |
page.hide / page.show | 分发页面和组件 pageLifetimes | 只切换可见性,不销毁渲染树 |
page.unload | onUnload;组件 detached;清理逻辑树和 relations | 关闭 render root |
普通页面保留 pageId、path、query、data、methods 和生命周期记录。组件型页面(bodyType=component)使用整理后的组件定义:URL query 会按 property 类型转换,并执行 created/attached/ready/detached。
页面根节点也会进入逻辑树(page:<pageId>),所以 relations 和 selectComponent 能识别页面边界。
普通页面会把顶层函数(包括生命周期)绑定到实例,兼容 Taro 读取 this.onReady.called 的写法;组件型页面走标准 Component 定义。
这是兼容路径,并不等于完整还原了微信的页面语义。
每页 render 模式下,每个页面有独立的 render iframe,返回时只需切换可见性,DOM、滚动位置和定时器都能保留。单 render 模式下所有页面共用一个 render 线程,返回时需要重新挂载页面。devtool 默认使用每页 render 模式,测试宿主默认使用单 render 模式。
命名边界
runtimeCompatibility 只决定应用需要哪些兼容全局和 behavior scheme;逻辑树和渲染树的内部名称由 TINY_ENGINE_NAMING 统一提供。
// tiny.config.json
{
"compatibility": {
"api": { "global": "my", "aliases": ["wx"] },
"globals": {
"app": "HostApp",
"page": "HostPage",
"component": "HostComponent"
},
"behaviorScheme": "my://"
}
}
// manifest.runtimeCompatibility
{
"api": { "global": "my", "aliases": ["wx"] },
"globals": { "app": "HostApp", "page": "HostPage", ... },
"behaviorScheme": "my://",
"opaqueGlobals": ["webpackJsonp", "cloud", "env"]
}
// 固定内部命名
{
"hostTag": "tiny-add-button",
"hostAttribute": "wx-host", // 临时同步 glass-easel
"styleScopeAttribute": "wx-style-scope",
"scopeMarkerClass": "tiny-scope",
"tagClassPrefix": "tiny-tag-",
"behaviorPrefix": "tiny-",
"behaviorScheme": "tiny://"
}installMiniProgramGlobals() 按 runtimeCompatibility 安装配置的 App/Page/Component/Behavior/getApp/getCurrentPages,以及 API namespace 和 aliases。
canIUse()、诊断和建议信息也使用同一组 namespace。
逻辑树和渲染树使用 tiny-* host node;scope marker、tag marker 和内置 behavior 都从 TINY_ENGINE_NAMING 读取。
glass-easel 尚未公开 host/style scope 属性配置,因此 TINY_ENGINE_NAMING 暂时同步其 wx-host / wx-style-scope;runtime 不做 DOM 观察或属性改写。
| 名称 | 输入侧配置 | 当前内部值 | 生效位置 |
|---|---|---|---|
| 模板指令 | wx:if / directive:if | 编译期消除 | TmplGroup directivePrefix |
| API 全局 | wx / my | 不参与内部命名 | installMiniProgramGlobals |
| 组件宿主 | add-button | tiny-add-button | ComponentSpace hostNodeTagName |
| 宿主属性 | — | wx-host(临时) | render tree / 样式编译器 |
| style scope attribute | — | wx-style-scope(临时) | glass-easel DOM |
| 内置 behavior | my://component-export | tiny-base / tiny-item | runtime + web-components |
| tag / scope marker | — | tiny-tag-* / tiny-scope | 模板注入 + WXSS |
切换 API 或指令 namespace 不会改变 DOM、CSS scope 或内部 behavior。要支持第三方微信库时只增加 api.aliases: ["wx"]。当前唯一例外是 glass-easel 尚未配置化的 host/style scope 属性名。
数据同步
页面和组件的 setData 只改 service 的逻辑树。模板重新求值后,service 把旧虚拟树和新虚拟树做 diff,再把 patch 发给渲染树。
页面或组件调用 setData 时,先做 cloneJsonSafe,再按 a.b、list[0] 这样的路径写入 service 的 data。
cloneJsonSafe(patch) · applyDataPatch(data, patch)service 侧的 observers 和 property observer 按变更路径匹配执行;property observer 会收到旧值。
runObservers(instance, observers, changedPaths, oldData)逻辑树里的 glass-easel 重新计算模板,得到新的虚拟 RenderNode。
logicTree.updateRenderTree(pageId)旧快照和新快照按 text、replace、props、insert、remove、reorder、extra 做 diff,同一个 microtask 内的更新会合批。
diffRenderTree(previous, next)宿主 TreeRenderer 把 patch 应用到真实组件和 DOM;service 不等待 render 的确认。
tree.patchComponent({
properties: {
value: { type: String, value: '', observer: 'onValue' },
},
observers: {
value(value) { this.setData({ derived: value.toUpperCase() }) },
'a, b'(a, b) { this.setData({ sum: a + b }) },
'**'(data) { /* 任意变化 */ },
},
methods: {
onValue(value, oldValue) {
this.setData({ changed: value !== oldValue })
},
},
})组件 data、observer 和 property 都只在 service 逻辑树里。render 只收到节点 props 和 patch,不保存第二份小程序 data。
render 发现 DOM 事件后把 node id 和 native 字段发回 service;事件处理、setData、observer 和模板求值都在逻辑树完成。
observer 递归深度上限是 20,超过会产生 OBSERVER_RECURSION_LIMIT;observer 抛错会产生 OBSERVER_ERROR,不会影响其他逻辑。
事件
渲染树只负责把真实 DOM 事件转成 tree.event:带 node id、事件名、detail、native 字段和 touch 列表。service 用 node id 找到逻辑节点,取出带类型的 dataset,再走 glass-easel 的捕获/冒泡。
宿主内置组件和 DOM 监听器先处理点击、输入、滚动、触摸等原生事件,并归一化 detail。
host event listener渲染树按稳定 node id 发出事件;不再传 componentData,也不要求 service 根据 tag 猜事件语义。
sendEvent('tree', 'event', payload)逻辑树按 node id 找到绑定监听器的元素,读取 typed dataset 和 id,执行 capture/bubble。
logicTree.dispatchViewEvent(payload)逻辑树调用对应的方法;抛错会转成 EVENT_HANDLER_ERROR 诊断。
page.invokeMethod(handler, [event]){
viewId: 'node-42',
name: 'tap',
detail: { ... },
native: { clientX: 120, clientY: 80, ... },
touches: [ ... ],
changedTouches: [ ... ]
}EVENT_HANDLER_MISSING 和 EVENT_PAGE_NOT_FOUND 都是 warning,不会影响其他事件。
自定义组件的 triggerEvent 在 service 的逻辑树内部完成捕获和冒泡,不会为了一个自定义事件再走一次 render 往返。
button、input、scroll-view、swiper、canvas 等内置组件把原生字段归一化成小程序 detail;service 只负责分发,不再按 tag 合成领域事件。
组件语义
service 侧维护完整的组件定义、逻辑树和关系图。render 只保留组件容器和真实节点。
| 能力 | 实现 | 关键行为 |
|---|---|---|
| Behavior | normalizeComponentDefinition | 嵌套 behaviors 去重并检测循环;执行顺序是被引用 behavior → 引用者 → 组件;组件覆盖 properties/methods/data |
| component-export | wx://component-export(scheme 可配置)+ export() | selectComponent 返回 export() 的结果,而不是组件实例 |
| observers | observers / property observer | 支持对象/数组写法、逗号分隔的多路径、** 和 .** 通配;property observer 收到旧值;递归上限 20 |
| relations | RelationManager + ComponentTree | parent / child / ancestor / descendant;双方都声明才会建立;支持 linked / unlinked / linkChanged / linkFailed |
| 选择器 | selectComponent / selectAllComponents / selectOwnerComponent | 默认不穿透组件边界;>>> 可以显式穿透;结果按逻辑树顺序返回 |
| 组件型页面 | MiniProgramPageInstance + 整理后的定义 | query 按 property 类型转换;执行 created/attached/ready/detached;页面根也会进入逻辑树 |
每个 behavior 的 properties/data/methods/lifetimes/pageLifetimes/observers/relations/externalClasses/options 都会被收集;重复引用只执行一次。
内置 behavior id(默认 wx://...)目前只支持 component-export,其他会报 UNSUPPORTED_BEHAVIOR。Behavior 的 definitionFilter 已按引用链执行;写在 Component 上的 definitionFilter 仍会报诊断。
父组件声明 child 时,子组件也要声明对应的 parent;祖先和后代同理。只声明一侧不会连上。
attached 触发 linked,detached 触发 unlinked,移动时触发 linkChanged;父级或祖先找不到目标时触发 linkFailed。组件型页面的 relations 会按 page shadow tree 的 target path 匹配,并手动触发两端的 linked/unlinked。
逻辑树保存 pageId、path、tag、id、class、parent、顺序和虚拟节点;渲染树只保存 node id 到真实组件的映射,不保存组件 data 或 observer。
page.show/hide 会遍历 service 里的所有组件实例,逐个调用 pageLifetimes。render 没有 componentPageLifetime 回路,避免重复执行。
generics、definitionFilter 和 pureDataPattern 已接入;componentPlaceholder 仍未实现。
multipleSlots、virtualHost、styleIsolation、addGlobalClass 和 externalClasses 会随 component schema/flow 下发给渲染树,渲染树用它们创建组件容器和作用域。
渲染树
渲染树由宿主实现。runtime 只定义 TreeRenderer 契约:mount、applyPatches、applyStyles、query、measure、inspectTree、observeIntersection、dispose。它不知道 WXML,也不执行 app methods。
export type TreeRenderer = {
mount(root: RenderNode): void
applyPatches(patches: RenderPatch[]): void
applyStyles?(styles: string[]): void
setEventSink?(sink: (event: TreeEventPayload) => void): void
setPagePath?(pagePath: string): void
query?(selector: string, all: boolean): unknown[]
measure?(ids: string[]): Record<string, unknown>
inspectTree?(): unknown
observeIntersection?(request, emit): (() => void) | undefined
dispose?(): void
}@tiny/host-web-components 的 Web 渲染树注册真实 tiny-* 内置组件,维护 slot、shadow root、class/scope、样式和 DOM;createGlassRenderEntry 负责创建它并安装基础 CSS。
同一个 TreeRenderer 契约,把 RenderNode 和 patch 映射到原生视图。目前只有契约和 headless smoke test,没有实际原生宿主。
runtime 单测用 headless renderer 验证整棵树、patch 顺序、node id 和事件回传,不需要 iframe 或 DOM。
service 把组件定义、create-node、props、children 和 ready 编成 component flow,渲染树按顺序创建真实节点,最后把页面根挂到容器。
service 对虚拟树做 diff,渲染树只应用 text、replace、props、insert、remove、reorder、extra。patch 按 pageId 合批,保持页面内顺序。
渲染树按自己的真实组件树返回 InspectNode,包括 tag、attributes、className、style 和 children;宿主和 devtool 通过 runtime.inspectTree 读取。
渲染树不保存页面栈,也不决定页面切换。它只响应宿主转发的 tree flow/patch、query/canvas 控制和 inspectTree,并把事件按 node id 回传 service。
内置组件
内置组件不再是 service 里的玻璃组件,也不是 render 里重新求值的 WXML;service 只输出语义 tag,宿主把它们映射成真实内置组件。
build-templates.mjs 把 23 个 template.wxml 编成一个模板 group;build-styles.mjs 合成基础样式加 23 份组件样式。
生成物在 hosts/web-components/src/generated,不提交到仓库。
@tiny/host-web-components 用真实 tiny-* 自定义元素实现 view/button/input/textarea/picker 等组件,并维护 behaviors、默认 CSS、事件归一化和 canvas surface。
逻辑树里的内置节点是语义虚拟节点,不带 Web/native 实现。无论宿主是 Web 还是原生,service 发出的是 view、button、checkbox-group 这类小程序 tag。
| 组件组 | 当前支持 | 实现方式 |
|---|---|---|
| view / text / label / image / icon | hover、inline、selectable、decode、space、aria;label for;图片 mode/background | 真实 tiny-* 宿主 + slot;默认 CSS |
| button | type/size/plain/loading/disabled、formType、openType 事件映射、hover 时序 | tiny-button + 宿主 tap;form/hover behaviors |
| input / textarea / picker | value、focus、confirm、selection、composition;picker 列和值 | 原生输入 / 自定义 picker surface |
| checkbox / radio / switch / slider | checked/disabled/value/color;change/checkboxstate;group change | 自定义 22×22 等结构 + 隐藏原生输入 |
| scroll-view / swiper | scrollX/Y、scrollTop/Left、refresher、drag/scroll;swiper change/transition | CSS overflow / transform + touch/scroll 监听 |
| progress / canvas | percent/showInfo/active;canvas-id、2D/WebGL、rAF、createImage、touch | tiny-progress;canvas surface + OffscreenCanvas handoff |
navigator、video、rich-text、map、movable-view 等还没有实现;已有组件的 open-type、表单提交、无障碍和 Skyline 语义也不完整。
样式隔离
样式表按 cascade role 分类(builtin / app / page / component / page-scope / shared)。service 过滤并计算 scope,渲染树把样式应用到真实组件树;内置 CSS 始终归宿主。
| 配置 | 生效范围 | 行为 |
|---|---|---|
| 页面 isolation | 页面根 + 子组件 | 默认 shared;显式 isolated/apply-shared/shared 优先 |
| 组件 isolated | 组件自身 | 默认值;不接收页面样式 |
| apply-shared | 组件自身 | 页面样式可以影响组件;组件样式不影响页面 |
| shared | 组件自身 + 页面 | 组件样式按页面 scope 重新生成一份 |
| addGlobalClass | 组件自身 | 等价 apply-shared;可由 JSON 或 options 设置 |
| externalClasses | 组件根节点 | 把外部 class 透传到组件内部 |
CLI 仍会写 *.low.css,但目前没有启用 convertHost,runtime 也不会加载它。:host 组合和优先级还没有系统整理。
Canvas 与查询
canvas 已经从核心 runtime 移出。service 通过 ServiceCanvasAdapter 取得 OffscreenCanvas 控制面,渲染树提供画布 surface 和测量;selector query 和 IntersectionObserver 通过 query 控制面回到真实 DOM。
浏览器宿主把 render iframe 的 OffscreenCanvas 通过 postMessage 中继给 service;之后 service 直接持有控制面,支持 2D、WebGL、requestAnimationFrame 和 createImage。不再走 PNG 帧镜像。
createSelectorQuery 把 selector、page/component 作用域和 all 发给渲染树;宿主返回 rect、dataset 和 scroll。.node() 仍走 canvas handoff,并支持组件作用域下的样式 class。
service 把 observe 请求和 view id 发给渲染树;Web 宿主用原生 IntersectionObserver,entry 通过 query.intersection 事件回到逻辑树。
wx.createAnimation 会生成链式的 CSS transform/transition;编译期把 animation 属性改写成内联 style。多 step 时序和事件回调还没有还原。
限制
两侧树已经接管默认宿主,但下面这些仍是明确缺口。
事件只带 node id、name、detail、native 字段和 touches;目标节点的几何信息由 service 侧逻辑节点补足,和微信协议仍有差别。
createNativeTreeRenderer 有契约和 headless smoke test,没有实际原生宿主。原生宿主还需要实现 OffscreenCanvas/查询/相交观测等 host 能力。
componentPlaceholder、trait behaviors / chaining API 和大多数微信内置 behavior 仍未实现;generics、definitionFilter、pureDataPattern 已接入。
14 项页面生命周期和 5 个路由 API 已接,分享回调、下拉刷新和退出状态可用;分享菜单 UI、页面栈恢复、页面转场和自定义 tabBar 还缺。
目前支持 storage、导航、system、toast、animation、canvas 和 selector query;request、媒体、文件、账号、位置、WebSocket 还没有。