为使用 ConnectedRouter 制作的 React.JS 应用程序的 SSR 正确初始化服务器上​​的商店

Correctly initializing the store on the server for SSR of React.JS app made with ConnectedRouter

我正在尝试为使用 Redux、Saga 和 ConnectedRouter 制作的 React 应用程序执行 SSR。我找到了几个相关的例子,特别是 https://github.com/mnsht/cra-ssr and https://github.com/noveogroup-amorgunov/react-ssr-tutorial

我理解的那些应该有效。然而,我在连接状态和历史记录时遇到问题。

我的加载程序代码如下:

export default (req, res) => {
  const injectHTML = (data, { html, title, meta, body, scripts, state }) => {
    data = data.replace('<html>', `<html ${html}>`);
    data = data.replace(/<title>.*?<\/title>/g, title);
    data = data.replace('</head>', `${meta}</head>`);
    data = data.replace(
      '<div id="root"></div>',
      `<div id="root">${body}</div><script>window.__PRELOADED_STATE__ = ${state}</script>${scripts.join(
        ''
      )}`
    );

    return data;
  };
  // Load in our HTML file from our build
  fs.readFile(
    path.resolve(__dirname, '../build/index.html'),
    'utf8',
    (err, htmlData) => {
      // If there's an error... serve up something nasty
      if (err) {
        console.error('Read error', err);
        return res.status(404).end();
      }
      // Create a store (with a memory history) from our current url
      const { store } = createStore(req.url);
      // Let's do dispatches to fetch category and event info, as necessary
      const { dispatch } = store;
      if (
        req.url.startsWith('/categories') &&
        req.url.length - '/categories'.length > 1
      ) {
        dispatch(loadCategories());
      }
      const context = {};
      const modules = [];
      frontloadServerRender(() =>
        renderToString(
          <Loadable.Capture report={m => modules.push(m)}>
            <Provider store={store}>
              <StaticRouter location={req.url} context={context}>
                <Frontload isServer={true}>
                  <App />
                </Frontload>
              </StaticRouter>
            </Provider>
          </Loadable.Capture>
        )
      ).then(routeMarkup => {
        if (context.url) {
          // If context has a url property, then we need to handle a redirection in Redux Router
          res.writeHead(302, {
            Location: context.url
          });
          res.end();
        } else {
          // We need to tell Helmet to compute the right meta tags, title, and such
          const helmet = Helmet.renderStatic();
          ...

以下是我制作商店的方法:

import { createStore, applyMiddleware, compose } from 'redux';
import { routerMiddleware } from 'connected-react-router';
import createSagaMiddleware from 'redux-saga';
import history, { isServer } from './utils/history';
import createReducer from './reducers';

export default function configureStore(
  initialState = !isServer ? window.__PRELOADED_STATE__ : {}
) {
  // Delete state, since we have it stored in a variable
  if (!isServer) {
    delete window.__PRELOADED_STATE__;
  }

  let composeEnhancers = compose;
  const reduxSagaMonitorOptions = {};

  // If Redux Dev Tools and Saga Dev Tools Extensions are installed, enable them
  /* istanbul ignore next */
  if (
    process.env.REACT_APP_STAGE !== 'production' &&
    !isServer &&
    typeof window === 'object'
  ) {
    /* eslint-disable no-underscore-dangle */
    if (window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__)
      composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({});

    // NOTE: Uncomment the code below to restore support for Redux Saga
    // Dev Tools once it supports redux-saga version 1.x.x
    // if (window.__SAGA_MONITOR_EXTENSION__)
    //   reduxSagaMonitorOptions = {
    //     sagaMonitor: window.__SAGA_MONITOR_EXTENSION__,
    //   };
    /* eslint-enable */
  }

  const sagaMiddleware = createSagaMiddleware(reduxSagaMonitorOptions);

  // Create the store with two middlewares
  // 1. sagaMiddleware: Makes redux-sagas work
  // 2. routerMiddleware: Syncs the location/URL path to the state
  const middlewares = [sagaMiddleware, routerMiddleware(history)];

  const enhancers = [applyMiddleware(...middlewares)];

  const store = createStore(
    createReducer(),
    !isServer ? initialState : {},
    composeEnhancers(...enhancers)
  );

  // Extensions
  store.runSaga = sagaMiddleware.run;
  store.injectedReducers = {}; // Reducer registry
  store.injectedSagas = {}; // Saga registry

  // Make reducers hot reloadable, see http://mxs.is/googmo
  /* istanbul ignore next */
  if (module.hot) {
    module.hot.accept('./reducers', () => {
      store.replaceReducer(createReducer(store.injectedReducers));
    });
  }

  return { store, history };
}

和我的历史:

export const isServer = !(
  typeof window !== 'undefined' &&
  window.document &&
  window.document.createElement
);

const history = isServer
  ? createMemoryHistory({
      initialEntries: ['/']
    })
  : createBrowserHistory();

export default history;

我试着将上面的内容变成 createHistory(url) 并在服务器上做 initialEntries: [url].

这还不错,但它并没有解决我真正的问题,那就是 createReducer()。我找到的示例 createReducer(history) 很好。但是,它们不会动态注入 reducer,而我的代码会。因此,我无法轻易更改以下版本:

export default function createReducer(history, injectedReducers = {}) {
  const rootReducer = combineReducers({
    global: globalReducer,
    router: connectRouter(history),
    ...injectedReducers
  });

  return rootReducer;
}

进入该版本,它只静态组装如下所示的减速器(抱歉打字稿):

export default (history: History) =>
    combineReducers<State>({
        homepage,
        catalog,
        shoes,
        router: connectRouter(history),
    });

代码来自https://github.com/noveogroup-amorgunov/react-ssr-tutorial/blob/master/src/store/rootReducer.ts逐字。

有什么建议吗?我将如何完成上述所有操作并使下面的操作正常工作?

export function injectReducerFactory(store, isValid) {
  return function injectReducer(key, reducer) {
    if (!isValid) checkStore(store);

    invariant(
      isString(key) && !isEmpty(key) && isFunction(reducer),
      '(app/utils...) injectReducer: Expected `reducer` to be a reducer function'
    );

    // Check `store.injectedReducers[key] === reducer` for hot reloading when a key is the same but a reducer is different
    if (
      Reflect.has(store.injectedReducers, key) &&
      store.injectedReducers[key] === reducer
    )
      return;

    store.injectedReducers[key] = reducer; // eslint-disable-line no-param-reassign
    store.replaceReducer(createReducer(store.injectedReducers));
  };
}

目前,我的代码没有错误,但状态没有初始化。就好像我没有发送任何动作一样。值是它们最初设置的值。

事实证明,我的加载程序缺少 saga 支持。固定它。现在,它看起来像:

  // Create a store (with a memory history) from our current url
  const { store } = createStore(req.url);
  const context = {};
  const modules = [];
  store
    .runSaga(rootSaga)
    .toPromise()
    .then(() => {
      // We need to tell Helmet to compute the right meta tags, title, and such
      const helmet = Helmet.renderStatic();
      frontloadServerRender(() =>
        renderToString(
          <Loadable.Capture report={m => modules.push(m)}>
            <Provider store={store}>
              <StaticRouter location={req.url} context={context}>
                <Frontload isServer={true}>
                  <App />
                </Frontload>
              </StaticRouter>
            </Provider>
          </Loadable.Capture>
        )
      ).then(routeMarkup => {
        if (context.url) {
          // If context has a url property, then we need to handle a redirection in Redux Router
          res.writeHead(302, {
            Location: context.url
          });
          res.end();
        } else {
          // Otherwise, we carry on...
          // Let's give ourself a function to load all our page-specific JS assets for code splitting
          const extractAssets = (assets, chunks) =>
            Object.keys(assets)
              .filter(
                asset => chunks.indexOf(asset.replace('.js', '')) > -1
              )
              .map(k => assets[k]);
          // Let's format those assets into pretty <script> tags
          const extraChunks = extractAssets(manifest, modules).map(
            c =>
              `<script type="text/javascript" src="/${c.replace(
                /^\//,
                ''
              )}"></script>`
          );
          if (context.status === 404) {
            res.status(404);
          }
          // Pass all this nonsense into our HTML formatting function above
          const html = injectHTML(htmlData, {
            html: helmet.htmlAttributes.toString(),
            title: helmet.title.toString(),
            meta: helmet.meta.toString(),
            body: routeMarkup,
            scripts: extraChunks,
            state: JSON.stringify(store.getState()).replace(/</g, '\u003c')
          });
          // We have all the final HTML, let's send it to the user already!
          res.send(html);
        }
      });
    })
    .catch(e => {
      console.log(e.message);
      res.status(500).send(e.message);
    });
  // Let's do dispatches to fetch category and event info, as necessary
  const { dispatch } = store;
  if (
    req.url.startsWith('/categories') &&
    req.url.length - '/categories'.length > 1
  ) {
    dispatch(loadCategories());
  } else if (
    req.url.startsWith('/events') &&
    req.url.length - '/events'.length > 1
  ) {
    const id = parseInt(req.url.substr(req.url.lastIndexOf('/') + 1));
    dispatch(loadEvent(id));
  }
  store.close();
}