likes
comments
collection
share

Web端如何实现智库类报告?PDF下载和分页难点分析

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

前言

智库类报告长什么样子?

这篇文章是对咨询公司合作和智库类报告web端实现的技术调研,如果对于这类报告没有太多了解的前端。可以点击查看一下艾瑞咨询-研究报告的相关示例。

以这份《2023年2月ToB数字化产业投融资月报》举例:

Web端如何实现智库类报告?PDF下载和分页难点分析

这类业务具有什么特点?

这类报告大多数是以Word、WPS等Office工具创建的,他们都有以下三个特点:

1、具有排版、打印和装订的需求

这个页面的纸张大小一般是标准的A4纸的大小(即210mm×297mm),需要支持打印和装订的需求。这和平时开发的后台看板,以及使用的大部分在线编辑器都不一样。

之前的网页都是在主容器内无限填充内容,假设是<main>标签,<main>的高度是由子元素决定的,超过浏览器可视区域高度后,出现浏览器的滚动条让用户滚动查看内容。

但是对于具有排版需求的报告业务来说,本质上是在 <main> 标签内填充大小固定的 <div> 标签,该 <div> 的宽高为标准的A4纸的宽高,每个 <div> 为一页。报告内容是在这个固定大小的<div>容器内进行展示的。

所以纸张大小问题是前端开发者首先需要考虑的事,一页纸张的宽高是210mm×297mm,我们可以在css浏览器中直接写例如mmcm等绝对长度单位,这是用习惯了pxvw单位长度的前端可能忽视的,具体可查看MDN-CSS的值和单位。我们也可以使用常用的px,A4纸对应的宽高为794px * 1123px

其次,分页问题是前端开发者需要考虑的事情,如何把内容放到指定宽高的<div>中?这样的数据结构有什么不同?如果一个内容过长了(例如一个过长的表格)该如何分页?

最后,封面、封底、页眉和页脚、目录也是开发者需要考虑的事情,这类的数据结构和报告类的内容是不同的,Model层的定义会不一样。

2、具有大量的图表和表格

Web端如何实现智库类报告?PDF下载和分页难点分析

可以看到这类报告类中具有许多的图表和表格,这些是报告的主体内容的呈现。对于前端开发者来说,图表库的选型是我们需要考虑的。

对于图表,我们可以考虑选择Echarts Antv (G2或者G2Plot) Chart.js,我倾向于使用Echarts💯,因为它比G2Plot支持的配置项更丰富,并相对于G2或者D3来说,上手难度更低,大部分开发者也有使用过的经验。

如果有地图类的图表需求,高德开放平台提供的SDK是最优的选择。

对于表格,你可以使用自己UI框架提供的表格,例如Vue生态的Element/Table,或者React生态的Antd/Table。这类表格都提供不仅简单表格,也提供相应的透视表、表头合并和单元格合并的复杂功能。当然,如果你的业务中有极为复杂的专业表格实现需求,可以使用阿里提供的Antv/S2💯,或者其他的DataGrid或者spreedsheet库,你可以jsspreadsheets查看更多的技术选项方案

3、富文本需求

在对报告的图表和表格进行描述时,会运用到加粗、换行、分段、染色和斜体的文字描述。你可以定义一个特殊的Model,然后将这个特殊的Model转换成HTML + CSS的方式。

但是更好的是使用富文本编辑已有的Model,此时我们需要使用到富文本组件,可选的方案很多TinyMCECKEditor5quill.jsDraft.jsSlate.js等。

有哪些实现方式,各方式有什么缺点?

在前面我们了解到了这类报告的业务特点后,前端可以有以下四种技术实现方案,这四类技术方案的实现难度、开发成本和用户的体验都是逐步递增的。我们可以自己选择,并且每一种方案也并不是互斥的,完全可以增量迭代开发。

Step1:传统实现方式

Web端如何实现智库类报告?PDF下载和分页难点分析

