React服务器端渲染

原文地址:https://blog.coding.net/blog/React-server-rendering

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

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

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

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

Redux

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

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

关于 Store:

  • 整个应用只有一个唯一的 Store
  • Store 对应的状态树(State),由调用一个 reducer 函数(root reducer)生成
  • 状态树上的每个字段都可以进一步由不同的 reducer 函数生成
  • Store 包含了几个方法比如dispatch,getState来处理数据流
  • 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

import React from 'react';
import { Route } from 'react-router';
import { List,Item } from './components';

// 无状态(stateless)组件,一个简单的容器,react-router 会根据 route
// 规则匹配到的组件作为 `props.children` 传入
const Container = (props) => {
  return (
 <div>{props.children}</div>
  );
};

// route 规则:
// - `/list` 显示 `List` 组件
// - `/item/:id` 显示 `Item` 组件
const routes = (
  <Route path="/" component={Container} >
 <Route path="list" component={List} />
 <Route path="item/:id" component={Item} />
  </Route>
);

export default routes;

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

Reducer

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

./reducers/index.js

import listReducer from './list';
import itemReducer from './item';

export default function rootReducer(state = {},action) {
  return {
 list: listReducer(state.list,action),
 item: itemReducer(state.item,action)
  };
}

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

具体到./reducers/list.js

const initialState = [];

export default function listReducer(state = initialState,action) {
  switch(action.type) {
  case 'FETCH_LIST_SUCCESS': return [...action.payload];
  default: return state;
  }
}

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

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

const initialState = {};

export default function listReducer(state = initialState,action) {
  switch(action.type) {
  case 'FETCH_ITEM_SUCCESS': return [...action.payload];
  default: return state;
  }
}

Action

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

./actions/index.js

import fetch from 'isomorphic-fetch';

export function fetchList() {
  return (dispatch) => {
 return fetch('/api/list')
 .then(res => res.json())
 .then(json => dispatch({ type: 'FETCH_LIST_SUCCESS',payload: json }));
  }
}

export function fetchItem(id) {
  return (dispatch) => {
 if (!id) return Promise.resolve();
 return fetch(`/api/item/${id}`)
 .then(res => res.json())
 .then(json => dispatch({ type: 'FETCH_ITEM_SUCCESS',payload: json }));
  }
}

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

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

Store

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

import { createStore } from 'redux';
import rootReducer from './reducers';

// Apply middleware here
// ...

export default function configureStore(initialState) {
  const store = createStore(rootReducer,initialState);
  return store;
}

react-redux

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

./app.js

import React from 'react';
import { render } from 'react-dom';
import { Router } from 'react-router';
import createBrowserHistory from 'history/lib/createBrowserHistory';
import { Provider } from 'react-redux';
import routes from './routes';
import configureStore from './store';

// `__INITIAL_STATE__` 来自服务器端渲染,下一部分细说
const initialState = window.__INITIAL_STATE__;
const store = configureStore(initialState);
const Root = (props) => {
  return (
 <div>
 <Provider store={store}>
 <Router history={createBrowserHistory()}>
 {routes}
 </Router>
 </Provider>
 </div>
  );
}

render(<Root />,document.getElementById('root'));

至此,客户端部分结束。

Server Rendering

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

./server.js

import express from 'express';
import React from 'react';
import { renderToString } from 'react-dom/server';
import { RoutingContext,match } from 'react-router';
import { Provider } from 'react-redux';
import routes from './routes';
import configureStore from './store';

const app = express();

function renderFullPage(html,initialState) {
  return `
 <!DOCTYPE html>
 <html lang="en">
 <head>
 <meta charset="UTF-8">
 </head>
 <body>
 <div id="root">
 <div>
 ${html}
 </div>
 </div>
 <script>
 window.__INITIAL_STATE__ = ${JSON.stringify(initialState)};
 </script>
 <script src="/static/bundle.js"></script>
 </body>
 </html>
  `;
}

app.use((req,res) => {
  match({ routes,location: req.url },(err,redirectLocation,renderProps) => {
 if (err) {
 res.status(500).end(`Internal Server Error ${err}`);
 } else if (redirectLocation) {
 res.redirect(redirectLocation.pathname + redirectLocation.search);
 } else if (renderProps) {
 const store = configureStore();
 const state = store.getState();

 Promise.all([
 store.dispatch(fetchList()),
 store.dispatch(fetchItem(renderProps.params.id))
 ])
 .then(() => {
 const html = renderToString(
 <Provider store={store}>
 <RoutingContext {...renderProps} />
 </Provider>
 );
 res.end(renderFullPage(html,store.getState()));
 });
 } else {
 res.status(404).end('Not found');
 }
  });
});

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

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

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

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

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


react 中的高阶组件主要是对于 hooks 之前的类组件来说的,如果组件之中有复用的代码,需要重新创建一个父类,父类中存储公共代码,返回子类,同时把公用属性...
我们上一节了解了组件的更新机制,但是只是停留在表层上,例如我们的 setState 函数式同步执行的,我们的事件处理直接绑定在了 dom 元素上,这些都跟 re...
我们上一节了解了 react 的虚拟 dom 的格式,如何把虚拟 dom 转为真实 dom 进行挂载。其实函数是组件和类组件也是在这个基础上包裹了一层,一个是调...
react 本身提供了克隆组件的方法,但是平时开发中可能很少使用,可能是不了解。我公司的项目就没有使用,但是在很多三方库中都有使用。本小节我们来学习下如果使用该...
mobx 是一个简单可扩展的状态管理库,中文官网链接。小编在接触 react 就一直使用 mobx 库,上手简单不复杂。
我们在平常的开发中不可避免的会有很多列表渲染逻辑,在 pc 端可以使用分页进行渲染数限制,在移动端可以使用下拉加载更多。但是对于大量的列表渲染,特别像有实时数据...
本小节开始前,我们先答复下一个同学的问题。上一小节发布后,有小伙伴后台来信问到:‘小编你只讲了类组件中怎么使用 ref,那在函数式组件中怎么使用呢?’。确实我们...
上一小节我们了解了固定高度的滚动列表实现,因为是固定高度所以容器总高度和每个元素的 size、offset 很容易得到,这种场景也适合我们常见的大部分场景,例如...
上一小节我们处理了 setState 的批量更新机制,但是我们有两个遗漏点,一个是源码中的 setState 可以传入函数,同时 setState 可以传入第二...
我们知道 react 进行页面渲染或者刷新的时候,会从根节点到子节点全部执行一遍,即使子组件中没有状态的改变,也会执行。这就造成了性能不必要的浪费。之前我们了解...
在平时工作中的某些场景下,你可能想在整个组件树中传递数据,但却不想手动地通过 props 属性在每一层传递属性,contextAPI 应用而生。
楼主最近入职新单位了,恰好新单位使用的技术栈是 react,因为之前一直进行的是 vue2/vue3 和小程序开发,对于这些技术栈实现机制也有一些了解,最少面试...
我们上一节了了解了函数式组件和类组件的处理方式,本质就是处理基于 babel 处理后的 type 类型,最后还是要处理虚拟 dom。本小节我们学习下组件的更新机...
前面几节我们学习了解了 react 的渲染机制和生命周期,本节我们正式进入基本面试必考的核心地带 -- diff 算法,了解如何优化和复用 dom 操作的,还有...
我们在之前已经学习过 react 生命周期,但是在 16 版本中 will 类的生命周期进行了废除,虽然依然可以用,但是需要加上 UNSAFE 开头,表示是不安...
上一小节我们学习了 react 中类组件的优化方式,对于 hooks 为主流的函数式编程,react 也提供了优化方式 memo 方法,本小节我们来了解下它的用...
开源不易,感谢你的支持,❤ star me if you like concent ^_^
hel-micro,模块联邦sdk化,免构建、热更新、工具链无关的微模块方案 ,欢迎关注与了解
本文主题围绕concent的setup和react的五把钩子来展开,既然提到了setup就离不开composition api这个关键词,准确的说setup是由...
ReactsetState的执行是异步还是同步官方文档是这么说的setState()doesnotalwaysimmediatelyupdatethecomponent.Itmaybatchordefertheupdateuntillater.Thismakesreadingthis.staterightaftercallingsetState()apotentialpitfall.Instead,usecom