背景
为什么是 React?
React 今年在国内特别火,一时间虚拟 DOM(Virtual DOM)等酷炫概念一下刷新了很多前端开发同学的三观,关于性能优劣的争论也在知乎上看到不少。不得不说 React 解决了一些前端项目开发的痛点,而我最近的一年多的工作重心,都在兴趣部落这样一个基于兴趣社交的 web 产品上,有很多感同身受的地方。兴趣部落这个产品从初期只有移动端的 2、3 个页面,发展到现在 50+移动页面,加上 PC 版的最近上线,中间经历了从 2-3 人的小项目到 10+人团队的大型前端项目的巨大转变。这个过程中除了人员相对业务的线性增加,代码量、维护成本也是以指数速度增长的,很快代码臃肿、难以维护与测试等问题就凸显出来。虽然内部经过一些轻量的重构优化,但开发模式还是与高度的迭代节奏很不匹配。这时候,React+Webpack 的组件开发模式让我眼前一亮,暗下决心要让这样的先进开发模式推广到项目团队,好东西一定要让大家有所受益,而不仅仅是技术的尝鲜、摆设。
为什么要在服务端渲染?
除了代码维护性的问题,项目代码膨胀导致的一个问题是基础库、公共资源的变大,从而导致页面加载性能日益下降。在使用 React 时,也第一时间考虑到对加载性能的影响,本来页面就要等待 ajax 返回数据,页面会不会因为 React 的引入变得更慢呢?不要说 React 那点大小根本不是事儿,在移动网络下(包含 2G 啊 T_T)任何资源都是很珍贵的。有没有同时提高代码维护性,又能提升页面加载速度的好事呢?答案终于回到主题,就是在服务端渲染 React。这里可以一并总结该方案的好处:
- 利于 SEO
- 加速首屏渲染速度
- 享受 React 组件式开发的优势:高复用、低耦合
- 前后端维护一套代码(代码同构)
原理
如果听到 AngularJS 在服务器端使用,你可能会很惊讶。但是对 ReactJS,完全不必如此,因为 React 很好的分离了 DOM 的操作,使得在服务器端输出页面字符串有了可能。最新的 React 版本 0.14.1 中,已经彻底将服务端需要的源码分离出来。
我们使用的核心 API 就是 ReactDOMServer.renderToString,它只会在服务端使用,并返回组件渲染数据后的 HTML 字符串。而接下来要做的,就是将这个 HTML 片段拼接回页面模版,返回到前端浏览器进行用户侧的显示。
这里还要补充一句,React 组件在服务端的生命周期方法,只会执行到 componentDidMount 之前的方法,因为在服务端没有组件挂到文档 DOM 树的概念。
核心步骤
Node 端的组件加载
我们要在 Node 端渲染组件,首先需要加载到组件,像这样:
1 2 |
var React = require('react'); var someComponent = React.createFactory(require('./SomeComponent.jsx')); |
但是 Node 默认状况是不懂怎么解析 JSX 文件的,所以要在之前加上:
1 2 3 |
require('node-jsx').install({ extension: '.jsx' }); |
React 组件渲染
加载到了 React 组件,等到后端的数据 data 也拿到后,就可以进行组件的渲染了:
1 |
var reactHtml = React.renderToString(someComponent(data)); |
data 是一个 json 对象,比如 data={'type':'test'},这时候 data 会以 props 的形式传递给组件,类似于在 jsx 中这样写:
1 |
<SomeComponent type="test" /> |
拼接返回 HTML
这时候组件的 HTML 已经渲染完毕了,可以作为 HTTP 的返回体的一部分返回到浏览器了,这时候大家可以根据自己的项目框架进行操作。我们在这里使用了 Koa 的框架,并使用了 ejs 的模版,所以可以参考代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
var render = require('koa-ejs'); render(this.app, { root: path.join(__dirname, '../view'), layout: false, viewExt: 'ejs', cache: true, debug: false }); yield this.render(templateName, { reactOutput: reactHtml, }); |
到这里基本就完成了在服务端进行 React 组件的渲染,但其中还有一些小问题,我们接着继续探讨。
延伸话题
关于前后端共用代码的问题
我们的 JSX 文件在前端与 Node 端是完全复用的,而服务端用到的实际代码比浏览器需要的更少,但还是有一些差异需要注意:
前后端环境的判断方法
我个人建议通过 window 对象来判断,因为之前使用过 moduel 对象来判断,会因为 webpack 的打包代码导致客户端侧的运行会有问题。
1 2 |
var isNode = typeof window === 'undefined', React = require('react'); |
资源加载差异化
我们在进行组件化开发的模式式,样式文件也是通过 require 方法来引入的,而 Node 同样是无法默认解析样式文件的。类似的,一些浏览器环境需要的工具脚本,一定是依赖浏览器 API 的(BOM 对象),所以也不应该在 Node 加载,于是就有了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
if (isNode) { // 服务端需要的资源 } else { // 组件样式 require('./Main.less'); // 工具 var ajax = require('js/ajax.js'); ... // 前端渲染组件,服务端渲染时不需要 var SlidingBar = require('../../components/SlidingBar.jsx'); // 一些mixin也不需在Node加载 ... } |
生命周期方法注意
前面也提到了,React 组件在服务端只有 componentDidMount 之前的方法会被执行,因此也要保证在 getInitialState、render 等方法中不会使用到浏览器 API,而将相应的操作放到 componentDidMount 中执行,如 ajax。另外在使用 mixin 时也需要注意。
关于前后端复用代码这事儿
有必要吗?
有同学会想,既然都支持服务端渲染了,全部走后端渲染呗,还搞啥前端部分的代码。(干脆我用 php 也能搞定,那可是世界上最好的语言,不就是动态内容嘛)
我是这么想的,原始的纯前端渲染还是有必要的,一是开发调试方便,二是在任何情况下都有另外的选择,比如目前我们采用的方案是集群的负载均衡,一个用户访问的请求,有可能是通过 html+js 的页面渲染,也有可能是服务器直接吐出渲染好的页面,只是其中的权重有所不同(服务端渲染的概率会更高)。当前端或后端逻辑出现异常或服务波动,对于用户来说都不受影响。
如何复用模版?
对于页面的容器来说,纯前端渲染的容器是 HTML 文件,而后端则是 ejs 的模版文件。发布时需要对二者同时发布,并需保持内容的一致性。这里可以通过构建工具来解决,服务端需要的 ejs 相对 html 文件只是多需要一些占位变量。
后端性能相关
请求放在后端渲染,势必会多一些压力点。除了需要在接入层做到负载均衡,在缓存上也可以考虑分布式缓存等优化策略。在数据超时的情况,也需要保证页面的正常输出,而此时返回的内容其实就等同于非服务端渲染的 HTML 页面内容。
写在最后
在 React 后端渲染的范畴内,目前在国内没有找到较为系统的实践总结,同时在 React 研发团队的支持方面,可以看到目前还有很多优化的工作正在计划(国外有不少开发者吐槽过 React 在服务端的性能)。但我希望可以有更多的人来尝试这个方向,并与我们进行交流,共同进步~
欢迎感兴趣的同学进行留言,或关注我的微博 http://weibo.com/lovelovelt
InfaceMan 2017 年 12 月 3 日
楼主,你好。我按照你的思路:服务端将 react 组件渲染成字符串 填充到 ejs 模板中,在浏览器中可以看到这个组件,但是点击这个组件没有响应事件。具体内容 麻烦楼主移步 https://segmentfault.com/q/1010000012256097 源码在 https://github.com/InfaceMan/express-react
小平子 2017 年 5 月 26 日
你好 请问一下为什么转到前端跑路由逻辑的时候 点击查看页面源代码时还是有页面内容 这相当于是静态输出了?
TAT.Johnny 2017 年 5 月 27 日
是说 HTML 返回时带了 JSON 数据吗?因为服务端渲染后,到浏览器时如果又拉取一次 ajax 数据,会导致两个问题:
1. 有一个 ajax 请求的耗时开销
2. 这期间如果有数据变化,会导致二次渲染的闪动(虽然数据在百毫秒级别间变动几率不大,但不能完全保证)
而直接跟随 HTML 返回服务端渲染使用的 JSON 数据,就能避免上述问题,在组件里判断全局变量是否有 JSON 数据,从而决定首屏渲染时是否需要发起一次 ajax 请求。
wx 2017 年 4 月 20 日
如果只是为了减少首屏加载时间,可以使用路由动态加载
TAT.Johnny 2017 年 5 月 27 日
服务端渲染带来的网络性能,是任何路由或其他技术能取代的,因为页面可以在 1-2 个 RTT 内呈现
梓淋 2017 年 3 月 27 日
我觉得灭有 SEO 的需求就没有必要, 服务端渲染, 更加节省资源的方式, 将渲染好的模板缓存. 有很多环境 并不是要实时渲染的.
kid 2016 年 12 月 27 日
import { renderToStaticMarkup } from ‘react-dom/server’function render(res, Component, data) { const a = ( ) const html = renderToStaticMarkup(a) res.send(html)}
kid 2016 年 12 月 27 日
a = < Component {…data} />
Kayson Li 2016 年 12 月 20 日
服务端渲染只对移动端的首屏加载有意义吧
ChanceKing 2016 年 11 月 7 日
ai--22 2016 年 7 月 27 日
想知道,css 如何处理的
豆瓣 2015 年 12 月 27 日
博主近期是否准备开源一个同构开发的示例站点呢?
maochengjie 2015 年 12 月 22 日
这两天也在折腾服务端渲染,确实对 client 的代码还是 有影响的,比如在 component 里面,有 var imgSrc = require(‘./assets/logo.png’) ,然后 render 里 ,
render(){
return (
);
}
因为我们在 node 里,关闭了图片、LESS、CSS 的 require,返回的 null,上面代码,会导致输出到浏览器后,React diff 发现和 server 渲染的不一致,会有个 warning。
不知道你们有没有遇到过呢
maochengjie 2015 年 12 月 22 日
额,这货把 render 里的 img 给我识别出来了。。。
应该是 <img src={imgSrc} />
panzj 2016 年 7 月 25 日
https://github.com/halt-hammerzeit/webpack-isomorphic-tools
木鱼 2015 年 12 月 17 日
现在客户端配置越来越强大,不充分利用客户端,反而把大部分的渲染放在服务端,不一定是明智的决定。
碧青 2015 年 12 月 21 日
中间有个平衡,原因在 “为什么要在服务端渲染?” 一节有讨论 [呵呵]
陈小亮 2016 年 6 月 29 日
网络是只要原因。 运行效率不是首次加载的重点。
王熊猫 2016 年 9 月 13 日
感觉很大程度上因为客户端性能参差不齐
飞越人海 2016 年 10 月 8 日
网络情况是短板,因为纯前端处理渲染要耗费至少来回两次 http 请求,而后端渲染所做的 “首页直出” 则只需要一次首页请求。
陈浩然 2015 年 12 月 14 日
[good] 很不错!
iM丁丁 2015 年 12 月 13 日
赞
xtx 2015 年 12 月 3 日
在 node 用 react 而又不想在客户端用 react,那 react 也就相当于一个模板引擎
碧青 2015 年 12 月 15 日
为什么不在客户端用 react 呀?
陈小亮 2016 年 6 月 29 日
用。但首页渲染可以在服务端先复用前端写好的组件。直接请求一个完整的页面到客户端。
TEE7 2015 年 11 月 27 日
选个好的 才有保证 这话没错
亦驰fantasy 2015 年 11 月 12 日
跟我们的方案几乎完全一样诶。 有时候很纠结为了首屏增加这么多代码复杂性是否值得。
碧青 2015 年 11 月 13 日
代码复杂性并没有增加多少,PC 上其实没有太大必要,除非有较强的 SEO 需求。
移动端上的首屏直出还是很有价值的,这也是选择服务端渲染的初衷 [呵呵]
陈鹏 2015 年 11 月 10 日
仅用来做了模板渲染?想问下,事件怎么办?还是没理解用 react 渲染比起普通的模板引擎渲染的优势在哪里?
碧青 2015 年 11 月 12 日
服务端渲染后,前端还是需要加载 react 的页面逻辑(所谓的前后端代码复用),前端还是会进行一次 react 的 render。但此时前端的 render 已经可以和直出的 HTML 进行 diff,按需做 UI 的局部刷新,事件就像纯前端的 react 使用一样,在 jsx 中进行绑定。
在后端,react 渲染比起普通的模板渲染,优势在于前后端的组件开发是可复用的。我们目前的开发模式是,现在前端进行开发,开发完成后,简单地做一些服务端的兼容(比如资源加载差异化,上面有讲),5 分钟就能把前端的逻辑,跑在服务端了。
react 在我眼中的价值是,组件可复用性和代码可维护性。
0rangeT1ger 2015 年 11 月 9 日
开始用 babel 吧,es6 愉快的用起!
李米 2015 年 11 月 6 日
node-jsx 不是已经弃用了么
碧青 2015 年 11 月 8 日
确实,建议使用 babel 了,但还是可用的,我最近更新一下这里的内容吧。
多谢建议 [呵呵]
hacke2 2015 年 11 月 2 日
[good]