生命周期与动态片段
Kupola 的 walk() 会扫描 DOM 并绑定指令,返回的实例用于销毁这些绑定。只要页面会动态插入或移除 HTML,就应该显式管理生命周期。
基本初始化
import { walk } from '@kupola/platform/directives'
const instance = walk(document.body)walk() 返回的 $、$$ 默认限定在当前根节点:
const panel = walk('#users-panel')
const submit = panel.$('[type=submit]')
const rows = panel.$$('[data-row]')当整页即将卸载或容器将被移除时:
instance.destroy()局部初始化
后端局部刷新、弹窗内容、Tab 内容、抽屉内容等场景,推荐只初始化新增片段。
const container = document.querySelector('#users-panel')
container.innerHTML = await fetch('/users/fragment').then(r => r.text())
const usersPanel = walk(container)移除片段前销毁:
usersPanel.destroy()
container.remove()自动销毁
如果片段会被外部工具移除,例如弹窗关闭、HTMX/Turbo 局部替换或后端模板片段刷新,可以使用 walkAuto()。它等价于 walk(root, { autoDestroy: true }),会在 root 从 DOM 中移除后自动调用 destroy():
import { walkAuto } from '@kupola/platform/directives'
const panel = walkAuto(document.querySelector('#dialog-panel'))walkAuto() 只观察 root 是否被移除,不追踪每个内部节点。手动调用 panel.destroy() 仍然可用,并且会取消自动观察。
只初始化一次
如果初始化入口可能被外部回调重复触发,但你希望同一个 root 只绑定一次,可以使用 walkOnce()。它会在 root 未初始化时创建实例,已经初始化时直接返回已有实例:
import { walkOnce } from '@kupola/platform/directives'
function mountPanel(root) {
return walkOnce(root)
}walkOnce(root, { autoDestroy: true }) 也可用于局部片段。实例 destroy() 后,再次调用 walkOnce(root) 会创建新的实例。
查询与销毁实例
不方便保存实例引用时,可以按 root 查询或销毁当前实例:
import { destroyWalk, getWalk, hasWalk } from '@kupola/platform/directives'
if (hasWalk('#users-panel')) {
destroyWalk('#users-panel')
}
const instance = getWalk('#users-panel')getWalk(root) 返回当前实例或 null,hasWalk(root) 返回布尔值,destroyWalk(root) 在找到实例并销毁时返回 true,没有实例时返回 false。
多实例管理
如果一个页面有多个独立区域,可以保存多个实例。
const mounted = new Map()
document.querySelectorAll('[data-kupola-app]').forEach(root => {
mounted.set(root, walk(root))
})
function unmount(root) {
mounted.get(root)?.destroy()
mounted.delete(root)
root.remove()
}初始化数据
页面初始数据推荐由服务端输出为 JSON,再由 controller 使用。
<script type="application/json" id="users-data">
[{"id":1,"name":"Alice"}]
</script>
<section k-data="usersPage">
<p k-text="'共 ' + users.length + ' 条'"></p>
</section>const initialUsers = JSON.parse(document.querySelector('#users-data').textContent)
import { defineScope } from '@kupola/platform/directives'
defineScope('usersPage', () => ({
users: initialUsers,
}))mounted()
如果页面需要首次加载数据,推荐在命名 scope 中定义 mounted(ctx):
import { defineScope } from '@kupola/platform/directives'
defineScope('usersPage', () => ({
users: [],
loading: false,
mounted() {
this.load()
},
async load() {
this.loading = true
try {
this.users = await api.listUsers()
} finally {
this.loading = false
}
},
}))update() / patch()
mounted(ctx) 中可以使用 ctx.update() 和 ctx.patch() 统一顶层状态更新。它们适合数组追加、列表替换、对象浅合并等常见写法:
defineScope('usersPage', ({ update, patch }) => ({
users: [],
filters: { keyword: '', role: 'all' },
addUser(user) {
return update('users', users => [ ...users, user ])
},
setKeyword(keyword) {
return patch('filters', { keyword })
},
}))update(name, updater) 会把当前值传给 updater,并把返回值写回同名顶层属性。patch(name, partial) 要求当前值是对象,会浅合并并写回新对象。它们不做深层响应式代理,也不支持路径字符串。
watch()
mounted(ctx) 里可以使用 ctx.watch() 监听当前响应式状态,并在状态变化时执行 JS 副作用。它适合搜索请求、持久化、联动第三方库等不应该塞进模板表达式的逻辑。
import { defineScope } from '@kupola/platform/directives'
defineScope('usersPage', () => ({
keyword: '',
users: [],
mounted({ watch }) {
watch(
() => this.keyword,
keyword => {
if (keyword.length >= 2) {
this.search()
}
},
)
},
async search() {
this.users = await api.searchUsers(this.keyword)
},
}))需要首次立即执行时传入 immediate:
mounted({ watch }) {
watch(() => this.keyword, () => this.search(), { immediate: true })
}watch() 返回清理函数,并且会自动加入当前 walk() 实例的 destroy() 清理队列。只有当你想提前停止监听时,才需要手动调用返回值。
callback 可以返回 cleanup 函数。Kupola 会在下一次 callback 运行前执行上一次 cleanup,并在 destroy() 或手动停止监听时执行最后一次 cleanup,适合取消请求、清理定时器或解绑第三方订阅:
mounted({ watch }) {
watch(
() => this.keyword,
keyword => {
const controller = new AbortController()
this.search(keyword, { signal: controller.signal })
return () => controller.abort()
},
)
}watch() 只跟踪 getter 中读取的响应式状态。callback 里读取其他状态不会把它们变成监听依赖,适合在 callback 中组装请求参数、读取临时状态或调用业务方法。
k-init
简单页面也可以直接在 HTML 中使用 k-init:
<section k-data="usersPage" k-init="load()">
...
</section>k-init 只负责初始化,不应承担重复渲染、轮询或复杂生命周期编排。需要长期订阅时,仍应在 JavaScript 中保存清理函数,并在 destroy() 前后由业务代码管理。
动态片段
如果一个片段是通过 AJAX、HTMX、Turbo、后端模板局部刷新或弹窗懒加载插入的,推荐把“插入、初始化、移除”封装成一对函数:
let usersPanel
async function replaceUsersPanel() {
usersPanel?.destroy()
const container = document.querySelector('#users-panel')
container.innerHTML = await fetch('/users/fragment').then(r => r.text())
usersPanel = walk(container)
}
function removeUsersPanel() {
usersPanel?.destroy()
usersPanel = null
document.querySelector('#users-panel')?.replaceChildren()
}不要对同一段 DOM 重复 walk()。每次替换 HTML 前先销毁旧实例,每次移除 HTML 前也先销毁实例,这样旧事件、watch()、k-ref 和动态列表里的 effects 都会被清理。
常见错误
不要对同一个容器重复 walk() 而不销毁旧实例。Kupola 会在同一个 root 被重复初始化时输出 warning,因为事件、effects、watch() 和 refs 可能会叠加:
// 不推荐
walk(container)
walk(container)推荐:
currentInstance?.destroy()
currentInstance = walk(container)