Common Problems of Isomorphism and Isomorphism Using React's static Method

Posted by ik on Tue, 25 Jun 2019 21:19:03 +0200

Code address please github View If there is new content, I will update it regularly, and welcome you to star,issue and make progress together.

1. Where does our server render data come from?

1.1 How to write isomorphic components

Sometimes the HTML structure generated by the server side is not perfect, sometimes it is not possible without the help of js. For example, when our components need to poll the data interface of the server, it is very important to synchronize the data with the server. In fact, the process of data acquisition can be database acquisition or other. Reverse Proxy Server To get it. For the client, we can complete the ajax request by placing the ajax request in the componentDidMount method. There are two reasons for putting it in this method, the first is to ensure that DOM is mounted on the page at this time, and the other is that calling setState in this method will result in component re-rendering (see specifically). This article ) For the server side,
On the one hand, what it needs to do is to go to the database or reverse proxy server to pull data - > generate HTML - > spit it to the client based on the data. This is a fixed process. The process of pulling data and generating HTML can not be disrupted. There is no asynchronous process such as vomiting content to the client and pulling data. Therefore, componentDidMount is not applicable when the server renders components (because the render method has been invoked, but componentDidMount has not yet been executed, so rendering results in components without data. The reason is that the lifecycle method componentDidMount is called after render.

On the other hand, the componentDidMount method will never be implemented on the server side! Therefore, we need to adopt a completely inconsistent method with client rendering to solve the problem of data before rendering. You can see the difference between server-side rendering and client-side rendering Node's Outline Summary of Theory and Practice

var React = require('react');
var DOM = React.DOM;
var table = DOM.table, tr = DOM.tr, td = DOM.td;
var Data = require('./data');
module.exports = React.createClass({
    statics: {
        //Getting data in a real production environment is an asynchronous process, so our code needs to be asynchronous as well.
        fetchData: function (callback) {
            Data.fetch().then(function (datas) {
                callback.call(null, datas);
            });
        }
    },
    render: function () {
        return table({
                children: this.props.datas.map(function (data) {
                    return tr(null,
                        td(null, data.name),
                        td(null, data.age),
                        td(null, data.gender)
                    );
                })
            });
    },
    componentDidMount: function () {
        setInterval(function () {
            // this.constructor.xxx is used when the static method is called inside the component
            // Client obtains data in component DidMount and calls setState to modify status requirements
            // Component Rendering
            this.constructor.fetchData(function (datas) {
                this.setProps({
                    datas: datas
                });
            });
        }, 3000);
    }
});

The server-side processing logic render-server.js is as follows:

var React = require('react');
var ReactDOMServer = require('react-dom/server');
// table class
var Table = require('./Table');
// table instance
var table = React.createFactory(Table);
module.exports = function (callback) {
    //When the client calls Data.fetch, it initiates an ajax request, while when the server calls Data.fetch,
    //It is possible to obtain data and query database from other data servers through UDP protocol.
    Table.fetchData(function (datas) {
        var html = ReactDOMServer.renderToString(table({datas: datas}));
        callback.call(null, html);
    });
};

Below is the logical server.js of the server:

var makeTable = require('./render-server');
var http = require('http');
//Registration Middleware
http.createServer(function (req, res) {
    if (req.url === '/') {
        res.writeHead(200, {'Content-Type': 'text/html'});
        //First, access the database or anti-proxy server to get the data, and register callbacks to return the html structure containing the data to the client. Here, only one component is rendered. Otherwise, renderProps.components.forEach is needed to traverse all components to get the data.
        //http://www.toutiao.com/i6284121573897011714/
        makeTable(function (table) {
            var html = '<!doctype html>\n\
                      <html>\
                        <head>\
                            <title>react server render</title>\
                        </head>\
                        <body>' +
                            table +
                            //Here is the client's code to update data at regular intervals. For how to add the following script tags, you can refer to https://github.com/liangklfangl/react-universal-bucket here.
                            '<script src="pack.js"></script>\
                        </body>\
                      </html>';
            res.end(html);
        });
    } else {
        res.statusCode = 404;
        res.end();
    }
}).listen(1337, "127.0.0.1");
console.log('Server running at http://127.0.0.1:1337/');

Note: Because our react server rendering is only one-time and will not reRender with the call to setState, we need to add client code to the html returned to the client. The real logic of updating components at regular intervals is done by the client through ajax.

1.2 How to avoid client rendering after server rendering

What makes the data-react-checksum generated by the server? Let's think, even if the server doesn't initialize HTML data, it can completely render our components only by client's React. Will the server generate HTML data and be re-rendered when the client's React is executed? What we have worked so hard to produce on the server side has been mercilessly covered by the client side? Of course not! React generates checksum for components when rendering on the server side (in the case of redux, it should be a component tree, which generates checksum for the whole component tree, because the whole component tree is what we want to display on the front page) (checksum), so that client React reuses the original DOM generated on the server side and incrementally updates it when processing the same component. That is to say, when the checksum of client and server is inconsistent, dom diff and incremental update will be carried out. This is the function of data-react-checksum. It can be summarized in the following sentences:

 If data-react-checksum is the same, render is not re-rendered, the process of creating DOM and mounting DOM is omitted, and then events such as component DidMount are triggered to deal with the unfinished matters (event binding, etc.) on the server side, thus speeding up the interaction time; at different times, components are re-mounted render on the client side.

The difference between ReactDOMServer.renderToString and ReactDOMServer.renderToStaticMarkup is well explained at this time. The former generates checksum for components, while the latter does not. The latter generates only HTML structural data. Therefore, renderToStatic Markup can only be used if you don't want to operate the same component at the client-server side at the same time. Note: The statics block is used above, which is only available in createClass. You can use the following:

//Writing in Components
class Component extends React.Component {
    static propTypes = {
    ...
    }
    static someMethod(){
    }
}

Outside the components, you can write as follows:

class Component extends React.Component {
   ....
}
Component.propTypes = {...}
Component.someMethod = function(){....}

Specifically, you can Check out here . As for server-side rendering, the following warning often occurs, mostly because the data on the server is not returned together when HTML is returned, or because the data format returned is not correct.

Warning: React attempted to reuse markup in a container but the checksum was invalid. This generally means that you are using server rendering and the markup generatted on the server was not what the client was expecting. React injected new markup to compensate which works but you have lost many of the benefits of server rendering. Insted, figure out why the markup being generated is different on the client and server

2. How to distinguish client code from server code

2.1 Add client code to server rendered html string

adopt This example We know that the plug-in webpack-isomorphic-tools is added to the plugin of webpack:

module.exports = {
    entry:{
        'main': [
          'webpack-hot-middleware/client?path=http://' + host + ':' + port + '/__webpack_hmr',
        // "bootstrap-webpack!./src/theme/bootstrap.config.js",
        "bootstrap-loader",
        //Make sure bootstrap 3 is installed, bootstrap 4 does not support less
          './src/client.js'
        ]
    },
   output: {
      path: assetsPath,
      filename: '[name]-[hash].js',
      chunkFilename: '[name]-[chunkhash].js',
      publicPath: 'http://' + host + ':' + port + '/dist/'
      //Represents the prefix that must be prefixed before accessing the packaged resources of our client, that is, virtual paths.
    },
    plugins:[
        new webpack.DefinePlugin({
          __CLIENT__: true,
          __SERVER__: false,
          __DEVELOPMENT__: true,
          __DEVTOOLS__: true //,
        }),
     webpackIsomorphicToolsPlugin.development()
     //In the development mode of webpack, it must be called to support asset holding reloading!
     //https://github.com/liangklfang/webpack-isomorphic-tools
    ]
}

At this point, our client.js will be packaged into the corresponding file path, and then in our template, just add the packaged script file to html and return it to the client. Following is the logic returned by traversing our webpack-assets.json to get all our generated resources and then adding them to the html template:

export default class Html extends Component {
  static propTypes = {
    assets: PropTypes.object,
    component: PropTypes.node,
    store: PropTypes.object
  };
  render() {
    const {assets, component, store} = this.props;
    const content = component ? renderToString(component) : '';
    //If a component component component is passed in, we call renderToString directly.
    const head = Helmet.rewind();
    return (
      <html lang="en-us">
        <head>
          {head.base.toComponent()}
          {head.title.toComponent()}
          {head.meta.toComponent()}
          {head.link.toComponent()}
          {head.script.toComponent()}
          <link rel="shortcut icon" href="/favicon.ico" />
         <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.6.3/css/font-awesome.min.css"/>
        <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Work+Sans:400,500"/>
        <link rel="stylesheet" href="https://cdn.jsdelivr.net/violet/0.0.1/violet.min.css"/>
          <meta name="viewport" content="width=device-width, initial-scale=1" />
          {/* styles (will be present only in production with webpack extract text plugin)
             styles Attributes only exist in production mode, which is added through link. Easy caching
           */}
          {Object.keys(assets.styles).map((style, key) =>
            <link href={assets.styles[style]} key={key} media="screen, projection"
                  rel="stylesheet" type="text/css" charSet="UTF-8"/>
          )}
         {/*
            assets.styles If in development mode, it must be empty, then we can insert it directly in-line. At this point, our css is not extracted separately, that is, there is no ExtractText Web pack Plugin, packaged into js and inlined.
        */}
          {/* (will be present only in development mode) */}
          {/* outputs a <style/> tag with all bootstrap styles + App.scss + it could be CurrentPage.scss. */}
          {/* can smoothen the initial style flash (flicker) on page load in development mode. */}
          {/* ideally one could also include here the style for the current page (Home.scss, About.scss, etc) */}
        </head>
        <body>
          <div id="content" dangerouslySetInnerHTML={{__html: content}}/>
           {/*Place the component renderToString inside the div with id content*/}
          <script dangerouslySetInnerHTML={{__html: `window.__data=${serialize(store.getState())};`}} charSet="UTF-8"/>
          {/*Serialize store.getState and place it on window. data so that client code can be obtained*/}
          <script src={assets.javascript.main} charSet="UTF-8"/>
          {/*Put our main.js, resources from the client packaged and placed in a specific folder, on the page.
               This becomes the client's own js resource
          */}
        </body>
      </html>
    );
  }
}

So the following div#content is the html string rendered by the server and returned to the client as it is. In this way, the task for the server is completed.

 <div id="content" dangerouslySetInnerHTML={{__html: content}}/>

The content of our script tag below is the result of packaging our client code:

   <script src={assets.javascript.main} charSet="UTF-8"/>

At this point, both client and server logic has been completed, the client can continue to receive user operations and send ajax requests to update component status.

2.2 How to make the logic of requesting common between server and client

A good use is to use isomorphic-fetch

2.3 Notes in the Isomorphism of Immutable Data

First, the result of store.getState must be obtained when the server returns. serialize And if a part of the state returned by the store is immutbale, then the client needs to recreate this part of the state data to create a new immutable object (in the following example, our recipeGrid and connect are immutable):

  <script dangerouslySetInnerHTML={{__html: `window.__data=${serialize(store.getState())};`}} charSet="UTF-8"/>

For the client, we have to convert the state data injected from the server to HTML into an immutable object, which is used as an initial state to create a store:

  const data = window.__data;
  //Where data is the value of store.getState returned by the server, which is the current state of the store.
  if (data) {
     data.recipeGrid = Immutable.fromJS(data.recipeGrid);
     //It must be set here, otherwise the error will be reported as: paginator.equals is not a function
      data.connect = Immutable.fromJS(data.connect);
     //You can use https://github.com/liangklfang/redux-immutable JS
  }
  const store = finalCreateStore(reducer, data);
2.4 server does not support ES6 compatibility

If you want to use ES6 syntax such as import on the server side, you can configure the. babelrc file in the root directory of the project in the following way:

{
  "presets": ["react", "es2015", "stage-0"],
  "plugins": [
    "transform-runtime",
    "add-module-exports",
    "transform-decorators-legacy",
    "transform-react-display-name"
  ]
}

Then configure a separate file server.babel.js:

const fs = require("fs");
const babelrc = fs.readFileSync("./.babelrc");
let config ;
try{
    config = JSON.parse(babelrc);
}catch(err){
    console.error("Your.babelrc The document is incorrect. Please check it carefully.");
    console.error(err);
}
//You can specify ignore configuration to ignore certain files.
//https://github.com/babel/babel/tree/master/packages/babel-register
require("babel-register")(config);
//require("babel-register") causes all subsequent. es6,.es,.js,.jsx files to be processed by Babel

Finally, we add our server.js as follows (direct node server.js, and the real logic in.. / src/server):

#!/usr/bin/env node
require('../server.babel'); // babel registration (runtime transpilation for node)
var path = require('path');
var rootDir = path.resolve(__dirname, '..');
global.__CLIENT__ = false;
global.__SERVER__ = true;
global.__DISABLE_SSR__ = false;  
// <----- DISABLES SERVER SIDE RENDERING FOR ERROR DEBUGGING
global.__DEVELOPMENT__ = process.env.NODE_ENV !== 'production';
if (__DEVELOPMENT__) {
//Hot loading of server-side code
  if (!require('piping')({
      hook: true,
      ignore: /(\/\.|~$|\.json|\.scss$)/i
    })) {
    return;
  }
}
// https://github.com/halt-hammerzeit/webpack-isomorphic-tools
var WebpackIsomorphicTools = require('webpack-isomorphic-tools');
global.webpackIsomorphicTools = new WebpackIsomorphicTools(require('../webpack/webpack-isomorphic-tools-config'))
  .development(__DEVELOPMENT__)
  .server(rootDir, function() {
  //rootDir must be consistent with the context of webpack. Calling this method server can directly require any resource.
  //This path is used to get the webpack-assets.json file, which is the output of the webpack
  // webpack-isomorphic-tools is all set now.
  // here goes all your web application code:
  // (it must reside in a separate *.js file 
  //  in order for the whole thing to work)
  //  Now that webpack-isomorphic-tools are registered, you can write the code of your web application here, and the code must be in a separate file.
    require('../src/server');
  });

After the babel-register processing above, you can use any ES6 code in your.. / src/server.js.

2.5 Server-side code packaged separately using webpack

If the server-side code is to be packaged separately, the following settings must be made:

target: "node"

You can Refer here..

2.6 Server rendering ignores css/less/scss files

In 2.4, we use babel-register to help server recognize special js grammar, but we can't do anything about less/css files. Fortunately, in general, server rendering does not require the participation of style files. CSS files can only be introduced into HTML files. Therefore, all css/less files can be ignored through configuration items:

require("babel-register")({
  //By default ignore is node_modules indicating that require s for all files under node_modules will not be processed
  //It is explicitly specified here that css/less is not processed by babel
  ignore: /(.css|.less)$/, });

You can see the details. babel-register document . You can pass all the other options it specifies, including plugins and presets. But one thing to note is that the closest. babelrc to our source file always works, and its priority is higher than the options you configure here. At this point, we ignore that parsing style files does not cause the client to render the component again, because our checksum has nothing to do with the specific css/less/scss file, but with the result of component render.

2.7 Identify css/less/scss files using webpack-isomorphic-tools

Through babel-register, we can use Babel to solve the jsx grammar problem and ignore css/less. But in the case of using CSS Modules, the server must be able to parse less file to get the converted class name. Otherwise, the HTML structure rendered by the server and the client CSS file generated by packaging can not correspond to the class name. The reason is that when we use CSS Module on the server side, we must complete the class name setting in the following way:

const React = require("react");
const styles = require("./index.less");
class Test extends React.Component{
 render(){
     return (
        //If it's not a css module, this might be the case: className="banner"
           <div className={styles.banner}>This is banner<\/div>
        )
   }
}

If the server can't parse css/less, the final class name (className processed by css module) can't be obtained. This results in inconsistencies in checksum of components rendered by client and server (because of inconsistencies in class values). For the case of ignoring less/css file mentioned in 2.6, although the server did not parse the class name, the same string has been specified on our component through the class attribute value, so checksum is completely consistent.

To solve this problem, an additional tool, webpack-isomorphic-tools, is needed to help identify less files. With this tool, we will deal with less/css/scss files introduced by server-side components in particular. The following is the content of the SCSS files introduced by Widget components packaged and written into webpack-assets.json:

 "./src/containers/Widgets/Widgets.scss": {
      "widgets": "widgets___3TrPB",
      "refreshBtn": "refreshBtn___18-3v",
      "idCol": "idCol___3gf_9",
      "colorCol": "colorCol___2bs_U",
      "sprocketsCol": "sprocketsCol___3nkz0",
      "ownerCol": "ownerCol___fwn86",
      "buttonCol": "buttonCol___1feoO",
      "saving": "saving___7FVQZ",
      "_style": ".widgets___3TrPB .refreshBtn___18-3v {\n  margin-left: 20px;\n}\n\n.widgets___3TrPB .idCol___3gf_9 {\n  width: 5%;\n}\n\n.widgets___3TrPB .colorCol___2bs_U {\n  width: 20%;\n}\n\n.widgets___3TrPB .sprocketsCol___3nkz0 {\n  width: 20%;\n  text-align: right;\n}\n\n.widgets___3TrPB .sprocketsCol___3nkz0 input {\n  text-align: right;\n}\n\n.widgets___3TrPB .ownerCol___fwn86 {\n  width: 30%;\n}\n\n.widgets___3TrPB .buttonCol___1feoO {\n  width: 25%;\n}\n\n.widgets___3TrPB .buttonCol___1feoO .btn {\n  margin: 0 5px;\n}\n\n.widgets___3TrPB tr.saving___7FVQZ {\n  opacity: 0.8;\n}\n\n.widgets___3TrPB tr.saving___7FVQZ .btn[disabled] {\n  opacity: 1;\n}\n"
    }

At this point, on the server side, you can use styles.banner as mentioned above to set up the className, without worrying that using babel-register can only ignore the css/less/scss file and can not use the css module feature, which leads to checksum inconsistency! Specifically, you can Check out here

2.8 Different Processing for Front-end and Back-end Routing

A common problem with single-page applications is that all the code is loaded together when the page is initialized, even if this part of the code is unnecessary, which often results in a long white screen. webpack supports splitting your code into different chunk s and loading it on demand. When we load the code logic needed for a particular routing, which logic is not needed for the current page On-demand loading . For server-rendering, our server will not load on demand, and our client will often use it. System.import or require.ensure to load on demand.

For example, the following example:

module.exports = {
    path: 'complex',
    getChildRoutes(partialNextState, cb) {
       //If it's server-side rendering, we pack Page1,Page2 and all other components together. If it's client-side, we pack the logic of Page1 and Page2 into a single chunk to load on demand.
        if (ONSERVER) {
            cb(null, [
                require('./routes/Page1'),
                require('./routes/Page2')
            ])
        } else {
            require.ensure([], (require) => {
                cb(null, [
                    require('./routes/Page1'),
                    require('./routes/Page2')
                ])
            })
        }
    },
    //IndexRoute represents the default loaded subcomponent.
    getIndexRoute(partialNextState, cb) {
        if (ONSERVER) {
            const { path, getComponent } = require('./routes/Page1');
            cb(null, { getComponent });
        } else {
            require.ensure([], (require) => {
                // separate out the path part, otherwise warning raised
                // Get the path and getComponent of the next module, because it is exported directly by module.export
                // We pass getComponent directly to the callback function
                const { path, getComponent } = require('./routes/Page1');
                cb(null, { getComponent });
            })
        }
    },
    getComponent(nextState, cb) {
        if (ONSERVER) {
            cb(null, require('./components/Complex.jsx'));
        } else {
            require.ensure([], (require) => {
                cb(null, require('./components/Complex.jsx'))
            })
        }
    }
}

The routing of this example corresponds to / complex, and if it is rendered on the server side, we will package Page1,Page2 and other component code together. If it's client rendering, we pack Page1 and Page2 into a single chunk, which is loaded when the user accesses "/ complex". So why does the server render Page1 and Page2 together? In fact, you have to make sure that for server rendering, rendering Page1 and Page2 together is actually to get the DOM of the two sub-pages back to the client (two Tab pages that form the sub-pages of the current page). The client loads the chunk separately in order to make this part of the DOM respond to user clicks, scrolls and other events. Note: Server rendering is related to our req.url, as follows:

 match({ history, routes: getRoutes(store), location: req.originalUrl }, (error, redirectLocation, renderProps) => {
    if (redirectLocation) {
      res.redirect(redirectLocation.pathname + redirectLocation.search);
      //Redirection to add pathname+search
    } else if (error) {
      console.error('ROUTER ERROR:', pretty.render(error));
      res.status(500);
      hydrateOnClient();
      //Send 500 to tell the client that the request failed without caching
    } else if (renderProps) {
      loadOnServer({...renderProps, store, helpers: {client}}).then(() => {
        const component = (
          <Provider store={store} key="provider">
            <ReduxAsyncConnect {...renderProps} />
          <\/Provider>
        );
        res.status(200);
        global.navigator = {userAgent: req.headers['user-agent']};
        res.send('<!doctype html>\n' +
          renderToString(<Html assets={webpackIsomorphicTools.assets()} component={component} store={store}\/>));
      });
    } else {
      res.status(404).send('Not found');
    }
  });
});

