Next App - ssr redux

背景

最近在开发一个新项目的时候, 想尝试使用next.js来实现服务端渲染, 但最终碰到一个比较棘手的问题, 最终还是使用回了create-react-app客户端渲染, 这里记录下碰到的问题, 便于以后如果有解决方案了, 可以直接使用.

问题

在页面中, 用户信息和部分权限状态以前是保存在store中的, 这样便于全局统一使用. 在csr的情况下, 只需要在root组件中, 统一处理相关加载和更新请求并保存数据变化到store中即可. 但是在ssr的场景下, 需要优化首屏渲染, 而且这部分数据也是可以通过server端的请求写入store的, 于是尝试下在服务端使用redux并保存到客户端

如果这里使用客户端异步加载或者window.__PRELOADED_STATE__的方式注入, 就不能做到首屏渲染的时候带上这些信息了, 所以必须在服务端渲染的时候, 先把需要的数据提前加载好, 并且把数据通过getInitialProps/getServerSideProps传给组件, 并同步到全局store, 就能在首屏渲染的时候携带好数据了. 而且这样在服务端渲染的时候, 也能同步通过store获取需要的数据

同步store数据

在服务端使用redux生成store数据并同步到客户端有两种方案:

获取server-store单例

因为可能多个组件同时需要用到store, 所以我们将store实例化为单例, 并绑定在req上, 保证唯一实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { IncomingMessage } from 'http';
import _ from 'lodash';
import { AppStore } from '../reducers';
import initializeStore from '../reducers/store';

const STORE_KEY = '__store';

const getServerStore = (req: IncomingMessage): AppStore => {
if (!_.get(req, STORE_KEY)) {
_.set(req, STORE_KEY, initializeStore());
}
return _.get(req, STORE_KEY) as AppStore;
};

export default getServerStore;

通过_app.tsx全局同步

1
2
3
4
5
6
7
8
9
10
11
App.getInitialProps = async (appCtx: AppContext) => {
const store = UTILS.getServerStore(appCtx.ctx.req);

console.log('app store.getState() : ', store.getState());

store.dispatch(userInfoDucks.setUserInfo({ ...appCtx.ctx.req.user }));

return { storeState: store.getState() };
};

export default App;

通过page.tsx页面同步

1
2
3
4
5
6
7
8
9
10
11
12
export const getServerSideProps: GetServerSideProps = async (ctx) => {
const store = UTILS.getServerStore(ctx.req);
store.dispatch(userInfoDucks.setUserInfo({ ...ctx.req.user }));

console.log('home store.getState() : ', store.getState());

return {
props: {
storeState: store.getState(),
},
};
};

上面两个方案都能实现首屏渲染的时候能正确获取到store里的数据, 但是都有一些比较严重的问题:

  1. 通过_app.tsx全局同步
    • 每次页面切换, 都会看见有一个请求: /_next/data/development/xxx.json, 里面会携带storeState, 是一份完整的数据, 这样实际上会出现的效果是: 服务端的store数据会完全覆盖客户端store数据, 即使做好相关数据的reducer, 也会造成每个请求都会额外携带很多无用的信息, 而store里如果保存了比较大的数据, 也会造成性能影响.
    • 如果客户端修改了store暂存到本地的数据, 或者某个特殊页面想额外获取一部分新的需要保存在store中的数据, 在_app.tsx里就很难处理
    • 会导致这个问题: 静态优化失效
  2. 通过page.tsx页面同步
    • 可以实现不同页面加载不同store的数据, 但是同样存在一个问题: 某些基础信息, 每个页面都需要, 维护起来就比较麻烦, 但相比起_app.tsx好一点
    • 同样也存在每次进入页面, 都会请求一次完整的数据, 即使客户端本地已经有了这份数据
    • 某些依赖客户端动作修改store的功能无法实现: 如瀑布流加载/滚动加载更多等

本质原因

碰到这个问题后思考了一下, 引起这个问题的本质原因是: 服务端无法实时获得客户端状态

比如上面出现的问题:

  • 本地修改store无法同步到服务端
  • 客户端需要额外参数请求的接口, 服务端无法获取
  • 每个页面都需要请求一次完整的store数据

其实本质上都是一个原因: 服务端无法获取客户端store当前数据

如果能解决这个问题, 那么后端可以根据store的当前状态, 请求各自需要的数据, 并通过redux同步派发到页面中, 就不会造成数据请求过多等性能问题. 而传统的csr方式不会存在这个问题, 是因为在请求对应数据url的时候, 相关状态已经通过请求参数传递过去了

解决方案

既然能确定问题, 那就有解决方案, 虽然不够优雅

  1. 通过cookie来解决
    • 如果store数据量不大, 将store状态完整的通过cookie传递, 虽然数据量不大, 但也会造成流量问题
    • 将部分首屏渲染依赖数据通过cookie传递, 其他页面数据各自通过store传递, 比上一种方案好一点, 但是开发起来非常麻烦, 并且cookiestore的数据同步也不是很简单的事情
  2. 开源方案: next-redux-wrapper, 它实际上上做的事情和上面的方案是一样的, 其中提到Persist可以使用next-redux-cookie-wrapper来解决, 实际上也是使用cookie来解决, 但是优化了数据传输部分

小结

最终由于这个状态同步的问题, 可以看出ssr不太适合这种类型的项目, 更适合页面结构比较简单, 没有太多状态信息的应用, 但是这次研究仍然是非常有意义的, 彻底梳理清楚了和ssrcsr的优缺点, 可以为以后的项目选型提供参考

作者

Mosby

发布于

2020-12-22

许可协议

评论