首页 男生 其他 深入浅出React和Redux

12.4 同构实例

深入浅出React和Redux 程墨 10191 2021-04-06 02:29

  您可以在百度里搜索“深入浅出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

目录
设置
手机
书架
书页
评论