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

资讯详情

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

在 Beaker 浏览器中掌握 lit-html:高效、表达力强、可扩展的 JavaScript HTML 模板引擎

在 Beaker 浏览器中掌握 lit-html:高效、表达力强、可扩展的 JavaScript HTML 模板引擎
  • 前端

【免费下载链接】beaker

An experimental peer-to-peer Web browser

项目地址:https://gitcode.com/gh_mirrors/be/beaker
点击查看免费下载

导读

lit-html 是一套基于 JavaScript 模板字面量(template literals)的 HTML 模板方案,它把“写 HTML”的熟悉手感与 JavaScript 的完整表达能力结合起来,并以内建的高效 DOM 更新机制(只更新变化的部分)著称。在 Beaker 浏览器项目(一个实验性的点对点 Web 浏览器,仓库路径 app/README.md)的众多用户界面模块中,lit-html 被广泛用于渲染上下文菜单、弹窗、侧栏等动态组件,与 lit-element 共同构成其前端渲染基础。读完本文,你将掌握 lit-html 的核心 API(html标签与render函数)、模板缓存的底层原理、五大类 Part 的更新语义,以及基于指令(directives)扩展模板能力的具体用法,并能直接在本仓库源码中定位到每一个实现细节。

什么是 lit-html

lit-html 由 Polymer 项目(后并入 Lit 生态)开发,官方定位为 “Efficient, Expressive, Extensible HTML templates in JavaScript”(高效、表达力强、可扩展的 JavaScript HTML 模板)。它允许你在 JavaScript 中用模板字面量书写 HTML 模板,模板本身是纯 JavaScript,因此可以直接在模板插值位置(${...})调用函数、做条件判断、遍历数组,而不需要额外学习一套模板 DSL 或依赖字符串拼接。

它在 Beaker 仓库中作为 vendored 依赖存在,完整源码位于 app/userland/app-stdlib/vendor/lit-element/lit-html,同级的 app/userland/app-stdlib/vendor/lit-element 则是基于它构建的自定义元素基类。仓库内各用户界面模块(如 context-menu.js)直接通过import {LitElement, html, css} from '../../vendor/lit-element/lit-element.js'引入并使用这套模板体系。

核心设计目标

  • 高效(Efficient):模板只解析一次,之后对 DOM 做最小化的、精确到插值点的更新,而不是每次整体重写 innerHTML。
  • 表达力强(Expressive):表达式位置可以出现任意 JavaScript 值——函数、数组、可迭代对象、DOM 节点、甚至是另一个模板结果,嵌套组合极其自然。
  • 可扩展(Extensible):通过指令(directives)机制,可以注入自定义渲染逻辑;底层还提供了模板处理器(template processor)接口,允许替换整个模板解析/提交策略。

快速上手:html与render

lit-html 暴露两个核心导出:

  • html:一个模板标签函数(tagged template),用于产生一个TemplateResult——它是“模板 + 将要填充进模板的值”的容器;
  • render():把TemplateResult渲染进一个 DOM 容器(元素或 Shadow Root),并在后续再次调用时增量更新容器内容。

入口文件 lit-html.js 中可以看到这两个导出的真实定义:

export const html = (strings, ...values) => new TemplateResult(strings, values, 'html', defaultTemplateProcessor);

html接收模板字符串数组与插值表达式数组,构造出一个TemplateResult;render则负责把结果提交给容器。README 中的经典示例完整复刻如下:

import {html, render} from 'lit-html'; // 这是一个 lit-html 模板函数:返回一个 lit-html 模板 const helloTemplate = (name) => html`<div>Hello ${name}!</div>`; // 渲染出 <div>Hello Steve!</div> 到 document.body render(helloTemplate('Steve'), document.body); // 更新为 <div>Hello Kevin!</div>,但只更新 ${name} 这一部分 render(helloTemplate('Kevin'), document.body);

第二次调用render时,容器中的旧 DOM 不会被整体重建,只有${name}对应的文本节点会被改写——这正是 lit-html 的“最小更新”承诺。

render的增量更新原理

render的实现位于 render.js。它用WeakMap(parts)以容器节点为键,记住上一次渲染时创建的NodePart。首次渲染时清空容器、创建 Part 并appendInto(container);之后的渲染则直接复用同一个 Part,依次执行setValue(result)与commit():

export const render = (result, container, options) => { let part = parts.get(container); if (part === undefined) { removeNodes(container, container.firstChild); parts.set(container, part = new NodePart(Object.assign({ templateFactory }, options))); part.appendInto(container); } part.setValue(result); part.commit(); };

这意味着:同一个容器连续渲染同类型模板时,DOM 会增量更新;不同容器之间则互不影响。parts这个WeakMap还保证了容器被垃圾回收后,对应的 Part 状态也会随之释放。

安装与引入方式

README 给出的 npm 安装命令为:

$ npm install lit-html

