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),仅供参考

相关新闻

科研自动化神器:GitHub上最值得收藏的Skill清单与实战指南

科研自动化神器:GitHub上最值得收藏的Skill清单与实战指南

1. 先搞明白:科研自动化里的Skill到底是什么1.1 Skill的本质:一套让AI“上手干活”的说明书先说结论:Skill本质上是给AI Agent准备的一套“岗位说明书操作手册”。它不是一个独立软件,也不是一个需要部署的微服务,而是…

2026/9/20 9:07:05 阅读更多 →
OpenResearch实操指南:从研究仓库到可复现实验日志

OpenResearch实操指南:从研究仓库到可复现实验日志

“OpenResearch”这个词最近在不少研究圈子里反复出现。很多人把它理解成“把研究资料传到 GitHub 就算开放”,但实际动手跑过一个完整开放研究项目之后,你会发现事情远没那么简单。这篇文章我想站在实操角度,把开放研究从选题、记录、实验、…

2026/9/20 9:06:03 阅读更多 →
终端渲染性能优化实战:从卡顿到流畅的架构演进

终端渲染性能优化实战:从卡顿到流畅的架构演进

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/9/20 9:06:03 阅读更多 →

最新新闻

把 Cursor 的模型通道改到 TaoToken 之后,Excalidraw 绘图 Skill 才跑得顺

把 Cursor 的模型通道改到 TaoToken 之后,Excalidraw 绘图 Skill 才跑得顺

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/9/20 9:48:34 阅读更多 →
FLAC3D主应力方向提取与Matlab可视化技术

FLAC3D主应力方向提取与Matlab可视化技术

1. 项目背景与核心需求在岩土工程数值模拟领域,FLAC3D作为一款专业的连续介质力学分析软件,其7.0版本在计算精度和功能扩展方面都有显著提升。但在实际工程应用中,我们经常遇到一个痛点:虽然软件能准确计算出各单元体的应力状态&a…

2026/9/20 9:48:34 阅读更多 →
Matlab低温多效蒸馏系统建模与热力仿真

Matlab低温多效蒸馏系统建模与热力仿真

简介:本资源是一套面向计算机、电子信息工程及数学类专业本科生的低温多效蒸馏(MED)海水淡化产水过程仿真教学实践材料,适用于课程设计、期末大作业或毕业设计参考。内容基于Matlab平台构建热力学与传热过程模型,完整实…

2026/9/20 9:48:34 阅读更多 →
C++低延迟量化交易系统全链路设计解析

C++低延迟量化交易系统全链路设计解析

简介:这是一套面向量化交易开发者与C高性能系统实践者的开源项目,聚焦于低延迟交易场景下的策略执行优化。针对Java版NFD2.0在行情响应与订单抢跑中遇到的性能瓶颈,作者重构为C实现的NFD3.0量化交易系统,显著提升核心路径处理速度…

2026/9/20 9:48:34 阅读更多 →
VMware复制粘贴拖拽失效?从原理到排查一次讲清

VMware复制粘贴拖拽失效?从原理到排查一次讲清

装好VMware虚拟机,最让人抓狂的事情之一,就是宿主机和虚拟机之间像个“孤岛”——想复制一段服务器配置命令到Ubuntu里,粘贴没反应;想把Windows里的安装包拖进Win7虚拟机,鼠标变成禁止符号。这个问题几乎每个VMware用户…

2026/9/20 9:48:33 阅读更多 →
STM32F103贪吃蛇实战:标准库v3.50图形驱动与实时控制

STM32F103贪吃蛇实战:标准库v3.50图形驱动与实时控制

简介:本资源是基于STM32F103微控制器实现的嵌入式贪吃蛇游戏完整工程,面向嵌入式初学者、单片机课程设计学生及硬件爱好者,旨在通过经典游戏项目实践掌握GPIO驱动、定时器控制、LCD/OLED显示、用户输入处理与状态机设计等核心技能。压缩包共9…

2026/9/20 9:47:33 阅读更多 →

日新闻

Flutter for OpenHarmony游戏卡片渐变背景实战:从原理到性能优化

