天天看點

使用React的static方法實作同構以及同構的常見問題

代碼位址請在github檢視,如果有新内容,我會定時更新,也歡迎您star,issue,共同進步

1.我們服務端渲染資料從何而來

1.1 如何寫出同構的元件

服務端生成HTML結構有時候并不完善,有時候不借助js是不行的。比如當我們的元件需要輪詢伺服器的資料接口,實作資料與伺服器同步的時候就顯得很重要。其實這個擷取資料的過程可以是資料庫擷取,也可以是從其他的反向代理伺服器來擷取。對于用戶端來說,我們可以通過ajax請求來完成,隻要将ajax請求放到componentDidMount方法中來完成就可以。而之是以放在該方法中有兩個原因,第一個是為了保證此時DOM已經挂載到頁面中;另一個原因是在該方法中調用setState會導緻元件重新渲染(具體你可以檢視這個文章)。而對于服務端來說,

一方面它要做的事情便是:去資料庫或者反向代理伺服器拉取資料 -> 根據資料生成HTML -> 吐給用戶端。這是一個固定的過程,拉取資料和生成HTML過程是不可打亂順序的,不存在先把内容吐給用戶端,再拉取資料這樣的異步過程。是以,componentDidMount在伺服器渲染元件的時候,就不适用了(因為render方法已經調用,但是componentDidMount還沒有執行,是以渲染得到的是沒有資料的元件。原因在于生命周期方法componentDidMount在render之後才會調用)。

另一方面,componentDidMount這個方法,在服務端确實永遠都不會執行!是以我們要采用和用戶端渲染完全不一緻的方法來解決渲染之前資料不存在問題。關于服務端渲染和用戶端渲染的差別你可以檢視Node直出理論與實踐總結

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: {
        //擷取資料在實際生産環境中是個異步過程,是以我們的代碼也需要是異步的
        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 () {
            // 元件内部調用statics方法時,使用this.constructor.xxx
            // 用戶端在componentDidMount中擷取資料,并調用setState修改狀态要求
            // 元件重新渲染
            this.constructor.fetchData(function (datas) {
                this.setProps({
                    datas: datas
                });
            });
        }, );
    }
});
           

其中伺服器端的處理邏輯render-server.js如下:

var React = require('react');
var ReactDOMServer = require('react-dom/server');
// table類
var Table = require('./Table');
// table執行個體
var table = React.createFactory(Table);
module.exports = function (callback) {
    //在用戶端調用Data.fetch時,是發起ajax請求,而在服務端調用Data.fetch時,
    //有可能是通過UDP協定從其他資料伺服器擷取資料、查詢資料庫等實作
    Table.fetchData(function (datas) {
        var html = ReactDOMServer.renderToString(table({datas: datas}));
        callback.call(null, html);
    });
};
           

下面是伺服器的邏輯server.js:

var makeTable = require('./render-server');
var http = require('http');
//注冊中間件
http.createServer(function (req, res) {
    if (req.url === '/') {
        res.writeHead(, {'Content-Type': 'text/html'});
        //先通路資料庫或者反代理伺服器來擷取到資料,并注冊回調,将含有資料的html結構傳回給用戶端,此處隻是渲染一個元件,否則需要renderProps.components.forEach來周遊所有的元件擷取資料
        //http://www.toutiao.com/i6284121573897011714/
        makeTable(function (table) {
            var html = '<!doctype html>\n\
                      <html>\
                        <head>\
                            <title>react server render</title>\
                        </head>\
                        <body>' +
                            table +
                            //這裡是用戶端的代碼,實作每隔一定事件更新資料,至于如何添加下面的script标簽内容,可以參考這裡https://github.com/liangklfangl/react-universal-bucket
                            '<script src="pack.js"></script>\
                        </body>\
                      </html>';
            res.end(html);
        });
    } else {
        res.statusCode = ;
        res.end();
    }
}).listen(, "127.0.0.1");
console.log('Server running at http://127.0.0.1:1337/');
           

注意:因為我們的react服務端渲染隻是一次性的,不會随着調用setState而重新reRender,是以我們需要在傳回給用戶端的html中加入用戶端的代碼,真正的每隔一定時間更新元件的邏輯是用戶端通過ajax來完成的。

1.2 如何避免服務端渲染後用戶端再次渲染