需要说明的是:lit-html 在 1.0 正式版发布前仍处于活跃开发期,README 明确提示“内部 API 仍可能变化,但html与render这两个公开 API 已保持稳定”。这一点与本仓库中 vendored 的版本一致——lit-html.js 中的版本标记为1.0.0。

在 Beaker 项目中无需单独 npm 安装:它是作为本地 vendor 依赖直接以源码(lit-html.js+lib/目录 +directives/目录)形式提供的,应用层统一从 lit-element.js 入口引入,再由 lit-element 内部转发 lit-html 的导出。如果要在自己的项目中使用,除了 npm 安装,也可以像 Beaker 一样把vendor目录整体拷贝进项目后按相对路径引入。

模板渲染的完整链路

理解 lit-html 的高效性,需要看清一条贯穿始终的管线:TemplateResult → Template → TemplateInstance → Part → DOM。下面按源码顺序拆解。

1. TemplateResult:模板与值的容器

template-result.js 中的TemplateResult保存了strings(模板字面量的静态字符串数组)、values(插值表达式的结果数组)、type('html'或'svg')以及processor。它的getHTML()方法负责把静态片段与**表达式标记(marker)**拼合成一段可在<template>元素中解析的 HTML——对属性位置第一个表达式使用boundAttributeSuffix($lit$)加标记以规避 IE11/Edge 的特殊属性值解析,其余位置使用注释型标记。

同一文件中还定义了SVGTemplateResult:它先将内容包进<svg>标签以便在 SVG 命名空间下解析,随后在getTemplateElement()里把外层<svg>摘除,将子节点重挂回 fragment,从而让用户拿到干净的 SVG 片段。

2. Template:把字符串解析成可克隆的模板与 Part 清单

TemplateResult通过getTemplateElement()生成<template>元素,随后 template.js 中的Template类用TreeWalker遍历模板内容,把每个插值点记录为一个part 描述对象(type: 'attribute'或type: 'node'),并记录它在节点序列中的索引。所有绑定属性都会挂上$lit$后缀用于查表(见 template.js 的boundAttributeSuffix定义)。

关键设计在于模板只解析一次:同一段模板字符串在渲染任意次数时都复用同一个Template对象,动态部分由“克隆 + Part 定位”来实例化。

3. templateFactory:模板缓存

template-factory.js 实现了默认模板工厂与缓存:按result.type划分缓存桶,桶内先用WeakMap(以strings数组为键)命中,未命中时再把静态字符串用 marker 连接成 key,在Map中查找,最终创建并缓存Template。由于字符串数组与 key 双缓存的存在,结构相同、只有值不同的模板字面量(例如在循环中重复调用同一模板函数)只会被解析一次。

4. TemplateInstance:把模板实例化到 DOM

template-instance.js 的TemplateInstance在_clone()中克隆模板内容,再次用TreeWalker按模板记录的 part 索引,把占位标记替换为真实的 Part 对象:文本位置交给processor.handleTextExpression生成NodePart,属性位置交给processor.handleAttributeExpressions生成对应的属性类 Part。update(values)则分两轮遍历所有 Part——先全部setValue,再全部commit——保证同一模板内的更新语义一致。

5. Part:最小粒度的更新单元

parts.js 定义了多种 Part,各自负责一类 DOM 写入,这是 lit-html “精准更新”的直接体现:

Part 类型作用更新行为
NodePart文本/节点位置(元素之间、文本中)按值类型分派:原始值走_commitText,TemplateResult走_commitTemplateResult(模板相同时仅update),Node走_commitNode,可迭代对象走_commitIterable;nothing哨兵值触发clear()
AttributePart普通属性值变更时通过AttributeCommitter组装字符串并setAttribute,避免同一属性的多个插值重复写入
PropertyPartDOM 属性(property)通过PropertyCommitter直接赋值element[name] = value,整个表达式独占时跳过字符串拼接
BooleanAttributePart布尔属性真值 →setAttribute(name, ''),假值 →removeAttribute(name);且只允许单个表达式,否则抛错
EventPart事件监听用_boundHandleEvent统一接管addEventListener/removeEventListener,支持capture、once、passive选项(内部通过特性探测兼容 IE11 不支持 options 的情况)