对于上面的页面,我们完全可以选择最简单的写死页面的方案。后端给到每个模块具体的数据,例如:

{
  "title": "2023年2月ToB数字化产业投融资月报",
  "data": {
    "menu": {},
    "chapter1": {
      "title": "2023年2月ToB数字化产业投资汇编",
      "text": "xxxxx",
      "tableData": []
    },
    "chapter2": {
      "title": "2023年2月ToB数字化产业投融资分析",
      "text": "xxxxx",
      "barData": {},
      "pieData": {}
    }
  }
}

前端然后转换成前端的Model,例如:

[
  [
    {
      "type": "cover",
      "data": "2023年2月ToB数字化产业投融资月报"
    }
  ],
  [
    {
      "type": "menu",
      "data": []
    }
  ],
  [
    {
      "type": "title",
      "data": "2023年2月ToB数字化产业投资汇编"
    },
    {
      "type": "text",
      "data": "xxxx"
    },
    {
      "type": "table",
      "data": []
    },
    {
      "type": "bar",
      "data": []
    },
    {
      "type": "pie",
      "data": []
    }
  ]
]

可以看到上面前端定义的Model是一个二维数组的形式,外层数组<Array>[]中的每个内层数组Array代表一页,Array中的每个对象{}代表前端组件,对象中的type表示组件类型,用于前端封装渲染组件使用。前端只需要在状态管理工具中编写一个transform()方法,将后端响应数据转换成前端各个组件可用的数据props即可。

以Vue举例,我们可以使用一个简单循环实现整个报告的生成:

<Page v-for="pageContents in pages">
  <v-component v-for="content in pageContents" :is="content.type"></v-component>
</Page>

这是一个平时编写普通业务的时候会采用的工作流方式,例如看板类、手机端页面的开发。但是采用这样的方式,我们虽然能够很简单的完成智库报告的快速交付,但是有两个最大的问题:

  1. 后端响应数据结构不具备可扩展性,如果不同报告之间后端的数据字段有变动,前端很难进行扩展维护
  2. 报告前端样式不具备可扩展性,不同报告之间可能会有样式上的定制化差异。例如同一个饼图组件的颜色不同、同一个柱状图有着完全不同的图表规则。

Step2:扩展前端Model

面对上面的的问题,我们完全可以从前端的Model入手,扩展前端的Model的能力,让其拥有支持跨报告、定制化报告的能力。

以一个柱状图组件举例:

{
  "type": "bar",
  "ui": {
    "color": [],
    "xAixs": {},
    "yAxis": {}
  },
  "config": {},
  "data": []
}

此时一份报告存储的就是这样的一个具有数据和UI样式规则的JSON文档。

[
	[component,component,component,component],
        [component,component,component,component],
	[component,component,component,component],
	[component,component,component,component]
]

前后端共同定义好每个组件字段的规则,并且落实在文档中,前端拿到后端响应,生成报告:

<Page v-for="pageContents in pages">
  <v-component v-for="content in pageContents" :is="content.type"></v-component>
</Page>

但是这样的报告同样存在这个巨量JSON维护的问题,对于定制化报告而言,需要频繁修改这个JSON的结构和规则。

此时可能需要有一个JSON编辑器去修改JSON结构。对于目前的代码编辑器而言,微软推出的monaco💯是唯一正确的选择。它可以让前端开发者完全继承VsCode的编码体验。

此时通过JSON编辑器 + 复杂Model(即JSON),这个报告基本就可以实现完整的定制化和可扩展性需求了。但是这个方式还有以下两个问题:

  1. 产品用户只局限于开发者:如果想要进行报告的配置,用户不仅需要接受JSON编辑器配置的方式,还要掌握每个字段的含义。上手难度基本就限制了报告的生产环节,只能由开发人员实现,而不能由真正掌握统计学知识和更加了解业务的业务方实现。
  2. 数据的扩展性差、后端维护工作繁重: 不同图表类型组件之间的前端数据格式是不一致的,例如散点图和柱状图的数据格式不一样;数据之间的数据源格式也不一样,例如某个表格选择的是MongoDB中某个集合的A、B、C键,另外一个表格选择的另外一个集合中的C、D、E键。并且后端甚至需要掌握完全没有必要掌握的前端UI相关的配置,此时生成这个巨量JSON的维护工作会全部会给到后端。

