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

React code splitting and server side rendering with System.import or require.ensure

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

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

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

这是一个非常基本的 class 示例,它将加载和呈现拆分代码包的内容 SplitComponent

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

然后对于客户端,componentDidMount 将异步加载 SplitComponentSystem.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;

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

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

规则:

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

这里是 Lazy class,它负责管理 SplitComponent 的代码拆分。它利用了 split.js

中的 2 个函数

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 store 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,那么在 React 渲染 Lazy 时,componentDidMount 将触发一个导入 ./SplitComponent 并使用 redux 注册它的异步操作。与任何 redux 操作一样,这种状态变化将导致 Lazy 组件尝试重新呈现,并且由于 SplitComponent 现在已加载并注册,它可以这样做。

如果您正在寻找一种可以大大减少解决方案中涉及的样板文件数量的方法,我建议您查看 'react-async-component' (https://github.com/ctrlplusb/react-async-component)

来自 github 页面的描述:

Create Components that resolve asynchronously, with support for server side rendering and code splitting.

This library is an evolution of code-split-component. Unlike code-split-component this library does not require that you use either Webpack or Babel. Instead it provides you a pure Javascript/React API which has been adapted in a manner to make it generically useful for lazy-loaded Components, with support for modern code splitting APIs (e.g import(), System.import, require.ensure).

我遇到了同样的问题(half-second 在 client-side 渲染器上闪烁),你的解决方案是我找到的唯一解决它的方法,但从那以后我遇到了这个图书馆,它对我来说就像一个魅力。

它通过将组件的状态存储在 server-rendered 模板中的 window 对象上来工作,它使用 client-side 直到异步 client-side 渲染完成.

文档也很好,总是很好。

在服务器端和客户端使用少量样板包装您的渲染方法后,它就像:

import React from 'react';
import { createAsyncComponent } from 'react-async-component';

const AsyncComponent = createAsyncComponent({
  resolve: () => System.import('./components/MyComponent')
});

<AsyncComponent myProp={1} />

试一试。我希望它对你和我一样有效。

正如我在评论中所说,这里我展示了一个普通的解决方案。

我只使用 React。lazy/Suspense 只有当我在浏览器上离开应用程序呈现服务器端的其余部分时。

Here you can check my code