React/Redux打造的同构Web应用

大家好,我是原一成(@herablog),目前在CyberAgent主要担任前端开发。

Ameblo(注: Ameba博客,Ameba Blog,简称Ameblo)于2016年9月,将前端部分由原来的Java架构的应用,重构成为以node.js、React为基础的Web应用。这篇文章介绍了本次重构的起因、目标、系统设计以及最终达成的结果。

新系统发布后,立即就有人注意到了这个变化。

 
twitter_msg.png

系统重构的起因

2004年起,Ameblo成为了日本国内最大规模的博客服务。然而随着系统规模的增长,以及很多相关人员不断追加各种模块、页面引导链接等,最终使得页面展现缓慢、对网页浏览量(PV)造成了非常严重的影响。并且页面展现速度方面,绝大多数是前端的问题,并非是后端的问题。

基于以上这些问题,我们决定以提高页面展现速度为主要目标,对系统进行彻底重构。与此同时后端系统也在进行重构,将以往的数据部分进行API化改造。此时正是一个将All-in-one的巨型Java应用进行适当分割的绝佳良机。

目标

本次系统重构确立了以下几个目标。

页面展现速度的改善(总之越快越好)

用于测定用户体验的指标有很多,我们认为其中对用户最重要的指标就是页面展现速度。页面展现速度越快,目标内容就能越快到达,让任务在短时间内完成。这次重构的目标是尽可能的保持博客文章、以及在Ameblo内所呈现的繁多的内容的固有形式,在不破坏现有价值、体验的基础上,提高展现和页面行为的速度。

系统的现代化(搭乘生态系统)

从前的Web应用是将数据以HTML的形式返回,那个时候并没有什么问题。然而,随着内容的增加,体验的丰富化,以及设备的多样化,使得前端所占的比重越来越大。此前要开发一个好的Web应用,如果要高性能,就一定不要将前后端分隔开。当年以这个要求开发的系统,在经历了10年之后,已经远远无法适应当前的生态系统。

「跟上当前生态系统」,以此来构建系统会带来许许多多的好处。因为作为核心的生态系统,其开发非常活跃,每天都会有许许多多新的idea。因而最新的技术和功能更容易被吸纳,同时实现高性能也更加容易。同时,这个「新」对于年轻的技术新人也尤为重要。仅懂得旧规格旧技术的大叔对于一个优秀的团队来说是没有未来的(自觉本人膝盖也中了一箭)。

升级界面设计、用户体验(2016年版Ameblo)

Ameblo的手机版在2010年经历了一次改版之后,就基本上没有太大的变化。这其间很多用户都已经习惯了原生应用的设计和体验。这个项目也是为了不让人觉得很土很难用,达到顺应时代的2016年版界面设计和用户体验。

OK,接下来让我具体详细聊聊。

页面加载速度的改善

改善点

系统重构前,通过 SpeedCurve 进行分析,得出了下面结论:

  • 服务器响应速度很快
  • HTML文档较大(页面所有要素都包含其中)
  • 阻塞页面渲染的资源(JavaScript、Stylesheet)较多
  • 资源读取的次数过多,体积过大

依据这些确定了下面这几项基本方针:

  • 为了不致于降低服务器响应速度,对代码进行优化,缓存等
  • 尽可能减少HTML文档大小
  • JavaScript异步地加载与执行
  • 最初呈现页面时,仅仅加载所需的必要资源

SSR还是SPA

近年来相比于添加到收藏夹中,用户更倾向于通过搜索结果、Facebook、Twitter等社交媒体上的分享链接打开博客页面。Google和Twitter的AMP, Facebook的Instant Article表明第一页的展现速度极大影响到用户满意度。

此外,从Google Analytics等日志记录中了解到在文章列表页面和前后文章间进行跳转的用户也很多。这或许是因为博客作为个人媒体,当某一用户看到一篇不错的文章,非常感兴趣的时候,他也同时想看一看同一博客内的其它文章。也就是说,博客这种服务 第一页快速加载与页面间快速跳转同等重要