服務端生成的data-react-checksum是幹嘛使的?我們想一想,就算服務端沒有初始化HTML資料,僅僅依靠用戶端的React也完全可以實作渲染我們的元件,那服務端生成了HTML資料,會不會在用戶端React執行的時候被重新渲染呢?我們服務端辛辛苦苦生成的東西,被用戶端無情地覆寫了?當然不會!React在服務端渲染的時候,會為元件生成相應的校驗和(在redux的情況下其實應該是一個元件樹,為整個元件樹生成校驗和,因為這整個元件樹就是我們首頁要顯示的内容)(checksum),這樣用戶端React在處理同一個元件的時候,會複用服務端已生成的初始DOM,增量更新(也就是說當用戶端和服務端的checksum不一緻的情況下才會進行dom diff,進行增量更新),這就是data-react-checksum的作用。可以通過下面的幾句話來總結下:

如果data-react-checksum相同則不重新render,省略建立DOM和挂載DOM的過程,接着觸發 componentDidMount 等事件來處理服務端上的未盡事宜(事件綁定等),進而加快了互動時間;不同時,元件在用戶端上被重新挂載 render。
      

ReactDOMServer.renderToString 和 ReactDOMServer.renderToStaticMarkup 的差別在這個時候就很好解釋了,前者會為元件生成checksum,而後者不會,後者僅僅生成HTML結構資料。是以,隻有你不想在用戶端-服務端同時操作同一個元件的時候,方可使用renderToStaticMarkup。注意:上面使用了statics塊,該寫法隻在createClass中可用,你可以使用下面的寫法:

//元件内的寫法
class Component extends React.Component {
    static propTypes = {
    ...
    }
    static someMethod(){
    }
}
           

在元件外面你可以按照如下寫法:

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

具體你可以檢視這裡。關于服務端渲染經常會出現下面的warning,大多數情況下是因為在傳回 HTML 的時候沒有将服務端上的資料一同傳回,或者是傳回的資料格式不對導緻

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.如何區分用戶端與服務端代碼

2.1 添加用戶端代碼到服務端渲染的html字元串

通過這個例子我們知道,将webpack-isomorphic-tools這個插件添加到webpack的plugin中:

module.exports = {
    entry:{
        'main': [
          'webpack-hot-middleware/client?path=http://' + host + ':' + port + '/__webpack_hmr',
        // "bootstrap-webpack!./src/theme/bootstrap.config.js",
        "bootstrap-loader",
        //確定安裝bootstrap3,bootstrap4不支援less
          './src/client.js'
        ]
    },
   output: {
      path: assetsPath,
      filename: '[name]-[hash].js',
      chunkFilename: '[name]-[chunkhash].js',
      publicPath: 'http://' + host + ':' + port + '/dist/'
      //表示要通路我們用戶端打包好的資源必須在前面加上的字首,也就是虛拟路徑
    },
    plugins:[
        new webpack.DefinePlugin({
          __CLIENT__: true,
          __SERVER__: false,
          __DEVELOPMENT__: true,
          __DEVTOOLS__: true //,
        }),
     webpackIsomorphicToolsPlugin.development()
     //在webpack的development模式下一定更要調用它支援asset hold reloading!
     //https://github.com/liangklfang/webpack-isomorphic-tools
    ]
}
           

此時我們client.js會被打包到相應的檔案路徑下,然後在我們的模版中,隻要将這個打包好的script檔案添加到html傳回給用戶端就可以了。下面是周遊我們的webpack-assets.json來擷取到我們所有的産生的資源,然後添加到html模闆中傳回的邏輯:

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) : '';
    //如果有元件component傳遞過來,那麼我們直接調用renderToString
    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屬性隻有在生産模式下才會存在,此時通過link來添加。便于緩存
           */}
          {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如果開發模式下,那麼肯定是空,那麼我們直接采用内聯的方式來插入即可。此時我們的css沒有單獨抽取出來,也就是沒有ExtractTextWebpackPlugin,打包到js中進而内聯進來
        */}
          {/* (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}}/>
           {/*将元件renderToString後放在id為content的div内部*/}
          <script dangerouslySetInnerHTML={{__html: `window.__data=${serialize(store.getState())};`}} charSet="UTF-8"/>
          {/*将store.getState序列化後放在window.__data上,讓用戶端代碼可以拿到*/}
          <script src={assets.javascript.main} charSet="UTF-8"/>
          {/*将我們的main.js,來自于用戶端打包并放在特定檔案夾下的資源放在頁面中,
               這就成了用戶端自己的js資源了
          */}
        </body>
      </html>
    );
  }
}
           

是以說下面的div#content中是服務端渲染後得到的html字元串,并被原樣傳回給用戶端。這樣的話,對于服務端的任務就完成了

而我們的下面的script标簽的内容就是我們的用戶端代碼打包後的結果:

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

此時用戶端和服務端的邏輯都已經完成了,用戶端可以繼續接收使用者操作而發送ajax請求更新元件狀态。

2.2 如何使得服務端和用戶端發起請求的邏輯通用

一個好的用法在于使用isomorphic-fetch

2.3 immutable資料在同構中的注意事項