Step3:前端配置工具(类BI)

Step2中的两个问题,本质上是没有一个前端界面去配置UI和选择数据源

一旦拥有可配置的前端界面,我们不仅能够解决JSON配置上手难度高的问题,实现让业务方去进行智库类报告的配置。而且还能够很好的通过前端界面配置出完整的数据源规则,这样后端无需维护繁重的JSON字段规则,只需要开发选取数据源到映射数据的代码逻辑即可。

我们完全可以借鉴现有的BI解决方案。市面上有很多的产品可供参考,例如阿里云的QuickBI、微软的PowerBITableau等。

Web端如何实现智库类报告?PDF下载和分页难点分析

上面是阿里云QuickBI的一个参考图。在配置一个图表型组件的时候,我们只需要简单进行以下步骤:

  1. 选取数据集(数据集的前置步骤是选取数据源)
  2. 配置出哪个数据集字段用于使用图表中
  3. 配置图表样式

BI产品的使用流程大同小异,大体都是以上的步骤,这里就不做过多的介绍了。但是如果使用了前端的配置工具,会极大的加大前端的开发复杂度和成本,这个成本的增加不只局限于开发这个配置工具本身。还需要考虑以下两个个方面:

1、 前端的数据流会完全改变

之前的前端数据流逻辑是通过JSON直接获取,现在因为用户会去选择每个组件使用不同的数据字段,此时每个组件的数据会请求接口获取,再进行渲染。如果报告组件过多,此时我们还需要进行接口的批处理,避免出现成百上千个并发请求的情况。

2、前端需要做大量的数据处理工作: 通常在BI类产品中,前端拿到的数据都是非常原始的数据,你可以理解为一个原始表格,也就是一张大宽表。如下所示:

[
  { "name": "name", "key1": 1, "key2": 2, "key3": 3 },
  { "name": "name", "key1": 1, "key2": 2, "key3": 3 },
  { "name": "name", "key1": 1, "key2": 2, "key3": 3 }
]

前端需要根据不同的图表组件类型进行数据的处理并渲染,因为现在的Model数据不仅仅有UI和数据,而且还有数据集字段的配置,例如:

  1. 用户选取了1个数值类型的字段:生成一个饼图。
  2. 用户选取了1个数字类型字段 + 1个字符类型维度:生成折线图或柱状图
  3. 用户选取了2个数字类型字段 + 1个字符类型维度:生成多维折线图、柱状图或表格
  4. 用户选取了2个数字类型字段 :可生成散点图
  5. 用户选取了N个数字类型字段 :可生成表格

当然这部分逻辑也可以放到后端来做,但是这部分逻辑放到前端来做用户体验会更好(例如在图表类型切换的时候,可以进行实时的响应)。

虽然增加了大量的前端工作和复杂度,但是为了更好的用户体验和未来更少的维护成本,😀 这部分的工作是值得的。但是虽然BI类型产品这种方式已经完美,还有两个待解决的问题并没有得到解决:

  1. 我们的产品是一个具有排版、打印和装订的需求的A4报告。现在BI产品配置并不能满足所见即所得的排版需求。
  2. 报告类产品有很多的文字输入逻辑,最终交付的也是PDF型产品,采用BI的交互方式并不太符合Word的交互逻辑

Step4:完备的文档编辑器(在线文档)

如果采用在线文档的方式去实现报告的产出流程,那么会更加的符合大部分长期使用Office的用户的使用习惯,对于有着较多富文本支持的报告也更加友好。

现在常用的在线文档工具(阿里语雀、飞书文档),在富文本编辑器中插入各个自研功能模块是完全可以实现。

Web端如何实现智库类报告?PDF下载和分页难点分析

对于这类的富文本编辑器,我们在选型富文本编辑的时候不能选用开箱即用的富文本编辑器,而是应该使用具有扩展性的富文本编辑器框架,例如Slate.js,它支持自定义Block用来实现直观、具有富交互的Block节点,这些节点可以是我们报告中的图表组件、表格组件或者目录组件。

采用富文本的选型方案,前端需要将现有的组件Model集成到各个富文本编辑器框架的Schema定义中。这对于前端开发会更加具有挑战,需要前端拥有编辑器开发的相关经验。

还会遇到什么挑战?

完成了基本的前端实现方式的技术调研和选型后,对于这类报告业务,前端还有几个挑战需要解决。下面我们来对这几个挑战逐一分解。

1. 分页问题

前文中有提到过,一个A4纸的宽高在浏览器中是794px * 1123px。那么一旦一个页面的组件的总高度超过1123px的时候,我们就应该进行分页处理。

对于分页问题,我们应该编写一个独立的分页引擎去解决这个问题。

分页引擎需要解决以下几个问题:

  1. 对页面进行初步分页规则的处理(例如产品定义一级标题开启新的一页等)
  2. 获取到各个组件的高度
  3. 对各个组件的高度进行累加,一旦某个组件的高度累加后,当前页面总高度大于1123px。需要进行分页
  4. 分页完成后,保存各个页面的状态数据:
    1. Map结构保存页码=>页面内容高度的映射关系,方便之后页面高度有更改的时候不用二次计算
    2. Map结构保存页码=>页面拥有的组件列表的映射关系,方便之后计算目录和其他使用
  1. 监听组件的高度改变,如果当前组件的高度改变后,判断当前页的总高度是否大于1123px,如果为true需要进行分页。分页应该从当前页开始更新计算,避免重新计算前面不受影响的页面。

对于分页引擎实现几个问题中,有几个前端需要特别重视的问题

1、 如何进行分页?

对于像大部分的图表类型组件,例如散点图、折线图、饼图等,图表的高度本质上是固定的,就是图表容器Canvas画布的高度,图片也是如此。如果这类组件当前页放不下,直接挪到下一页即可

但是有以下三种组件类型需要注意⛔️:

  1. 表格组件
  2. 条形图(Y轴为维度、X轴为数据)
  3. 文本类型

Web端如何实现智库类报告?PDF下载和分页难点分析

这三类组件的高度是由数据的数量决定的,所以组件的高度会变得完全不可控,很有可能一个组件的高度就超过了整个A4的高度,此时我们需要进行以数据拆分的方式,去进行组件的拆分

例如1个条形图组件有200条数据,每条数据20px的高度,这个条形图需要占用200 * 20 = 4000px的高度。但是我们一个页面最多就只能有1123px,此时这1个条形图就需要在前端分页引擎中被拆分为4个条形图才能被展示。

⛔️表格就更加特殊,不能假设表格每行的高度是一致的,因为由于表格列可能文字过多出现换行的情况,所以需要遍历每一个tr的高度去进行高度的累加,以此进行表格组件的拆分。

🤔对于分页问题,这个当然不能算是一个最好的方法,但是对于像WPS和Google Docs那样的分页实现,笔者没有查看到太多的资料。对于前端分页算法有更好的实现方式或者推荐资料的,👏🏻欢迎评论区留言。

2、如何进行组件高度变化的监听

我们可以使用浏览器的MutationObserver去实现组件高度变化的监听,当用户选取的数据发生变化导致组件高度发生变化时,此时分页引擎进行判断是否需要分页。

3、如何避免在分页过程中出现屏幕闪烁?

从表格组件的特殊性可以看出,如果期望使用组件类型 + 组件数据数量这两个变量,计算出组件的高度是不准确的。(但如果表格的宽度规则能够完全自定义也是可以的)

