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

时间:2016-09-07 20:43:06

标签: javascript reactjs webpack isomorphic-javascript

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

我似乎无法找到一种方法来为服务器端渲染引入代码拆分(和导入),这种方式可以干净地传递到客户端。

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

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

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

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

3 个答案:

答案 0 :(得分:3)

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

github页面的描述:

  

创建异步解析的组件,支持服务器端呈现和代码拆分。

     

这个库是代码拆分组件的演变。与代码拆分组件不同,此库不要求您使用Webpack或Babel。相反,它为您提供了一个纯Javascript / React API,它已经过调整,使其对延迟加载的组件一般有用,并支持现代代码拆分API(例如import(),System.import,require.ensure)。

我遇到了同样的问题(客户端渲染上的半秒闪烁),你的解决方案是我找到解决它的唯一一个,但从那时起我遇到了这个库,它就像对我来说很有魅力。

它的工作原理是将组件的状态存储在服务器呈现模板中的window对象上,它使用客户端,直到异步客户端呈现完成为止。

文档也很好,总是很好。

在服务器和客户端上包含少量样板文件以包装渲染方法之后,它就像以下一样简单:

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

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

<AsyncComponent myProp={1} />

试一试。我希望它对你有效,就像对我一样。

答案 1 :(得分:2)

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

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

规则:

  1. 导入必须在服务器端同步,因为只有一个渲染。
  2. 服务器端必须能够通知客户端服务器呈现的任何视图都需要哪些捆绑包。
  3. 然后,在React开始渲染之前,客户端必须加载服务器通知的任何捆绑包。
  4. 客户端可以从此时起继续普通的代码分割实践。捆绑包是异步加载的,一旦加载,React会重新渲染以将它们包含在渲染中。
  5. 以下是Lazy类,负责管理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存储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现在已加载并注册,它可以执行此操作。

答案 2 :(得分:0)

就像我在评论中说的那样,在这里我展示了一种香草解决方案。

仅当我在浏览器上离开应用程序渲染服务器的其余部分时,我才使用React.lazy / Suspense。

Here you can check my code