React服务端渲染之路05——redux-02

全部源代码、文档和图片都在 github 的仓库里,点击进入仓库javascript

相关阅读

1. 服务端异步获取数据(最难最复杂)

  • 刚才咱们用了客户端的同步和异步,其实仍是比较简单的,由于用法与客户端渲染的用法基本上如出一辙,差异很小
  • 可是服务端异步获取数据修改 store,这个是很是复杂的,咱们要首先明白这其中的原理

1.1 服务端怎么知道它要获取数据

  • 服务端异步获取数据的前提是,服务端须要知道它要去获取什么数据,就像是去餐馆吃饭,你是客人,厨师确定不知道客人要吃什么,因此必定要客人告诉厨师,我要吃什么。这里的客人就比如是客户端,厨师比如是服务端
  • 因此,客人告诉厨师,我要吃蛋炒饭,厨师知道了你要吃蛋炒饭,就开始给你作饭。这里就是客户端告诉服务端,客户端须要什么数据(客人要吃什么),而后服务端知道了客户端须要什么(客人要吃蛋炒饭),而后服务端去获取客户端须要的数据(厨师去作蛋炒饭),最终返给客户端(把蛋炒饭端给客人)

1.2 客户端怎么告诉服务端它须要数据

  • 可是如今问题又来了,客人怎么告诉厨师他要吃什么,客人是点外卖,仍是打电话,仍是直接去店里吃呢?客人怎么告诉厨师这个方法,就比如是客户端怎么告诉服务端,客户端须要什么数据,因此这里的关键就在于,客户端经过什么方式把它须要的东西,告诉服务端
  • 经过什么方式传递呢?咱们如今客户端和服务端所共有的有 redux 和 router,能经过路由传递吗?确定不行,路由主要是作路由跳转的,路由可以传递参数是能够,可是要让路由去传递大量的数据,而且要对数据进行屡次的修改,这显然不合适,因此咱们最好的选择依然仍是 redux
  • 好,咱们已经确认了要使用 redux 这个方式来告诉厨师,客人要吃什么。也就是客户端使用 redux 告诉服务端,它须要什么数据,接下来就须要调用 redux 里的方法
  • 咱们能够给组件添加一个静态方法,由于组件就是类,类就有本身的静态方法,咱们定义一个 loadData 的静态方法,在这个静态方法里,经过 redux 告诉服务端,客户端须要什么数据,咱们以 Home 组件为例,Home 组件如今有一个客户端异步获取 schoolList 的方法,咱们让服务端调用这个方法
  • Home/index.js
Home.loadData = store => store.dispatch(UserActions.getSchoolList());
  • 能够看到,咱们给 Home 定义了一个 loadData 的静态方法,把 store 做为参数传递进去,而后 dispatch UserActions 里的 getSchoolList 方法,由于 loadData 是静态方法,只有经过类才能调用,因此就是说,咱们能够本身定义这个方法是在客户端执行仍是在服务端执行

1.3 把 loadData 方法放在路由上

  • 咱们不要去考虑客户端,如今与客户端没有关系,咱们只考虑服务端该怎么调用 loadData 这个方法
  • 路由,咱们在 Home 这个组件的路由上边,添加一个 loadData 的方法属性,这也就解释了为何咱们上一节要把 routes.js 的形式修改为数组对象的形式,目的就是为了服务于调用 loadData 这个方法,因此咱们修改一下路由
  • src/routes.js
// src/routes.js
export default [
  {
    path: '/',
    component: Home,
    loadData: Home.loadData,
    exact: true,
    key: '/'
  },
  {
    path: '/news',
    component: News,
    exact: true,
    key: '/news'
  }
];

1.4 服务端怎么调用路由上的 loadData 方法

  • src/server/render.js
  • 路由上的方法定义好了以后,咱们就要在使用路由的地方作一些其余的操做,首先咱们看一下以前咱们在服务端是如何是使用路由的,咱们看一下 src/server/render.js 文件
// src/server/render.js
<div className="container" style={{ marginTop: 70 }}>
  {
    routes.map(route => <Route {...route} />)
  }
</div>
  • 能够看到,咱们是直接使用路由,没有对路由再作其余的任何操做,可是如今不同了,路由里有了 loadData 这个方法,并且咱们须要的是在服务端渲染 HTML 模板以前调用 loadData 这个方法,因此咱们要使用这个方法,咱们修改一下 src/server/render.js 里的代码
