返回资讯中心
外部精选
软件工程
#开发者实时流

Show HN 新技能:用原生 Web 取代所有 JavaScript 框架

来源标题:Show HN: Vanilla-web skill to replace all JavaScript frameworks

一个名为 Vanilla-web 的新技能发布,主张用静态 HTML 文档加自定义元素构建应用,取代所有 JavaScript 框架。它不依赖框架、打包器或服务端逻辑,由浏览器提供模块、组件、文档间路由和懒加载,该技能则提供保持可组合与可访问性的约定。其理念还包括先有可用的 HTML 再增强、优先使用原生元素、只依赖广泛可用的 API。

Hacker News 最新baalimago2026/11/3 · 4 天前17 分钟阅读

以下正文由 AI 翻译自 Hacker News 最新(原文版权归原站所有),可能存在偏差,重要内容请以原文为准。

把应用做成一堆静态 HTML 文档,由自定义元素来增强。没有框架、没有打包器、没有服务端逻辑。模块、组件、文档间路由和懒加载都由浏览器提供;这份技能提供一套约定,让这些部件保持可组合、可访问。

发布静态文档。初始 HTML 是任意主机上的一个文件,JavaScript 在首次绘制之后对其进行增强。主机唯一的契约是 MIME 类型、HTTP/2、压缩和缓存头。

每份文档只承担一个用途。由 head 来声明:title、description、社交预览、预加载。<main> 承载内容。

让 HTML 重复,绝不让代码重复。标记就是内容,所以导航链接会出现在每份文档里。行为和表现只存在一处,放在每份文档都引用的 .js 和 .css 文件里。

先能用,再升级。链接、表单、标签和标题在任何脚本运行之前就存在于标记中;元素只添加行为,不添加内容。

优先使用原生元素。只有在没有元素具备相应语义时才动用 ARIA;一个 role 就是一份承诺——承诺实现它的键盘模型。

只依赖广泛可用的 API。把新近可用的 API 当作特性检测之后的增强;参见 platform status。

术语 术语 含义 document 一份只承担一个用途的 .html 文件 verbatim block 在每份文档中逐字节相同的 head 和 body 标记 purpose block 每份文档各不相同的 head 标记 island 一个模块按需加载的自定义元素 element 一个自定义元素类 sheet 某个元素的 tag.css,由其文档或其 shadow root 链接 store 一个 EventTarget 子类,拥有状态的一个切片 布局 app/ ├── index.html todo.html profile.html 每个用途一份文档 ├── css/ │ ├── main.css 只声明层级顺序,不含 @import │ └── reset.css tokens.css base.css … 每个文件都把自身包在 @layer 中 ├── js/ │ ├── main.js 立即定义 + 懒加载 island │ ├── lib/ store.js fetch.js dom.js lazy-define.js │ ├── stores/ 每个关注点一个 store │ └── components/<tag>/ Tag.js tag.js tag.css tag.test.js ├── data/ 静态 JSON,一份路径契约 ├── assets/ 图片、图标、og 预览图 └── test/shell-equality.test.js verbatim block 完全一致

Tag.js 导出类,且没有副作用。tag.js 导入它并调用 customElements.define。测试和限定作用域的注册表导入类文件。完整示例见 assets/example-app。

从已有文档复制 verbatim block:从 charset 到 manifest 的 head 块,以及 skip link、header 和 footer 的 body 块。只改动导航上的 aria-current。

编写 purpose block:<title>(页面在前,站点在后)、description、canonical、带 og:image:alt 的 Open Graph、如果页面是一个实体则加结构化数据、theme-color。字段清单见 document head。

声明页面加载什么:它所包含的每个 light-DOM island 的 sheet、LCP 图片的 preload、每个 island 所导入模块的 modulepreload、针对可能的下一份文档的 speculation rules。

编写 <main>,内容完整,且在没有脚本时也可用。把交互区域包在它的 island 标签里。

运行 equality 测试以及 Validate 中的校验器。

添加一个元素 确认标记需要行为。没有行为的标记保持为纯 HTML 和 CSS;一个只负责渲染的元素是只有成本、没有收益。

决定根。默认把已有标记包在 light DOM 中。只有对自包含、结构私有、且与外部元素没有 ARIA 关系的部件才使用 shadow DOM;在文档中用 <template shadowrootmode> 声明它。规则见 elements。

创建 components/<tag>/,包含类文件、注册文件和 sheet。在 main.js 中注册标签:每份文档都需要时就立即注册,island 则通过 lazyDefine 注册。

在标记所在处链接 sheet,这样在升级之前就已应用样式。light-DOM sheet 把规则嵌套在 tag 之下,位于 links it 之内。shadow sheet 使用 :host,并由 root 内的一个 <link> 链接,因为严格的 CSP 会阻止那里的内联 <style>。

在 connectedCallback 中创建一个 AbortController,把每个监听器、观察器和 store 订阅都带上它的 signal 添加进去,读取已有的子节点,然后渲染。在 disconnectedCallback 中 abort 它。

把基础类型暴露为会反射的 attribute 和 property;把富数据只暴露为 property。让 attributeChangedCallback 除了重新渲染之外不产生副作用。

向上用名为 tag:verb 的 CustomEvent 通信,只在有父级监听时冒泡,绝不 composed。向下用 attribute、property 和方法调用通信。

在真实浏览器中针对元素编写 tag.test.js。

规则 Head 的顺序是固定的:meta、CSP、样式表、import map、main.js,然后是 purpose block。import map 是内联的,并且位于每个模块之前。CSP 携带每个内联脚本的哈希,并强制执行 Trusted Types;equality 测试会检查这些哈希。

按路径带扩展名导入;通过 import map 给裸说明符加前缀。不要 barrel 文件;导入图深度最多三层。

绝不用 @import 串联样式表。从 head 链接每个 sheet,让它们同时下载;每个文件把自身包在它的 @layer 中,而最先链接的 main.css 只声明顺序。模块脚本会等待样式表,所以串联会同时拖慢首次绘制和每个 island。

每个关注点一个 store。store 是唯一的写入者;元素调用它的方法,并在它的 change 事件上重新渲染。子级发出意图,绝不修改共享状态。

固定结构只渲染一次,之后只改动目标。列表行按 id 作为键:用 importNode 从 <template> 复制出新行,移除已离开的行,原地更新保留的行。移除获得焦点的节点会丢失焦点。

绝不要丢失升级之前输入的内容。island 的表单有真实的 action 和 method;island 在升级时会接管提前发生的原生提交。

绝不要懒加载 LCP 候选。只用 :not(:defined) 为升级会改变的部分预留空间。优先用 "Load more" 按钮而非无限滚动。

不受信任的字符串走 textContent 或带文本回退的 setHTML,绝不用 innerHTML。

通过 @layer 实现文件优先级,以及一层嵌套。

(原文较长,此处译出前 6000 字,完整内容请点「打开原文」。)

正文由 探How 从来源站点 RSS 同步,内容未经改写;遇到排版缺失或需要图片、视频时请以原文为准。

打开原文
去社区讨论 →