这似乎是一个棘手的问题,但我有一个似乎可行的解决方案。这并不理想,我非常希望看到替代方案。
基本思想是一个 React 组件可以触发另一个的import,以便于代码拆分。这相当简单,但扩展它以支持服务器端渲染增加了很多复杂性。
规则:
- 导入必须在服务器端同步,因为只有一个渲染。
- 服务器端必须能够通知客户端服务器正在呈现的任何视图都需要哪些包。
- 在 React 开始渲染之前,客户端必须加载服务器通知它的任何包。
- 然后客户端可以从此时开始继续进行普通的代码拆分练习。 Bundles 是异步加载的,一旦加载,React 会重新渲染以将它们包含在渲染中。
这里是Lazy 类,它负责管理SplitComponent 的代码拆分。它利用了split.js中的两个函数
当Lazy 在服务器端呈现时,componentWillMount 会运行并检查它是否真的是服务器端。如果是,它会导致同步加载SplitComponent。加载的模块默认值存储在Lazy 组件的状态中,以便可以立即呈现。它还向 Redux 分派一个操作,以注册正在呈现的视图需要此捆绑包这一事实。
服务器端将成功渲染应用程序,redux 存储将包含客户端需要包含 ./SplitComponent 的包这一事实。
//Lazy.jsx
import React from 'react';
import { connect } from 'react-redux';
import { splitComponent, splitComponentSync } from './split';
const canUseDOM = !!(
(typeof window !== 'undefined' &&
window.document && window.document.createElement)
);
class Lazy extends React.Component {
constructor() {
super();
this.state = {
module: null
};
}
componentWillMount() {
// On server side only, synchronously load
const { dispatch } = this.props;
if (!canUseDOM) {
// Also, register this bundle with the current component state as on
// the server there is only a single render and thus the redux state
// available through mapStateToProps is not up-to-date because it was
// requested before the above dispatch.
this.setState({
module: splitComponentSync(dispatch)
});
}
}
componentDidMount() {
const { dispatch, modules } = this.props;
if (!modules.hasOwnProperty('./SplitComponent')) {
splitComponent(dispatch);
}
}
render() {
const { module } = this.state;
const { modules } = this.props;
// On server side, rely on everything being loaded
if (!canUseDOM && module) {
return React.createElement(module);
// On client side, use the redux store
} else if (modules.hasOwnProperty('./SplitComponent') && modules['./SplitComponent']) {
return React.createElement(modules['./SplitComponent']);
}
return null;
}
}
function mapStateToProps(state) {
const modules = state.modules;
return {
modules
};
}
export default connect(mapStateToProps)(Lazy);
//split.js
export const splitComponent = dispatch => {
return System.import('./SplitComponent').then((m) => {
dispatch({
type: 'MODULE_IMPORT',
moduleName: './SplitComponent',
module: m.default
});
});
};
export const splitComponentSync = dispatch => {
// This must be an expression or it will cause the System.import or
// require.ensure to not generate separate bundles
const NAME = './SplitComponent';
const m = require(NAME);
// Reduce into state so that the list of bundles that need to be loaded
// on the client can be, before the application renders. Set the module
// to null as this needs to be imported on the client explicitly before
// it can be used
dispatch({
type: 'MODULE_IMPORT',
moduleName: './SplitComponent',
module: null
});
// Also, register this bundle with the current component state as on
// the server there is only a single render and thus the redux state
// available through mapStateToProps is not up-to-date because it was
// requested before the above dispatch.
return m.default;
};
//reducer.js (Excerpt)
export function modules(
state={}, action) {
switch (action.type) {
case 'MODULE_IMPORT':
const newState = {
...state
};
newState[action.moduleName] = action.module;
return newState;
}
return state;
}
客户端按照从服务器渲染合并 redux 存储的常规过程进行初始化。
一旦发生这种情况,就必须确保在开始渲染之前导入所有必需的包。我们检查 redux 存储 modules 以查看需要什么。我在这里用一个简单的 if 语句查找它们。对于每个需要的包,它是异步加载的,它是模块默认存储在 redux 存储中,并返回一个 Promise。一旦所有这些承诺都得到解决,那么 React 将被允许渲染。
//configureStore.js (Excerpt)
let ps;
if (initialState && initialState.hasOwnProperty('modules')) {
ps = Object.keys(initialState.modules).map(m => {
if (m === './SplitComponent') {
return splitComponent(store.dispatch);
}
});
}
// My configureStore.js returns a Promise and React only renders once it has resolved
return Promise.all(ps).then(() => store);
以后,无论何时使用Lazy+SplitComponent,都不需要加载代码,因为它已经存在于 redux 存储中。
如果初始应用程序没有包含Lazy+SplitComponent,那么当Lazy被React渲染时,componentDidMount会触发一个异步动作来导入./SplitComponent并注册这个与还原。与任何 redux 操作一样,这种状态更改将导致 Lazy 组件尝试重新渲染,并且由于 SplitComponent 现在已加载并注册,它可以这样做。