news 2026/9/16 17:57:14

Nue HTML 语法深度指南:用表达式、控制流与组件扩展标准 HTML

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Nue HTML 语法深度指南:用表达式、控制流与组件扩展标准 HTML

Nue HTML 语法深度指南:用表达式、控制流与组件扩展标准 HTML

【免费下载链接】nueFastest way to build modern websites项目地址: https://gitcode.com/GitHub_Trending/nu/nue

Nue 在标准 HTML 之上增加了一套声明式模板语法:{ }表达式、:each循环、:if条件、组件、事件处理与生命周期钩子,让静态标记与动态逻辑在同一份文档中自然共存。本文以 html-syntax.md 为骨架,结合 nuedom 包的编译器与渲染器源码(tokenizer.js、attributes.js、node.js)以及测试用例(render.test.js),逐条讲解语法规则、底层实现与可运行的完整示例。读完本文,你将能够独立编写服务端渲染的静态页面、客户端交互组件以及可复用的 UI 组件库。

标准 HTML 就是 Nue

Nue 的核心设计原则是“不发明新语言,只扩展标准”。任何一份合法的 HTML 文档,同时也就是一份合法的 Nue 文档:

<!doctype html> <html> <head>...</head> <body> <article> <button onclick="history.go(-1)">Back</button> <button popovertarget="confirm-delete">Delete</button> </article> <dialog id="confirm-delete"> <h2>Delete user?</h2> </dialog> </body> </html>

这份代码不需要任何改动即可被 Nue 原样处理。这与 html-file-types.md 中描述的文档类型体系一致:以<!doctype html>开头的文件按服务端静态页面处理,构建时生成完整的 HTML 文档;不带动态特性的内容无需任何特殊声明。

从源码看,Nue 的模板解析从 tokenizer.js 开始:它按<script、普通标签、{表达式、纯文本四种情况切分模板,随后由 document.js 组装成文档对象,再经 ast.js 生成 AST,最终由 node.js 渲染为 DOM。整个过程对标准 HTML 零侵入。

表达式:{ }插值与转义控制

用花括号在文本内容中插入动态值:

<!-- text content --> <span>{ username }</span> <!-- JavaScript expressions --> <p>{ username.toUpperCase() }</p> <!-- unescaped HTML --> <div>{{ markdown(description) }}</div> <div>{{ renderContent(article) }}</div> <!-- triple brackets also supported --> <div>{{{ userSubmittedContent }}}</div>

要点说明:

  • 单花括号{ }:求值后作为纯文本插入,值会被转义(text node 方式写入),undefinednull会被清空,NaN显示为N/A,表达式抛错时渲染为[Error]——这些行为都有 render.test.js 中的测试用例支撑。
  • 双花括号{{ }}:按 HTML 解析插入,用于渲染 Markdown、富文本等已信任的 HTML 内容;值为false时渲染为空(见 render.test.js)。
  • 三花括号{{{ }}}:双花括号的等价写法。tokenizer 在解析时会先把{{{ foo }}}规范化为{{ foo }}(见 tokenizer.js),三种写法最终走同一条渲染路径。

表达式的“上下文注入”原理

表达式之所以能直接写usernamecount这类裸变量,是因为编译器会做上下文注入。在 context.js 中,addContext会把非保留字标识符改写为_.xxx_代表组件实例),同时:

  • this会被改写为_,所以组件脚本里this.count++与模板里的{ count }指向同一数据;
  • 字符串字面量、对象属性键、./之后的标识符跳过改写;
  • $event会被统一替换为$e,作为事件对象占位符;
  • documentwindowMathJSONconsole等 Web 平台保留字(定义在 html5.js)保持原样,模板里可以直接调用。

属性:动态值、布尔属性与 class 处理

属性使用与文本相同的表达式语法,并在编译期被 parseAttributes 分类处理:

