探索 React 服务器端加载

Posted by Zhangjd on August 11, 2016

迅雷下载宝官网使用 React + react-router + redux 构建,出于 SEO 考虑,决定使用 Node.js 搭建一套服务端同构渲染的环境(称为 Universal app / Isomorphic app),在此记录遇到的问题和解决方案。

本项目的模板 repo 地址,供参考:https://github.com/Zhangjd/react-webpack-isomorphic-boilerplate

为什么可以同构渲染?

在 react 中,react 组件和渲染的概念已经分离出来了。比如从 v0.14 开始,最常用的浏览器端渲染 render() 方法分离到 react-dom 中,而在服务器端,我们想要得到的不是 DOM 对象,而是字符串形式而已,因此我们可以使用 renderToString() 方法,该方法位于 react-dom/server 中。正是因为这样的分离思路,react 才标榜自己 “learn once, use everywhere.”(虽然代码还不能完全复用)。

浏览器环境和 Node 环境的差异

Node 环境没有 window 对象,window 下面的子对象包括 location, navigator 等,直接在 node 环境使用会报错。

浏览器路由

React-router 使用 browserHistory 代替 hashHistory

URL 中的 hash 部分不会发送到服务器,因此服务器端不能根据 hash 区分请求 URL,还好 react-router 支持 browserHistory + HTML5 history API 方式的路由。注意的是,组件的 <a> 标签需要使用 react-router 的 <Link> 标签代替,这样改变路由很方便。

Redux

服务端把初始化的状态输出到一个全局变量 window.__INITIAL_STATE__ 中,然后浏览器端的 redux 入口调用 createStore 方法时,把 window.__INITIAL_STATE__ 传入到函数中。用法详见 Redux 文档

ES6 语法转换

Node.js v4 对 ES6 和 ES7 的支持还不全面(参见 https://nodejs.org/en/docs/es6),因此需要使用 babel v6 对源码作转换,方法也很简单,在入口加一句 require('babel-core/register') 即可。

Express

使用 Express 框架处理请求,注意区分开发模式和生产模式,开发模式需要引入 Webpack 中间件(见下文),生产模式对于静态资源直接读取磁盘文件。

Webpack

其实服务器端加载可以不使用 Webpack,直接加载打包后的静态文件亦可,在这里为了开发环境的方便,也在服务器端引入了 Webpack。

Webpack 要在服务器端使用,需要用到下面三个库。

webpack-isomorphic-tools

地址:https://github.com/halt-hammerzeit/webpack-isomorphic-tools

模仿了 Webpack 的 require() 方法,由于 Node.js 环境中本身不支持 require(‘../image.png’) 这样的用法,所以需要这个库提供支持。

用法参考项目 readme,注意第一次运行之前需要修改 Webpack 配置,引入 webpack_isomorphic_tools_plugin 并打包一次,目的是生成 webpack-assets.json 文件。

webpack-dev-middleware

地址:https://github.com/webpack/webpack-dev-middleware

Webpack 的一个包裹层中间件,注意仅在开发环境使用。用法参考项目 readme。

webpack-hot-middleware

地址:https://github.com/glenjamin/webpack-hot-middleware

配合 webpack-dev-middleware 使用,目的是实现热加载。用法参考项目 readme。

newrelic 监控模块

需要使用 npm install 来安装整个项目依赖,使用 cnpm 会导致 newrelic 扫描 node_modules 目录时陷入死循环,原因和 cnpm 处理的目录结构有关。

参考资料