【问题标题】:React code splitting and server side rendering with System.import or require.ensure使用 System.import 或 require.ensure 反应代码拆分和服务器端渲染
【发布时间】:2017-01-15 15:54:51
【问题描述】:

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

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

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

这是一个非常基本的类示例,它将加载和呈现拆分代码包 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;

【问题讨论】:

    标签: javascript reactjs webpack isomorphic-javascript


    【解决方案1】:

    就像我在 cmets 中所说的,这里我展示了一个普通的解决方案。

    我只在使用浏览器时才使用 React.lazy/Suspense,而其余的应用程序呈现服务器端。

    Here you can check my code

    【讨论】:

      【解决方案2】:

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

      来自github页面的描述:

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

      这个库是代码拆分组件的演变。与 code-split-component 不同,这个库不需要你使用 Webpack 或 Babel。相反,它为您提供了一个纯 Javascript/React API,该 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} />
      

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

      【讨论】:

        【解决方案3】:

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

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

        规则:

        1. 导入必须在服务器端同步,因为只有一个渲染。
        2. 服务器端必须能够通知客户端服务器正在呈现的任何视图都需要哪些包。
        3. 在 React 开始渲染之前,客户端必须加载服务器通知它的任何包。
        4. 然后客户端可以从此时开始继续进行普通的代码拆分练习。 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 现在已加载并注册,它可以这样做。

        【讨论】:

        • 我的服务器端渲染页面也面临闪烁问题。我没有使用 redux ,我们可以在没有 redux 的情况下做同样的解决方案吗?
        • 从那以后我就再也没有继续这样做了。我会看看上面 clint.beacock 的答案。我预计这个领域的事情正在迅速发展,因此值得对新方法进行调查(例如克林特的答案)。要明确回答您的问题,是的,我认为您可以通过我使用 redux 的自定义状态对象。正如我在回答中所说,我不喜欢这个解决方案,如果可以的话,我会避免它!
        • @John 我通过在浏览器上加载 esnext 导入来管理它,而应用程序的其余部分在服务器端呈现:stackoverflow.com/a/57486954/1951947
        猜你喜欢
        • 2020-02-26
        • 1970-01-01
        • 1970-01-01
        • 2017-06-04
        • 2021-12-05
        • 2018-07-20
        • 2016-02-24
        • 2017-01-14
        • 2019-02-09
        相关资源
        最近更新 更多