利用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 `<br/>`;
    },

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

    paragraph: ({ data }) => {
        return `<p>${data.text}</p>`;
    },
...

格式化为 wxml

const transformsWxml: transforms = {
    delimiter: () => {
        return `<view class="br"/>`;
    },

    header: ({ data }) => {
        return `<view class="h${data.level}">${data.text}</view>`;
    },

    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 `<view class="paragraph">${text}</view>`;
    },
...

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 的编辑器,只是大家的方案各有不同。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,937评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,503评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,712评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,668评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,677评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,601评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,975评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,637评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,881评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,621评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,710评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,387评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,971评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,947评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,189评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,805评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,449评论 2 342

推荐阅读更多精彩内容