序列化:文档模型存盘与加载
目标
内容要存数据库、要发给协同伙伴,必须能序列化成稳定格式。本节课把 l4 的模型升级成 ProseMirror 风格 {type:'doc', content:[...]},并写 toHTML / fromHTML,在 Node 里可运行。
运行环境:Node.js v18+。运行方式:保存为 serialize.js,执行 node serialize.js(复用 l4 的解析思路)。
// serialize.js —— 文档模型与 HTML 互转(Node 可运行)
function htmlToDoc(html) {
let i = 0
function parse() {
const content = []
while (i < html.length) {
if (html[i] === '<') {
if (html.startsWith('</', i)) return content
const end = html.indexOf('>', i)
const tag = html.slice(i + 1, end).split(' ')[0]
i = end + 1
if (tag === 'br') { content.push({ type: 'break' }); continue }
const node = { type: 'element', tag, content: parse() }
const close = '</' + tag + '>'
if (html.startsWith(close, i)) i += close.length
content.push(node)
} else {
const next = html.indexOf('<', i)
const text = html.slice(i, next === -1 ? html.length : next)
if (text) content.push({ type: 'text', value: text })
i = next === -1 ? html.length : next
}
}
return content
}
return { type: 'doc', content: parse() }
}
function docToHtml(doc) {
return doc.content.map(n => {
if (n.type === 'text') return n.value
if (n.type === 'break') return '<br>'
if (n.type === 'element') return '<' + n.tag + '>' + docToHtml(n) + '</' + n.tag + '>'
return ''
}).join('')
}
// 简单 schema 校验:不允许孤立的 element 没有 content 字段
function validate(doc) {
function walk(node) {
if (node.type === 'element' && !Array.isArray(node.content)) throw new Error('element 必须含 content 数组')
if (node.content) node.content.forEach(walk)
}
if (doc.type !== 'doc') throw new Error('根必须是 doc')
walk(doc)
return true
}
// ===== 调用示例 =====
const doc = htmlToDoc('<p>你好<b>世界</b></p>')
console.log('文档模型 =', JSON.stringify(doc))
console.log('校验通过 =', validate(doc))
console.log('还原 HTML =', docToHtml(doc))把模型统一成 {type:'doc', content:[...]},存盘就是 JSON.stringify(doc),加载就是 JSON.parse,协同时也只传这个 JSON。
名词解释
序列化(Serialization):把内存里的对象转成可存储/传输的字符串(如 JSON、HTML)。反序列化是其逆过程。
Schema(模式):对模型结构的约定(哪些节点合法、能含什么子节点)。校验 schema 能在存盘前拦住非法结构。
文档模型(Document Model):编辑器内部"一份内容"的标准表示,如 ProseMirror 的 {type:'doc', content:[...]}。它是存储、协同、渲染的共同语言。
课后练习
练习:写一个 cloneDoc(doc),深拷贝一份文档模型(不改变原对象)。
答案:
function cloneDoc(doc) {
return JSON.parse(JSON.stringify(doc)) // 模型是纯 JSON,直接结构化克隆最稳
}
const a = htmlToDoc('<p>hi</p>')
const b = cloneDoc(a)
b.content[0].tag = 'div'
console.log('原 doc 未被改:', a.content[0].tag) // p总结
序列化决定了一门编辑器"能不能落地":内容存不进数据库、发不到后端,再漂亮也没用。把模型定成纯 JSON 的 {type:'doc', content:[...]},序列化就变成了 JSON.stringify,协同伙伴之间也只传这个 JSON,前后端一套结构。schema 校验是保险丝——在存盘/加载前拦掉非法节点,避免脏数据进入模型后把渲染搞崩。本课你跑通的 htmlToDoc / docToHtml / validate,正是 ProseMirror 里 DOMParser 与 Schema 的迷你版。记住:模型是真相之源,HTML 只是它的一种外衣。