正确初始化用ConnectedRouter制作的React.JS应用的SSR在服务器上的存储



我正在尝试为Redux, Saga和ConnectedRouter制作的React应用程序做SSR。我找到了几个相关的例子,特别是https://github.com/mnsht/cra-ssr和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;
}

到版本中,它只静态地汇编如下的reducer(对不起,typescript):

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();
}

最新更新