Flutter for OpenHarmony游戏卡片渐变背景实战:从原理到性能优化

直接铺开项目本身吧。这几个月我一直在折腾一件事:用Flutter给OpenHarmony做一款游戏集合类的App,说白了就是把若干小游戏塞进一个壳里,用统一入口分发。这个方向本身不算新鲜,真正让我花了不少心思的,是首页那堆游戏卡…

2026/9/20 0:00:46 阅读更多 →
Word表格编号全攻略:从列表编号到题注交叉引用

Word表格编号全攻略:从列表编号到题注交叉引用

写Word文档,最让人头疼的往往是那些“看起来不起眼”的小问题。比如表格编号这事:今天在表后面多加了两个空白行,明天给客户交稿前发现整个章节的编号全部错位,光是挨个改序号就能耗掉大半个下午。我前阵子帮人整理一份上百页的技…

2026/9/20 0:00:46 阅读更多 →
从第一个站到第二个站:独立开发者的静态网站选型与落地实践

从第一个站到第二个站:独立开发者的静态网站选型与落地实践

1. 项目概述1.1 核心需求解析做独立开发者这几年,说实话,第一个网站上线的那天晚上我兴奋得没睡着。但等它跑了半年,流量惨淡、功能臃肿、代码自己都懒得看第二遍之后,我才慢慢琢磨明白一个道理:第一个网站是练手&…

2026/9/20 0:00:46 阅读更多 →

周新闻

Flutter for OpenHarmony游戏卡片渐变背景实战:从原理到性能优化

Flutter for OpenHarmony游戏卡片渐变背景实战:从原理到性能优化

直接铺开项目本身吧。这几个月我一直在折腾一件事:用Flutter给OpenHarmony做一款游戏集合类的App,说白了就是把若干小游戏塞进一个壳里,用统一入口分发。这个方向本身不算新鲜,真正让我花了不少心思的,是首页那堆游戏卡…

2026/9/20 0:00:46 阅读更多 →
Word表格编号全攻略:从列表编号到题注交叉引用

Word表格编号全攻略:从列表编号到题注交叉引用

写Word文档,最让人头疼的往往是那些“看起来不起眼”的小问题。比如表格编号这事:今天在表后面多加了两个空白行,明天给客户交稿前发现整个章节的编号全部错位,光是挨个改序号就能耗掉大半个下午。我前阵子帮人整理一份上百页的技…

2026/9/20 0:00:46 阅读更多 →
从第一个站到第二个站:独立开发者的静态网站选型与落地实践

从第一个站到第二个站:独立开发者的静态网站选型与落地实践

1. 项目概述1.1 核心需求解析做独立开发者这几年,说实话,第一个网站上线的那天晚上我兴奋得没睡着。但等它跑了半年,流量惨淡、功能臃肿、代码自己都懒得看第二遍之后,我才慢慢琢磨明白一个道理:第一个网站是练手&…

2026/9/20 0:00:46 阅读更多 →

月新闻

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能分类:[AI/大模型]细分主题:AI 增强型 CI/CD 流水线自动化与 GitOps 实践:Agent 工作流、工具调用与任务拆解:从原型到生产的验收清单很多团队在尝试用大…

2026/9/19 23:01:36 阅读更多 →
容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场

容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场

容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场分类:[工程技术]细分主题:Kubernetes 生产环境运维与排障实战:可复制的项目复盘模板与决策记录大部分团队的事故复盘报告,最后都变成了躺在 Confluence 或钉…

2026/9/19 17:50:38 阅读更多 →
容器 容器化技术与镜像安全管理:核心链路应该先拆哪一步

容器 容器化技术与镜像安全管理:核心链路应该先拆哪一步

容器 容器化技术与镜像安全管理:核心链路应该先拆哪一步分类:[工程技术]细分主题:Docker 容器化技术与镜像安全管理:核心链路的逐步实现与关键代码取舍面对一个积累了五六年历史包袱的单体架构应用(包含 Web 接口、后台…

2026/9/19 23:35:34 阅读更多 →