首先在服務端傳回的時候必須将store.getState得到的結果序列化,而且此時如果store傳回的某一個部分state是immutbale的,那麼用戶端要重新通過這部分state資料來建立新的immutable對象(如下面的例子中我們的recipeGrid和connect是immutable的):

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

對于用戶端來說,我們必須将從服務端注入到HTML上的state資料轉成 immutable對象,并将該對象作為initialState來建立store:

const data = window.__data;
  //其中data是服務端傳回的store.getState的值,也就是store的目前狀态
  if (data) {
     data.recipeGrid = Immutable.fromJS(data.recipeGrid);
     //這裡必須設定,否則報錯說:paginator.equals is not a function
      data.connect = Immutable.fromJS(data.connect);
     //可以使用https://github.com/liangklfang/redux-immutablejs
  }
  const store = finalCreateStore(reducer, data);
           
2.4 服務端server不支援ES6的相容

如果你想在服務端使用import等ES6的文法的話,你可以采用下面的方式,首先在項目的根目錄下配置.babelrc檔案,内容如下:

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

然後配置一個單獨的檔案server.babel.js:

const fs = require("fs");
const babelrc = fs.readFileSync("./.babelrc");
let config ;
try{
    config = JSON.parse(babelrc);
}catch(err){
    console.error("你的.babelrc檔案有誤,請仔細檢查");
    console.error(err);
}
//你可以指定ignore配置來忽略某些檔案。
//https://github.com/babel/babel/tree/master/packages/babel-register
require("babel-register")(config);
//require("babel-register")會導緻以後所有的.es6,.es,.js,.jsx的檔案都會被babel處理
           

最後我們添加我們的server.js,内容如下(直接node server.js,而真正的邏輯放在../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__) {
//服務端代碼熱加載
  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必須和webpack的context一緻,調用這個方法伺服器就可以直接require任何資源了
  //這個路徑用于擷取webpack-assets.json檔案,這個是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)
  //  此時webpack-isomorphic-tools已經注冊好了,這裡可以寫你的web應用的代碼,而且這些代碼必須在一個獨立的檔案中
    require('../src/server');
  });
           

經過上面的babel-register的處理,此時你的../src/server.js中可以使用任意ES6的代碼了。

2.5 服務端代碼單獨使用webpack打包

如果對于服務端的代碼要單獨打包,那麼必須進行下面的設定:

你可以參考這裡。

2.6 服務端渲染之忽略css/less/scss檔案

在2.4中我們使用了babel-register幫助服務端識别特殊的js文法,但對less/css檔案無能為力,慶幸的是,在一般情況下,服務端渲染不需要樣式檔案的參與,css檔案隻要引入到HTML檔案中即可,是以,可以通過配置項,忽略所有 css/less 檔案:

require("babel-register")({
  //預設情況ignore是node_modules表示node_modules下的所有檔案的require不會進行處理
  //這裡明确指定css/less不經過babel處理
  ignore: /(.css|.less)$/, });
           

具體内容你可以檢視babel-register文檔。你可以傳遞其指定的所有的其他選項,包括plugins和presets。但是有一點要注意,就是距離我們源檔案的最近一個.babelrc始終會起作用,同時其優先級也要比你在此配置的選項優先級高。此時我們忽略了樣式檔案的解析并不會導緻用戶端對元件再次渲染,因為我們的checksum和具體的css/less/scss檔案無關,隻是群組件render的結果有關。

2.7 使用webpack-isomorphic-tools識别css/less/scss檔案

通過 babel-register 能夠使用babel解決jsx文法問題,對 css/less 隻能進行忽略,但在使用了CSS Modules 的情況下,服務端必須能夠解析 less檔案,才能得到轉換後的類名,否者服務端渲染出的HTML結構和打包生成的用戶端 css 檔案中,類名無法對應。其原因在于:我們在服務端使用了CSS Module的情況下必須采用如下的方式來完成類名設定:

const React = require("react");
const styles = require("./index.less");
class Test extends React.Component{
 render(){
     return (
        //如果不是css module,那麼可能是這種情況:className="banner"
           <div className={styles.banner}>This is banner<\/div>
        )
   }
}
           

如果服務端無法解析css/less肯定無法得到最終的class的名稱(經過css module處理後的className)。進而導緻用戶端和服務端渲染得到的元件的checksum不一緻(因為class的值不一緻)。而對于2.6提到的忽略less/css檔案的情況,雖然服務端沒有解析該類名,但是我們的元件上已經通過class屬性值指定了相同的字元串,是以checksum是完全一緻的。

為了解決這個問題,需要一個額外的工具,即webpack-isomorphic-tools,幫助識别less檔案。通過這個工具,我們會将伺服器端元件引入的less/css/scss檔案進行特别的處理,如下面是Widget元件引入的scss檔案被打包成的内容并寫入到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"
    }
           

