12.4 同构实例
您可以在百度里搜索“深入浅出React和Redux 艾草文学(www.321553.xyz)”查找最新章节!
12.4 同构实例
现在我们来看一看实现同构的完整例子。
首先,CounterPage的定义要发生变化,之前CounterPage导出一个intialState是固定的值,现在我们希望服务器和浏览器端共用一个的数据源,这个数据源就是之前我们定义的API接口,所以将intialState改为initState函数,这个函数返回一个Promise实例,在下面的代码中可以看到,使用Promise可以大大简化代码的结构。
在src/pages/CounterPage.js文件中,initState函数根据环境变量HOST_NAME决定API地址,如果没有HOST_NAME那就是用一个指向本地开发环境的域名,这是一个常用的技巧。
可以看到,initState返回的是一个Promise对象,代码如下:
const END_POINT = process.env.HOST_NAME || 'localhost:9000';
const initState = () => {
return fetch(`http://${END_POINT}/api/count`).then(response => {
if (response.status !== 200) {
throw new Error('Fail to fetch count');
}
return response.json();
}).then(responseJson => {
return responseJson.count;
});
}
而且,src/pages/CounterPage.js的导出语句和其他页面文件不一样,对于CounterPage,不只要导出视图page,还要导出reducer、stateKey和initState,代码如下:
export {page, reducer, initState, stateKey};
处理完CounterPage,接下来就要准备服务器端的路由逻辑。
因为实际的渲染工作和路由关系紧密,所以要把这两个功能都集中在文件server/routes.Server.js中,让server/app.dev.js和server/app.prod.js只需要提供assetManifest,把req和res参数传递给renderPage函数,代码如下:
const renderPage = require('./routes.Server.js').renderPage;
app.get('*', (req, res) => {
if (!assetManifest) {
assetManifest = getAssetManifest();
}
return renderPage(req, res, assetManifest);
});
所以,主要的功能其实是在server/routes.Server.js文件中,我们来看看这个文件。
在服务器端渲染,没有必要使用分片,自然也不需要动态加载模块,所有的页面都是直接导入,比如导入Home页面的代码就是这样:
import Home from '../src/pages/Home.js';
对于App、Home、About和NotFound页面用上面的方法import就可以,但是CounterPage导入有一点特殊,因为不仅要导入视图,还要导入这个页面对应的reducer、stateKey和初始状态initState,代码如下:
import {page as CounterPage, reducer, stateKey, initState} from '../src/pages/CounterPage.js';
路由规则,因为不需要分片加载,所以非常简单,可以直接用一个routes变量代表,代码如下:
const routes = (
);
最关键的就是renderPage函数,这个函数承担了来自浏览器请求的路由和渲染工作。
服务器端路由使用React-Router提供的match函数,如果匹配路由成功,那就调用另一个函数renderMatchedPage渲染页面结果,代码如下:
export const renderPage = (req, res, assetManifest) => {
match({routes: routes, location: req.url}, function(err, redirect, renderProps) {
// 检查error和redirect,如果存在就让res结束
return renderMatchedPage(req, res, renderProps, assetManifest);
});
};
当renderMatchedPage函数被调用时,代表已经匹配中了某个路由,接下来要做的工作就是获得相关数据把这个页面渲染出来。
之前已经说过,为了保证服务器对每个请求的独立处理,必须每个请求都建一个Store,所以renderMathedPage函数做的第一件事就是创建一个Store,然后获取初始化Store状态的Promise对象,代码如下:
function renderMatchedPage(req, res, renderProps, assetManifest) {
const store = configureStore();
// 获取匹配Page的initStata函数
const statePromise = (initState) ? initState() : Promise.resolve(null);
在这个应用中,只有CounterPage才提供initState,如果匹配的页面不提供initState,那就使用Promise.resolve产生一个Promise对象,这个对象不提供任何数据。但是,在没有initState的情况下也能给statePromise变量一个PromiseB对象,从而使得后续处理代码不用关心如何获得数据。
当statePromise的then函数被调用时,代表页面所需的文件已经准备好,这时候首先要设置Store上的状态,同时也要更新Store上的reducer,通过我们定义的reset enhancer可以完成,代码如下:
statePromise.then((result) => {
if (stateKey) {
const state = store.getState();
store.reset(combineReducers({
...store._reducers,
[stateKey]: reducer
}), {
...state,
[stateKey]: result
});
}
至此,Store已经准备好了,接下来就通过React提供的服务器端渲染函数render-ToString产生对应的HTML字符串,存放在appHtml变量中,代码如下:
const appHtml = ReactDOMServer.renderToString(
);
返回给浏览器的HTML中只包含appHtml还不够,还需要包含“脱水数据”,所以在渲染完HTML之后,要立即把Store上的状态提取出来作为“脱水数据”,因为服务器端React组件用的是同样的状态,所以浏览器端用这样“脱水数据”渲染出来的结果绝对是一样的。
获取“脱水数据”的代码如下:
const dehydratedState= store.getState();
到这里,React组件产生的HTML准备好了,“脱水数据”也准备好了,接下来就通过Express提供的res.render渲染结果就可以,代码如下:
res.render('index', {
title: 'Sample React App',
PUBLIC_URL: '/',
assetManifest: assetManifest,
appHtml: appHtml,
dehydratedState: safeJSONstringify(dehydratedState)
});
这里,“脱水数据”dehydratedState通过函数safeJSONstringify被转化为字符串,这样在ejs模板文件中直接渲染这个字符串就行。注意,这里不能直接使用JSON.stringify转化为字符串,因为“脱水数据”可能包含不安全的字符,需要避免跨站脚本攻击的漏洞。
safeJSONstringify函数的代码如下:
function safeJSONstringify(obj) {
return JSON.stringify(obj).replace(/
}
最后,让我们看一看ejs模板文件server/views/index.ejs,服务器端渲染产生的appHtml字符串和脱水数据dehydratedState在这里被渲染到返回给浏览器的HTML中,代码如下:
- appHtml
其中dehydratedState是被渲染到内嵌的script中,赋值给一个DEHYDRATED_STATE变量,这个变量在浏览器端可以被访问到,利用这个变量就可以对React组件“注水”,让它们重生,接下来我们看看浏览器端如何实现“注水”。
和服务器端一样,入口函数src/index.js把渲染的工作完全交给Routes.js,所做的只是提供装载React组件的DOM元素。
import {renderRoutes} from './Routes.js';
renderRoutes(document.getElementById('root'));
在src/Routes.js文件中是更新的浏览器端路由和渲染功能,主要的改变在getCounter-Page函数中,代码如下:
const getCounterPage = (nextState, callback) => {
require.ensure([], function(require) {
const {page, reducer, stateKey, initState} = require('./pages/CounterPage.js');
const dehydratedState = (win && win.DEHYDRATED_STATE);
const state = store.getState();
const mergedState = {...dehydratedState, ...state};
const statePromise = mergedState[stateKey]
? Promise.resolve(mergedState[stateKey])
: initState();
// 继续处理statePromise
}
和服务器端类似,首先要获取一个statePromise,优先从“脱水数据”中获得初始状态,只有没有脱水初始状态的时候,才使用initState函数去异步获取初始化数据。
当statePromise完成时,一样可以使用reset功能设置Store的状态和reducer,代码如下:
statePromise.then((result) => {
store.reset(combineReducers({
...store._reducers,
[stateKey]: reducer
}), {
...state,
[stateKey]: result
});
因为使用了服务器端渲染,同时浏览器端使用了React-Router的代码分片功能,所以浏览器端也需要用match函数来实现路由,代码如下:
export const renderRoutes = (domElement) => {
match({history, routes}, (err, redirectLocation, renderProps) => {
ReactDOM.render(
,
domElement
);
});
}
至此,一个同构应用完成了,以http://localhost:9000/counter页面为例,如果在浏览器中直接访问这个页面,在服务器端就会通过API服务器获得初始计数值,返回的网页中包含完整HTML和脱水的初始计数值;如果直接访问其他页面,然后通过顶栏链接切换到Counter页面,浏览器就会通过一个API请求获得初始数据完成浏览器端渲染。 深入浅出React和Redux