如果想要最精确获取组件的高度,最好的方式还是渲染后直接获得。

那么如果是在报告页面直接渲染后进行分页操作,因为浏览器的重排和重绘,用户会有明显的屏幕闪烁,这是我们需要避免的。

此时最好的方案是在页面内使用一个不用分页的<div>,它的宽度是A4纸宽度794px,高度auto。这个<div>采用脱离文档流的定位方式,并且移除屏幕相当距离,保证用户不会看到。我们现在这个<div>中渲染出全量组件,并且拿到高度后进行分页。

分页完成后,再在报告的主体<div>中进行渲染展示。这样初次渲染的时候,就不会出现浏览器重排和重绘的产生,虽然组件在浏览器被渲染了两次,但整体的渲染性能提升了,避免出现对用户体验不佳的屏幕闪烁。

这样复制一个完全相同,但不可见的HTML来获取高度的方式,是十分常见的。例如ElementUI中可自适应高文本高度的文本域,其实就是在网页上复制了一个样式上一模一样,但不可见的文本域<textarea>,检测该副本文本域<textarea>中文本的高度,以此设置主体文本域<textare>height样式。

Web端如何实现智库类报告?PDF下载和分页难点分析

2. 目录问题

Web端如何实现智库类报告?PDF下载和分页难点分析

如果采用分页引擎解决了分页问题,目录问题就迎刃而解了,目录本质上是1-N级内容标题的前端展示。假设我们在报告配置规则中配置了规则是目录展示到2级标题

此时分页引擎中保存了页面的页码、各个页码中有哪些组件,示例如下:

{
  "1": [{ "id": "id_e503982f", "type": "title", "level": "1", "name": "1.1 一级标题" }],
  "2": [
    { "id": "id_85b73436", "type": "title", "level": "1", "name": "2.1 一级标题" },
    { "id": "id_03a642c0", "type": "title", "level": "2", "name": "2.1.1 二级标题" }
  ]
}

此时的目录显示如下所示:

1.1 一级标题 ···································· 1

2.1 一级标题 ···· ······························· 2

2.1.1 二级标题 ······························· 2

前端页面也可以通过增加#,或者Element.scrollIntoView()的API,实现网页点击跳转相应页面的效果。

但是如果你想生成的PDF实现点击跳转效果,PDF的目录页需要使用后端生成,具体可以参考OpenOfficeAPI(有一定的实现难度)。

3. 组件类型定义

在组件类型的定义上,我们不仅需要定义图表、表格、标题、文本等业务组件。有时候还需要定义一些UI布局组件,用于满足灵活的布局需求。这些布局组件,在用户配置的时候也需要使用。

Web端如何实现智库类报告?PDF下载和分页难点分析

上图所示,我们在前端代码上可能长这样:

<div style="display: flex">
  <!-- 左侧柱状图 -->
  <Bar></Bar>
  <!-- 右侧 -->
  <div style="display: flex; flex-direction: column">
    <!-- 右侧上部分 -->
    <div style="display: flex">
      <Pie></Pie>
      <Pie></Pie>
    </div>
    <!-- 右侧下部分 -->
    <Pie></Pie>
  </div>
</div>

此时<div></div>,应该变成各个<Layout>的UI布局组件,布局的组件Model的合理设计也是我们前端需要考虑的。可能的数据结构如下所示:

{
  "type": "layout",
  "direction": "row",
  "contents": [
    {
      "type": "bar",
      "data": []
    },
    {
      "type": "layout",
      "direction": "column",
      "contents": [
        {
          "type": "layout",
          "direction": "row",
          "contents": [
            {
              "type": "pie",
              "data": []
            },
            {
              "type": "pie",
              "data": []
            }
          ]
        },
        {
          "type": "pie",
          "data": []
        }
      ]
    }
  ]
}
Ï

4. PDF生成问题

方式1:前端实现(⛔️不推荐)

1、采用CSS打印样式表 + print.js或者浏览器打印的方式

