技术型文章,感兴趣可以看看,来源闲鱼官方团队:《闲鱼技术》
背景
闲鱼会玩社区是一个以分享个人趣味生活为主的内容社区,在社区的运营过程中,经常有一些文章形式的内容在会玩广场上投放。此前文章创作是在一个主要承载营销搭建的平台上完成的,这种方式首先不是专门为文章场景服务的,导致搭建出来的页面是一个静态页面,数据无法被理解,也无法进入审核和分发链路。而且搭建工具无法对外使用,从而无法让外部的创作者参与文章创作。因此,我们想从零开始开发一个能快速发布且高可扩展文章结构形式的发布工具。
调研
业界也有许多同类型的产品,如常见的知乎、掘金、微信公众号等。微信公众号的文章无论从发布体验、消费体验以及内容结构扩展上都做得很好。我们总结微信公众号文章有以下几个特点:
闲鱼会玩文章希望能在内容形式,浏览体验上尽可能对齐微信公众号文章的标准。但闲鱼会玩文章与微信公众号也有一定的区别,微信公众号面向不同的自媒体,不同垂类的自媒体对文章的展示调性是不同的,因此对文章的内容样式不会做太多限制,而闲鱼的社区有着自己的调性以及自己的品牌主题色,内容形式会更加收敛。
目标
文章内容结构自由度高,内容排列可以是标题、文字段落、图片以及交互组件(如投票器、链接卡片等)的排列组合,因此高可扩展性非常重要。此外,发布器作为文章创作入口,需要存储创作者全部内容信息,包括字体、字号、颜色、段落、图片等。还有展示页在端上的性能体验也是非常重要的指标。因此我们定了以下几个系统的关键目标:
方案
围绕既定的设计目标,如何实现呢?我们的方案是根据一套约定的schema协议来表达文章内容的所有信息,协议记录和表达所有需要在客户端还原展示的信息,最后通过这套协议把发布器和展示页连接起来。可以看出,设计一套通用、简洁的文章内容协议是方案的关键。文章发布器负责产生schema,结构化存储后,文章展示页获取到该schema然后在端上解析协议并展示出对应的内容信息。方案大图如下所示,标橘黄色表示协议相关的部分,它出现在了几乎整个链路中。
协议设计
1.规则简洁易懂。规则越简单,则根据协议创建schema数据以及解析schema数据就越容易。
2.可扩展。未来的文章形式可以是非常丰富的,要求用这套协议表达任意的内容形式。
3.结构化存储。将图文内容可以进行结构化抽取和存储,文章内容的schema可能非常大,而数据库存储字段一般会有字符上限,需要压缩schema。此外文章的实体内容需要进入安全审核链路,这也要求我们必须进行内容结构化抽取。
围绕着以上三个诉求,我们我们基于钉钉的富文本协议,定制了一套符合要求的协议规则。
协议逻辑
现有元素标签
现有属性
如何扩展
我们规定未来所有新增的插件,都作为一种子类的卡片,可以自定义卡片类型,卡片数据统一放到metadata字段中,然后端上根据卡片类型做对应的组件,并将metadata中的卡片数据信息作为参数导入组件中去,从而做到未来任意插件,都能映射到协议里。
结构化存储
协议是一种json schema,这种在关系型数据库中存储是一个比较头疼的问题。图文内容存储会将内容分成三个字段,分别是文本、图片数组和自定义扩展字段。文本和图片的结构化信息会用于安全审核以及算法推荐识别,自定义字段用来存储业务上的其他信息。我们的第一版方案是将文本和图片内容提取出来单独存储,并将整个json字符串全部放到自定义字段中,这样只需要一存一读就可以了。然而,真实场景中,自定义字段有字符数限制。因此需要对json字符串进行一定的转译和压缩,只保留必要的样式和排版信息即可。
发布器
发布器的核心是一个富文本编辑器,市场上主流的react编辑器有开源的slate.js、facebook的draft.js,阿里内部比较成熟的富文本工具有语雀富文本编辑器和钉钉文档团队的we-editor。从协议的复杂度和可扩展性上考虑,我们选型了钉钉的we-editor。
富文本编辑器的使用上,除了要处理在编辑器上手写文章场景外,从外部粘贴内容进编辑器的场景复杂度会更高。因为内容本身就带有富文本样式,从而导致文章样式不可控,生成的富文本协议内容混乱,不利于结构化审核和端上展示页渲染。
粘贴场景处理
粘贴进来的内容是带上内联样式属性的标签,如div、span、a、h1、h2、img、video等。我们的做法是,在粘贴的时候,将所有的内联样式清除,并只处理格式范围内的标签。对于img、video标签,则需要更多的处理,因为img和video的src是一个地址链接,这些链接如果是站外连接,就会存在访问跨域,同时对平台来说也存在安全风险。做法是对站外资源做转存处理,即将站外链接下载后通过内部服务将资源转存到可靠的资源服务器上。
插件扩展
插件扩展通过定制编辑器插件来完成。我们只保留了基础的redo/undo、字体加粗、字体对齐、添加图片等编辑器自带能力,其他如视频、连接能力通过自定义插件完成。通过对we-editor插件体系封装,开发者可以像开发react组件一样开发插件。封装过程是将扩展插件都当作一种卡片,在schema里指定工具栏内容,对应点击事件,以及插入富文本的卡片样式等,即可插入任意插件。以一个插入视频的插件为例:
点击该插件工具栏按钮的时候,选择插入idleVideoCard类型的卡片即可。
函数服务层
在发布器的发布链路中,我们设计了一层faas函数服务层。主要考虑以下几个原因。
1.安全问题。结构化信息抽取算法应该放到服务端计算,否则会存在刷接口绕过安审链路的漏洞。
2.抽取和还原都通过js实现。从而保证一套规则技术栈统一。
文章展示
文章展示的原理是通过协议规则将经过处理的schema还原成真正的schema信息,并解析信息转化成对应的可视化组件。我将重点讲协议解析和性能优化。
协议解析
理论上只要能正确解析出富文本schema协议表达的富文本信息,在端上可以还原成任意对应的设计规范,这也是未来我们可以做集团统一文章发布工具的基础。即只要保证协议一致,端上的展示可以大不相同。还原函数伪代码如下所示:
前端的性能性能优化话题是一个老生常谈的话题,限于篇幅有限,这里只讲这次在文章详情页的几个主要优化方案。性能数据结果上,跨端首屏渲染时间从1700ms优化到1000ms左右,做到了秒开。先上优化前后效果对比:
在讲具体方案前,我们先来看一下一个h5页面在webview里是加载流程:
在这个链路中,最消耗时间的是各种资源的IO,包括页面文档的IO,样式文件、js文件和图片的IO以及数据接口请求的IO。其次耗时的是webview的启动耗时。因此我们的优化主要围绕减少IO以及提前IO的思路去进行。
-
资源combo
页面加载中除了包含业务的js文件之外,还包含jstracker的资源,rax的框架资源,安全相关的js等,将这些资源合并成一个资源请求,则可以减少很多次请求IO,从而降低首屏渲染时间。
-
图片懒加载
文章中一般会有很多图片,这些图片大部分不会在第一屏中就出现,因此可以将未出现在屏幕中的图片先不加载,等用户下滑至该图片出现在屏幕中时,才请求该图片资源。
-
本地资源缓存
文档资源下载以及js资源一般来说是一个长时间不变的东西,如果这些资源提前在客户端空闲的时候就已经下载好,等到请求这些资源的时候,客户端发现本地已经有了同名的资源,就拦截这次资源请求,返回本地缓存好的资源,则可以大大降低首屏渲染时间。
-
数据预取
首屏渲染的时间长短有一部分取决于第一次调用的接口的返回速度,而接口请求一般要等到js逻辑触发接口请求时才发出。如果首屏需要获取的请求是一个确定的参数,那么是否可以将请求接口这个时机提前呢?我们数据预取的方案就是在用户点开页面的请求URL中就带上首屏需要请求的接口参数,然后客户端获取到这个参数后异步地请求这个数据,将结果缓存到客户端上。等到js逻辑需要发出请求的时候,判断当前请求是否已经被请求过,若请求过,则直接返回缓存在客户端上的接口数据。
-
去掉客户端loading
客户端loading本来是webview自带的能力,表示当前页面还有资源在加载,但其实虽然有资源在加载,但首屏的页面信息其实已经加载出来的。去掉客户端的loading,可以给用户带来更快的体感,虽然实际上并没有加快这个流过程。优化后的加载流程如下图所示:
性能优化无止尽,因此文章展示页的优化也会一直做下去,可以考虑提前启动webview容器,ESR、NSR等方式优化。
展望
到目前为止已经完成了文章发布工具从0到1的建设,未来需要做的事情还有很多。基于这套高可扩展的协议,可以扩展出更多更丰富的文章内容形式以及交互玩法,如投票器、弹幕、文章模版等,最终可以沉淀出一套可开放的基于当前协议的模版插件开发体系,以适应不同的文章内容体系。此外,性能体验优化会继续以高标准的要求不断进行优化。
再往上层看,文章内容只是会玩社区内容的其中一种承载形式,我们希望有一个包含更多创作能力的创作者站点工具。pc侧的创作者中心和手机端上的创作者发布中心呈一个互补的态势。普通的创作者更倾向于即来即发,随手发,随时发,而对内容质量有更高追求的一些pgc创作者则更关注内容质量,发布效率,内容消费数据等比较专业的指标。
因此在完成了基本的发布能力后,我们会逐渐完善整个创作者发布链路,集普通图文创作、视频内容创作、文章创作、内容管理、数据中心、热点内容发现于一体的完整专业创作者发布工具。