// src/server/render.js
export default (req, res) => {

  let context = {};

  let store = getServerStore();

  let promises = [];

  routes.forEach(route => {
    if (route.loadData) {
      promises.push(route.loadData(store));
    }
  });

  Promise.all(promises).then(() => {

    console.log(store.getState());

    let domContent = renderToString(
      <Provider store={store}>
        <StaticRouter context={context} location={req.path}>
          <>
            <Header />
            <div className="container" style={{ marginTop: 70 }}>
              {
                routes.map(route => <Route {...route} />)
              }
            </div>
          </>
        </StaticRouter>
      </Provider>
    );
    let html = `
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
  <link href="https://cdn.bootcss.com/twitter-bootstrap/3.3.7/css/bootstrap.css" rel="stylesheet">
  <title>react-ssr</title>
</head>
<body>
<div id="root">${domContent}</div>
<script>
  window.context = {
    state: ${JSON.stringify(store.getState())}
  }
</script>
<script src="/client.js"></script>
</body>
</html>
`;

    res.send(html);
  });
};
  • 从代码里,咱们能够看到,咱们主要定义了一个 promises 数组,而后把 routes 遍历,若是 routes 里有 loadData 这个属性,那么把这个 loadData push 进 promises 数组,最后统一调用 Promise.all(promises) 执行全部的 loadData,执行完毕以后,redux 里的异步方法就已经执行完毕,修改了 store,而后开始渲染 HTML 模板
  • 咱们要明确一件事,就是 store 里的 action 的异步操做,咱们采用的是 axios,axios 请求返回的就是一个 promise,因此咱们的 loadData 在调用 action 的方法的时候,必定也是一个 promise,因此咱们能够把 loadData push 进 promises 数组,最终统一用 Promise.all 执行
  • 经过控制台的输出,咱们看到,如今服务端已经获取到数据了,并且也已经修改了 store 里的值,那么接下来,咱们就可使用 store 里的值了

1.5 脱水与注水

  • 脱水与注水,很容易让咱们想到洗衣机的脱水与注水。衣服放进洗衣机的时候,确定要注水。等衣服洗完以后,要进行脱水。
  • 在这里也是同样,服务端获取到数据后,怎么才能把数据返回给客户端,要知道服务端不能经过 ajax 的方式把数据再次返回给客户端。
  • 咱们看代码就能够看到,咱们是先获取到 store 里的数据,而后才开始渲染页面内容的,既然这样,我直接把 store 里的数据,放到 HTML 页面里就能够了,客户端页面渲染完以后,直接从 HTML 页面里拿数据去使用就能够了。
  • 注水, 咱们在 HTML 模板中,添加一个 script 标签,把 store 的值做为 window 对象的一个属性,挂到页面上,挂载到页面上的必定要是一个字符串,由于 HTML 页面只认识字符串,js 对象识别不了,因此,咱们就实现了服务端的注水
<script>
  window.context = {
    state: ${JSON.stringify(store.getState())}
  }
</script>
  • 脱水,服务端注水完成了,那么该客户端脱水了,怎么脱水呢,换句话说,就是怎么把服务端挂到 HTML 页面上的 js 数据拿出来,而后修改页面视图,咱们打开
  • /src/store/index.js 文件,修改 getClientStore,咱们把 HTML 模板里的 window.context.state 的值获取到,而后做为 createStore 里的第二个参数,就能够实现脱水的功能
// /src/store/index.js
export const getClientStore = () => {
  let initState = window.context.state;
  return createStore(
    reducers,
    initState,
    composeWithDevTools(applyMiddleware(thunk, logger))
  );
}
  • 这个时候,咱们不须要修改客户端的任何代码,直接重启服务,刷新页面,咱们能够看到,咱们不须要点击按钮,浏览器已经把 schoolList 数据挂到页面上。咱们查看页面源代码,就能够看到,ul 标签里的 li 标签里,有各个学校的内容。同时,在代码底部,有一个 script 的标签里,里边的内容就是咱们在服务端的 HTML 模板里注水的内容
  • 这个时候,咱们就实现了服务端异步获取数据
  • 总结一下就是,总体的代码量很少,最核心的是思路,客户端给本身定义一个 loadData 的静态方法,loadData 经过调用 redux 里的 actions 告诉服务端要获取什么数据,而后在路由里添加 loadData 这个属性,服务端调用 loadData 这个方法,本质上调用的是 redux 里的 actions 里的方法,由于可能有多个组件都有 loadData 方法,因此咱们遍历出有 loadData 属性的路由,把 loadData 属性统一放入 Promise.all 里进行处理,处理完毕以后, store 里的数据已经修改,咱们经过服务端的注水,客户端的脱水,就能够把服务端异步获取到的数据显示在页面上
  • 页面效果,学校列表是经过服务端渲染获取到的,再也不是客户端获取数据

页面效果

  • 页面源码,主要是看红框里的, HTML 里已经有了各个的 school 信息,同时,服务端注水的内容,也做为字符串显示在页面上

页面源码

  • 因为这一节的内容比较复杂,也比较难,因此咱们就介绍这么多,下一节咱们主要介绍一下优化当前的代码

相关阅读

相关文章
相关标签/搜索