以NodePart.commit()(见 parts.js)为例,其值分派逻辑清晰可见:原始值且未变化时跳过写入;TemplateResult且模板与上次相同时只更新值;数组/可迭代对象为每个元素维护独立子 Part,从而支持array.map(i => html\${i}`)` 这类表达式的增量复用。

哨兵值:noChange与nothing

part.js 定义了两个特殊导出:

  • noChange:标记“该值已由指令(directive)处理,无需写回 DOM”;
  • nothing:标记“让 NodePart 完全清空自身内容”。

二者在NodePart.commit()与各属性 Part 的commit()中被检查,是编写自定义指令时最重要的两个协议值。

指令(Directives):扩展模板的表达能力

指令是 lit-html 可扩展性的核心:任何函数经directive()包装后,在渲染时不再作为普通值写入 DOM,而是由 lit-html 调用它并把 Part 对象作为参数传入,从而获得对渲染过程的完全控制。实现见 directive.js:工厂函数产生的返回函数会被登记进WeakMap,isDirective据此识别;渲染时 Part 的commit()会循环执行指令函数(见 parts.js)。

仓库directives/目录下随附了 11 个官方指令,可直接引入使用:

指令文件典型用途
asyncAppend/asyncReplaceasync-append.js / async-replace.js消费异步可迭代对象,把每次推入的值追加到列表末尾 / 整体替换
cachecache.js在渲染不同模板之间切换时缓存先前模板的 DOM 与 Part 状态,避免重复克隆
classMapclass-map.js以对象批量设置/移除 class 属性
guardguard.js依赖值未变化时跳过表达式求值,优化重渲染成本
ifDefinedif-defined.js值为undefined时移除属性,否则正常设置
repeatrepeat.js基于用户提供的 key 对列表做最小化增删改排序(而非按索引整表重渲)
styleMapstyle-map.js以对象批量设置内联样式
unsafeHTMLunsafe-html.js把 HTML 字符串原样插入(有 XSS 风险,仅在信任输入时使用)
untiluntil.js在 Promise 就绪前渲染占位内容,就绪后渲染最终值

以repeat为例:它通过WeakMap缓存“上一次的 part 列表”和“key→索引映射”,当列表变化时只对新旧集合求差集做插入/移动/删除,而不是重建全部子项——这正是 repeat.js 注释中“efficiently updates those items when the iterable changes based on user-provided keys”的实现基础。

在 Beaker 中的实际用法

Beaker 的 context-menu.js 是 lit-html 实战的绝佳样本:它从 lit-element 引入html,从 directives 引入classMap、ifDefined,并在render()中直接嵌套模板、条件渲染与数组map:

return html` <div class="context-menu dropdown" style="${style}"> ${this.customRender ? this.customRender.call(this) : html` <div class="${cls}" style="${ifDefined(this.customStyle)}"> ${this.items.map(item => { if (item === '-') { return html`<hr />`; } // ...其余菜单项渲染 })} </div> `} </div> `;

这段代码演示了 lit-html 的几个实战要点:插值处可以放置三元表达式、函数返回值、数组映射结果;ifDefined保证customStyle未定义时不会残留style=""空属性;菜单项列表通过map产出嵌套TemplateResult,由NodePart._commitIterable自动做增量复用。

在浏览器项目中的适用性与限制

结合本仓库源码,可以给出以下结论(均以 vendored 版本为准):

  1. 依赖浏览器 DOM API:render直接操作document与<template>,模板解析依赖createTreeWalker、importNode/cloneNode等 DOM 能力,因此它面向浏览器环境而非 Node.js 服务端渲染;
  2. 兼容层:源码对 IE11/Edge 做了针对性处理——TemplateResult.getHTML()为绑定属性追加$lit$后缀规避特殊属性值解析(template-result.js)、EventPart探测事件 options 支持度(parts.js)、TemplateInstance在自定义元素 polyfill 下改用cloneNode+adoptNode(template-instance.js);
  3. 与 lit-element 的关系:lit-html 只解决“模板渲染”,LitElement基类(lit-element.js)在此基础上叠加了响应式属性、生命周期与 Shadow DOM 封装,二者组合才是 Beaker 各 UI 组件(如 context-menu)的完整渲染基础;
  4. 版本状态:README 明示其处于活跃开发期、尚无 1.0 正式发布,内部 API 可能调整,但html/render稳定——在集成第三方项目时建议锁定 vendor 版本(本仓库即为固定 vendored 快照)。

延伸阅读路径

如果你希望进一步深挖实现细节,可按以下路径在仓库内继续阅读:

  • 模板与标记:$lit$后缀、marker 与lastAttributeNameRegex的定义见 lib/template.js;
  • Part 更新语义的完整实现(含_commitTemplateResult、_commitIterable、EventPart):lib/parts.js;
  • 默认模板处理器(handleTextExpression/handleAttributeExpressions):lib/default-template-processor.js;
  • lit-element 如何消费 lit-html(属性渲染与render()集成):vendor/lit-element/lit-element.js;
  • 仓库内的真实调用范例:菜单 js/com/context-menu.js 等 UI 组件对html、classMap、ifDefined的组合运用。

掌握 lit-html 之后,你不仅能在 Beaker 这类浏览器项目里读懂其 UI 渲染代码,也能在自己的前端项目中以“模板字面量 + 最小 DOM 更新 + 指令扩展”的方式,写出既直观又高效的可维护界面。

  • 前端

【免费下载链接】beaker

An experimental peer-to-peer Web browser

项目地址:https://gitcode.com/gh_mirrors/be/beaker
点击查看免费下载

相关推荐

上一篇:Learn Harness Engineering:为什么“巨型指令文件”会让 Agent 失效,以及如何把指令拆分到多个文件
下一篇:learn-harness-engineering 第十讲实战:只有端到端测试才算真正验证——从 e2e-runner 到可执行架构规则的 Agent 验证闭环

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

返回列表