背景:
由于我们的客户的业务中有一些生成报告的功能,报告制作需要一定的富文本支持,并且支持PC端、小程序端的展示。但报告的模板并不是固定的,会有一些个性化比如段落的样式、文字的高亮、可交互图表、表格等。这样的要求就给我们的开发产生了一些非常规要求:
需求
- 可视化编辑
- PC端可预览
- 小程序可适配UI设计模板
- 丰富的组件展示、文本格式化
解决方案
数据结构设计
针对这个需求,我们首先考虑的是数据的格式,因为一般情况下我们做一些页面富文本展示,直接的数据格式为html就可以,但是需求上面是小程序端展示、非固定模板又可能会有一定的交互需求。一开始我们的选型是使用 meta+ markdown 方案,在小程序端去识别meta 信息处理逻辑,解析 markdown 处理格式,但这样的操作会给PC端的使用者造成困惑,要求也比较高需要他们清楚逻辑信息,这个对使用者显然很不友好。
由于我们以前做过一些基于 block 格式数据处理的业务,所谓 block 其实逻辑上面比较简单,将内容由原来的文本或富文本,转换为 json + field 格式,很多在线定制表单的数据格式就是这样的。所以我们只需要先定义一套可以满足报告展示并能描述报告逻辑的 json 格式即可。
思路是这样的,但还是需要解决PC端编辑器的问题,就想到了这款完全基于 block 样式的编辑器,并能直接输入 JSON格式数据,非常适合我们的需求。
编辑器选型
Editor.js 是一个块样式的编辑器,https://editorjs.io/,该编辑器是以块作为基本元素来的,每次编辑都是在一个块上进行编辑,主要的特点是,可以输出简洁的 JSON 而不是传统的html格式,当然也可以格式化为 html或小程序端支持的 wxml。并且可自定义插件,操作简单快捷。
试用了下编辑和输出,的确结果是我们想要的样子:
剩下的事情况就相对简单了,先设计一套可以在前端展示的 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 的编辑器,只是大家的方案各有不同。