因此,为了让两者都能发挥最佳性能,我们决定在第一页使用服务器端渲染(Server-side Rendering, SSR),从第二页起使用单页面应用(Single Page Application, SPA)。这样一来,既能确保第一页的展示速度和机器可读性(Machine-Readability)(含SEO),又能获得SPA带来的快速展示速度。

BTW,对于目前的架构,由于服务器和客户端使用相同的代码,全部进行SSR或是全部进行SPA也是可能的。目前已经实现即便在不能运行JavaScript的环境中,也可以正常通过SSR来浏览。可以预见将来等到Service Worker普及之后,初始页面将更加高速化,而且可以实现离线浏览。

z-ssrspa.png

以前的系统完全使用SSR,而现在的系统从第二页起变为SPA。

 
z-spa-speed.gif

SPA的魅力在于呈现速度之快。因为仅仅通过API获取所需的必要数据,所以速度非常快!

延迟加载

我们使用SSR+SPA的方法来优化页面间跳转这种横向移动的速度,并且使用延迟加载来改善页面的纵向移动速度。一开始要展现的内容以及导航,还有博客文章等最早呈现,在这些内容之下的次要内容随着页面的滚动逐渐呈现。这样一来,重要的内容不会受页面下面内容的影响而更快的显示出来。对于那些想尽快读文章的用户来说,既不增加用户体验上的压力,又能完整的提供页面下方的内容。

 
z-lazyload.png

之前的系统因为将页面内的全部内容都放到HTML文档里,所以使得HTML文档体积很大。而现在的系统,仅仅将主要内容放到HTML里返回,减少了HTML的体积和数据请求的大小。

HTML缓存

博客文章是静态文档,对于特定URL的请求会返回固定的内容,因此非常适合进行缓存。缓存使得服务器处理内容减少,在提高页面响应速度的同时减轻了服务器的负担。我们将不变的内容(文章等)生成的HTML进行缓存返回,对于由于变化的内容能过JavaScript、CSS等进行操作(比如显示、隐藏等)。

 
z-newrelic-entrylist.png

这张图显示了2016年9月最后一周New relic上的统计数据。文章列表页面的HTML的响应时间基本在50ms以下。

 
z-newrelic-entry.png

这张图是文章详细页面的统计数据。可以看出,这个页面的响应时间也基本上是在50ms以下。由于存在文章过长的时候会造成页面体积变大,以及文章页面不能完全缓存等情况,所以相比列表页面会存在更多较慢的响应。

对于因请求的客户端而产生变化部分的处理,我们在HTML的body标签中通过加入相应的class,然后在客户端通过JavaScript和CSS等进行操作。比如,一些内容不想在某些操作系统上显示,我们就用CSS对这些内容进行隐藏。由于CSS样式表会先载入,页面布局确定下来之后再进行页面渲染,所以这个也可以解决后面要提到的「咯噔」问题。

系统的现代化(搭乘生态系统)

技术选型

这次项目的技术选择时,遵循了尽可能采用当前当前市场上已经存在的普遍使用的技术这一原则。暗号就是:「活脱脱像范例应用一样Start」。这样一来,无论是谁都可以轻松的获取到相应的文档等信息,同时其它的团队和公司如果要参与到项目中来也能很快的上手。然而在真正进行开发的时候,一些细节实现上因为各种各样的原因存在一些例外的情况,但是在极大程度上保持了各个模块的独立性。最终系统的大体构成如下图所示:

 
z-bigpicture.png

(有些地方做了省略)

React with Redux

使用React和React进行开发的的时候,很多地方可以用 纯函数 的形式进行组合。纯函数是指特定的参数总是返回特定的结果,不会对函数以外的范围造成污染。使用纯函数进行开发可以保证各个处理模块最小化,不用担心会无意间改变引用对象的值。这样一来,十分有助于大规模开发以及在同一客户端中维持多个状态。