<!-- attribute values --> <time datetime="{ date.toISOString() }"> <!-- boolean attributes (falsy values remove the attribute) --> <button disabled="{ is_disabled }"> <!-- class name interpolation --> <div class="gallery { type }"> <!-- conditional classes --> <div class="[ is-active: isActive, has-error: hasError ]"> <!-- combine static and dynamic --> <div class="gallery { type } [ is-active: isActive ]">

行为细节:

  • 属性值表达式{ }内为任意 JavaScript 表达式,编译后被包裹为(expr)形式参与字符串拼接(见 parseExpression)。
  • 布尔属性disabledcheckedselectedhiddenrequiredreadonly等被列入 BOOLEAN 常量。真值时设置属性(setAttribute(name, '')),假值时整个移除。渲染端在 setAttributes 中实现这一逻辑。
  • 条件 class[ 类名: 条件, ... ]语法会被编译为$concat({...})对象映射(见 parseClassHelper),$concat只保留条件为真的类名(node.js)。含连字符的类名自动加引号。
  • 类名安全校验:渲染器会检查 class 中是否存在:[]等非法字符,并提示 class 数量过多(node.js),避免调试条件 class 时留下难排查的脏数据。

render.test.js 验证了 class 映射与函数条件([ active: isActive(), error: hasError() ])都能正确渲染。

循环::each渲染列表

:each指令把数组、对象条目渲染为重复元素:

<!-- basic loop --> <li :each="item in items">{ item.name }</li> <!-- with index --> <li :each="item, i in items"> { i }: { item.name } </li> <!-- destructuring --> <li :each="{ name, price } in products"> { name } costs { price } </li> <!-- loop objects --> <li :each="[key, val] in Object.entries(data)"> { key } = { val } </li> <!-- template loops (no wrapper element) --> <dl> <template :each="term in glossary"> <dt>{ term.word }</dt> <dd>{ term.definition }</dd> </template> </dl>

底层解析见 parseFor / parseForArgs:支持in/of两种分隔符;item, i形式中最后一个变量作为索引;{ name, price }[key, val]分别触发对象解构与 entries 模式。渲染循环在 renderLoop 中完成:每次迭代克隆一份组件数据、写入循环变量(含索引i),再逐个渲染。

<template>循环是免包装元素的关键——它不产生额外标签,直接把内部节点平铺到父容器,适合<dl><table>等不允许任意包裹元素的场景。

条件::if/:else-if/:else

<p :if="count > 100">Too many!</p> <p :else-if="count > 10">Getting there</p> <p :else>{ count } items</p> <!-- combine with loops (condition evaluated first) --> <ul :if="items.length"> <li :each="item in items">{ item }</li> </ul> <p :else>No items</p>

编译期,相邻的条件元素会被合并为一个some分支组(见 mergeConditionals),保证:else永远挂在最近的:if兄弟上;运行期 renderIf 从上到下求值,命中第一个为真的分支渲染,[Error]视为假。条件与循环组合时,条件优先求值,未命中则整块不渲染。

组件:可复用的 UI 单元

组件即“带<script>自定义标签的文档片段”。定义、使用与传参:

<!-- define a component --> <product-card> <h3>{ name }</h3> <p>{ price }</p> <script> // default values this.name = 'Untitled' this.price = 0 </script> </product-card> <!-- use the component --> <product-card/> <!-- pass properties --> <product-card :name="Coffee" :price="12"/> <!-- pass data variables --> <product-card :name="productName" :price="productPrice"/> <!-- shorthand (passes the name and price variables) --> <product-card :name :price/> <!-- regular attributes (no colon prefix) are rendered --> <product-card id="featured" class="highlight"/> <!-- loop components --> <product-card :each="item in products" :bind="item"/>

