landerqi

我们是如何把模板渲染引擎转换为React的 | Pinterest Engineering(这应该是博客发布者或机构)

landerqi · 2017-01-05翻译 · 136阅读 原文链接

在2015年,为了使业务跟上我们的快速增长,提高开发效率,我们决定把我们传统的web体验迁移至React。最终,我们发现React比我们之前的模板引擎有更快的渲染速度,React在特性迭代上阻碍很少还有一个庞大的开发者社区。在之前的博客中,我们讲述了如何把Pinner个人资料页迁移至React,这里我们将深入迁移web基础架构来服务React页面,这需要在不破坏网站的情况下改动大量的代码。

React路标

当我们开始这个项目的时候,Pinterest.com在已经存在的旧架构上平稳运行有一段时间了。在服务端, Django,一个Python web应用框架,为我们提供web请求,Jinja 为我们渲染模版。服务端返回浏览器所需要的所有标记,资源及数据来获取我们的JavaScript, 图片及CSS样式,并且初始化我们的客户端应用。Nunjucks,一个与Jinja使用相同模版语法的JavaScript模版渲染引擎,其在客户端渲染所有的后续模板。

模板语法和堆栈看起来像这样:

由于Jinja和Nunjucks模板语法基本一样,这个架构正常工作了。然而,模版渲染的基础组件和共同库需要复用,就像上图所示,这就意味着我们每增加一个Jinja Python基础组件,就不得不为Nunjucks写一个JavaScript版本。这太麻烦了,会导致大量Bugs产生,并且还有些其他的原因使我们不得不找一个客户端和服务端使用同一种语言和引擎渲染模板的解决方案。

就像下面这个图标描述的,我们的最终目标是用React统一渲染模板:

这看起来非常棒:我们可以在客户端和服务端之间共享基础组件和共用库,并且我们有一个引擎,React,在两端渲染模板。但是我们要怎样实现呢?如果我们把客户端的渲染引擎从Nunjucks换成React,为了共享同样的模板语法,我们就不得不改变服务端渲染。为了把我们所有的模板替换成React而停止业务发展显然是不明智的。

我们需要一个解决方案:能够允许我们迭代的替换成百上千的Pinterest组件而不会防碍产品团队的工作及用户体验。这个解决方案看起来像这样:

第一步就是把客户端和服务端合并成一个模版渲染引擎,直到我们能够用其他的引擎替换为止。如果服务器可以解析JavaScript,使用Nunjucks来渲染模板并且共享我们的客户端代码,这样我们就可以迭代的迁移至React了。

服务端Nunjucks架构

最开始我们考虑在服务端如何解析JavaScript,这里有两个主要的选择:PyV8Node。PyV8的优势是能够快速建立原型并且不用太担心起一个独立的服务,但是它不好维护并且包的内存占用(memory footprint)巨大。

Node是一个更自然的选择,尽管新起一个服务的开销(the overhead of)和我们与这个服务通过网络接口通信其本身所具有的复杂性(在下一个部分会详细描述)。这里有一个庞大的社区支持与使用Node并且我们可以更好的控制调整与优化这个服务。

在最后,我们在Ngigx代理层后面起了个Node进程并且用这种方法设计接口,这样每一个网络请求都将是无状态呈现。这样可以允许我们把请求移交到进程组,并且在需要的时候扩大进程的数量。

我们还重构了客户端渲染javascript,使其能够同时被客户端和服务端使用。这样结果是一个具有可以接受不可知环境请求的API的同步渲染模块,并且使用Nunjucks返回最终的标记。Node和浏览器都可以调用这个模块来获取HTML。

在Web服务器,我们减少模板渲染回路来代替调用Jinja,它提供网络请求并且把模板渲染移交给我们的Node服务。

之前: Jinja 一次性渲染整个模块树

Pinterest模板是一个树状结构。一个根模块调用大量子模块,而子模块又可能含有子模块等等,渲染器通过遍历这些模块来生成最终的HTML。

每个模块不仅可以基于父模块的数据渲染,也可以通过网络请求获取更多的数据继续渲染。由于在我们到达节点前我们并不知道渲染路径,这些数据请求必然会阻塞。这意味着模块树渲染被下游的、任何时候都可以发起的数据请求阻塞。

这是因为Python是用单线程做所有的渲染,渲染阻塞了线程,而且本质上是串行的。

如上图:当USER AGENT发起一个请求,紫色的空心圆圈出现描绘了一个无数据的模块渲染请求。API被调用来获取数据,填充进圆圈,并且准备渲染传递。子模块渲染实体化,当到达的子模块需要数据的时候渲染将停止。随后会调用API来完成这些数据请求,然后继续渲染。

