在前端性能优化中应用HTTP缓存的三部曲

Spike先生是Best Experience公司的IT运营部门主管,他的团队成功地利用Http Cache优化了前端工程。

Spike将通过三个Scenario来展示他的团队是如何做到这一点的:

  • 通过配置Http Cache Expire来消减访问压力,提高用户体验
  • 通过版本化来强制失效本地的过期缓存
  • 通过内容摘要命名文件来更精确的控制缓存以及实现非覆盖式的发布

第一个故事:我不想要那么多服务器和带宽

Best Experience面临的资源访问压力和用户体验方面的问题

随着Best Experience提供的前端应用越来越强大,Spike的压力也越来越大:

  • IT部门为了应对来自静态资源的访问压力,不断购置服务器和带宽。
  • 糟糕的用户体验使得用户转投到竞争对手的网站。

工程师们刚刚通过应用Minify、AMD、打包、Gzip等手段优化了前端页面的体验, 最终得到如下图所示的一个资源引用关系:

“还是很多东西要下载啊,该拿什么来拯救该死的延迟呢?”——Spike看着图想到。

他突然想起来:在早年间,Yahoo曾发布了关于优化前端体验的35条建议和指导,其中第三条是:“Add an Expires or a Cache-Control Header”。

Yahoo是这样描述这条建议的:

Web page designs are getting richer and richer, which means more scripts, stylesheets, images, and Flash in the page. A first-time visitor to your page may have to make several HTTP requests, but by using the Expires header you make those components cacheable. This avoids unnecessary HTTP requests on subsequent page views. Expires headers are most often used with images, but they should be used on all components including scripts, stylesheets, and Flash components.

Browsers (and proxies) use a cache to reduce the number and size of HTTP requests, making web pages load faster.

“这个正是我寻找的银弹”——Spike得意的笑了。

于是,Spike写下了第一个Technology Story。

作为IT 部门的老大:

我希望通过应用HTTP缓存技术,重用已经下载过的资源,

用于消减用户在浏览页面时产生的不必要的Http Request。

以此,来提升用户在浏览页面时候的体验,

以及降低对于公司服务器资源的访问压力。

并找来了工程师Tom。

Expire带来的美好生活

Tom刚刚参与了前一轮的优化工作,虽然成果显著,但是他并不满足。

当Tom看到Jim写下的Story时眼前一亮:“这个方法太赞了!我甚至可以在登录页面底部放置对其他页面资源的引用。提升用户在整个网站的浏览体验。”——Tom的小宇宙瞬间爆发,很快就完成了新的优化方案。

Best-Experience的用户在接下来的时间里浏览页面,会这样下载资源,以图片bgimage.png为例:

  • 用户第一次获取图片的时候,Http Request 如图:

  • 之后用户再次获取图片的时候,则完全可以从浏览器的缓存中读取数据了。

因为采用了Http缓存方案,

  • 用户的feedback越来越好,访问量提高了;
  • IT部门也不用那么多服务器和带宽了。

财务总监邀请Spike共进晚餐,并谈起了自己在希腊的度假。

“我想我也应该去圣托里尼度个假,犒劳下自己”——Spike美滋滋的想到。

第二个故事:失效缓存是个技术活

这个BUG我们明明修了啊!

一天,QA Tyke发现最近一轮发布的前端应用中没有包含很多新的feature。Jerry承诺说已经跟着这个月的release上线了,还测试过了。经过一番折腾,Jerry发现浏览器一直在使用旧的缓存,而不是最新的版本。Spike找来了Jerry 和Tom,三个人一起手动对引用的资源做了重命名、做了紧急修复。

“真是没有银弹啊,我的圣托里尼啊!”——Spike头疼的想到。

Spike、Jerry、Tom和Tyke坐在了一起,得出了新的结论:

  • 缓存前端工程中的资源时,需要考虑缓存有效期的问题
  • 虽然35条建议和指导中建议“Configure ETags”,但是很难确定静态资源缓存的有效期
  • 虽然Http缓存可以支持No-Cache或者max-age =0的方式,保证浏览器每次都向服务器验证缓存有效性,但是这样会大大增加服务器的压力
  • 可以通过在资源引用上增加形如:<…. src=”###.js?v=$version$”>的版本化方式,来强制浏览器更新缓存。

Spike写下了新的Technology Story

作为IT部门的老大:

我希望在前端系统中,对引用的静态资源进行版本化管理。

使之既可以通过Http缓存来提升用户体验,降低服务器压力;

也可以方便用户即时获得更新后的资源。

“这都10月了,看来是去不成圣托里尼了,总觉得这个方案哪里有问题”——Spike忐忑不安。

用版本机制来保证浏览器更新资源

Jerry和Tom(很难想象他们两怎么配合的)终于在前端工程中实现了自动化的资源版本化管理:用户在最初访问页面的时候,会得到这样一个资源引用:

而当新的版本上线后,用户会得到这样一个资源引用:

第三个故事:更精确的缓存管理和平滑升级

(这个案例来自于知乎的大公司里怎样开发和部署前端代码? 张云龙的回答,前一个 story的内容有涉及)

每次更新后的尖峰时刻

11月的Release后,运维人员Nibbles找到Spike,“这次上线以后,服务器压力突然剧增,从GA上看到用户花了很多时间在资源下载上”,Spike找来了Tom、Jerry、Tyke和Nibbles,几个人坐在一起分析原因:

“这是因为11月的部署完成后,前端应用引用的资源版本升级,所有缓存失效导致的”——Tom 想了想说

“所有的资源引用?我还以为我们能精确到每一个文件的更新呢”——Nibbles惊讶道

“如果单独标明每一个资源的版本,那么按照我们的实际情况来看,每次上线后访问压力就没那么大了”——Tyke

“我之前看WebPack做到了”——Jerry兴致勃勃的谈了起来。

“他们采用的是文件摘要的方式,就是用MD5对文件求值,如果两个文件是相同的,那么就求得同一个hash值;如果文件是不同的,就求得不同的hash值”——Jerry

“我们可以用这些文件的hash值作为版本号,就像这样”——Jerry

“能不能通过文件名做版本管理,我希望知道哪些文件是这次部署要移除的,哪些是新增的”——Nibbles

“这有什么问题么?”——Spike很疑惑

“明年不是要做CDN么?静态资源和页面文件会放置到不同的服务器上,很难做到页面文件和静态资源同批次更新,而且CDN的资源生效是有延迟的”——Nibbles

