最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

javascript - React code splitting and server side rendering with System.import or require.ensure - Stack Overflow

programmeradmin7浏览0评论

I am investigating code splitting for use in a React application.

I can't seem to find a way to introduce code splitting (and importing) for server side render which carries through to the client side cleanly.

FYI: I know there is a way to do this with React Router, but I think this is a more general issue and not everyone wants to use it. Also, I feel that code splitting is not synonymous with routes necessarily.

Here is a very basic example of a class that will load and render the contents of a split code bundle SplitComponent.

If the route that is rendered server side includes this ponent then ponentWillMount will ensure that the code is loaded synchronously with require before the render is called. It checks to see if it's server side so it doesn't do this client side.

Then for the client side, ponentDidMount will asynchronously load SplitComponent with System.import.

The result of this is that the server side renders the correct page and the client will display it, but then immediately the ponentDidMount will cause the client side to load the SplitComponent, during which time it will display (however briefly depends on load times) nothing. Finally, SplitComponent will load and be rendered. But there is the potential for flicker as it gets removed, then added again. This detracts from the advantages of doing the rendering on the server.

Is there a better way to handle this?

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
    };
  }

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

  ponentDidMount() {
    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;

I am investigating code splitting for use in a React application.

I can't seem to find a way to introduce code splitting (and importing) for server side render which carries through to the client side cleanly.

FYI: I know there is a way to do this with React Router, but I think this is a more general issue and not everyone wants to use it. Also, I feel that code splitting is not synonymous with routes necessarily.

Here is a very basic example of a class that will load and render the contents of a split code bundle SplitComponent.

If the route that is rendered server side includes this ponent then ponentWillMount will ensure that the code is loaded synchronously with require before the render is called. It checks to see if it's server side so it doesn't do this client side.

Then for the client side, ponentDidMount will asynchronously load SplitComponent with System.import.

The result of this is that the server side renders the correct page and the client will display it, but then immediately the ponentDidMount will cause the client side to load the SplitComponent, during which time it will display (however briefly depends on load times) nothing. Finally, SplitComponent will load and be rendered. But there is the potential for flicker as it gets removed, then added again. This detracts from the advantages of doing the rendering on the server.

Is there a better way to handle this?

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
    };
  }

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

  ponentDidMount() {
    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;
Share Improve this question asked Sep 7, 2016 at 20:43 dpwrdpwr 2,8221 gold badge24 silver badges42 bronze badges
Add a ment  | 

3 Answers 3

Reset to default 4

If you're looking for a method that greatly reduces the amount of boilerplate involved in your solution, I'd remend checking out 'react-async-ponent' (https://github./ctrlplusb/react-async-ponent)

Description from the github page:

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

This library is an evolution of code-split-ponent. Unlike code-split-ponent 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).

I was having the same issue (a half-second flicker on the client-side render), and your solution was the only one I had found to address it, but since then I came across this library, and it worked like a charm for me.

It works by storing the state of the ponent on the window object in the server-rendered template, which it uses client-side until the async client-side render is plete.

The documentation is good as well, which is always nice.

After a small amount of boilerplate to wrap your render methods on the server and client side, its as easy as:

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

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

<AsyncComponent myProp={1} />

Give it a try. I hope it works as well for you as it did for me.

This seems to be a tricky problem, but I have a solution which seems to work. It's not ideal and I would dearly like to see alternatives.

The basic idea is that one React ponent can trigger the import of another in order to facilitate code splitting. This is reasonably simple, but extending this to support server side rendering added a lot of plexity.

The rules:

  1. Import must be synchronous on the server side as there is only a single render.
  2. Server side must be able to inform the client side which bundles are required for whatever view is being rendered by the server.
  3. Client must then load any bundles that the server informed it about, before React begins rendering.
  4. Client then can continue ordinary code-splitting practice from this point onwards. Bundles are loaded asynchronously and once loaded, React rerenders to include them in the rendering.

Here is the Lazy class which is responsible for managing code splitting for the SplitComponent. It makes use of 2 functions from split.js

When Lazy is rendered on the server side, ponentWillMount is run and checks if it is actually the server side. If it is, it causes the loading of the SplitComponent synchronously. The module default that is loaded is stored in the state of the Lazy ponent so that it can then immediately be rendered. It also dispatches an action to Redux to register the fact that this bundle is required for the view that is being rendered.

The server side will successfully render the application and the redux store will contain the fact that the bundle containing ./SplitComponent is required on the client side.

//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
    };
  }

  ponentWillMount() {

    // On server side only, synchronously load
    const { dispatch } = this.props;

    if (!canUseDOM) {

      // Also, register this bundle with the current ponent 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)
      });

    }
  }

  ponentDidMount() {
    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 ponent 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;
    }

The client initialise as per the usual procedure for incorporating the redux store from server rendering.

Once that has happened, it is necessary to ensure that any required bundles are imported before rendering can begin. We examine the redux store modules to see what is required. I look them up in a simple if statement here. For each bundle that is required, it is loaded asynchronously, it's module default stored in the redux store and a Promise returned. Once all those promises are resolved, then React will be allowed to render.

//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);

Going forward, whenever Lazy+SplitComponent are used, no code loading is required because it already exists in the redux store.

In the case when the initial application did not include Lazy+SplitComponent, then at the point when Lazy is rendered by React, ponentDidMount will fire an asynchronous action to import ./SplitComponent and register this with redux. Like any redux action, this change in state will cause the Lazy ponent to attempt to rerender and as the SplitComponent is now loaded and registered, it can do so.

Like i said in ments, here i show a vanilla solution.

I just use React.lazy/Suspense only when i'm on browser leaving the rest of the app render server side.

Here you can check my code

发布评论

评论列表(0)

  1. 暂无评论