界面更新的流程是: Action(Event) -> Reducer (返回新的state(状态)) -> React (基于更新后的store内的state更新显示内容)

这是一个Redux Action的例子,演示了React Action (Action Creator) 基于参数返回一个Plain Object。处理异步请求的时候,我们参考 官方文档 ,分别定义了成功请求和失败请求。获取数据时使用了 redux-dataloader

Redux Reducer是一完全基于Action中携带的数据,对已有state进行复制并更新的函数。

React/Redux基于更新后的store中的数据,对UI进行更新。各个组件依据传递过来的props值,总是以相同的结果返回HTML。React将View组件也作为函数来对待。

有关Redux的信息在 官方文档 中说明得非常详细,推荐随时参考一下这个文档。

同构Web应用(Isomorphic web app)

Ameblo 2016年版基本上完全是用JavaScript重写的。无论是Node服务器上还是客户端上都使用了相同的代码和流程,也就是所谓的同构Web应用。项目的目录结构大体上如下所示,服务器端的入口文件是 server.js ,浏览器的入口文件是 client.js

  • actions/ Redux Action (服务器,客户端共用)
  • api/ 封装的API接口
  • components/ React组件 (服务器,客户端共用)
  • reducer/ <span class=”underline”>Redux Reducers</span> (服务器,客户端共用)
  • services/ 服务层模型,使用 Fetchr 对数据请求进行适当粒度的划分。同时这个也使得node.js作为代理,间接请求API(服务器专用)。
  • server.js 服务器入口(服务器专用)
  • app.js node服务器的配置、启动,由server.js调用(服务器专用)
  • client.js 客户端入口(客户端专用)
 
z-isomorphic.png

写好的JavaScript同时运行在服务器端还是客户端上的运行行为、以及从数据读取直到在页面上显示为止的整个浏程,都以相同的形式进行。

z-code-stats.png

使用Github的语言统计可以看出 ,JavaScript占了整个项目的94.0%,几乎全部都是由JavaScript写成的。

原子设计(Atomic Design)

对于组件的规划,我们采用了 原子设计 理念。其实项目并没有一开始就采用原子设计,而是根据 Presentational and Container Components ,对 containercomponent 进行了两层划分。然而Ameblo中的组件实在是太多,很容易造成职责不明确的情况,因此最终采用了原子设计理念。项目的实际运用中,采用了以下的规则。

 
z-atomic-design.png

Atoms

组件的最小单位,比如Icon、Button等。原则上不具有状态,从父组件中获取传递过来的props,并返回HTML。

Molecules

以复用为前提的组件,比如List、Modal、User thunmbnail等。原则上不具有状态,从父组件中获取传递过来的props,并返回HTML。

Organisms

页面上较大的一块组件,比如Header,Entry,Navi等。对于这一层的组件,可以在其中进行数据获取处理,以及使用Redux State 和 connect ,维护组件的状态。这里获取的组件状态以props的形式,传递给 MoleculesAtom

Template

各个请求路径(URL)所对应的组件。其职责是将所需的部件从Organisms中import过来,以一定的顺序和格式整合在一起。

Pages

作为页面的页面组件。基本上是把传递过来的 this.props.children 原原本本的显示出来。由于Ameblo是单页面应用,因而只有一个页面组件。

CSS Modules

CSS样式表使用 CSS Modules 将CSS样式规则的作用范围严格限制到了各个组件内。各个样式规则的作用范围进行限制使得样式的变更和删除更加容易。因为Ameblo是由许多人协同开发完成,不一定每个人都精通CSS,而且不免要时常对一些不知是谁何时写的代码进行更改,在这个时候将作用范围限制到组件的CSS Modules就发挥其作用了。

1 1 收藏 评论

相关文章

可能感兴趣的话题



直接登录
跳到底部
返回顶部