十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Ember.js Glimmer 嵌入 API 实战:在自定义宿主中集成多个全局组件

Ember.js Glimmer 嵌入 API 实战:在自定义宿主中集成多个全局组件 前端Web框架UI组件【免费下载链接】ember.jsEmber.js - A JavaScript framework for creating ambitious web applications项目地址https://gitcode.com/gh_mirrors/em/ember.js点击查看免费下载导读本文基于 Ember.js 仓库的 internal-docs/guides/embedding/05-components.md 文档讲解如何在 Glimmer 的 AOT 嵌入环境中从单一组件 若干 helper升级为多组件的完整宿主集成方案。你将掌握ResolverDelegate.lookupComponent、CompileTimeComponent、MINIMAL_CAPABILITIES与TEMPLATE_ONLY_COMPONENT的协作方式并能在自己的宿主环境中同时解析全局 helper 与全局组件实现带参数、带状态的多组件渲染与重渲染。一、为什么需要更多组件在前几篇文档中我们构建的最小嵌入环境已经能够编译并执行一个自包含的组件见 02-minimum-environment.md通过State注入可变的外部状态并以this访问见 03-adding-state.md通过ResolverDelegateRuntimeResolver将全局 helper 名称解析为运行时函数见 04-external-helpers.md。但正如文档所说A single component plus some helpers is nice and all, but real programs have more than one component.单个组件加几个 helper 固然不错但真实程序绝不会只有一个组件。真实应用由多个组件相互调用组成而嵌入宿主必须回答两个问题编译期模板里写下的组件名如{{Second}}对应哪个已编译的程序运行期这个程序对应的运行时句柄handle又对应哪个组件实现答案就是本文的主角ResolverDelegate的lookupComponent与RuntimeResolver的resolve。二、全局组件与全局 helper 的对称性文档明确指出Global components work similarly to global helpers: theResolverDelegateturns a component name into a handle, and the runtime takes that handle and produces the component.即全局组件与全局 helper 的工作方式是对称的同样分为两个阶段阶段helper 的流程component 的流程编译期lookupHelper(name)返回 handle 编号lookupComponent(name)返回CompileTimeComponent内含 handle运行期RuntimeResolver.resolve(handle)返回 helper 函数RuntimeResolver.resolve(handle)返回组件实现在仓库源码中这个分工体现得十分清晰。delegate.ts 定义了完整的ResolverDelegate接口export interface ResolverDelegateR unknown { lookupHelper?(name: string, referrer: R): Nullablenumber | void; lookupModifier?(name: string, referrer: R): Nullablenumber | void; lookupComponent?(name: string, referrer: R): NullableCompileTimeComponent | void; // For debugging resolve?(handle: number): R; }可以看到helper 解析返回的是裸的numberhandle而组件解析返回的是携带更多信息的CompileTimeComponent——因为组件比函数复杂得多它不仅有模板compilable还有描述其行为的能力位capabilities。三、建立组件表与运行时常量表文档给出的第一步是建立两套数据结构一套给编译期查名字一套给运行期查 handle。// New imports: import { MINIMAL_CAPABILITIES } from glimmer/opcode-compiler; import { TEMPLATE_ONLY_COMPONENT } from glimmer/runtime; // A map of helpers to runtime handles (that will be passed to the runtime resolver). const HELPERS { increment: 0, }; // A map of components to their source code and the runtime handle (that will be passed // to the runtime resolver). const COMPONENTS: Dict{ source: string; handle: number } { Second: { source: p{{hello}} {{world}}{{suffix}} ({{increment num}})/p, handle: 1, }, }; // Used to make lookup by the RuntimeResolver straightforward const TABLE [ increment, // 0 TEMPLATE_ONLY_COMPONENT // 1 ];这里有几个关键设计值得展开COMPONENTS以组件名为键值是{ source, handle }。source是模板源码交给编译期生成可执行程序handle是编译期写入字节码、运行期查询时使用的整数标识。组件Second的模板使用了hello、world、suffix、num这些开头的位置/命名参数说明它期望父级传参——这与上一篇中直接使用this外部状态的组件形成了对照。TABLE以 handle 为索引0号是incrementhelper 函数1号是TEMPLATE_ONLY_COMPONENT。运行期解析只需一次数组下标访问O(1) 完成 handle → 实现的映射。这就是文档所说的 make lookup by the RuntimeResolver straightforward。handle 在编译期与运行期是同一个整数它充当了两侧之间唯一的协议。注意文档中的TEMPLATE_ONLY_COMPONENT对应仓库源码中的TEMPLATE_ONLY_COMPONENT_MANAGER定义在 template-only.ts下文第五节会详解。四、编译期lookupComponent返回CompileTimeComponent接下来是嵌入宿主两侧的核心实现。先看编译期const RESOLVER_DELEGATE: ResolverDelegate { lookupComponent(name: string): OptionCompileTimeComponent | void { let component COMPONENTS[name]; if (component null) return null; let { handle, source } component; return { handle, compilable: Compilable(source), capabilities: MINIMAL_CAPABILITIES, }; }, lookupHelper(name: keyof typeof HELPERS): Optionnumber | void { if (name in HELPERS) return HELPERS[name]; }, };lookupComponent的返回值CompileTimeComponent在仓库的 serialize.d.ts 中有明确接口定义export interface CompileTimeComponent { handle: number; capabilities: CapabilityMask; compilable: NullableCompilableProgram; }三个字段各司其职字段作用handle编译期写入字节码的整数标识运行期据此在TABLE中取回实现compilable组件的可编译程序由precompileComponent生成供编译器生成指令capabilities能力位掩码告知 VM 该组件支持哪些生命周期与渲染行为Compilable(source)就是我们前几篇一直在用的辅助函数function Compilable(source: string): CompilableProgram { return Component(precompile(source)); }precompile来自glimmer/compilerComponent来自glimmer/opcode-compiler——Component()期望的正是序列化/预编译后的模板。在编译器内部遇到{{Second}}这样的组件调用时会通过resolver?.lookupComponent?.(name, owner)查询定义见 resolution.ts随后在 statements.ts 中把结果编码进HighLevelResolutionOpcodes.Component指令最终以 handle 形式落入程序字节码。也就是说模板源码中的组件名不会出现在字节码里只有 handle 整数——这正是嵌入 API 能显著减少浏览器需解析的 JavaScript 量的原因之一。五、capabilities告诉 VM 组件有多重lookupComponent返回的capabilities: MINIMAL_CAPABILITIES值得单独解释。组件能力capabilities是一组布尔标志描述组件是否具备动态布局、参数预处理、生命周期钩子等行为VM 会根据这些标志决定走哪条执行路径。仓库 delegate.ts 中MINIMAL_CAPABILITIES的定义将所有标志置为falseexport const MINIMAL_CAPABILITIES: InternalComponentCapabilities { dynamicLayout: false, dynamicTag: false, prepareArgs: false, createArgs: false, attributeHook: false, elementHook: false, dynamicScope: false, createCaller: false, updateHook: false, createInstance: false, wrapped: false, willDestroy: false, hasSubOwner: false, };作为对照同文件中的DEFAULT_CAPABILITIESdelegate.ts则把dynamicLayout、dynamicTag、prepareArgs、createArgs、updateHook、createInstance等置为true。对嵌入场景而言MINIMAL_CAPABILITIES的含义是这个组件是纯模板组件没有类实例、没有自定义元素钩子、没有动态作用域因此 VM 可以走最轻量的执行路径。它告诉 VM别为这个组件做多余的准备。六、TEMPLATE_ONLY_COMPONENT无类组件实现TEMPLATE_ONLY_COMPONENT源码中的TEMPLATE_ONLY_COMPONENT_MANAGER见 template-only.ts是TemplateOnlyComponentManager的单例它实现了InternalComponentManager接口export class TemplateOnlyComponentManager implements InternalComponentManager { getCapabilities(): InternalComponentCapabilities { return CAPABILITIES; // 全部为 false } getDebugName({ name }: TemplateOnlyComponentDefinition): string { return name; } getSelf(): Reference { return NULL_REFERENCE; } getDestroyable(): null { return null; } }几点值得注意能力位与MINIMAL_CAPABILITIES完全一致全部false因此它和上文的capabilities声明互相印证。getSelf()返回NULL_REFERENCE模板组件没有this所以args是它访问外部数据的唯一途径。这解释了为什么Second的模板使用hello而非this.hello。仓库还提供了配套的工厂函数templateOnlyComponent(moduleName?, name?)template-only.ts用于创建带调试名的模板组件定义其文档注释说明模板组件会以 outer HTML 语义直接渲染模板不添加包裹元素。现实中的模板组件通常由构建工具生成例如将.hbs文件编译为templateOnly()导出而不是手写进应用代码——但对于嵌入宿主而言TEMPLATE_ONLY_COMPONENT是让源码字符串组件跑起来的最简实现。七、运行期RuntimeResolver.resolve查表返回实现编译期完成后字节码与常量池被打包为programartifacts(context)交给运行期。运行期一侧的解析器只需把 handle 映射回真实实现const RUNTIME_RESOLVER: RuntimeResolver { resolve(handle:number): ResolvedValue | void { if (handle TABLE.length) { return TABLE[handle]; } } };对照上一篇04-external-helpers.md的实现这里把硬编码的if (handle 0)泛化成了if (handle TABLE.length)的查表逻辑——当组件数量增长时这一侧完全无需改动只扩展TABLE数组即可。与编译期对称运行期解析的调用发生在 VM 执行字节码、遇到组件指令时resolve(handle)返回TABLE[handle]例如0→increment函数、1→TEMPLATE_ONLY_COMPONENT管理器。之后 VM 用管理器 编译期存下的模板完成组件实例化与渲染。八、把两部分拼起来完整的多组件嵌入流程文档的第二段给出了 resolver 两侧的定义而Previously部分05-components.md 开头给出了承接上一篇的完整主程序骨架。将两者拼接便得到完整的可运行宿主import { Component, Context, MINIMAL_CAPABILITIES } from glimmer/opcode-compiler; import { artifacts } from glimmer/program; import { precompile } from glimmer/compiler; import { AotRuntime, renderAot, TEMPLATE_ONLY_COMPONENT } from glimmer/runtime; import createHTMLDocument from simple-dom/document; import Serializer from simple-dom/serializer; import voidMap from simple-dom/void-map; import { State, map } from glimmer/references; /// 数据表helper / 组件名 → { 源码, handle } const HELPERS { increment: 0 }; const COMPONENTS: Dict{ source: string; handle: number } { Second: { source: p{{hello}} {{world}}{{suffix}} ({{increment num}})/p, handle: 1, }, }; const TABLE [increment, TEMPLATE_ONLY_COMPONENT]; // 0, 1 /// 编译期解析器 const RESOLVER_DELEGATE: ResolverDelegate { lookupComponent(name: string): OptionCompileTimeComponent | void { let component COMPONENTS[name]; if (component null) return null; let { handle, source } component; return { handle, compilable: Compilable(source), capabilities: MINIMAL_CAPABILITIES }; }, lookupHelper(name: keyof typeof HELPERS): Optionnumber | void { if (name in HELPERS) return HELPERS[name]; }, }; /// 运行期解析器 const RUNTIME_RESOLVER: RuntimeResolver { resolve(handle: number): ResolvedValue | void { if (handle TABLE.length) return TABLE[handle]; }, }; /// 编译 let source {{#let hello world as |hello world|}} Second hello{{hello}} world{{world}} suffix{{this.prefix}} num{{this.count}} / {{/let}} ; let context Context(RESOLVER_DELEGATE); let handle Compilable(source).compile(context); let program artifacts(context); /// 运行 let document createHTMLDocument(); let runtime AotRuntime(document, program, RUNTIME_RESOLVER); let main document.createElement(main); let state State({ prefix: !, count: 5 }); let cursor { element: main, nextSibling: null }; let iterator renderAot(runtime, handle, cursor, state); let result iterator.sync(); console.log(serialize(main)); // mainphello world! (count: 6)/p/main state.update({ prefix: ?, count: 10 }); result.rerender(); console.log(serialize(main)); // mainphello world? (count: 11)/p/main function Compilable(source: string): CompilableProgram { return Component(precompile(source)); } function serialize(element: SimpleElement): string { return new Serializer(voidMap).serialize(element); }完整执行链路如下Context(RESOLVER_DELEGATE)创建带编译期解析器的编译上下文Compilable(source).compile(context)编译主模板遇到{{Second}}时调用lookupComponent得到 handle1与子模板遇到{{increment}}时调用lookupHelper得到 handle0两者以整数形式写入字节码artifacts(context)把上下文序列化为{ heap, constants }程序AotRuntime(document, program, RUNTIME_RESOLVER)将程序 运行期解析器水合为RuntimeContextrenderAot(runtime, handle, cursor, state)以主组件 handle 为入口执行字节码state作为this注入遇到Second指令时通过RUNTIME_RESOLVER.resolve(1)取出TEMPLATE_ONLY_COMPONENT完成渲染state.update(...)result.rerender()触发增量重渲染输出同步更新为hello world? (count: 11)。这也印证了上一篇文档描述的双侧契约编译期ResolverDelegate把名称变成 handle运行期RuntimeResolver把 handle 变回实现——组件与 helper 概莫能外。九、从最小宿主到真实 Ember 的映射本文的最小嵌入实现并非凭空发明它与 Ember 自身的组件解析机制同构。在 resolver.ts 中Ember 应用运行时同样会为模板组件返回TEMPLATE_ONLY_COMPONENT_MANAGER作为 manager而ember/component等更重的组件则带有完整的能力位如updateHook、createInstance让 VM 走完整生命周期路径。二者的差异只在解析策略本文的最小宿主用硬编码的COMPONENTS/TABLE常量表解析适合教学与嵌入式定制场景Ember 应用由容器container按名称注册组件类运行时再解析为 handle 与 manager。但底层的CompileTimeComponent三要素handlecapabilitiescompilable与编译期编码 handle、运行期解码 handle的协议是共享的。想深入 AOT/JIT 两种模式差异的读者可继续阅读 01-introduction.md嵌入 API 总览与 02-minimum-environment.mdAOT 前置说明。十、小结与下一步通过本文你已经在最小宿主中完整实现了多组件渲染编译期lookupComponent把组件名解析为CompileTimeComponenthandlecapabilitiescompilable能力声明MINIMAL_CAPABILITIES让 VM 走轻量路径运行期RuntimeResolver.resolve通过TABLE把 handle 还原为TEMPLATE_ONLY_COMPONENT等实现状态与参数外部State以this注入模板组件通过args接收参数incrementhelper 完成计算rerender()驱动增量更新。下一步的自然延伸包括为自定义数据结构实现Reference承接 03-adding-state.md 末尾的伏笔以及将本示例从 AOT 模式适配到 JIT 模式——届时编译器会在遇到组件/块调用时插入额外的编译指令实现按需编译。这些内容在 embedding 系列的后续指南中会逐一展开。赞分享前端Web框架UI组件【免费下载链接】ember.jsEmber.js - A JavaScript framework for creating ambitious web applications项目地址https://gitcode.com/gh_mirrors/em/ember.js点击查看免费下载相关推荐PowerShell 宿主 API在 .NET 应用中嵌入 PowerShell 引擎与原生宿主自定义程序集加载实现PowerShell 宿主 API在 .NET 应用中嵌入 PowerShell 引擎与原生宿主自定义程序集加载实现 本文以 PowerShell 仓库中的宿编程语言语言运行时CLI在 Vanilla JS 中集成 Lexical Extension 与 React Plugin Host无 React 宿主挂载 React 组件实战在 Vanilla JS 中集成 Lexical Extension 与 React Plugin Host无 React 宿主挂载 React 组件实战 导前端富文本UI组件Pinta快捷键大全提升工作效率的50个必备快捷键Pinta快捷键大全提升工作效率的50个必备快捷键 Pinta是一款简单易用的GTK 图像编辑程序提供了丰富的快捷键功能帮助用户快速完成各种编辑操作。掌握桌面应用上一篇为什么选择Ruby进行机器学习开发5大优势与3个成功案例下一篇Saber手写笔记为什么这款免费开源应用值得你尝试创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表