likes
comments
collection
share

Slate.js 入门(六)- 数据存储和序列化

作者站长头像
站长
· 阅读数 14

前言

通过之前的文章,相信你已经了解了如何向 Slate 编辑器添加一些基础的功能,那你知道如何实时存储你正在编辑的富文本内容吗?

本篇文章,将带你学会如何实时保存、序列化和反序列化你所编辑的富文本内容,即使页面重载之后你仍然能获取并加载它。

富文本的存储

本地存储

我们从一个基本的编辑器开始:

const initialValue = [
    {
        type: 'paragraph',
        children: [{ text: '你好,我是林个了檬' }],
    },
]

const App = () => {
    const [editor] = useState(() => withReact(createEditor()))

    return (
        <div className="container">
            <h3>Slate 编辑器</h3>
            <div className="editor-wrapper">
                <Slate
                    editor={editor}
                    value={initialValue}
                    >
                    <button type="button" onClick={onReplace}>
                        替换
                    </button>
                    <Editable />
                </Slate>
            </div>
        </div>
    )
}

此时,你的页面上会呈现的 Slate 编辑器,当输入内容时会发生变化。但如果你刷新页面,富文本内容就会恢复到原来的值。

你需要做的是将富文本的更改保存在某个地方。比如,使用本地存储 localStorage,接下来的问题就是在哪里保存富文本的内容。

编辑器有个 onChange 事件,当富文本内容发生变化时都会触发,我们选择在 onChange 事件中做保存逻辑:除了「选择」动作之外的任何动作使富文本内容发生了变化,就保存该值:

<Slate
    editor={editor}
    value={initialValue}
    onChange={value => {
        // 判断编辑器内容是否发生改变
        const isAstChange = editor.operations.some(
            op => 'set_selection' !== op.type
        )
        if (isAstChange) {
            // 将值保存到 Local Storage.
            const content = JSON.stringify(value)
            localStorage.setItem('content', content)
        }
    }}
    >
    <Editable />
</Slate>

现在,每当你编辑页面时,打开 devTools-Application-Local Storage 查看本地存储,你将会看到 content 的值发生变化

Slate.js 入门(六)- 数据存储和序列化

现在如果你刷新页面,一切仍将重置。因为我们还需要在页面初始化的时候提取 content 的值并交给编辑器,添加如下代码:

const initialValue = useMemo(
    () =>
    JSON.parse(localStorage.getItem('content')) || [
        {
            type: 'paragraph',
            children: [{ text: '你好,我是林个了檬' }],
        },
    ],
    []
)

添加代码后,现在如果刷新了页面,你会发现仍然能够显示之前刷新前的富文本内容:

Slate.js 入门(六)- 数据存储和序列化

序列化和反序列化

上面的例子中,我们保存的是 Slate 原始值的 JSON 格式,如果你想要 JSON 格式以外的东西呢?比如纯文本 string 格式。

没问题,不过需要以特殊的方式序列化 Slate 的值。为此,我们可以编写一些逻辑来序列化和反序列化纯文本值:

// 定义一个序列化函数:接受一个值并返回一个字符串
const serialize = (value) => {
    console.log(value);
    return (
        value
        // 返回值 value 子项中每个段落的字符串内容
        .map((n) => {
            console.log("1", Node.string(n));
            return Node.string(n);
        })
        // 用表示段落的换行符将它们全部连接起来
        .join("\n")
    );
};

// 定义一个反序列化函数:它接受一个字符串并返回一个值
const deserialize = (string) => {
    // 返回通过拆分字符串派生的子值数组
    return string.split("\n").map((line) => {
        return {
            children: [{ text: line }],
        };
    });
};

onChange 事件中保存富文本值的时候进行序列化:

onChange={(value) => {
    const isAstChange = editor.operations.some(
        (op) => "set_selection" !== op.type
    );
    if (isAstChange) {
        localStorage.setItem("content", serialize(value));
    }
}}

回到页面,你会发现,在 Local Storage 中保存的值变成字符串了。在刷新页面后,编辑器仍然能正常显示:

Slate.js 入门(六)- 数据存储和序列化

完整代码如下:

// 定义一个序列化函数:接受一个值并返回一个字符串
const serialize = (value) => {
    console.log(value);
    return (
        value
        // 返回值 value 子项中每个段落的字符串内容
        .map((n) => {
            console.log("1", Node.string(n));
            return Node.string(n);
        })
        // 用表示段落的换行符将它们全部连接起来
        .join("\n")
    );
};

// 定义一个反序列化函数:它接受一个字符串并返回一个值
const deserialize = (string) => {
    // 返回通过拆分字符串派生的子值数组
    return string.split("\n").map((line) => {
        return {
            children: [{ text: line }],
        };
    });
};

const App = () => {
    // 创建一个在渲染中保持不变的 slate editor 对象
    const [editor] = useState(() => withReact(createEditor()));

    const initialValue = useMemo(
        () =>
        deserialize(
            localStorage.getItem("content") || "你好,我是林了个檬。"
        ),
        []
    );

    return (
        <div className="container">
            <h3>Slate 编辑器</h3>
            <div className="editor-wrapper">
                <Slate
                    editor={editor}
                    value={initialValue}
                    onChange={(value) => {
                        const isAstChange = editor.operations.some(
                            (op) => "set_selection" !== op.type
                        );
                        if (isAstChange) {
                            localStorage.setItem("content", serialize(value));
                        }
                    }}
                    >
                    <Editable />
                </Slate>
            </div>
        </div>
    );
};

富文本替换

如果需要通过响应来自 Slate 外部的事件来更新富文本编辑器的内容,你可以直接更改 editor.children 属性。最简单的方法是替换 editor.children = newValue 的值并触发重新渲染。

当然也有其他的方法,比如可以使用 Slate 的内部操作来转换富文本的值,这样当你需要替换富文本内容的任何时候就能派上用场,代码如下:

// 自定义resetNodes方法:重新设置编辑器的值
// 需要注意的是,at 参数可能会导致 "Cannot resolve a DOM point from Slate point" 错误
const resetNodes = (editor, options = {}) => {
    const children = [...editor.children];

    children.forEach((node) =>
        editor.apply({ type: "remove_node", path: [0], node })
    );

    if (options.nodes) {
        const nodes = Node.isNode(options.nodes)
            ? [options.nodes]
            : options.nodes;

        nodes.forEach((node, i) =>
            editor.apply({ type: "insert_node", path: [i], node: node })
        );
    }

    const point =
        options.at && Point.isPoint(options.at)
            ? options.at
            : Editor.end(editor, []);

    if (point) {
        Transforms.select(editor, point);
    }
};

最后

你可以通过模拟此策略实现你期望的任何数据格式。比如说序列化为 HTMLMarkdown,甚至是为你的用例量身定制的自定义 JSON 格式。

注意,即使你可以随意序列化富文本内容,也要权衡取舍。对于数据来说,序列化过程本身需要成本,甚至有一些特殊的格式比其他格式更难处理。一般来说,我们建议仅当你的用例有特定需求时才去重构自己的富文本数据格式。否则,你最好将数据保留为 Slate 原始的格式。

往期回顾