玩转 React 服务器端渲染
React 提供了两个方法 服务器端渲染除了要解决对浏览器环境的依赖,还要解决两个问题:
React 生态提供了很多选择方案,这里我们选用 Redux 和 react-router 来做说明。 ReduxRedux 提供了一套类似 Flux 的单向数据流,整个应用只维护一个 Store,以及面向函数式的特性让它对服务器端渲染支持很友好。 2 分钟了解 Redux 是如何运作的关于 Store:
Redux 的数据流:
所以对于整个应用来说,一个 Store 就对应一个 UI 快照,服务器端渲染就简化成了在服务器端初始化 Store,将 Store 传入应用的根组件,针对根组件调用 react-routerreact-router 通过一种声明式的方式匹配不同路由决定在页面上展示不同的组件,并且通过 props 将路由信息传递给组件使用,所以只要路由变更,props 就会变化,触发组件 re-render。 假设有一个很简单的应用,只有两个页面,一个列表页 可以这样定义路由, 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; 从这里开始,我们通过这个非常简单的应用来解释实现服务器端渲染前后端涉及的一些细节问题。 ReducerStore 是由 reducer 产生的,所以 reducer 实际上反映了 Store 的状态树结构
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) }; }
具体到 const initialState = []; export default function listReducer(state = initialState,action) { switch(action.type) { case 'FETCH_LIST_SUCCESS': return [...action.payload]; default: return state; } } list 就是一个包含 items 的简单数组,可能类似这种结构: 然后是 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,这里我们定义
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 通过 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接下来实现
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,在服务器端用一个
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'); } }); }); 服务器端渲染部分可以直接通过共用客户端 最后关于页面内链接跳转如何处理?react-router 提供了一个 比如在 更多参考
(编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |