序列化

Slate 在构建数据模型的时候就考虑到了序列化。具体来说,它的文本节点的定义方式使其更容易一目了然地阅读,也容易被序列化为其它格式,比如 HTML 和 Markdown 。

并且,因为 Slate 使用纯 JSON 数据,你可以更容易地编写序列化逻辑。

纯文本

比如,从编辑器获取值然后返回为纯文本:

  1. import { Node } from 'slate'
  2. const serialize = nodes => {
  3. return nodes.map(n => Node.string(n)).join('\n')
  4. }

这里我们获取到了 Editor 的子节点(as a nodes argument ),然后返回一个纯文本结构,其中每一个顶级节点通过换行符来分隔新行。

比如这个输入:

  1. const nodes = [
  2. {
  3. type: 'paragraph',
  4. children: [{ text: 'An opening paragraph...' }],
  5. },
  6. {
  7. type: 'quote',
  8. children: [{ text: 'A wise quote.' }],
  9. },
  10. {
  11. type: 'paragraph',
  12. children: [{ text: 'A closing paragraph!' }],
  13. },
  14. ]

输出结果为:

  1. An opening paragraph...
  2. A wise quote.
  3. A closing paragraph!

注意这种方式无法区分块引用,这是因为我们讨论的是纯文本。但是你可以序列化数据为任何你喜欢的格式 — 毕竟它只是 JSON 。

HTML

对于这个例子,这是一个序列化为类 HTML 的 serialize 函数:

  1. import escapeHtml from 'escape-html'
  2. import { Node, Text } from 'slate'
  3. const serialize = node => {
  4. if (Text.isText(node)) {
  5. return escapeHtml(node.text)
  6. }
  7. const children = node.children.map(n => serialize(n)).join('')
  8. switch (node.type) {
  9. case 'quote':
  10. return `<blockquote><p>${children}</p></blockquote>`
  11. case 'paragraph':
  12. return `<p>${children}</p>`
  13. case 'link':
  14. return `<a href="${escapeHtml(node.url)}">${children}</a>`
  15. default:
  16. return children
  17. }
  18. }

这比上面的序列化为纯文本更清晰一些。它实际上执行了递归,通过子节点持续迭代,直到到达文本叶子节点。对于每一个接收到的节点,它都转换为一个 HTML 字符串。

它也可以将单个节点而不是一个数组作为输入,所以如果你像这样传递给编辑器:

  1. const editor = {
  2. children: [
  3. {
  4. type: 'paragraph',
  5. children: [
  6. { text: 'An opening paragraph with a ' },
  7. {
  8. type: 'link',
  9. url: 'https://example.com',
  10. children: [{ text: 'link' }],
  11. },
  12. { text: ' in it.' },
  13. ],
  14. },
  15. {
  16. type: 'quote',
  17. children: [{ text: 'A wise quote.' }],
  18. },
  19. {
  20. type: 'paragraph',
  21. children: [{ text: 'A closing paragraph!' }],
  22. },
  23. ],
  24. // `Editor` 对象还有被省略的其他的属性...
  25. }

结果为(增加换行符保证可读性):

  1. <p>An opening paragraph with a <a href="https://example.com">link</a> in it.</p>
  2. <blockquote><p>A wise quote.</p></blockquote>
  3. <p>A closing paragraph!</p>

这真的很简单!

反序列化

Deserializing

在 Slate 中的另一个常见的用例是 — 反序列化。当你有一些特定格式的输入想要转换为 Slate 可编辑的 JSON 结构时。比如,当有人粘贴 HTML 到你的编辑器中,然后你想要保证它被解析成对你的编辑器正确的格式。

Slate 已经内建了辅助包 slate-hyperscript 来完成这件事。

最常见的用例就是使用 slate-hyperscript 来编写 JSX 文档,比如编写测试时。你可能会这样使用它:

  1. /** @jsx jsx */
  2. import { jsx } from 'slate-hyperscript'
  3. const input = (
  4. <fragment>
  5. <element type="paragraph">A line of text.</element>
  6. </fragment>
  7. )

然后你的编译器(Babel,TypeScript等等)的 JSX 特性会转换这个 input 变量为:

  1. const input = [
  2. {
  3. type: 'paragraph',
  4. children: [{ text: 'A line of text.' }],
  5. },
  6. ]

这非常适合测试用例,或者当你希望能够易于阅读的格式编写大量的 Slate 对象的地方。

但是!这些不能有助于反序列化。

但是 slate-hyperscript 不仅仅是用于 JSX 的。它只是构建 Slate 内容树的办法。而这恰恰是当你想要反序列化一些像是 HTML 这样的内容时候想要做的事情。

比如,这里有一个对于 HTML 的 deserialize 函数:

  1. import { jsx } from 'slate-hyperscript'
  2. const deserialize = el => {
  3. if (el.nodeType === 3) {
  4. return el.textContent
  5. } else if (el.nodeType !== 1) {
  6. return null
  7. }
  8. const children = Array.from(el.childNodes).map(deserialize)
  9. switch (el.nodeName) {
  10. case 'BODY':
  11. return jsx('fragment', {}, children)
  12. case 'BR':
  13. return '\n'
  14. case 'BLOCKQUOTE':
  15. return jsx('element', { type: 'quote' }, children)
  16. case 'P':
  17. return jsx('element', { type: 'paragraph' }, children)
  18. case 'A':
  19. return jsx(
  20. 'element',
  21. { type: 'link', url: el.getAttribute('href') },
  22. children
  23. )
  24. default:
  25. return el.textContent
  26. }
  27. }

它接受一个名为 el 的 HTML 元素对象,返回一个 Slate 对象片段。所以如果你有一个 HTML 字符串,你可以像这样解析和反序列化它:

  1. const html = '...'
  2. const document = new DOMParser().parseFromString(html, 'text/html')
  3. deserialize(document.body)

当你输入:

  1. <p>An opening paragraph with a <a href="https://example.com">link</a> in it.</p>
  2. <blockquote><p>A wise quote.</p></blockquote>
  3. <p>A closing paragraph!</p>

你会得到的结果为:

  1. const fragment = [
  2. {
  3. type: 'paragraph',
  4. children: [
  5. { text: 'An opening paragraph with a ' },
  6. {
  7. type: 'link',
  8. url: 'https://example.com',
  9. children: [{ text: 'link' }],
  10. },
  11. { text: ' in it.' },
  12. ],
  13. },
  14. {
  15. type: 'quote',
  16. children: [
  17. {
  18. type: 'paragraph',
  19. children: [{ text: 'A wise quote.' }],
  20. },
  21. ],
  22. },
  23. {
  24. type: 'paragraph',
  25. children: [{ text: 'A closing paragraph!' }],
  26. },
  27. ]

就像序列化函数一样,你可以扩展它来满足你自定义域模型的需要。