Isomorphic JavaScript Applications – 解读JS应用同构

谈谈同构

有个俗套的出场:

Write once, run everywhere
这是一则Web世界关于Java的经典格言。但其影响的深远意义不局限于Web。

一套代码,跨平台,以下是前人尝试过的事情:
– 曾经的Adobe构建了庞大的Flash平台帝国,一套代码可以发布到Flash Player,Air,并且兼顾Web、PC和移动端(Android)。
– 曾经PhoneGap,JQuery Mobile + HTML5,打包分发到iOS、Android、WinPhone等移动设备。
– 在web端,前五年流行的响应式网站,一套CSS,吃遍所有屏幕。

种种例子,就是想做一件事情,用一套通用的标准满足存在差异化的环境需求。

再说回到Web世界

5年前,我曾经开发过这样的PC站点:
– 用户在页面内发生的访问请求,通过路由(#hash)参数控制内嵌iframe加载替换页面内容,而实际location不发生变化(当年的PC并不能兼容HTML5的History特性)。
– 用户也可以通过不同location访问到指定的页面,规则与路由参数一一对应,比如:
http://www.apptranz.com/wk/12345 <==> http://www.apptranz.com/#/wk/12345
– 对搜索引擎,只暴露 http://www.apptranz.com/wk/12345 这样的链接,即网站页面中的a标签采用这样的方式定位资源。因为搜索引擎会忽略URL中#之后的部分。

如此设计的原因简单说就2个:
1、实现SPA(单页应用/网站)的用户体验。
2、兼顾SEO。

在架构中,设计了两套渲染机制,即浏览器端使用JS模板渲染工具进行渲染,服务端使用JSP技术;除此外还设计了两套API实现,一套面向前端JS调用接口,一套面向服务端页面渲染,前端接口层封装了服务端的某些API。这样看起来,最大的差异不是在接口,而是在页面DOM的渲染实现,以及用户状态同步的相关细节。

开发过程中,同一个页面需要做一套JSP的模板、一套纯HTML(用于iFrame嵌套)。这两个页面会应用不同的安全机制,比如CSP策略、domain限制策略等。涉及的细节不胜枚举。

再说回到同构

事实上,在以上的架构中,前端实现SPA体验是最终目的。而SPA基于的是JavaScript与HTML。如今,由于Node.js的普及,客户端与服务端有了共同语言。所谓同构JavaScript,就是可以在客户端和服务端运行,只需要写一次代码,就可以在服务端渲染静态页面。

如今的SPA,核心的功能是状态管理和模板渲染。
以React为例,React中提出了虚拟DOM的概念,虚拟DOM以对象树的形式保存在内存中,与真实DOM相映射,通过ReactDOM的Render方法,渲染到页面中,并维护DOM的创建、销毁、更新等过程,以最高的效率,得到相同的DOM结构。
在服务端,React中还提供了两个方法:ReactDOMServer.renderToString 和 ReactDOMServer.renderToStaticMarkup,二者将虚拟DOM渲染成一段完整的HTML结构。

那么同构仅仅是将整个网页连同JS在服务端环境渲染一遍吗?其实不然。
React在服务端渲染的页面,在第一时间可以输出到浏览器,但是一个页面中还包括了各种事件响应、用户交互。这就意味着在浏览器,还得执行一段JS代码绑定事件、处理异步交互。而在React中,意味着整个页面的组件需要重新渲染一次,这样的重复劳动不是我们想看到的。

同构的奥义

在服务端和客户端中,使用完全一致的React组件,这样能够保证两个端中渲染出的DOM结构是完全一致的。

React 的虚拟 Dom 以对象树的形式保存在内存中,并存在前后端两种展露原型的形式。

客户端在渲染过程中,会判断已有的DOM结构是否和即将渲染出的结构相同,若相同,不重新渲染DOM结构,只是进行事件绑定。

除此外,React同构还涉及 Component 生命周期与客户端的 render 时机。
– 在前后端渲染相同的 Compponent,将输出一致的 Dom 结构。但不同的是生命周期,在服务端上 Component 生命周期只会到 componentWillMount,客户端则是完整的。
– 同构时,服务端结合数据将 Component 渲染成完整的 HTML 字符串并将数据状态返回给客户端,客户端会判断是否可以直接使用或需要重新挂载。

参考

同构JavaScript

发表评论

电子邮件地址不会被公开。 必填项已用*标注