# 序列化

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

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

# 纯文本(Plaintext)

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

import { Node } from 'slate'

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

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

比如这个输入:

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

输出结果为:

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

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

# HTML

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

import escapeHtml from 'escape-html'
import { Node, Text } from 'slate'

const serialize = node => {
  if (Text.isText(node)) {
    return escapeHtml(node.text)
  }

  const children = node.children.map(n => serialize(n)).join('')

  switch (node.type) {
    case 'quote':
      return `<blockquote><p>${children}</p></blockquote>`
    case 'paragraph':
      return `<p>${children}</p>`
    case 'link':
      return `<a href="${escapeHtml(node.url)}">${children}</a>`
    default:
      return children
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

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

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

const editor = {
  children: [
    {
      type: 'paragraph',
      children: [
        { text: 'An opening paragraph with a ' },
        {
          type: 'link',
          url: 'https://example.com',
          children: [{ text: 'link' }],
        },
        { text: ' in it.' },
      ],
    },
    {
      type: 'quote',
      children: [{ text: 'A wise quote.' }],
    },
    {
      type: 'paragraph',
      children: [{ text: 'A closing paragraph!' }],
    },
  ],
  // `Editor` objects also have other properties that are omitted here...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

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

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

这真的很简单!

# 反序列化(Deserializing)

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

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

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

/** @jsx jsx */
import { jsx } from 'slate-hyperscript'

const input = (
  <fragment>
    <element type="paragraph">A line of text.</element>
  </fragment>
)
1
2
3
4
5
6
7
8

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

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

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

然而!这些不能有助于反序列化。

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

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

import { jsx } from 'slate-hyperscript'

const deserialize = el => {
  if (el.nodeType === 3) {
    return el.textContent
  } else if (el.nodeType !== 1) {
    return null
  }

  const children = Array.from(el.childNodes).map(deserialize)

  switch (el.nodeName) {
    case 'BODY':
      return jsx('fragment', {}, children)
    case 'BR':
      return '\n'
    case 'BLOCKQUOTE':
      return jsx('element', { type: 'quote' }, children)
    case 'P':
      return jsx('element', { type: 'paragraph' }, children)
    case 'A':
      return jsx(
        'element',
        { type: 'link', url: el.getAttribute('href') },
        children
      )
    default:
      return el.textContent
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

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

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

当你输入:

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

你会得到的结果为:

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

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