使用 System.import 或 require.ensure 响应代码分割和服务器端渲染

dpw*_*pwr 5 javascript reactjs webpack isomorphic-javascript

我正在研究在 React 应用程序中使用代码分割。

我似乎找不到一种方法来为服务器端渲染引入代码分割(和导入),从而干净地传递到客户端。

仅供参考:我知道有一种方法可以使用 React Router 来做到这一点,但我认为这是一个更普遍的问题,并不是每个人都想使用它。另外,我觉得代码分割不一定与路由同义。

下面是一个非常基本的类示例,它将加载和呈现分割代码包的内容SplitComponent

如果服务器端渲染的路由包含此组件,则将确保在调用之前componentWillMount同步加载代码。它检查它是否是服务器端,因此它不会执行此客户端操作。requirerender

然后对于客户端,componentDidMount将异步SplitComponent加载System.import.

这样做的结果是服务器端呈现正确的页面并且客户端将显示它,但随后将立即componentDidMount导致客户端加载SplitComponent,在此期间它将不显示任何内容(但短暂地取决于加载时间)。最后,SplitComponent将加载并渲染。但当它被移除然后再次添加时,可能会出现闪烁。这削弱了在服务器上进行渲染的优势。

有更好的方法来处理这个问题吗?

import React from 'react';

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

class Lazy extends React.Component {
  constructor() {
    super();
    this.state = {
      module: null
    };
  }

  componentWillMount() {
    if (!canUseDOM) {
      const m = require('./SplitComponent');
      this.setState({
        module: m.default
      });
    }
  }

  componentDidMount() {
    if (!this.state.module) {
      System.import('./SplitComponent').then(m => {
        this.setState({
          module: m.default
        });
      });
    }
  }

  render() {
    const { module } = this.state;
    console.log('Rendering Lazy', module);
    if (module) {
      return React.createElement(module);
    }

    return null;
  }
}

export default Lazy;
Run Code Online (Sandbox Code Playgroud)

dpw*_*pwr 2

这似乎是一个棘手的问题,但我有一个似乎有效的解决方案。这并不理想,我非常希望看到替代方案。

基本思想是一个 React 组件可以触发import另一个 React 组件,以促进代码分割。这相当简单,但是扩展它以支持服务器端渲染增加了很多复杂性。

规则:

  1. 导入必须在服务器端同步,因为只有一个渲染。
  2. 服务器端必须能够通知客户端服务器呈现的任何视图需要哪些捆绑包。
  3. 然后,在 React 开始渲染之前,客户端必须加载服务器通知它的任何包。
  4. 然后,客户端可以从此时开始继续普通的代码分割实践。捆绑包是异步加载的,加载后,React 会重新渲染以将它们包含在渲染中。

这是Lazy负责管理SplitComponent. 它使用了 2 个函数split.js

Lazy在服务器端渲染时,componentWillMount运行并检查它是否实际上是服务器端。如果是,则会导致同步加载SplitComponent。加载的模块默认值存储在组件的状态中Lazy,以便可以立即呈现。它还向 Redux 分派一个操作来注册正在渲染的视图需要此包的事实。

./SplitComponent服务器端将成功渲染应用程序,并且 redux 存储将包含客户端需要包含的包这一事实。

//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);
Run Code Online (Sandbox Code Playgroud)
//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;
};
Run Code Online (Sandbox Code Playgroud)
//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;
    }
Run Code Online (Sandbox Code Playgroud)

客户端按照从服务器渲染合并 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);
Run Code Online (Sandbox Code Playgroud)

展望未来,每当使用Lazy+SplitComponent时,都不需要加载代码,因为它已经存在于 redux 存储中。

如果初始应用程序不包含Lazy+ ,那么在React 渲染SplitComponent时,将触发一个异步操作来导入并注册它到 redux。与任何 redux 操作一样,这种状态更改将导致组件尝试重新渲染,并且现在已加载并注册,它可以这样做。LazycomponentDidMount./SplitComponentLazySplitComponent


归档时间:

查看次数:

1546 次

最近记录:

6 年,3 月 前