21.同构应用中你所忽略的细节

2021-06-15  本文已影响0人  ikonan

不管是服务端渲染还是服务端渲染衍生出的同构应用,现在来看已经并不新鲜了,实现起来也并不困难。可是有的开发者认为:同构应用不就是调用一个 renderToString(React 中)类似的 API 吗?

讲道理确实是这样的,但是讲道理你也许并没有真正在实战中领会同构应用的精髓。

同构应用能够完成的本质条件是虚拟 DOM,基于虚拟 DOM 我们可以生成真实的 DOM,并由浏览器渲染;也可以调用不同框架的不同 APIs,将虚拟 DOM 生成字符串,由服务端传输给客户端。

但是同构应用也不只是这么简单。拿面试来说,同构应用的考察点不是「纸上谈兵」的理论,而是实际实施时的细节。这一讲我们就来聊一聊「同构应用中往往被忽略的细节」,需要读者提前了解服务端渲染和同构应用的概念。

相关知识点如下:


打包环境区分

第一个细节:我们知道同构应用实现了客户端代码和服务端代码的基本统一,我们只需要编写一种组件,就能生成适用于服务端和客户端的组件案例。可是你是否知道,服务端代码和客户端代码大多数情况下还是需要单独处理?比如:

const App = () => {
  return (
    <Provider store={store}>
      <BrowserRouter>
        <Route path="/" component={Home} />
        <Route path="/product" component={Product} />
      </BrowserRouter>
    </Provider>
  );
};
ReactDom.render(<App />, document.querySelector('#root'));

BrowserRouter 组件根据 window.location 以及 history API 实现页面切换,而服务端肯定是无法获取 window.location 的,服务端代码如下:

const App = () => {
  return <Provider store={store}>
    <StaticRoute location={req.path} context={context}>
      <Route path="/" component={Home}/>
    </StaticRoute>
  </Provider>
 }
 return ReactDom.renderToString()

需要使用 StaticRouter 组件,并将请求地址和上下文信息作为 location 和 context 这两个 props 传入 StaticRouter 中。

由于路由在服务端和客户端的差别,因此 webpack 配置文件的 entry 会不相同:

{
   entry: './src/client/index.js',
}

{
   entry: './src/server/index.js',
}

注水和脱水

什么叫做注水和脱水呢?这个和同构应用中数据的获取有关:在服务器端渲染时,首先服务端请求接口拿到数据,并处理准备好数据状态(如果使用 Redux,就是进行 store 的更新),为了减少客户端的请求,我们需要保留住这个状态。一般做法是在服务器端返回 HTML 字符串的时候,将数据 JSON.stringify 一并返回,这个过程,叫做脱水(dehydrate);在客户端,就不再需要进行数据的请求了,可以直接使用服务端下发下来的数据,这个过程叫注水(hydrate)。用代码来表示:

ctx.body = `
<meta charset="UTF-8">
<script>
window.context = {
  initialState: ${JSON.stringify(store.getState())}
}

</script>
<div id="app">
  //...
</div>
`;

客户端:

export const getClientStore = () => {
 const defaultState = JSON.parse(window.context.state)
 return createStore(reducer, defaultState, applyMiddleware(thunk))
}

这一系列过程非常典型,但是也会有几个细节值得探讨:在服务端渲染时,服务端如何能够请求所有的 APIs,保障数据全部已经请求呢?

一般有两种方法:

在服务端代码中:

const routes = [
  {
    path: '/',
    component: Root,
    loadData: () => getSomeData(),
  },
  // etc.
];

import { routes } from './routes';

function App() {
  return (
    <Switch>
      {routes.map((route) => (
        <Route {...route} />
      ))}
    </Switch>
  );
}

在服务端代码中:

import { matchPath } from "react-router-dom"

const promises = []
routes.some(route => {
 const match = matchPath(req.path, route)
 if (match) promises.push(route.loadData(match))
 return match
})

Promise.all(promises).then(data => {
 putTheDataSomewhereTheClientCanFindIt(data)
})

注水和脱水,是同构应用最为核心和关键的细节点。

上一篇 下一篇

猜你喜欢

热点阅读