采用这种方式,我们首先需要使用CSS的媒体查询,隐藏掉除了报告主体内容外的所有组件,并且确保A4的主体内容之间没有任何的margin

@media print {
  .aside {
    display: none;
  }
  .page {
    margin: 0;
  }
}

为什么要这么做?

因为这样能够确保浏览器中只有1024px*n页的高度,此时如果使用print.js或者【浏览器-打印-另存为Pdf】的方式,浏览器会根据1024px的A4高度为分割点进行页面切分,生成PDF。说白了,就是确保浏览器能够在正确的位置,进行正确的网页切分。

⛔️但是这样的实现方式其实会受到打印预设配置的内边距、浏览器兼容性等影响,实际效果经常不能达到正确的网页切分。

在分页实现中,有几个不常使用但十分重要的css配置需要掌握:

page-break-inside: avoid; 
page-break-after: always;
page-break-before: always;

2、采用html2Canvas

我们可以使用html2Canvas.js去截取每个.page元素(也就是每一页的<div>容器),生成图片。然后使用jsPdf创建,可以参考这篇文章《前端生成PDF文件实现方案》

这个方式也不推荐,它会存在两个严重问题:

  1. html2Canvas.js耗用内存严重,因为它会递归获取所有的元素并通过getComputedStyle的API获取样式后在Canvas上,这个过程会耗费大量内存。在100多页的PDF报告中可能会出现浏览器内存耗尽的现象。
  2. getComputedStyle绘制到Canvas的过程中,并不能完全还原所有浏览器的样式,例如阴影效果无法实现
  3. 前端生成时间耗时长

方式2:后端实现(✅推荐)

在上面前端实现的过程中,还有个最重要的问题是,无法在用户打开之前批量生成PDF报告。只有引入后端实现才能解决这个问题。

Web端如何实现智库类报告?PDF下载和分页难点分析

这里最推荐的是使用无头浏览器puppeteer去生成PDF。我们可以通过puppeteer,在服务端打开报告的网页端,然后获取到页面中每个页面元素,通过ElementHandle.screenshot()API文档)方法去截取图片。然后通过拼接图片的方式生成PDF文件。

puppeteer需要Node.js支持。如果后端的Node技术栈不成熟,Python也有替代方案pyppetter,其他的后端语言等也有对应的无头浏览器支持方案。

在后端实现的过程中,PDF生成基本不需要前端参与,有两点需要前端配合的问题:

  1. 前端网页渲染、请求数据、分页等过程需要耗时。前端可以在确认渲染完毕后,在网页中通知puppetter可以开始截图(通常可以插入一个特殊类名的<div>用作标识,puppetter定时器检测到标识存在,开始截图流程)
  2. 服务端可能会出现字体不存在的情况,导致用户看到的效果和服务端生成字体不一致。前端网页最好使用网络.woff字体,也可以使用Google Fonts中的开源字体,确保表现一致。

还有鉴权问题,报告网页本身需要鉴权打开的时候,服务端打开需要关闭鉴权。

总结

本文介绍了咨询公司和智库类报告的前端实现方式的技术调研,这类报告具有最显著的特点是具有排版、打印和装订的需求。 所以设计到两个难点:

  • 如何进行分页?
  • 如何进行PDF生成和打印?

对于分页,前端需要在渲染引擎外额外编写分页引擎来实现。PDF推荐使用后端puppetter实现后端截图创建的方式。这个PDF创建和前端渲染流程如下图所示:

Web端如何实现智库类报告?PDF下载和分页难点分析

除了技术难点,我们还探讨了报告产品+技术上的实现方式,各个技术方式的实现难度不一致,并且可以通过迭代达到以最好体验的文档编辑器的方式创建报告内容。

在前端设计前期,需要关注组件的Model的设计,务必让其可扩展、可组合、可灵活配置,并做到数据与样式分离。

转载自:https://juejin.cn/post/7208367266533883962
评论
请登录