React 提供了两个方法 renderToString 和 renderToStaticMarkup 用来将组件(Virtual DOM)输出成 HTML 字符串,这是 React 服务器端渲染的基础,它移除了服务器端对于浏览器环境的依赖,所以让服务器端渲染变成了一件有吸引力的事情。

服务器端渲染除了要解决对浏览器环境的依赖,还要解决两个问题:

  • 前后端可以共享代码
  • 前后端路由可以统一处理

React 生态提供了很多选择方案,这里我们选用 Redux 和 react-router 来做说明。

Redux

Redux 提供了一套类似 Flux 的单向数据流,整个应用只维护一个 Store,以及面向函数式的特性让它对服务器端渲染支持很友好。

2 分钟了解 Redux 是如何运作的

关于 Store:

  • 整个应用只有一个唯一的 Store
  • Store 对应的状态树(State),由调用一个 reducer 函数(root reducer)生成
  • 状态树上的每个字段都可以进一步由不同的 reducer 函数生成
  • Store 包含了几个方法比如 dispatchgetState 来处理数据流
  • Store 的状态树只能由 dispatch(action) 来触发更改

Redux 的数据流:

  • action 是一个包含 { type, payload } 的对象
  • reducer 函数通过 store.dispatch(action) 触发
  • reducer 函数接受 (state, action) 两个参数,返回一个新的 state
  • reducer 函数判断 action.type 然后处理对应的 action.payload 数据来更新状态树

所以对于整个应用来说,一个 Store 就对应一个 UI 快照,服务器端渲染就简化成了在服务器端初始化 Store,将 Store 传入应用的根组件,针对根组件调用 renderToString 就将整个应用输出成包含了初始化数据的 HTML。

react-router

react-router 通过一种声明式的方式匹配不同路由决定在页面上展示不同的组件,并且通过 props 将路由信息传递给组件使用,所以只要路由变更,props 就会变化,触发组件 re-render。

假设有一个很简单的应用,只有两个页面,一个列表页 /list 和一个详情页 /item/:id,点击列表上的条目进入详情页。

可以这样定义路由,./routes.js

  1. import React from 'react';
  2. import { Route } from 'react-router';
  3. import { List, Item } from './components';
  4. // 无状态(stateless)组件,一个简单的容器,react-router 会根据 route
  5. // 规则匹配到的组件作为 `props.children` 传入
  6. const Container = (props) => {
  7. return (
  8. <div>{props.children}</div>
  9. );
  10. };
  11. // route 规则:
  12. // - `/list` 显示 `List` 组件
  13. // - `/item/:id` 显示 `Item` 组件
  14. const routes = (
  15. <Route path="/" component={Container} >
  16. <Route path="list" component={List} />
  17. <Route path="item/:id" component={Item} />
  18. </Route>
  19. );
  20. export default routes;

从这里开始,我们通过这个非常简单的应用来解释实现服务器端渲染前后端涉及的一些细节问题。

Reducer

Store 是由 reducer 产生的,所以 reducer 实际上反映了 Store 的状态树结构

./reducers/index.js

  1. import listReducer from './list';
  2. import itemReducer from './item';
  3. export default function rootReducer(state = {}, action) {
  4. return {
  5. list: listReducer(state.list, action),
  6. item: itemReducer(state.item, action)
  7. };
  8. }

rootReducer 的 state 参数就是整个 Store 的状态树,状态树下的每个字段对应也可以有自己的
reducer,所以这里引入了 listReducer 和 itemReducer,可以看到这两个 reducer
的 state 参数就只是整个状态树上对应的 list 和 item 字段。

具体到 ./reducers/list.js

  1. const initialState = [];
  2. export default function listReducer(state = initialState, action) {
  3. switch(action.type) {
  4. case 'FETCH_LIST_SUCCESS': return [...action.payload];
  5. default: return state;
  6. }
  7. }

list 就是一个包含 items 的简单数组,可能类似这种结构:[{ id: 0, name: 'first item'}, {id: 1, name: 'second item'}],从'FETCH_LIST_SUCCESS' 的 action.payload 获得。

然后是 ./reducers/item.js,处理获取到的 item 数据

  1. const initialState = {};
  2. export default function listReducer(state = initialState, action) {
  3. switch(action.type) {
  4. case 'FETCH_ITEM_SUCCESS': return [...action.payload];
  5. default: return state;
  6. }
  7. }

Action

对应的应该要有两个 action 来获取 list 和 item,触发 reducer 更改 Store,这里我们定义 fetchList 和 fetchItem 两个 action。

./actions/index.js

  1. import fetch from 'isomorphic-fetch';
  2. export function fetchList() {
  3. return (dispatch) => {
  4. return fetch('/api/list')
  5. .then(res => res.json())
  6. .then(json => dispatch({ type: 'FETCH_LIST_SUCCESS', payload: json }));
  7. }
  8. }
  9. export function fetchItem(id) {
  10. return (dispatch) => {
  11. if (!id) return Promise.resolve();
  12. return fetch(`/api/item/${id}`)
  13. .then(res => res.json())
  14. .then(json => dispatch({ type: 'FETCH_ITEM_SUCCESS', payload: json }));
  15. }
  16. }

isomorphic-fetch 是一个前后端通用的 Ajax 实现,前后端要共享代码这点很重要。

另外因为涉及到异步请求,这里的 action 用到了 thunk,也就是函数,redux 通过 thunk-middleware 来处理这类 action,把函数当作普通的 action dispatch 就好了,比如 dispatch(fetchList())

Store

我们用一个独立的 ./store.js,配置(比如 Apply Middleware)生成 Store

  1. import { createStore } from 'redux';
  2. import rootReducer from './reducers';
  3. // Apply middleware here
  4. // ...
  5. export default function configureStore(initialState) {
  6. const store = createStore(rootReducer, initialState);
  7. return store;
  8. }

react-redux

接下来实现 <List><Item> 组件,然后把 redux 和 react 组件关联起来,具体细节参见 react-redux

./app.js

  1. import React from 'react';
  2. import { render } from 'react-dom';
  3. import { Router } from 'react-router';
  4. import createBrowserHistory from 'history/lib/createBrowserHistory';
  5. import { Provider } from 'react-redux';
  6. import routes from './routes';
  7. import configureStore from './store';
  8. // `__INITIAL_STATE__` 来自服务器端渲染,下一部分细说
  9. const initialState = window.__INITIAL_STATE__;
  10. const store = configureStore(initialState);
  11. const Root = (props) => {
  12. return (
  13. <div>
  14. <Provider store={store}>
  15. <Router history={createBrowserHistory()}>
  16. {routes}
  17. </Router>
  18. </Provider>
  19. </div>
  20. );
  21. }
  22. render(<Root />, document.getElementById('root'));

至此,客户端部分结束。

Server Rendering

接下来的服务器端就比较简单了,获取数据可以调用 action,routes 在服务器端的处理参考 react-router server rendering,在服务器端用一个 match 方法将拿到的 request url 匹配到我们之前定义的 routes,解析成和客户端一致的 props 对象传递给组件。

./server.js

  1. import express from 'express';
  2. import React from 'react';
  3. import { renderToString } from 'react-dom/server';
  4. import { RoutingContext, match } from 'react-router';
  5. import { Provider } from 'react-redux';
  6. import routes from './routes';
  7. import configureStore from './store';
  8. const app = express();
  9. function renderFullPage(html, initialState) {
  10. return `
  11. <!DOCTYPE html>
  12. <html lang="en">
  13. <head>
  14. <meta charset="UTF-8">
  15. </head>
  16. <body>
  17. <div id="root">
  18. <div>
  19. ${html}
  20. </div>
  21. </div>
  22. <script>
  23. window.__INITIAL_STATE__ = ${JSON.stringify(initialState)};
  24. </script>
  25. <script src="/static/bundle.js"></script>
  26. </body>
  27. </html>
  28. `;
  29. }
  30. app.use((req, res) => {
  31. match({ routes, location: req.url }, (err, redirectLocation, renderProps) => {
  32. if (err) {
  33. res.status(500).end(`Internal Server Error ${err}`);
  34. } else if (redirectLocation) {
  35. res.redirect(redirectLocation.pathname + redirectLocation.search);
  36. } else if (renderProps) {
  37. const store = configureStore();
  38. const state = store.getState();
  39. Promise.all([
  40. store.dispatch(fetchList()),
  41. store.dispatch(fetchItem(renderProps.params.id))
  42. ])
  43. .then(() => {
  44. const html = renderToString(
  45. <Provider store={store}>
  46. <RoutingContext {...renderProps} />
  47. </Provider>
  48. );
  49. res.end(renderFullPage(html, store.getState()));
  50. });
  51. } else {
  52. res.status(404).end('Not found');
  53. }
  54. });
  55. });

服务器端渲染部分可以直接通过共用客户端 store.dispatch(action) 来统一获取 Store 数据。另外注意 renderFullPage 生成的页面 HTML 在 React 组件 mount 的部分(<div id="root">),前后端的 HTML 结构应该是一致的。然后要把 store 的状态树写入一个全局变量(__INITIAL_STATE__),这样客户端初始化 render 的时候能够校验服务器生成的 HTML 结构,并且同步到初始化状态,然后整个页面被客户端接管。

最后关于页面内链接跳转如何处理?

react-router 提供了一个 <Link> 组件用来替代 <a> 标签,它负责管理浏览器 history,从而不是每次点击链接都去请求服务器,然后可以通过绑定 onClick 事件来作其他处理。

比如在 /list 页面,对于每一个 item 都会用 <Link> 绑定一个 route url:/item/:id,并且绑定 onClick 去触发 dispatch(fetchItem(id)) 获取数据,显示详情页内容。

更多参考

