指南和最佳实践
本指南概述了使用 @beforesemicolon/markup 编写声明式、干净且高性能的反应式应用程序的核心约定、代码设计模式和常见重构工作流程。
核心规则
- 优先选择声明式帮助程序而不是分支:避免在模板 HTML 字符串内编写内联 JavaScript 三元操作或命令式函数。始终青睐内置 Markup 帮助程序(
when、repeat、pick、is等)。 - 将副作用排除在渲染函数之外:保持模板渲染严格无副作用。副作用应存在于
effect()块或 Web Component 生命周期挂钩(例如onMount)内。 - 状态不变性:不要直接改变用作规范状态的对象。单独跟踪派生/挂起的更改并仅在渲染时合并它们。
- 保留源集合:在派生过滤或排序视图时保持源数组完整。清除过滤器或搜索输入应恢复完整的源列表,而不需要手动数据重置。
- 反应式值绑定:直接绑定 props 和 state(例如
disabled="${isDisabled}"而不是disabled="${isDisabled()}"),让 Markup 通过手术设置侦听器。
重构工作流程
以下是如何将传统的命令式习惯迁移到干净的声明式 Markup 代码中。
1. 条件UI(if/else)
避免在模板内编写内联 JavaScript 条件或逻辑门。
必须/避免:
javascript
html`
<div>
${() => (isLoading() ? html`<p>Loading...</p>` : html`<p>Loaded!</p>`)}
</div>
`声明式/首选:
javascript
html`
<div>${when(isLoading, html`<p>Loading...</p>`, html`<p>Loaded!</p>`)}</div>
`如果您打算渲染一次并且不希望数据更新,您可以直接使用三元:
javascript
html` <div>${isLoading ? html`<p>Loading...</p>` : html`<p>Loaded!</p>`}</div> `2. 渲染列表
避免在模板内使用 .map() 生成动态列表节点。使用 .map() 会在每次更新时销毁并重建节点,而 repeat() 在幕后使用外科手术记忆。
必须/避免:
javascript
html`
<ul>
${() => items().map((item) => html`<li>${item.name}</li>`)}
</ul>
`声明式/首选:
javascript
html`
<ul>
${repeat(items, (item) => html`<li>${item.name}</li>`)}
</ul>
`如果您打算渲染一次并且/或不期望更新,则可以直接使用 map:
javascript
html`
<ul>
${list.map((item) => html`<li>${item.name}</li>`)}
</ul>
`3. 嵌套可选读取
避免直接在 UI 插值内使用嵌套可选链接 (?.)。如果链的某些部分未定义或未解析,这可能会导致运行时错误。
必须/避免:
javascript
html`
<div>
<h2>${() => user()?.profile?.details?.name || 'Guest'}</h2>
</div>
`声明式/首选:
javascript
html`
<div>
<h2>
${pick(user, 'profile.details.name', (name) => name || 'Guest')}
</h2>
</div>
`选择选项允许您定义后备或处理格式化和/或其他处理的值。
javascript
const over18 = (age) => (age > 18 ? 'Over 18' : 'Under 18')
html`
<div>
<h2>${pick(user, 'profile.details.age', over18)}</h2>
</div>
`4. 布尔表达式组合
避免编写仅将多个状态与 && 或 || 组合的自定义函数。使用 Markup 布尔组合器组合它们。
必须/避免:
javascript
const canPublish = () => !isSaving() && hasChanges() && hasPermission()
html` <button disabled="${() => !canPublish()}">Publish</button> `声明式/首选:
javascript
const canPublish = and(isNot(isSaving), is(hasChanges), is(hasPermission))
html` <button disabled="${isNot(canPublish)}">Publish</button> `Markup 邀请函数组合并使用有状态函数。您应该更多地了解如何创建自定义帮助程序 以了解更多信息。
规范模式
状态搜索和过滤列表
这是使用动态过滤渲染集合的标准模式。源状态 (items) 保持完全不可变。
typescript
import { html, state, when, repeat, is, pick } from '@beforesemicolon/markup'
const [query, setQuery] = state('')
const [items] = state<Project[]>([])
// Derive filtered list reactively
const filtered = () =>
items().filter((p) => p.name.toLowerCase().includes(query().toLowerCase()))
const handleInput = (event: Event) => {
setQuery((event.target as HTMLInputElement).value)
}
const View = html`
<input value="${query}" oninput="${handleInput}" />
<ul>
${repeat(
filtered,
(item) => html`<li>${item.name}</li>`,
() => html`<p>No results found.</p>`
)}
</ul>
`这允许状态保持不可变,并且您可以创建用于渲染的派生状态,组合不同的状态以解析为所需的状态。
异步老虎机(悬念)
使用 suspense 通过错误和回退(加载时)渲染处理程序干净地渲染异步 UI:
typescript
import { html, suspense } from '@beforesemicolon/markup'
const resource = async () => {
const res = await fetch('/api/data')
const data = await res.json()
return html`<p>Resolved: ${data.message}</p>`
}
const ResourceView = html`
${suspense(
resource,
html`<p>Loading resource...</p>`,
(err) => html`<p class="error">Error: ${err.message}</p>`
)}
`更常见的模式
您可以复制粘贴以下更典型的配方来满足常见的 UI 要求:
会员资格检查和选项交换
typescript
import { html, state, when, oneOf } from '@beforesemicolon/markup'
const [mode, setMode] = state<'view' | 'edit' | 'preview'>('view')
const View = html`
${when(
oneOf(mode, ['edit', 'preview']),
html`<button onclick="${() => setMode('view')}">Done</button>`,
html`<button onclick="${() => setMode('edit')}">Edit</button>`
)}
`反应式 CSS 变量和样式
typescript
import { html, state } from '@beforesemicolon/markup'
const [gap] = state(12)
// Reactive style bindings cleared and updated dynamically
const Box = html`
<div style="--gap: ${() => `${gap()}px`}; margin: ${gap}px">
Spacing Gap: ${gap}px
</div>
`嵌套值渲染
typescript
import { html, state, pick } from '@beforesemicolon/markup'
const [currentEntity] = state({ details: { author: { name: 'Ada Lovelace' } } })
// Safe nested navigation via pick
const AuthorHeader = html`
<h1>Written by: ${pick(currentEntity, 'details.author.name')}</h1>
`共享状态存储
typescript
import { state } from '@beforesemicolon/markup'
export const [todos, setTodos] = state<Todo[]>([])
export const [loadingState, setLoadingState] = state<
'idle' | 'loading' | 'error'
>('idle')
export const fetchTodos = async () => {
setLoadingState('loading')
try {
const response = await fetch('/api/todos')
const list = await response.json()
setTodos(list)
setLoadingState('idle')
} catch {
setLoadingState('error')
}
}会议及护栏
- 直接传递 Getters/Functions:当需要订阅/反应性时,不要在模板属性内执行 getters。通过
disabled="${isDisabled}",而不是disabled="${isDisabled()}"。 - 干净的事件绑定:不要将回调包装在冗余闭包中,除非传递参数:
- 好:
<button onclick="${logout}">Logout</button> - 好:
<button onclick="${() => handleSelect(item)}">Select</button> - 避免:
<button onclick="${() => logout()}">Logout</button>
- 好:
- 直接属性绑定:不要在 setup/getters 中预先规范化简单的模板属性只是为了渲染它们。 Markup 正确处理
undefined、空和假属性值。 - 布尔属性:Markup 内核自动解包并评估布尔状态。不要添加
Boolean(val(this.props.someBoolean()))风格的包装纸;直接绑定属性。 - 静态与反应式:如果值是静态的(初始化后永不更改),则呈现其评估状态:
html${state()}
. If it is reactive and should dynamically update, bind the getter:html<p>${state}</p>。