(关于 CDN 和非覆盖部署式部署,请参考张云龙的大公司里怎样开发和部署前端代码?前端工程之CDN部署

“恩,那么就这样吧,我回去写Story。”——Spike 一锤定音。

“还好,我们之前用了WebPack,这就简单了”——Jerry

Spike写下了第三个story

作为IT 部门的老大:

我希望能用文件hash来命名静态资源文件,

使之可以按照文件来控制缓存和部署

“我觉得这回是最后一个Story了”——Spike越来越乐观。

过渡到非覆盖式部署——大圆满?

如何应用WebPack的具体过程不再概述。

图片来源大公司里怎样开发和部署前端代码?

这样,Nibbles就可以很愉快的通过文件名比对,来分析每次部署变更的内容;而Best Experience未来上线的流程也会变为:

  • 先将新增的静态资源文件发布到静态资源服务器上
  • 验证新的静态资源是否正确发布
  • 服务器暂时离线,替换 html 文件等
  • 删除无用的静态资源文件

“终于可以踏踏实实过圣诞节了”——Spike看着日历。

总结

Spike的总结

年底了,Spike在年终总结中写到:

以后在实施前端工程中,我们可以通过:

  • 配置永不过期的本地缓存——节约带宽,提升用户体验
  • 采用文件摘要作为缓存依据——更精确的缓存控制
  • 采用CDN——降低用户请求资源时解析DNS的延迟
  • 利用文件摘要作为文件名——实现非覆盖式的部署,降低down time

我的总结

我引用前端工程之CDN部署一文中对非覆盖式、缓存设计、CDN这些解决方案间的前因后果做的总结:

如果考虑到项目开发阶段,那么这将是更为复杂的软件工程问题。在这个问题域中,还需要囊括文件压缩、合并、打包、重命名、目录设置等问题。还好Gulp、Webpack、FIS、AMD、RequireJS这些工具及对应的插件能帮助到我们。WebPack提供了Hash、ChunkHash、ContentHash,与此同时,社区提供了MD5-Hash。

当然这些都是关于工具的话题了,这次我们主要谈的是工程。浅谈前端集成解决方案里提到了前端领域的8个技术元素与分类,挺有意思的。

再终——没有银弹

我们的Spike先生来到了2月的北京旅游,放个带色的图:

我们自强不吸

在机场,Spike还是接到了Tyke的电话,“老爹啊,WebPack那个文件摘要不准啊……..”

“您好,因为天气原因,去往####的飞机延误,请您耐心等候……..”

“…….”


更多精彩洞见,请关注微信公众号:思特沃克

Share

前端组件化开发方案及其在React Native中的运用

文/刘先宁

本文首发于InfoQ:http://www.infoq.com/cn/articles/front-end-component-develop-and-application-in-react-native

随着SPA,前后端分离的技术架构在业界越来越流行,前端(注:本文中的前端泛指所有的用户可接触的界面,包括桌面,移动端)需要管理的内容,承担的职责也越来越多。再加上移动互联网的火爆,及其带动的Mobile First风潮,各大公司也开始在前端投入更多的资源。

这一切,使得业界对前端开发方案的思考上多了很多,以React框架为代表推动的组件化开发方案就是目前业界比较认可的方案,本文将和大家一起探讨一下组件化开发方案能给我们带来什么,以及如何在React Native项目的运用组件化开发方案

一、为什么要采用组件化开发方案?

在讲怎么做之前,需要先看看为什么前端要采用组件化开发方案,作为一名程序员和咨询师,我清楚地知道凡是抛开问题谈方案都是耍流氓。那么在面对随着业务规模的增加,更多的业务功能推向前端,以及随之而来的开发团队扩张时,前端开发会遇到些什么样的问题呢?

1. 前端开发面临的问题

  1. 资源冗余:页面变得越来越多,页面的交互变得越来越复杂。在这种情况下,有些团队成员会根据功能写自己的CSS、JS,这会产生大量的新的CSS或JS文件,而这些文件中可能出现大量的重复逻辑;有些团队成员则会重用别人的逻辑,但是由于逻辑拆分的粒度差异,可能会为了依赖某个JS中的一个函数,需要加载整个模块,或者为了使用某个CSS中的部分样式依赖整个CSS文件,这导致了大量的资源冗余。
  2. 依赖关系不直观:当修改一个JS函数,或者某个CSS属性时,很多时候只能靠人力全局搜索来判断影响范围,这种做法不但慢,而且很容易出错。
  3. 项目的灵活性和可维护性差:因为项目中的交叉依赖太多,当出现技术方案变化时,无法做到渐进式的、有节奏地替换掉老的代码,只能一次性替换掉所有老代码,这极大地提升了技术方案升级的成本和风险。
  4. 新人进组上手难度大:新人进入项目后,需要了解整个项目的背景、技术栈等,才能或者说才敢开始工作。这在小项目中也许不是问题,但是在大型项目中,尤其是人员流动比较频繁的项目,则会对项目进度产生非常大的影响。
  5. 团队协同度不高:用户流程上页面间的依赖(比方说一个页面强依赖前一个页面的工作结果),以及技术方案上的一些相互依赖(比方说某个文件只能由某个团队修改)会导致无法发挥一个团队的全部效能,部分成员会出现等待空窗期,浪费团队效率。
  6. 测试难度大:整个项目中的逻辑拆分不清晰,过多且杂乱的相互依赖都显著拉升了自动化测试的难度。
  7. 沟通反馈慢:业务的要求,UX的设计都需要等到开发人员写完代码,整个项目编译部署后才能看到实际的效果,这个反馈周期太长,并且未来的任何一个小修改又需要重复这一整个流程。

2.组件化开发带来的好处

组件化开发的核心是“业务的归业务,组件的归组件”。即组件是一个个独立存在的模块,它需要具备如下的特征:

(图片来自:http://serenity.bh/wp-content/)

  • 职责单一而清晰:开发人员可以很容易了解该组件提供的能力。
  • 资源高内聚: 组件资源内部高内聚,组件资源完全由自身加载控制。
  • 作用域独立: 内部结构密封,不与全局或其他组件产生影响。
  • 接口规范化: 组件接口有统一规范。
  • 可相互组合: 组装整合成复杂组件,高阶组件等。
  • 独立清晰的生命周期管理:组件的加载、渲染、更新必须有清晰的、可控的路径。

而业务就是通过组合这一堆组件完成User Journey。下一节中,会详细描述采用组件化开发方案的团队是如何运作的。

在项目中分清楚组件和业务的关系,把系统的构建架构在组件化思想上可以:

  1. 降低整个系统的耦合度:在保持接口不变的情况下,我们可以把当前组件替换成不同的组件实现业务功能升级,比如把一个搜索框,换成一个日历组件。
  2. 提高可维护性:由于每个组件的职责单一,在系统中更容易被复用,所以对某个职责的修改只需要修改一处,就可获得系统的整体升级。独立的,小的组件代码的更易理解,维护起来也更容易。
  3. 降低上手难度:新成员只需要理解接口和职责即可开发组件代码,在不断的开发过程中再进一步理解和学习项目知识。另外,由于代码的影响范围仅限于组件内部,对项目的风险控制也非常有帮助,不会因为一次修改导致雪崩效应,影响整个团队的工作。
  4. 提升团队协同开发效率:通过对组件的拆分粒度控制来合理分配团队成员任务,让团队中每个人都能发挥所长,维护对应的组件,最大化团队开发效率。
  5. 便于自动化测试:由于组件除了接口外,完全是自治王国,甚至概念上,可以把组件当成一个函数,输入对应着输出,这让自动化测试变得简单。
  6. 更容易的自文档化:在组件之上,可以采用Living Style Guide的方式为项目的所有UI组件建立一个‘活’的文档,这个文档还可以成为业务,开发,UX之间的沟通桥梁。这是对‘代码即文档’的另一种诠释,巧妙的解决了程序员不爱写文档的问题。
  7. 方便调试:由于整个系统是通过组件组合起来的,在出现问题的时候,可以用排除法直接移除组件,或者根据报错的组件快速定位问题。另外,Living Style Guide除了作为沟通工具,还可以作为调试工具,帮助开发者调试UI组件。

二、组件化开发方案下,团队如何运作?

前面大致讲了下组件化开发可以给项目带来的好处,接下来聊一聊采用组件化开发方案的团队是应该如何运作?

在ThoughtWorks,我们把一个项目的生命周期分为如下几个阶段:

组件化开发方案主要关注的是在迭代开发阶段的对团队效率的提升。 它主要从以下几个方面提升了开发效率:

1. 以架构层的组件复用降低工作量

在大型应用的后端开发中,为了分工、复用和可维护性,在架构层面将应用抽象为多个相对独立的模块的思想和方法都已经非常成熟和深入人心了。

但是在前端开发中,模块化的思想还是比较传统,开发者还是只有在需考虑复用时才会将某一部分做成组件,再加上当开发人员专注在不同界面开发上时,对于该界面上哪些部分可以重用缺乏关注,导致在多个界面上重复开发相同的UI功能,这不仅拉升了整个项目的工作量,还增加了项目后续的修改和维护成本。

在组件化开发方案下,团队在交付开始阶段就需要从架构层面对应用的UI进行模块化,团队会一起把需求分析阶段产生的原型中的每一个UI页面抽象为一颗组件树,UI页面自己本身上也是一个组件。如下图:

通过上面的抽象之后,我们会发现大量的组件可以在多个UI界面上复用,而考虑到在前端项目中,构建各个UI界面占了80%以上的工作量,这样的抽象显著降低了项目的工作量,同时对后续的修改和维护也会大有裨益。

在这样的架构模式下,团队的运作方式就需要相应的发生改变:

  1. 工程化方面的支持,从目录结构的划分上对开发人员进行组件化思维的强调,区分基础组件,业务组件,页面组件的位置,职责,以及相互之间的依赖关系。
  2. 工作优先级的安排,在敏捷团队中,我们强调的是交付业务价值。而业务是由页面组件串联而成,在组件化的架构模式下,必然是先完成组件开发,再串联业务。所以在做迭代计划时,需要对团队开发组件的任务和串联业务的任务做一个清晰的优先级安排,以保证团队对业务价值的交付节奏。

2.以组件的规范性保障项目设计的统一性

在前端开发中,因为CSS的灵活性,对于相同的UI要求(比如:布局上的靠右边框5个像素),就可能有上十种的CSS写法,开发人员的背景,经历的不同,很可能会选择不同的实现方法;甚至还有一些不成熟的项目,存在需求方直接给一个PDF文件的用户流程图界面,不给PSD的情况,所有的设计元素需要开发人员从图片中抓取,这更是会使得项目的样式写的五花八门。因为同样的UI设计在项目中存在多种写法,会导致很多问题,第一就是设计上可能存在不一致的情况;第二是UI设计发生修改时,出现需要多种修改方案的成本,甚至出现漏改某个样式导致bug的问题。

在组件化开发方案下,项目设计的统一性被上拉到组件层,由组件的统一性来保障。其实本来所有的业务UI设计就是组件为单位的,设计师不会说我要“黄色”,他们说得是我要“黄色的按钮……”。是开发者在实现过程中把UI设计下放到CSS样式上的,相比一个个,一组组的CSS属性,组件的整体性和可理解性都会更高。再加上组件的资源高内聚特性,在组件上对样式进行调整也会变得容易,其影响范围也更可控。

在组件化开发方案下,为了保证UI设计的一致性,团队的运作需要:

  1. 定义基础设计元素,包括色号、字体、字号等,由UX决定所有的基础设计元素。
  2. 所有具体的UI组件设计必须通过这些基础设计元素组合而成,如果当前的基础设计元素不能满足需求,则需要和UX一起讨论增加基础设计元素。
  3. UI组件的验收需要UX参与。

3. 以组件的独立性和自治性提升团队协同效率

在前端开发时,存在一个典型的场景就是某个功能界面,距离启动界面有多个层级,按照传统开发方式,需要按照页面一页一页的开发,当前一个页面开发未完成时,无法开始下一个页面的开发,导致团队工作的并发度不够。另外,在团队中,开发人员的能力各有所长,而页面依赖降低了整个项目在任务安排上的灵活性,让我们无法按照团队成员的经验,强项来合理安排工作。这两项对团队协同度的影响最终会拉低团队的整体效率。

在组件化开发方案下,强调业务任务和组件任务的分离和协同。组件任务具有很强的独立性和自治性,即在接口定义清楚的情况下,完全可以抛开上下文进行开发。这类任务对外无任何依赖,再加上组件的职责单一性,其功能也很容易被开发者理解。

所以在安排任务上,组件任务可以非常灵活。而业务任务只需关注自己依赖的组件是否已经完成,一旦完成就马上进入Ready For Dev状态,以最高优先级等待下一位开发人员选取。

在组件化开发方案下,为了提升团队协同效率,团队的运作需要:

  1. 把业务任务和组件任务拆开,组件的归组件,业务的归业务。
  2. 使用Jira,Mingle等团队管理工具管理好业务任务对组件任务的依赖,让团队可以容易地了解到每个业务价值的实现需要的完成的任务。
  3. Tech Lead需要加深对团队每个成员的了解,清楚的知道他们各自的强项,作为安排任务时的参考。
  4. 业务优先原则,一旦业务任务依赖的所有组件任务完成,业务任务马上进入最高优先级,团队以交付业务价值为最高优先级。
  5. 组件任务先于业务任务完成,未纳入业务流程前,团队需要Living Style Guide之类的工具帮助验收组件任务。

4.以组件的Living Style Guide平台降低团队沟通成本

在前端开发时,经常存在这样的沟通场景:

  • 开发人员和UX验证页面设计时,因为一些细微的差异对UI进行反复的小修改。
  • 开发人员和业务人员验证界面流程时,因为一些特别的需求对UI进行反复的小修改。
  • 开发人员想复用另一个组件,寻找该组件的开发人员了解该组件的设计和职责
  • 开发人员和QA一起验证某个公用组件改动对多个界面上的影响

当这样的沟通出现在上一小节的提到的场景,即组件出现在距离启动界面有多个层级的界面时,按照传统开发方式,UX和开发需要多次点击,有时甚至需要输入一些数据,最后才能到达想要的功能界面。没有或者无法搭建一个直观的平台满足这些需求,就会导致每一次的沟通改动就伴随着一次重复走的,很长的路径。使得团队的沟通成本激增,极大的降低了开发效率。

在组件化开发方案下, 因为组件的独立性,构建Living Style Guide平台变得非常简单,目前社区已经有了很多工具支持构建Living Style Guide平台(比如getstorybook)

开发人员把组件以Demo的形式添加到Living Style Guide平台就行了,然后所有与UI组件的相关的沟通都以该平台为中心进行,因为开发对组件的修改会马上体现在平台上,再加上平台对组件的组织形式让所有人都可以很直接的访问到任何需要的组件,这样,UX和业务人员有任何要求,开发人员都可以快速修改,共同在平台上验证,这种“所见即所得”的沟通方式节省去了大量的沟通成本。

此外,该平台自带组件文档功能,团队成员可以从该平台上看到所有组件的UI,接口,降低了人员变动导致的组件上下文知识缺失,同时也降低了开发者之间对于组件的沟通需求。

想要获得这些好处,团队的运作需要:

  1. 项目初期就搭建好Living Style Guide平台。
  2. 开发人员在完成组件之后必须添加Demo到平台,甚至根据该组件需要适应的场景,多添加几个Demo。这样一眼就可以看出不同场景下,该组件的样子。
  3. UX,业务人员通过平台验收组件,甚至可以在平台通过修改组件Props,探索性的测试在一些极端场景下组件的反应。

5. 对需求分析阶段的诉求和产品演进阶段的帮助

虽然需求分析阶段产品演进阶段不是组件化开发关注的重点,但是组件化开发的实施效果却和这两个阶段有关系,组件化方案需要需求分析阶段能够给出清晰的Domain数据结构,基础设计元素和界面原型,它们是组件化开发的基础。而对于产品演进阶段,组件化开发提供的两个重要特性则大大降低了产品演进的风险:

  • 低耦合的架构,让开发者清楚的知道自己的修改影响范围,降低演进风险。开发团队只需要根据新需求完成新的组件,或者替换掉已有组件就可以完成产品演进。
  • Living Style Guide的自文档能力,让你能够很容易的获得现有组件代码的信息,降低人员流动产生的上下文缺失对产品演进的风险。

三、组件化开发方案在React Native项目中的实施

前面已经详细讨论了为什么和如何做组件化开发方案,接下来,就以一个React Native项目为例,从代码级别看看组件化方案的实施。

1. 定义基础设计元素

在前面我们已经提到过,需求分析阶段需要产出基本的设计元素,在前端开发人员开始写代码之前需要把这部分基础设计元素添加到代码中。在React Native中,所有的CSS属性都被封装到了JS代码中,所以在React Native项目开发中,不再需要LESS,SCSS之类的动态样式语言,而且你可以使用JS语言的一切特性来帮助你组合样式,所以我们可以创建一个theme.js存放所有的基础设计元素,如果基础设计元素很多,也可以拆分位多个文件存放。

import { StyleSheet } from 'react-native'; 
module.exports = StyleSheet.create({   
        colors: {...},   
        fonts: {...},   
        layouts: {...},   
        borders: {...},   
        container: {...}, 
  });

然后,在写具体UI组件的styles,只需要引入该文件,按照JS的规则复用这些样式属性即可。

2.拆分组件树之Component,Page,Scene

在实现业务流程前,需要对项目的原型UI进行分解和分类,在React Native项目中,我把UI组件分为了四种类型:

  • Shared Component: 基础组件,Button,Label之类的大部分其它组件都会用到的基础组件
  • Feature Component: 业务组件,对应到某个业务流程的子组件,但其不对应路由, 他们通过各种组合形成了Pag组件。
  • Page: 与路由对应的Container组件,主要功能就是组合子组件,所有Page组件最好名字都以Page结尾,便于区分。
  • Scene: 应用状态和UI之间的连接器,严格意义上它不算UI组件,主要作用就是把应用的状态和Page组件绑定上,所有的Scene组件以Scene后缀结尾。

Component和Page组件都是Pure Component,只接收props,然后展示UI,响应事件。Component的Props由Page组件传递给它,Page组件的Props则是由Scene组件绑定过去。下面我们就以如下的这个页面为例来看看这几类组件各自的职责范围:

(1)searchResultRowItem.js

export default function (rowData) {
    const {title, price_formatted, 
            img_url, rowID, onPress} = rowData;
    const price = price_formatted.split(' ')[0];
    return (
        <TouchableHighlight
          onPress={() => onPress(rowID)}
          testID={'property-' + rowID}
          underlayColor='#dddddd'>
          <View>
           <View style={styles.rowContainer}>
                  <Image style={styles.thumb} source={{ uri: img_url }}/>
              <View style={styles.textContainer}>
                    <Text style={styles.price}>{price}</Text>
                    <Text style={styles.title} numberOfLines={1}>{title}</Text>
              </View>
            </View>
            <View style={styles.separator }/>
        </View>
           </TouchableHighlight>
  );}

(2)SearchResultsPage.js

import SearchResultRowItem from '../components/searchResultRowItem';
export default class SearchResultsPage extends Component {

  constructor(props) {
    super(props);
    const dataSource = new ListView.DataSource({
      rowHasChanged: (r1, r2) => r1.guid !== r2.guid});
    this.state = {
      dataSource: dataSource.cloneWithRows(this.props.properties),
      onRowPress: this.props.rowPressed,
    };
  }

  renderRow(rowProps, sectionID, rowID) {
    return <SearchResultRowItem {...rowProps} rowID={rowID}
                onPress={this.state.onRowPress} />;
  }

  render() {
    return (
      <ListView
        style={atomicStyles.container}
        dataSource={this.state.dataSource}
        renderRow={this.renderRow.bind(this)} />
    );
  }}

(3)SearchResultsScene.js

import SearchResults from '../components/searchResultsPage';
function mapStateToProps(state) {
  const {propertyReducer} = state;
  const {searchReducer:{properties}} = propertyReducer;
  return {
    properties,
  };
}

function mapDispatchToProps(dispatch) {
  return {
    rowPressed: (propertyIndex) => {
      dispatch(PropertyActions.selectProperty(propertyIndex));
      RouterActions.PropertyDetails();
    }
  };
}

module.exports = connect(
  mapStateToProps,
  mapDispatchToProps,)(SearchResults);

3.Living Style Guide

目前社区上,最好的支持React Native的Living Style Guide工具是getstorybook,关于如何使用getstorybook搭建React Native的Living Style Guide平台可以参见官方文档或者我的博客

搭建好Living Style Guide平台后,就可以看到如下的界面:

接下来的工作就是不断在往该平台添加UI组件的Demo。向storybook中添加Demo非常简单,下面就是一个关于SearchPage的Demo:

import React from 'react';
import {storiesOf, action} from '@kadira/react-native-storybook';
import SearchPage from '../../../../src/property/components/searchPage';
storiesOf('Property', module)
  .add('SearchPage', () => (
    <SearchPage request={{place_name:"London"}} 
        isLoading={false} search={action('Search called')}/>
));

从上面的代码可以看出,只需要简单的三步就可以完成一个UI组件的Demo:

  1. import要做Demo的UI组件。
  2. storiesOf定义了一个组件目录。
  3. add添加Demo。

在构建项目的storybook时,一些可以帮助我们更有效的开发Demo小Tips:

  1. 尽可能的把目录结构与源代码结构保持一致。
  2. 一个UI组件对应一个Demo文件,保持Demo代码的独立性和灵活性,可以为一个组件添加多个Demo,这样一眼就可以看到多个场景下的Demo状态。
  3. Demo命名以UI组件名加上Demo缀。
  4. 在组件参数复杂的场景下,可以单独提供一个fakeData的目录用于存放重用的UI组件Props数据。

4.一个完整的业务开发流程

在完成了上面三个步骤后,一个完整的React Native业务开发流程可简单分为如下几步:

  1. 使用基础设计元素构建基础组件,通过Living Style Guide验收。
  2. 使用基础组件组合业务组件,通过Living Style Guide验收。
  3. 使用业务组件组合Page组件,通过Living Style Guide验收。
  4. 使用Scene把Page组件的和应用的状态关联起来。
  5. 使用Router把多个Scene串联起来,完成业务流程。

四、总结

随着前后端分离架构成为主流,越来越多的业务逻辑被推向前端,再加上用户对于体验的更高要求,前端的复杂性在一步一步的拔高。对前端复杂性的管理就显得越来越重要了。经过前端的各种框架,工具的推动,在前端工程化实践方面我们已经迈进了很多。而组件化开发就是笔者觉得其中比较好的一个方向,因为它不仅关注了当前的项目交付,还指导了团队的运作,帮助了后期的演进,甚至在程序员最讨厌的写文档的方面也给出了一个巧妙的解法。希望对该方法感兴趣的同学一起研究,改进。


更多精彩洞见,请关注微信公众号:思特沃克

Share

HTML也可以静态编译?

More than React系列文章:

More than React(一)为什么ReactJS不适合复杂的前端项目?

More than React(二)React.Component损害了复用性?

More than React(三)虚拟DOM已死?

More than React(四)HTML也可以静态编译?


《More than React》系列的上一篇文章《虚拟DOM已死?》比较了Binding.scala和其他框架的渲染机制。本篇文章中将介绍Binding.scala中的XHTML语法。

其他前端框架的问题

对HTML的残缺支持

以前我们使用其他前端框架,比如Cycle.js 、Widok、ScalaTags时,由于框架不支持 HTML语法,前端工程师被迫浪费大量时间,手动把HTML改写成代码,然后慢慢调试。

就算是支持HTML语法的框架,比如ReactJS,支持状况也很残缺不全。

比如,在ReactJS中,你不能这样写:

class BrokenReactComponent extends React.Component {
  render() {
    return (
      <ol>
        <li class="unsupported-class">不支持 class 属性</li>
        <li style="background-color: red">不支持 style 属性</li>
        <li>
          <input type="checkbox" id="unsupported-for"/>
          <label for="unsupported-for">不支持 for 属性</label>
        </li>
      </ol>
    );
  }
}

前端工程师必须手动把 classfor 属性替换成 classNamehtmlFor,还要把内联的 style 样式从CSS语法改成JSON语法,代码才能运行:

class WorkaroundReactComponent extends React.Component {
  render() {
    return (
      <ol>
        <li className="workaround-class">被迫把 class 改成 className</li>
        <li style={{ backgroundColor: "red" }}>被迫把样式表改成 JSON</li>
        <li>
          <input type="checkbox" id="workaround-for"/>
          <label htmlFor="workaround-for">被迫把 for 改成 htmlFor</label>
        </li>
      </ol>
    );
  }
}

这种开发方式下,前端工程师虽然可以把HTML原型复制粘贴到代码中,但还需要大量改造才能实际运行。比Cycle.js、Widok或者ScalaTags省不了太多事。

不兼容原生DOM操作

此外,ReactJS等一些前端框架,会生成虚拟DOM。虚拟DOM无法兼容浏览器原生的DOM API ,导致和jQuery、D3等其他库协作时困难重重。比如ReactJS更新DOM对象时常常会破坏掉jQuery控件。

Reddit很多人讨论了这个问题。他们没有办法,只能弃用jQuery。我司的某客户在用了ReactJS后也被迫用ReactJS重写了大量jQeury控件。

Binding.scala中的XHTML

现在有了Binding.scala ,可以在@dom方法中,直接编写XHTML。比如:

@dom def introductionDiv = {
  <div style="font-size:0.8em">
    <h3>Binding.scala的优点</h3>
    <ul>
      <li>简单</li>
      <li>概念少<br/>功能多</li>
    </ul>
  </div>
}

以上代码会被编译,直接创建真实的DOM对象,而没有虚拟DOM。

Binding.scala对浏览器原生DOM的支持很好,你可以在这些DOM对象上调用DOM API,与 D3、jQuery等其他库交互也完全没有问题。

ReactJS对XHTML语法的残缺不全。相比之下,Binding.scala支持完整的XHTML语法,前端工程师可以直接把设计好的HTML原型复制粘贴到代码中,整个网站就可以运行了。

Binding.scala中XHTML的类型

@dom方法中XHTML对象的类型是Node的派生类。

比如,<div></div> 的类型就是HTMLDivElement,而 <button></button> 的类型就是 HTMLButtonElement

此外, @dom 注解会修改整个方法的返回值,包装成一个Binding

@dom def typedButton: Binding[HTMLButtonElement] = {
  <button>按钮</button>
}

注意typedButton是个原生的HTMLButtonElement,所以可以直接对它调用 DOM API。比如:

@dom val autoPrintln: Binding[Unit] = {
  println(typedButton.bind.innerHTML) // 在控制台中打印按钮内部的 HTML
}
autoPrintln.watch()

这段代码中,typedButton.bind.innerHTML 调用了 DOM API HTMLButtonElement.innerHTML。通过autoPrintln.watch(),每当按钮发生更新,autoPrintln中的代码就会执行一次。

其他HTML节点

Binding.scala支持HTML注释:

@dom def comment = {
  <!-- 你看不见我 -->
}

Binding.scala也支持CDATA块:

@dom def inlineStyle = {
  <section>
    <style><![CDATA[
      .highlight {
        background-color:gold
      }
    ]]></style>
    <p class="highlight">Binding.scala真好用!</p>
  </section>
}

内嵌Scala代码

除了可以把XHTML内嵌在Scala代码中的 @dom 方法中,Binding.scala 还支持用 { ... } 语法把 Scala 代码内嵌到XHTML中。比如:

@dom def randomParagraph = {
  <p>生成一个随机数: { math.random.toString }</p>
}

XHTML中内嵌的Scala代码可以用 .bind 绑定变量或者调用其他 @dom 方法,比如:

val now = Var(new Date)
window.setInterval(1000) { now := new Date }

@dom def render = {
  <div>
    现在时间:{ now.bind.toString }
    { introductionDiv.bind }
    { inlineStyle.bind }
    { typedButton.bind }
    { comment.bind }
    { randomParagraph.bind }
  </div>
}

上述代码渲染出的网页中,时间会动态改变。

强类型的 XHTML

Binding.scala中的XHTML 都支持静态类型检查。比如:

@dom def typo = {
  val myDiv = <div typoProperty="xx">content</div>
  myDiv.typoMethod()
  myDiv
}

由于以上代码有拼写错误,编译器就会报错:

typo.scala:23: value typoProperty is not a member of org.scalajs.dom.html.Div
        val myDiv = <div typoProperty="xx">content</div>
                     ^
typo.scala:24: value typoMethod is not a member of org.scalajs.dom.html.Div
        myDiv.typoMethod()
              ^

内联CSS属性

style 属性设置内联样式时,style 的值是个字符串。比如:

@dom def invalidInlineStyle = {
  <div style="color: blue; typoStyleName: typoStyleValue"></div>
}

以上代码中设置的 typoStyleName 样式名写错了,但编译器并没有报错。

要想让编译器能检查内联样式,可以用 style: 前缀而不用 style 属性。比如:

@dom def invalidInlineStyle = {
  <div style:color="blue" style:typoStyleName="typoStyleValue"></div>
}

那么编译器就会报错:

typo.scala:28: value typoStyleName is not a member of org.scalajs.dom.raw.CSSStyleDeclaration
        <div style:color="blue" style:typoStyleName="typoStyleValue"></div>
         ^

这样一来,可以在编写代码时就知道属性有没有写对。不像原生JavaScript / HTML / CSS那样,遇到bug也查不出来。

自定义属性

如果你需要绕开对属性的类型检查,以便为HTML元素添加定制数据,你可以属性加上 data: 前缀,比如:

@dom def myCustomDiv = {
  <div data:customAttributeName="attributeValue"></div>
}

这样一来Scala编译器就不会报错了。

结论

本文的完整DEMO请访问 ScalaFiddle

从这些示例可以看出,Binding.scala 一方面支持完整的XHTML ,可以从高保真HTML 原型无缝移植到动态网页中,开发过程极为顺畅。另一方面,Binding.scala 可以在编译时静态检查XHTML中出现语法错误和语义错误,从而避免bug 。

以下表格对比了ReactJS和Binding.scala对HTML语法的支持程度:

ReactJS Binding.scala
是否支持HTML语法? 残缺支持 完整支持
是否支持标准的style属性? 不支持,必须改用 JSON 语法 支持,既支持标准的style属性也支持style:前缀
是否支持标准的class属性? 不支持,必须改用className 支持,既支持class也支持className
是否支持标准的for属性? 不支持,必须改用htmlFor 支持,既支持for也支持htmlFor
是否支持HTML注释? 不支持 支持
是否兼容原生DOM操作? 不兼容 兼容
是否兼容jQuery? 不兼容 兼容
能否在编译时检查出错误? 不能

我将在下一篇文章中介绍 Binding.scala 如何实现服务器发送请求并在页面显示结果的流程。

相关链接

Share

从2016年11月期《技术雷达》看前端的未来

本文仅代表作者个人观点,来听听一个前端程序员的YY。

新一期的技术雷达有点出乎意料,使用new标签的框架、工具、技术、语言等等超过了一半——Vue.js、ES2017上榜,Three.js凭着VR的火又上榜了,还有熟悉的Electron,以及微前端的概念。

让我们先看看一些技术亮点。

前端在可见的未来

在那篇《最流行的编程语言JavaScript能做什么?》的文章里,我们看到了JavaScript在各个领域的应用。在这一期里,仍然有很多亮点(new):

Vue.js

Vue.js,如果你在使用Vue.js,那么你应该更加自信了,现在它已经被列入评估期。Vue.js是一个简单易上手的框架,并且相当的轻量,在最近的这段时间里,它发挥的相当的出色。

可惜,笔者现在在用Angular.js 和 Angular 2,毕竟我现在的所做的事情是开发混合应用。不过相信在半年后,Angular 2 和 Ionic 2是会上榜的。

Ember.js,我现在对这个框架还缺乏深入的了解,而且还没有证据表明它会在国内火起来。

ECMAScript 2017,尽管我现在已经倾向于使用TypeScript,不过ES2017还是会用到的,只是我觉得Babel对我来说就是个坑。

PWA

Electron,我在很多场合中都使用过这个框架,从NodeWebkit开始写编辑器,再到用Electron完成Growth 1.0的桌面版。

Physical Web,现在我们可以通过蓝牙低功耗技术在浏览器上控制真实世界。

不过与此相比,我更看好 Progressive Web App,毕竟他可以让Web应用接触到更多的底层API,而不是局限于蓝牙,还可以是Push Notification等等。

VR

Three.js,它上榜的原因是WebVR的流行。这一点可以从我去年写的那篇《Oculus + Node.js + Three.js 打造VR世界》中可以看到一些趋势。这些就和现在的单页面应用一样,虽然运行起来不是那么流畅,但还是行得通。因而在可见的未来使用Web技术来开发VR也有一点苗头,未来浏览器上应该是可以运行编译过后的代码,而不是在运行时。

WebRTC,它可以让我们在浏览器端实现实时视频聊天。第一次接触到这个视频流技术是在两年多以前,上一次接触则是在半年多以前使用WebRTC + Oculus,你可以在我博客的那篇《JavaScript在VR世界的应用》中了解到更多的详细信息。当然如雷达所说,WebRTC将会形成未来在Web上进行AR/VR协作的基础。

接着再让我们看看一些架构上的变化吧。

前端引起的架构变化

在过去的两三年里,前端火得一塌糊涂——对于后端程序员来说,这有点winter is coming 的感觉。我在那篇《前端演进史》对前端的演进做了相当多的介绍,并在《后台即服务演进史》里对”后台即服务”开了个头,在这篇文章里让我们根据《技术雷达》来继续补几刀。

前后端分离

我们可以看到,很多中大型团队已经分解为前端和后台两个小组,沟通可以通过接口、契约等方式来进行。但是这一点儿也不精益,沟通在这时仍然是一个问题,让我有点怀念起之前前、后端都做的项目了——自己可以创建自己想要的接口。

不过,这意味着前端和后台在技术选型上更加独立了。

臃肿的前端——微前端

前端单体应用

在上一个项目里,我们一步步地将一个有近10年历史的系统替换掉。起初这是一个传统的Spring + JSP网站,然后我们用JSP创建了JSON API,后来创建了一个新的API来服务移动应用和单页面应用,再后来这个API被拆分成了几个API。我们的后台已经从一个单体应用变成了一个微服务架构的应用,但是这一点并没有在前端上应用——前端应用正在变得难以维护。

因此在这一期的雷达里,你可以看到微前端的概念(micro frontends)。这也是在上一个项目里,我们尝试做的一部分,遗憾的是并没有完全成功实施。这是一个搜索类型的网站,网站的首页承担着大部分的访问量,而详情页的主要流量来源则是搜索引擎。我们在首页上使用jQuery + Require.js技术栈,而在其他页面(搜索结果页 + 详情页)使用 React.js,我们在最初的时候考虑过将详情页静态化——因为需要SEO的缘故,这样可以让我们降低SEO带来的复杂度。

MicroServices

后来,我也在我的博客上解耦了两部分,为了更快的访问首页——将首页独立出来,不使用JS,直接使用Pure.css来担重任;在其他页面里使用Material Design Lite作为UI部分。

有一点值得考虑的是:对于微服务架构来说,在一个系统的不同部分使用不同的技术栈是一种不错的体验;而对于一个前端团队来说,在同一个系统中使用不同的技术栈就不是一种不错的体验。

API设计——应该变得简单

Backend

如我们所见的Spring Boot已经变成推荐采用的程度了,按雷达上的习惯用语:“我们已经在多个项目上使用这个框架”——反正我最近的项目都是用这个框架。如果你考虑使用 Java,那么一定不要错过这个框架,以及使用这个框架来实施前后端分享。

对于大部分不需要考虑SEO的应用来说,将后台变成一系列RESTful的API并不是一件复杂的事,但是在后台API上的设计就变成一件麻烦的事。因此尽管在实践的过程中有契约作为保证,但是不一定是可靠的。作为一个前端程序来说,我们在调用后台API的过程中,总会遇到这样、那样的问题。除此,还有接口不好用的问题——“要是你可以在这里使用超媒体API,那么我的代码就会更加简单了”。

因此在 API 设计上,雷达上给出了两个不错的案例:

强化后台查询

GraphQL

代表例子就是Facebook的GraphQL,它是在Facebook内部应用多年的一套数据查询语言和 runtime。原本为了请求一个用户及其好友信息,需要发起多个API请求。现在,我们只需要在客户端拼装好对应的Query语句,在这个语句里将大部分需要查询的东西写好,即 JSON格式的数据,然后发给服务端来处理。而在我们客户端上,我们所获取到的结果都是我们所需要的,不需要再做特殊处理了。

这一切,看上去很美好——除了,在客户端上拼查询语句。

过去,我们使用搜索引擎来搜索数据,就需要在前端拼好对应的Query,再传给后台API,由后台API返回我们需要的结果。在这个过程里,我们在Query做一些对应的数据处理。

反正,他们都是使用查询语言来搜索结果。如果你考虑使用QL的话,不妨做一层Wrapper,以后好做迁移。

前后端同时优化

Falcor

Netflix在这样复杂的API请求下,创建了自己的库Falcor——它可以从多个数据源获取数据,并在服务端上汇总成一个JSON model;在客户端上,请求时我们只需要加上对应的参数即可——可以将多个请求合并到一起,也可以只针对某一个部分发出请求。这样可以降低发出多个请求所带来的复杂度。

我想,一种最实用的做法:就是将一些更新频率较低的API合并成一个大的API(大部分人都会这样做吧)。

简化的后台——无服务器架构

ServerLess

除了上面的这些内容,后台还有一些东西蛮好玩的,其中一个就是Serverless架构,即无服务器架构。不过,这种架构目前在国内运行起来还是有点难度的,缺少一系列的配套措施。如在这期的雷达上,Auth0可以为我们提供一个授权服务,以及AWS Lambda可以直接使用 AWS系列云服务来对数据进行处理。

关于这期技术雷达我就不多说了,读者可以自己去看。点击原文就可以获取最新一期ThoughtWorks技术雷达。

那么未来,你想玩哪种技术。

Share

虚拟DOM已死?

More than React系列文章:

More than React(一)为什么ReactJS不适合复杂的前端项目?

More than React(二)React.Component损害了复用性?

More than React(三)虚拟DOM已死?

More than React(四)HTML也可以静态编译?


本系列的上一篇文章《React.Component损害了复用性?》探讨了如何在前端开发中编写可复用的界面元素。本篇文章将从性能和算法的角度比较 Binding.scala 和其他框架的渲染机制。

Binding.scala 实现了一套精确数据绑定机制,通过在模板中使用 bindfor/yield 来渲染页面。你可能用过一些其他 Web 框架,大多使用脏检查或者虚拟 DOM 机制。和它们相比,Binding.scala 的精确数据绑定机制使用更简单、代码更健壮、性能更高。

ReactJS虚拟DOM的缺点

比如, ReactJS 使用虚拟 DOM 机制,让前端开发者为每个组件提供一个 render 函数。render 函数把 propsstate 转换成 ReactJS 的虚拟 DOM,然后 ReactJS 框架根据 render 返回的虚拟 DOM 创建相同结构的真实 DOM。

每当 state 更改时,ReactJS 框架重新调用 render 函数,获取新的虚拟 DOM 。然后,框架会比较上次生成的虚拟 DOM 和新的虚拟 DOM 有哪些差异,进而把差异应用到真实 DOM 上。

这样做有两大缺点:

  1. 每次 state 更改,render 函数都要生成完整的虚拟 DOM,哪怕 state 改动很小,render函数也会完整计算一遍。如果 render 函数很复杂,这个过程就会白白浪费很多计算资源。
  2. ReactJS 框架比较虚拟 DOM 差异的过程,既慢又容易出错。比如,你想要在某个 <ul> 列表的顶部插入一项 <li> ,那么 ReactJS 框架会误以为你修改了 <ul> 的每一项 <li>,然后在尾部插入了一个 <li>

这是因为 ReactJS 收到的新旧两个虚拟 DOM 之间相互独立,ReactJS 并不知道数据源发生了什么操作,只能根据新旧两个虚拟 DOM 来猜测需要执行的操作。自动的猜测算法既不准又慢,必须要前端开发者手动提供 key 属性、shouldComponentUpdate 方法、componentDidUpdate 方法或者 componentWillUpdate 等方法才能帮助 ReactJS 框架猜对。

AngularJS的脏检查

除了类似 ReactJS 的虚拟 DOM 机制其他流行的框架比如 AngularJS 还会使用基于脏检查的定值算法来渲染页面。

脏检查算法和 ReactJS 有一样的缺点无法得知状态修改的意图这使得 AugularJS 必须反复执行`$digest`轮循、反复检查各个ng-controller中的各个变量。除此之外,AngularJS 更新 DOM 的范围往往会比实际所需大得多所以会比 ReactJS 还要慢。

Binding.scala的精确数据绑定

Binding.scala 使用精确数据绑定算法来渲染 DOM 。

在 Binding.scala 中,你可以用 @dom 注解声明数据绑定表达式。@dom 会自动把 = 之后的代码包装成 Binding 类型。

比如:

@dom val i: Binding[Int] = 1
@dom def f: Binding[Int] = 100
@dom val s: Binding[String] = "content"

@dom 既可用于 val 也可以用于 def ,可以表达包括 IntString 在内的任何数据类型。

除此之外,@dom 方法还可以直接编写 XHTML,比如:

@dom val comment: Binding[Comment] = <!-- This is a HTML Comment -->
@dom val br: Binding[HTMLBRElement] = <br/>
@dom val seq: Binding[BindingSeq[HTMLBRElement]] = <br/><br/>

这些 XHTML 生成的 CommentHTMLBRElement 是 HTML Node 的派生类。而不是 XML Node

每个 @dom 方法都可以依赖其他数据绑定表达式:

val i: Var[Int] = Var(0)
@dom val j: Binding[Int] = 2
@dom val k: Binding[Int] = i.bind * j.bind
@dom val div: Binding[HTMLDivElement] = <div>{ k.bind.toString }</div>

通过这种方式,你可以编写 XHTML 模板把数据源映射为 XHTML 页面。这种精确的映射关系,描述了数据之间的关系,而不是 ReactJS 的 render 函数那样描述运算过程。所以当数据发生改变时,只有受影响的部分代码才会重新计算,而不需要重新计算整个 @dom 方法。

比如:

val count = Var(0)

@dom def status: Binding[String] = {
  val startTime = new Date
  "本页面初始化的时间是" + startTime.toString + "。按钮被按过" + count.bind.toString + "次。按钮最后一次按下的时间是" + (new Date).toString
}

@dom def render = {
  <div>
    { status.bind }
    <button onclick={ event: Event => count := count.get + 1 }>更新状态</button>
  </div>
}

以上代码可以在ScalaFiddle实际运行一下试试。

注意,status 并不是一个普通的函数,而是描述变量之间关系的特殊表达式,每次渲染时只执行其中一部分代码。比如,当 count 改变时,只有位于 count.bind 以后的代码才会重新计算。由于 val startTime = new Date 位于 count.bind 之前,并不会重新计算,所以会一直保持为打开网页首次执行时的初始值。

有些人在学习 ReactJS 或者 AngularJS 时,需要学习 keyshouldComponentUpdate$apply$digest 等复杂概念。这些概念在 Binding.scala 中根本不存在。因为 Binding.scala 的 @dom 方法描述的是变量之间的关系。所以,Binding.scala 框架知道精确数据绑定关系,可以自动检测出需要更新的最小部分。

结论

本文比较了虚拟 DOM 、脏检查和精确数据绑定三种渲染机制。

AngularJS ReactJS Binding.scala
渲染机制 脏检查 虚拟DOM 精确数据绑定
数据变更时的运算步骤
  1. 重复检查数据是否更改
  2. 大范围更新页面,哪怕这部分页面根本没有修改
  1. 重新生成整个虚拟DOM
  2. 比较新旧虚拟DOM的差异
  3. 根据差异更新页面
  1. 直接根据数据映射关系,更新最小范围页面
检测页面更新范围的准确性 不准 默认情况下不准,需要人工提供keyshouldComponentUpdate才能准一点
需要前端工程师理解多少API和概念才能正确更新页面 很多 很多 只有@dombind两个概念
总体性能 非常差

这三种机制中,Binding.scala 的精确数据绑定机制概念更少,功能更强,性能更高。我将在下一篇文章中介绍 Binding.scala 如何在渲染 HTML 时静态检查语法错误和语义错误,从而避免 bug 。

相关链接

 


你想看到的洞见,都在这里

wechat

Share