玩转 React 服务器端渲染的更多相关文章

  1. 基于React服务器端渲染的博客系统

    系统目录及源码由此进入 目录 1. 开发前准备 1.1 技术选型1.2 整体设计1.3 构建开发 2. 技术点 2.1 react2.2 redux, react-router2.3 server-r ...

  2. React 服务器端渲染流程

    其实我们在访问客户端渲染的页面时,请求到的只是一个 html 空壳,里面引入了一个 js 文件,所有的内容都是通过 js 进行插入的,正是因为页面是由 js 渲染出来的,所以会带来如下几个问题: 1. ...

  3. React服务器端渲染值Next.js

    昨天leader给分配了新任务,让熟悉一下ssr,刚开始有点懵,啥玩意?百度了一下,不就是服务器端渲染(server side render,简称: ssr). ssr简介 服务端渲染一个很常见的场景 ...

  4. React服务器端渲染框架next.js项目实战及部署上下文context问题解决办法

    在前段时间折腾过搭建react服务器端项目架构,点击这里了解下,正好最近有个重构PC网站的需求,考虑SEO等因素.在做技术选型的时候决定采用nextjs.项目开发过程中,nextjs的体验非常棒,个人 ...

  5. react 服务器端渲染 ssr 中 localstorage/history/window is not defined 解决方案

    1.原因 ssr 会在后端执行组件的 componentWillMount 以及在它这个生命周期之前的生命周期 也就是说 ssr 阶段是不会执行 componentDidMount 方法的 当你在 c ...

  6. react实现ssr服务器端渲染总结和案例(实例)

    1.什么是 SSR SSR 是 server side render 的缩写,从字面上就可以理解 在服务器端渲染,那渲染什么呢,很显然渲染现在框架中的前后端分离所创建的虚拟 DOM 2.为什么要实现服 ...

  7. 【大前端之前后分离01】JS前端渲染VS服务器端渲染

    前言 之前看了一篇文章:@Charlie.Zheng Web系统开发构架再思考-前后端的完全分离,文中论述了为何要前后分离,站在前端的角度来看,是很有必要的:但是如何说服团队使用前端渲染方案却是一个现 ...

  8. 服务器端渲染VS浏览器端渲染

    1)浏览器渲染和服务器渲染区别:何为渲染?如果我们只是想显示一堆不变的数据,那么我们直接写一个a.html丢到服务器上让客户端访问就可以了.但这是基本不可能的事情,数据一般是变化的.你不可能为每套数据 ...

  9. 细说后端模板渲染、客户端渲染、node 中间层、服务器端渲染(ssr)

    细说后端模板渲染.客户端渲染.node 中间层.服务器端渲染(ssr) 前端与后端渲染方式的发展大致经历了这样几个阶段:后端模板渲染.客户端渲染.node 中间层.服务器端渲染(ssr). 1. 后端 ...

随机推荐

  1. Golang程序性能分析

    前言 程序性能分析我相信是每个程序员都会遇到的问题,比如说一个程序的CPU为什么占用这么高?有没有优化的空间?又比如程序出现了内存泄漏如何排查等等.如果是C++程序会借助于Google pprof c ...

  2. Mybatis资料

    1. 入门案例 https://www.cnblogs.com/xdp-gacl/p/4261895.html 2. 详细笔记 以及配套视频教程: 笔记:https://blog.csdn.net/S ...

  3. html学习笔记(2)-字母大小写转换练习

    主要应用了text-transform属性值: uppercase:所有单词的字母都大写: lowercase:所有单词的字母都小写: capitalize:每个单词的首字母都大写: none:默认值 ...

  4. 九度OJ 1167:数组排序 (排序)

    时间限制:1 秒 内存限制:32 兆 特殊判题:否 提交:5395 解决:1715 题目描述: 输入一个数组的值,求出各个值从小到大排序后的次序. 输入: 输入有多组数据. 每组输入的第一个数为数组的 ...

  5. mysql date函数相关用法整理(持续更新)

    date_add(now(), INTERVAL 1 day)   增加一天 date_format(d,'%Y-%m-%d %T')   这里的d为datestamp类型,格式化成  yyyy-MM ...

  6. 洛谷 3275 [SCOI2011]糖果

    题目戳这里 N句话题意 有N个人,k个限制,有五种限制 如果X=1, 表示第A个小朋友的糖果必须和第B个小朋友的糖果一样多: 如果X=2, 表示第A个小朋友的糖果必须少于第B个小朋友的糖果: 如果X= ...

  7. QQ登录集成到自己网站php代码(转载)

    我们现在在各大网站论坛都可以看到点击一个QQ图标就可以利用自己的QQ号在网站进行登录了,下面我来告诉你一段QQ登录集成到自己网站php代码,有需要的朋友可参考. 1.打开open.qq.com 添加创 ...

  8. 20170326 ABAP调用外部webservice 问题

    1.SE80 创建企业服务: 代理生成:出现错误 库处理程序中出现例外 错误的值:未知类型参考ns1:ArrayOfMLMatnrResource 尝试: 一.测试本地文件:---无效 1. 将网址链 ...

  9. Java for LeetCode 116 Populating Next Right Pointers in Each Node

    Given a binary tree struct TreeLinkNode { TreeLinkNode *left; TreeLinkNode *right; TreeLinkNode *nex ...

  10. Java for LeetCode 114 Flatten Binary Tree to Linked List

    Given a binary tree, flatten it to a linked list in-place. For example, Given 1 / \ 2 5 / \ \ 3 4 6 ...