利用Editor.Js 编辑器生成多端报告页面

editjs-Cover.png

背景:

由于我们的客户的业务中有一些生成报告的功能,报告制作需要一定的富文本支持,并且支持PC端、小程序端的展示。但报告的模板并不是固定的,会有一些个性化比如段落的样式、文字的高亮、可交互图表、表格等。这样的要求就给我们的开发产生了一些非常规要求:

需求

  • 可视化编辑
  • PC端可预览
  • 小程序可适配UI设计模板
  • 丰富的组件展示、文本格式化
image.png

解决方案

数据结构设计

针对这个需求,我们首先考虑的是数据的格式,因为一般情况下我们做一些页面富文本展示,直接的数据格式为html就可以,但是需求上面是小程序端展示、非固定模板又可能会有一定的交互需求。一开始我们的选型是使用 meta+ markdown 方案,在小程序端去识别meta 信息处理逻辑,解析 markdown 处理格式,但这样的操作会给PC端的使用者造成困惑,要求也比较高需要他们清楚逻辑信息,这个对使用者显然很不友好。

由于我们以前做过一些基于 block 格式数据处理的业务,所谓 block 其实逻辑上面比较简单,将内容由原来的文本或富文本,转换为 json + field 格式,很多在线定制表单的数据格式就是这样的。所以我们只需要先定义一套可以满足报告展示并能描述报告逻辑的 json 格式即可。

思路是这样的,但还是需要解决PC端编辑器的问题,就想到了这款完全基于 block 样式的编辑器,并能直接输入 JSON格式数据,非常适合我们的需求。

编辑器选型

Editor.js 是一个块样式的编辑器,https://editorjs.io/,该编辑器是以块作为基本元素来的,每次编辑都是在一个块上进行编辑,主要的特点是,可以输出简洁的 JSON 而不是传统的html格式,当然也可以格式化为 html或小程序端支持的 wxml。并且可自定义插件,操作简单快捷。

试用了下编辑和输出,的确结果是我们想要的样子:

image.png

剩下的事情况就相对简单了,先设计一套可以在前端展示的 blocks,可以在中间加入一些逻辑标识,然后在服务端做解析处理,PC端访问时生成 html,小程序访问时输入 wxml,以及原始的 blocks。

服务端解析

由于 editorjs 只是一个客户端编辑器,服务端的处理是全都交给用户自己处理的,所以我们需要一个 editorjs 输入的 json 格式解析器,并将数据进行格式化才可以满足需求。

我们的解析器只做两件事,一个是接收数据进行解析,另一个是进行格式化。

transform.ts 文件中主要有一些类型定义和接口

转换器定义

export type transforms = {
    [key: string]: any;
    delimiter(): string;
    header(block: block): string;
    paragraph(block: block): string;
    list(block: block): string;
    image(block: block): string;
    quote(block: block): string;
    code(block: block): string;
    embed(block: block): string;
    warning(block: block): string;
    keypoint(block: block): string;
    table(block: block): any;
    component(block: block): any;
    widget(block: block): any;
    chart(block: block): any;
};

block格式定义

export type block = {
    type: string;
    data: {
        text?: string;
        level?: number;
        caption?: string;
        file?: {
            url?: string;
        };
        stretched?: boolean;
        withBackground?: boolean;
        withBorder?: boolean;
        items?: string[];
        style?: string;
        code?: string;
        service?: 'vimeo' | 'youtube';
        source?: string;
        embed?: string;
        width?: number;
        height?: number;
        title: string;
        message: any;
        content?: any;
        value?: any;
        name?: string;
        template?: string;
        props?: any;
    };
};

格式化为 html

const transformsHtml: transforms = {
    delimiter: () => {
        return `
`; }, header: ({ data }) => { return `${data.text}`; }, paragraph: ({ data }) => { return `

${data.text}

`; }, ...

格式化为 wxml

const transformsWxml: transforms = {
    delimiter: () => {
        return ``;
    },

    header: ({ data }) => {
        return `${data.text}`;
    },

    paragraph: ({ data }) => {
        let text = data?.text?.replace(/<[\/]?(b)([^<>]*)>/g, (m, m1) => {
            return m.replace('b', 'text');
        });
        if (text) {
            text = text.replace(/<[\/]?(mark)([^<>]*)>/g, (m, m1) => {
                return m.replace('mark', 'text');
            });
        }
        return `${text}`;
    },
...

index.ts 接口实现导出
** 定义接口**

type parser = {
    parse(OutputData: OutputData): string[];
    parseStrict(OutputData: OutputData): string[] | Error;
    parseBlock(block: block): string;
    validate(OutputData: OutputData): string[];
};

parseWxml

const wxmlParser = (plugins = {}): parser => {
    const parsers = Object.assign({}, transformsWxml, plugins);

    return {
        parse: ({ blocks }) => {
            return blocks.map(blockItem => {
                return parsers[blockItem.type]
                    ? parsers[blockItem.type](blockItem)
                    : ParseFunctionError(blockItem.type);
            });
        },

        parseBlock: blockItem => {
            return parsers[blockItem.type]
                ? parsers[blockItem.type](blockItem)
                : ParseFunctionError(blockItem.type);
        },
...

parseHtml

const htmlParser = (plugins = {}): parser => {
    const parsers = Object.assign({}, transformsHtml, plugins);

    return {
        parse: ({ blocks }) => {
            return blocks.map(blockItem => {
                return parsers[blockItem.type]
                    ? parsers[blockItem.type](blockItem)
                    : ParseFunctionError(blockItem.type);
            });
        },

        parseBlock: blockItem => {
            return parsers[blockItem.type]
                ? parsers[blockItem.type](blockItem)
                : ParseFunctionError(blockItem.type);
        },
...
export { htmlParser, wxmlParser };

这样就完成了一个简单的 editorjs 解析器,我们将它打包成了一个包,在报告的具体业务类中就可以直接使用

import { wxmlParser } from '@caixie/editorjs-parser';
...

        const parser = wxmlParser();
        const reportOutputContent = this.reportDqiReport.content;
        const reportResult = parser.parse(this.reportDqiReport.content);
        const mealTime = new Date(format(new Date(date), 'yyyy-MM-dd'));
        const source = {
            outputData: reportOutputContent,
            rawData: {
                ...foodLog.dietAnalysis,
            },
        };
        return new UserReport({
            type: ReportType.DQI_DAILY,
            user: {
                id: userId,
            },
            date: mealTime,
            // creator: ctx.session.user
            content: new UserReportContent(
                reportResult,
                { dqiScore: Math.ceil(dqiScoreValue), mealTime },
                '评估报告',
                source,
            ),
        });
... 

总结

本文介绍了一款基于块内容的 editor.js 编辑器,并利用它解决一个针对多场景(PC端、小程序端)、个性化报告需求的实现,包含 editorjs的特点以及服务端的解析处理。希望对有想用基于块内容(block)编辑需求的朋友所帮助。当前市面上 editrojs 应该是最好的基于 block 实现的编辑器。类似 notition、国产的我来这些商业的笔记服务都是基于 block 的编辑器,只是大家的方案各有不同。

你可能感兴趣的:(利用Editor.Js 编辑器生成多端报告页面)