为使用 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();
}
我正在尝试为使用 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();
}