Our server gets it based on req.url renderProps To render a component tree into an html string and return it to the client. So our server will not render on demand, the result is only to render more part of the DOM under the path, and this has the advantage of fast response to user operations (or to register events on the client) rather than re-render the part of the DOM by the client. From the client side, I just need to load the corresponding chunks under the path at this time, instead of loading the entire application chunks together, so as to load on demand, faster and more reasonable.

Attention should be paid to match ing routing on the server side: try to pre-redirect (write to onEnter of routing). Do not redirect after routing is determined unless you need to pull data for judgment. Because after getting the routing configuration, you have to pull the data according to the corresponding page. It's wasteful to redirect after that. The following examples are given:

  const requireLogin = (nextState, replace, cb) => {
    function checkAuth() {
      const { auth: { user }} = store.getState();
      if (!user) {
        // oops, not logged in, so can't be here!
        replace('/');
      }
      cb();
    }
    if (!isAuthLoaded(store.getState())) {
      store.dispatch(loadAuth()).then(checkAuth);
    } else {
      checkAuth();
    }
  };

The following routing configuration uses the onEnter hook function:

    <Route onEnter={requireLogin}>
       //Without login, the following routing components will not instantiate at all, let alone pull data
        <Route path="chat" component={Chat}/>
        <Route path="loginSuccess" component={LoginSuccess}/>
  <\/Route>

Reference material:

React isomorphism

Why does React data acquisition have to be called in component DidMount?

ReactJS life cycle, data flow and events

React statics with ES6 classes

Summary of React Isomorphism Outline Optimization

Tencent News React Isomorphism Straightens out Optimizing Practice

Node's Outline Summary of Theory and Practice

React+Redux Isomorphic Application Development

ReactJS Server-side Isomorphic Practice of "QQ Music web Team"

Code Splitting - Use require.ensure

Performance optimization Trilogy No. 3 - Node opens your web page in seconds 6

Topics: React Webpack less github