此時,在服務端你可以使用上面說的styles.banner這種方式來設定className,而不用擔心使用babel-register隻能忽略css/less/scss檔案而無法使用css module特性,進而導緻checksum不一緻!具體你可以檢視這裡

2.8 前後端路由不同的處理

單頁應用一個常見的問題在于:所有的代碼都會在頁面初始化的時候一起加載,即使這部分的代碼是不需要的,這常常會産生長時間的白屏。webpack支援将你的代碼進行切分,進而分割成為不同的chunk而按需加載。當我們在特定路由的時候加載該路由需要的代碼邏輯,哪些目前頁面不需要的邏輯按需加載。對于server-rendering來說,我們服務端不會采用按需加載的方式,而我們的用戶端常常會使用System.import或者require.ensure來實作按需加載。

比如下面的例子:

module.exports = {
    path: 'complex',
    getChildRoutes(partialNextState, cb) {
       //如果是服務端渲染,我們将Page1,Page2和其他所有的元件打包到一起,如果是用戶端,那麼我們會将Page1,Page2的邏輯單獨打包到一個chunk中進而按需加載
        if (ONSERVER) {
            cb(null, [
                require('./routes/Page1'),
                require('./routes/Page2')
            ])
        } else {
            require.ensure([], (require) => {
                cb(null, [
                    require('./routes/Page1'),
                    require('./routes/Page2')
                ])
            })
        }
    },
    //IndexRoute表示預設加載的子元件,
    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
                // 擷取下一個子產品的path和getComponent,因為他是采用module.export直接導出的
                // 我們直接将getComponent傳遞給callback函數
                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'))
            })
        }
    }
}
           

這個例子的路由對應于/complex,如果是服務端渲染,那麼我們會将Page1,Page2代碼和其他的元件代碼打包到一起。如果是用戶端渲染,那麼我們會将Page1,Page2單獨打包成為一個chunk,當使用者通路”/complex”的時候才會加載這個chunk。那麼為什麼服務端渲染要将Page1,Page2一起渲染呢?其實你要弄清楚,對于服務端渲染來說,将Page1,Page2一起渲染其實是擷取到了該兩個子頁面的DOM傳回給用戶端(形成目前頁面的子頁面的兩個Tab頁面)。而用戶端單獨加載chunk其實隻是為了讓這部分DOM能夠響應使用者的點選,滾動等事件而已。注意:服務端渲染和我們的req.url有關,如下面的例子:

match({ history, routes: getRoutes(store), location: req.originalUrl }, (error, redirectLocation, renderProps) => {
    if (redirectLocation) {
      res.redirect(redirectLocation.pathname + redirectLocation.search);
      //重定向要添加pathname+search
    } else if (error) {
      console.error('ROUTER ERROR:', pretty.render(error));
      res.status();
      hydrateOnClient();
      //發送500告訴用戶端請求失敗,同時不讓緩存了
    } 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');
    }
  });
});
           

我們的服務端根據req.url擷取到renderProps,進而将一個元件樹渲染成為html字元串傳回給用戶端。是以我們服務端不會按需渲染,最終導緻的結果隻是多渲染了該path下的一部分DOM而已,而且這樣有一個好處就是快速響應使用者操作(還是要用戶端進行注冊事件等)而不用用戶端重新render該部分DOM。而從用戶端來說,我此時隻需要加載該path下對應的chunk就可以了,而不是将整個應用的chunk一起加載,進而按需加載,速度更快,更加合理。

服務端match路由需要注意的問題:盡量前置重定向(寫到路由的 onEnter 裡)。除非需要拉取資料進行判斷,不要在路由确定之後再重定向。因為在拿到路由配置之後就要根據相應的頁面去拉資料了。這之後再重定向就比較浪費。如下面的例子:

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

下面使用onEnter鈎子函數的路由配置:

<Route onEnter={requireLogin}>
       //如果沒有登入,那麼下面的路由元件根本不會執行個體化,更不用說拉取資料了
        <Route path="chat" component={Chat}/>
        <Route path="loginSuccess" component={LoginSuccess}/>
  <\/Route>
           

參考資料:

React同構思想

React資料擷取為什麼一定要在componentDidMount裡面調用?

ReactJS 生命周期、資料流與事件

React statics with ES6 classes

React同構直出優化總結

騰訊新聞React同構直出優化實踐

Node直出理論與實踐總結

React+Redux 同構應用開發

ReactJS 服務端同構實踐「QQ音樂web團隊」

代碼拆分 - 使用 require.ensure

性能優化三部曲之三——Node直出讓你的網頁秒開 #6

繼續閱讀