规则梳理:

  • 属性传递:name="Coffee"传字面量;:name="productName"传变量;:name简写等价于:name="name"(源码见 attributes.js,带冒号且无值时会自动补为同名变量)。这些属性以is_data标记,只进入组件数据、不渲染为 DOM 属性(node.js)。
  • 普通属性:不带冒号的idclass等原样渲染在组件根元素上。
  • :bind="item":把对象整体展开合并进组件数据(getAttrData),配合:each可把列表项整体注入组件。
  • 组件默认值<script>this.xxx = ...在组件实例化时执行(node.js),未传入的属性回落到默认值。

组件根元素::is

组件默认渲染为<div>包裹;用:is换成任意标签:

<!-- this component renders as <figure> --> <image-card :is="figure"> <img src="{ url }"> <figcaption>{ caption }</figcaption> </image-card>

从渲染路径看,renderComponent 会优先采用:is指定的标签名作为根元素;:is也用于把原生标签升级为交互组件(如<form :is="member-form">,见 html-file-types.md 的 DHTML 库示例)。自定义标签(含连字符或非 HTML5 标签)由 ast.js 判定为is_custom,触发组件渲染逻辑。

事件处理(客户端)

客户端专属——:on前缀为任意受支持事件绑定处理逻辑:

<counter> <button :onclick="count++">{ count }</button> <script> this.count = 0 </script> </counter> <!-- method handlers --> <counter> <button :onclick="increment">+</button> <button :onclick="decrement">-</button> <p>Count: { count }</p> <p>Double: { double }</p> <script> this.count = 0 increment() { this.count++ } decrement() { if (this.count > 0) this.count-- } // getter methods are supported get double() { return this.count * 2 } </script> </counter> <!-- event object --> <form :onsubmit="handleSubmit"> <script> handleSubmit(e) { // forms automatically call e.preventDefault() console.log('Submitted:', e.target) } </script> </form>

实现要点:

  • 可绑定的事件集合定义在 EVENTS 常量(click、submit、input、keydown、pointerdown、transitionend 等数十种)。:onclick解析为click事件监听(attributes.js)。
  • 内联表达式:onclick="count++"直接写语句;方法名:onclick="increment"会自动补全为increment($e)调用(同一段源码)。
  • 脚本中的increment() { ... }方法写法会被编译器转换为this.increment = function(...)(见 convertFunctions);get double()则被转换为Object.defineProperty(this, 'double', { get() {...} })(convertGetters),因此 getter 也能直接在模板与事件中读取。
  • 表单事件submit事件会自动调用e.preventDefault(),避免页面刷新(node.js),配合e.target可取回 FormData。
  • 每次事件触发后组件自动update()重渲染(node.js),数据变更即时反映到视图。

生命周期方法(客户端)

客户端专属——在关键时间点执行逻辑:

<user-profile> <h2>{ user.name }</h2> <script> // before mounting to DOM onmount() { console.log('About to mount') } // after mounting to DOM mounted() { console.log('Mounted!') } // before updating onupdate() { console.log('About to update') } // after updating updated() { console.log('Updated!') } </script> </user-profile>

这些钩子在 node.js 中被显式调用:onmount在插入 DOM 前触发、mounted在挂载后触发(mount,见 L47-L54);onupdate在重渲染前触发,若返回false可取消本次更新,updated在更新完成后触发(update,见 L13-L25)。组件作为子组件被渲染时同样会依次触发(renderComponent)。

手动更新:this.update(data)

客户端专属——事件处理器之外的场景需要手动触发重渲染:

this.update(data)

事件处理器触发后会自动更新视图;但异步操作(如fetch)或外部事件(如 WebSocket 消息)结束后,需要显式调用this.update()把新数据合并进组件并重渲染:

<script> async mounted() { const data = await fetch('/api/user') const user = await data.json() // Manual update required after async operations this.update({ user }) } </script>

update(values)Object.assign合并数据,再执行 onupdate → 渲染 → domdiff → updated 全流程(node.js),差异更新复用 diff.js 的 DOM 比对算法。

动态挂载:this.mount(name, target, data)

客户端专属——在单页应用中按需把组件挂载到指定位置:

this.mount(name, target, data)

参数说明:

参数类型说明
namestring组件名
targetDOM 元素或 CSS 选择器挂载目标,字符串会被querySelector解析
dataobject(可选)传给组件的数据
<my-app> <article/> <script> state.on('id', ({ id }) => { this.mount(id ? 'user-details' : 'user-list', 'article') }) </script> </my-app>

实现上,this.mount会在已加载的组件依赖(opts.deps)中按名字查找组件,动态创建实例并挂载(node.js)。路由切换等场景的配套模式见 single-page-apps。

共享脚本:跨组件复用函数与常量

顶层<script>中定义的函数、常量对所有组件可见:

<!-- top-level script --> <script> // available to all components const TAX_RATE = 0.08 function formatPrice(num) { return '$' + num.toFixed(2) } </script> <!-- use in components --> <product-card> <p>{ formatPrice(price) }</p> <p>Tax: { formatPrice(price * TAX_RATE) }</p> <script> this.price = 10 </script> </product-card> <!-- another component definition --> <shopping-cart> <p>{ formatPrice(price) }</p> <script> // ... </script> </shopping-cart>

编译期 document.js 会把文档中的所有<script>内容合并到page.script,再通过 parseNames 提取其中声明的变量与函数名,作为后续表达式注入的“已知标识符”,因此共享函数在任意组件模板中都能被直接调用。

JavaScript 导入(客户端)

客户端专属——导入外部模块,并在模板中使用其导出:

<script> import { formatDistance } from './utils.js' import { store } from './store.js' </script> <!-- imported functions available in templates --> <article> <time>{ formatDistance(date) }</time> <p>Cart items: { store.cart.length }</p> </article>

import语句的文档会被 document.js 自动判定为动态 HTML(is_dhtml),导入的名字同样进入parseNames的已知标识符列表,模板可直接引用。这也是 html-file-types.md 中“自动检测 DHTML”的依据之一。

Passthrough scripts(服务端)

服务端专属——带typesrc属性的<script>不参与 Nue 处理,原样输出到客户端:

<!-- these render as-is to the client --> <script src="/analytics.js"></script> <script type="module"> console.log('This runs on the client') </script>

区分逻辑在 tokenizer 中:<script>标签连同属性整体被保留为原始 token(tokenizer.js),而真正承载组件逻辑的裸<script>才会被提取执行。

插槽:<slot/>组合模式

组件通过<slot/>声明内容插槽,使用方填入的内容会被渲染到插槽位置:

<!-- component with slot --> <card> <div class="card"> <slot/> </div> </card> <!-- using the slot --> <card> <h2>This goes inside the card</h2> <p>So does this</p> </card> <!-- multiple instances --> <card :each="post in posts"> <h2>{ post.title }</h2> <p>{ post.excerpt }</p> </card>

渲染端遇到slot节点时,会回填父组件的子内容(node.js)。插槽与:each天然组合:循环创建的每个<card>实例都会收到各自 posts 项的内容。这是 Nue 实现卡片、弹层、布局组件等组合型 UI 的基础,配合 layout-system 可搭建完整的布局体系。

CSS 变量:设计令牌直通样式

--前缀属性把值写入元素的 CSS 变量,无需内联样式:

<!-- renders as style="--spacing: 2rem" --> <section --spacing="2rem"> <style> section { padding: var(--spacing); } </style> </section> <!-- dynamic values --> <div --columns="{ columnCount }">

编译期,--xxx属性被标记为is_var且剥掉前缀(attributes.js);渲染时收集所有 CSS 变量属性,统一合并为一条style="--name:value;..."(setAttributes)。这样设计系统(设计令牌)可以保持单一事实来源:不在标记里塞内联样式、不靠 class 堆叠覆盖,主题、间距、栅格等令牌通过组件属性直接注入样式。

与文档类型体系的衔接

Nue 语法本身是“服务端与客户端同构”的:{ }表达式、:each:if、组件、插槽、CSS 变量在服务端构建与客户端运行时都可用;事件处理、生命周期、this.update()this.mount()、JS 导入则标记为客户端专属。页面的最终行为由文档类型决定:

  • <!doctype html>:服务端渲染的静态页面;
  • <!doctype dhtml>:客户端组件,挂载后交互;
  • <!html lib>/<!dhtml lib>/<!html+dhtml>:可复用组件库(含同构组件)。

完整说明见 html-file-types.md。值得一提的是,即使不写文档类型,document.js 也会根据是否存在:on*事件处理器、import语句等自动检测 DHTML,但显式声明<!dhtml>更为稳健清晰。

小结

Nue 的 HTML 语法是对标准 HTML 的“增量扩展”:表达式与控制流解决数据渲染,组件与插槽解决复用与组合,事件与生命周期解决交互,CSS 变量解决设计令牌传递。每条语法规则都能在 nuedom 的编译管线(tokenize → parse → AST → render)中找到对应实现,并有 render.test.js 等测试用例背书。以此为起点,你可以继续阅读 html-file-types.md 了解文档类型体系,或通过 examples/nue-counter.html 看到一个最小的计数器组件示例。

【免费下载链接】nueFastest way to build modern websites项目地址: https://gitcode.com/GitHub_Trending/nu/nue

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/16 17:57:01

Open Agents用量统计:如何查看Token消耗与成本

Open Agents用量统计&#xff1a;如何查看Token消耗与成本 【免费下载链接】open-agents An open source template for building cloud agents. 项目地址: https://gitcode.com/GitHub_Trending/op/open-agents Open Agents 是一款开源的云端 AI Agent 构建模板&#xf…

作者头像 李华
网站建设 2026/9/16 17:56:20

如何为 OpenSRE 新增一个集成:面向贡献者的完整教程

如何为 OpenSRE 新增一个集成&#xff1a;面向贡献者的完整教程 【免费下载链接】opensre Build your own AI SRE agents. The open source toolkit for the AI era. 项目地址: https://gitcode.com/GitHub_Trending/op/opensre OpenSRE 是一个开源的 AI SRE 智能体框架…

作者头像 李华
网站建设 2026/9/16 17:55:24

网络 - 缓存

一、概念 当某些网络访问获取的内容不是每次都变的&#xff0c;而是短时间不变的&#xff08;每月榜单&#xff09;或长时间不变的&#xff08;歌曲的信息&#xff09;&#xff0c;每次访问都联网获取的话&#xff0c;可能响应慢&#xff0c;不支持离线浏览&#xff0c;浪费用户…

作者头像 李华
网站建设 2026/9/16 17:55:14

conda install 命令深度解析:从 CLI 参数到求解器执行链路

conda install 命令深度解析&#xff1a;从 CLI 参数到求解器执行链路 【免费下载链接】conda A system-level, binary package and environment manager running on all major operating systems and platforms. 项目地址: https://gitcode.com/GitHub_Trending/co/conda …

作者头像 李华
网站建设 2026/9/16 17:54:22

Mole|Mac 清理,一条命令释放 20GB

Mole&#xff5c;Mac 清理&#xff0c;一条命令释放 20GB 【免费下载链接】Mole &#x1f439; Clean, uninstall, analyze, optimize, and monitor your Mac. Free open-source CLI, plus a native Mac app. 项目地址: https://gitcode.com/GitHub_Trending/mole15/Mole …

作者头像 李华
网站建设 2026/9/16 17:52:17

用 RTranslator 实现安卓离线实时翻译:从零到上手的完整指南

用 RTranslator 实现安卓离线实时翻译&#xff1a;从零到上手的完整指南 【免费下载链接】RTranslator Open source real-time translation app for Android that runs locally 项目地址: https://gitcode.com/GitHub_Trending/rt/RTranslator RTranslator 是一款免费、…

作者头像 李华