之后: Nunjucks 请求 over the wire(google也没搜索到这个词组意思,倒是搜到了一个叫Over The Wire的公司,他们提供电信和IT服务)

在最开始,一个USER AGENT 发起一个请求,这个请求导致了一个潜在的需要数据的模块渲染。调用API,数据被再次获取,但是另一个网络回调向同地协作Node进程发起用来渲染模板,一直伴随着它所具有的数据。

然后Node返回一个渲染好的模板,和一个“空洞”数组,这表明进程还不能渲染此模块,因为他们仍然需要数据。然后我们的Python Webapp调用API,提供他们所需要的数据,并且各个模块做为完全独立的模块请求平行的送回给Node。这个过程会不断重复直到全部模板树都渲染完毕以及所有的请求返回都没有“空洞”。

首次展示

对新系统的信心是它能做出来的关键。开发者仍然可以用Jinja来构建,也可以新建和修改新的Python基础组件,并且我们不得不确定新系统不会给用户加载页面时带来隐患。我们还得做错误处理,服务监控,告警,以及一个运行手册来维持新的Node进程的稳定和故障排查。

其中有相当多的依赖来确保一个平滑的转变,以及两个对项目成功非常重要的工具。

代码检测工具及单元测试 Jinja和Nunjucks语法非常相似,但是并不完全相同。各个模板引擎支持的差异,Python和Nunjucks的差异使我们不得不在工程师修改模板的时候严格限制。最终,我们需要确保服务端渲染的模板同样的可以在客户端渲染,而且Jinja渲染的模板同样也可以用Nunjucks渲染。

在Pinterest, 我们非常依赖构建时代码验证,这样可以阻止开发者在开发过程中做一些可能损坏网站的事情,这可以确保所有模板开发仅仅只使用Jinja和Nunjucks支持的特性子集。甚至我们写了一个特殊的可拓展的Nunjucks插件,这个插件可以定义我们自己写的通用的规则,就像ESLint一样,在构建过程中它可以应用到所有Nunjucks模板。我们还开发了一套特别的全包含的单元测试套件,叫做 “渲染所有的测试” ,它可以逐个渲染每一个模板,并且确保他们在Jinja与Nunjucks中,客户端与Node中渲染的完全相同。这帮助我们避免产生一些难以追踪的、疯狂的Bug。

Pinterest实验框架. 最开始我们只向内部员工开放了新的架构,然后向很小一部分的用户开放。我们通过实验面板观察追踪用户的行为和表现。在向大面积用户开放新系统之前,逐步开放使我们能追踪到一些棘手的渲染bugs,Python/JavaScript的差异以及性能问题。

举个例子:我们用实验面板捕获了一个微妙的仅仅在客户端存在的渲染bug,而且只影响很小一部分使用特定浏览器做一些特殊操作的用户。追踪这个行为允许我们细化bug,并且在修复之后可以得到验证。

性能

服务端渲染在Pinterest向用户提供富文本的时候扮演重要的角色。为了提供更快的体验及维持一个好的SEO,我们非常依赖高性能的服务响应时间。

在早期迭代期间,在服务端Nunjucks架构比我们之前的Jinja架构更慢。发起多个网络调用会带来额外的开销(准备请求,序列化和反序列化数据),并且往返也给我们渲染时间增加了重要的毫秒数。

我们做了两件事来减小差量,使我们可以开始工作。

平行化. 使用Jinja,我们不需要在网络协议上再调用一个附接的进程来渲染模板。然而,由于模板渲染的计算密集型本质,这也意味着Jinja模板渲染不能实现真正意义上的平行化。而我们的Nunjucks渲染调用不存在这种情况。使用gevent平行化我们的调用,我们可以同时抛送多个网络连接给我们的nginx代理层,这个代理层可以高效的把请求移交给可用的进程(workers:这里应该是node服务的workers)。

避免不必要的数据序列化. 有一些麻烦点在我们模板渲染里,那些返回给浏览器的需要嵌入大量数据的模板。这些麻烦点主要在静态的头部及body结尾标签周围,而且每个web请求都有。序列化和反序列化这些巨大的JSON二进制数据文件提供给我们的进程大大的降低了速度。避免这些,使用我们增加了其他的性能瓶颈,最终达到了相同的性能。

这里有个表示结果的图表(红色的是Nunjucks,绿色的是Jinja):

React正在持续发生

以前Nunjucks引擎就绪并且服务Pinterest.com所有模板,开发者就开始把他们的模块转换成React组件。今天,随着代码库里的Nunjucks代码正在快速被React替换,我们正在否决我们老的框架,并且很高兴能应对创建一个完全的React应用的挑战,而且我们从迁移中也看到了很多成绩及开发者生产力的提升。