React同构总结

最近花了点时间研究React同构实践,遇上过年回家,心也散了,两个月没写文章。javascript

同构也算是前端的一个应用模式,目的是为了加速首屏显示时间和seo优化,不少公司都将同构做为前端优化的一个优化点来作,同时Raect16版本中也添加了不少对同构的支持,能够看出FB也是默认支持这一场景使用的。css

同构原理

什么是同构

一套代码既能够在服务端运行又能够在客户端运行,这就是同构应用。简而言之, 就是服务端直出和客户端渲染的组合, 可以充分结合二者的优点,并有效避免二者的不足。html

归纳地说,同构就是服务端(Node)替客户端请求接口,获取到数据后,将有数据和结构的页面渲染好以后返回给客户端,这样避免了客户端页面首次渲染,同时服务端RPC比客户端请求要快。前端

为何要同构

  • 性能: 经过Node直出, 将传统的三次串行http请求简化成一次http请求,下降首屏渲染时间
  • SEO: 服务端渲染对搜索引擎的爬取有着自然的优点,虽然阿里电商体系对SEO需求并不强,但随着国际化的推动, 愈来愈多的国际业务加入阿里你们庭,不少的业务依赖Google等搜索引擎的流量导入,好比Lazada.
  • 兼容性: 部分展现类页面可以有效规避客户端兼容性问题,好比白屏。

同构与SPA流程对比

clipboard.png
SPA:服务端替客户端请求数据,完成第一次render,将render完成以后的html页面返回给客户端,相对于客户端渲染,客户端第一次获取的html是个有数据有结构的html,结合样式文件下载客户端能够较快的看到首屏内容。java

SSR:服务端Node也能够运行React解析出页面内容,而且要比客户端更快;客户端一般要在render一次以后请求数据,数据返回以后再render一次,服务端渲染能够解决客户端重复渲染问题。node

同构与SPA时间对比

clipboard.png
以一个常见的场景为例:react

进入页面,componentDidMount中请求数据,同时页面loading,请求返回后,取消loading,页面可交互。webpack

SPAgit

  1. 客户端请求页面,服务端返回SPA的html,此html不可视;(request&response)
  2. html加载完以后,去加载页面中的js;(processing)
  3. js加载完成以后开始执行;(rendering)
  4. 页面首次渲染完毕,向后端请求数据(loading)
  5. 请求返回,页面再次渲染,用户可交互(useing)

SSRes6

  1. 客户端请求页面,服务端去请求数据,请求返回后渲染页面,将渲染好的html返回给客户端,此时页面可视;(request&response)
  2. html加载完以后,去加载页面中的js;(processing)
  3. js加载完成以后开始执行;(rendering)
  4. js解析完毕,用户可交互;(useing)

经过上述流程图可发现,理论上同构要比客户端渲染要快,并且体验要好。

预期问题

原理了解以后,动手以前思考一些可能出现的问题:

1. Node服务器如何识别es6以及React

Node识别ES6可使用babel-register插件,该插件使用起来跟.babelrc同样简便。

React中有一个renderToString方法,该方法将解析好的jsx片断以html字符串形式输出,就是为了同构而诞生。

2. 服务端如何引入js,css,图片,字体等静态资源

实现方法有多种,我这里使用webpack-isomorphic-tools插件来实现,以后会作介绍。

3. 服务端如何路由匹配

一般咱们只作首页,或者关键页面的服务端渲染,至关于从首页进去是服务端渲染,可是从项目其余页面进入就跟正常的SPA同样。因此在服务端将要ssr的路由匹配出来,其余的路由仍交给SPA。

4. SSR的Redux怎么办

一般来说,咱们从接口获取数据,都要将一些数据放到store中,便于其余页面共享。

SSR中,服务端跟SPA公用一部分action和reducer,相同的reducer生成的store是同样的,以后再经过createStore时候将store注入进入,返回给客户端。

5. SSR的开发流程怎样

实际上SSR开发一般是在一个项目基础上改,而不是从新搭建一个项目,比较不少人拿它当作优化,而不是重构。

一般来讲咱们一个项目按照SPA模式开发,针对特定页面作SSR的修改,修改以后的项目既能够SPA也能够SSR,只不过SPA模式时对应页面获取不到数据,由于获取数据的方法均被修改。

6. SSR以后,项目的JS体积是否会减少

不会减少,所谓同构,其实就是服务端借助客户端的JS去渲染页面,没有影响到客户端的JS,仍是正常打包,客户端作代码分割也不会受影响。

同构实现

带着上面的问题来看同构如何实现。

React实现同构方法有多重,并且都较为成熟,这里选用的webpack-isomorphic-tools插件来实现。

Next.js

先插一嘴,如今有个叫Next框架,索性也试了下,真的很简便,快速搭建SSR项目,可是问题也很明显:

  1. 框架高度封装,扩展性有限。
  2. 适合从头搭建项目,不适合现有项目SSR迁移。
  3. 上手很快,可是初学者不知道里面原理如何,适合熟练手玩。

因此没有选用该框架进行尝试,不过该框架凭借着简单易上手,将来仍是颇有市场的。

webpack-isomorphic-tools

上面第二个问题就提到了服务端如何处理静态资源,这里使用webpack-isomorphic-tools插件,该插件处理静态资源的。

首先一个webpack-isomorphic-tools-configuration.js文件配置你想要处理的文件格式与处理方法,跟webpack配置loader相似:

const webpackIsomorphicToolsPlugin = require('webpack-isomorphic-tools/plugin');
const config = require('../config/');

module.exports = {
    webpack_assets_file_path: `${config.base_path}/webpack-assets.json`,
    webpack_stats_file_path: `${config.base_path}/webpack-stats.json`,
    assets: {
        images: {
            extensions: ['png', 'jpg', 'gif', 'ico', 'svg']
        },
        fonts: {
            extensions: ['woff', 'woff2', 'eot', 'ttf', 'swf', 'otf']
        },
        // styles: {
        //     extensions: ['scss', 'css'],
        //     filter: function(module, regex, options, log) {
        //         if (options.development) {
        //             // in development mode there's webpack "style-loader",
        //             // so the module.name is not equal to module.name
        //             return webpackIsomorphicToolsPlugin.style_loader_filter(module, regex, options, log);
        //         } else {
        //             // in production mode there's no webpack "style-loader",
        //             // so the module.name will be equal to the asset path
        //             return regex.test(module.name);
        //         }
        //     },
        //     // How to correctly transform kinda weird `module.name`
        //     // of the `module` created by Webpack "css-loader"
        //     // into the correct asset path:
        //     path: webpackIsomorphicToolsPlugin.style_loader_path_extractor,
        //
        //     // How to extract these Webpack `module`s' javascript `source` code.
        //     // Basically takes `module.source` and modifies its `module.exports` a little.
        //     parser: webpackIsomorphicToolsPlugin.css_loader_parser
        // }
    }
}

该文件配置能够参考官方文档

而后在webpack中,配置对应的资源的时候,引入该文件

// 同构处理静态资源的插件
const webpackIsomorphicToolsPlugin = require('webpack-isomorphic-tools/plugin');
const webpackIsomorphicToolsPluginIns =
    new webpackIsomorphicToolsPlugin(require('./webpack-isomorphic-tools-configuration')).development();
...
module: {
  rules: [
    ...
    {
                test: webpackIsomorphicToolsPluginIns.regular_expression('images'),
                loader: 'url-loader?limit=8192', // 这样在小于8K的图片将直接以base64的形式内联在代码中,能够减小一次http请求。
                options: {
                    name: 'assets/images/[name]_[hash:8].[ext]'
                }
            },
            {
                test: webpackIsomorphicToolsPluginIns.regular_expression('fonts'),
                loader: 'url-loader',
                options: {
                    name: 'assets/fonts/[name].[ext]'
                }
            }
  ]
}
plugins: [
  webpackIsomorphicToolsPluginIns,
  ....
]

而后运行webpack,将文件打包以后,会生成一个webpack-assets.json文件,该文件就是存储静态资源的映射关系的json:

{
  "javascript": {
    "app": "/assets/js/app_360a53bf78ee0e398bb2.js",
    "vendor": "/assets/js/vendor_360a53bf78ee0e398bb2.js"
  },
  "styles": {
    "app": "/assets/css/app_360a53bf78ee0e398bb2.css"
  },
  "assets": {
    "./public/images/react.svg": "data...."
  },
  "webpack": {
    "version": "2.7.0"
  }
}

这样咱们经过该json文件就能够获取到对应静态资源。

Express服务

这里选用Express框架做为服务器,缘由就是简单,不少人也选用koa,都同样。

这里服务端启动部分跟正常的Express启动相似:

import render from "./render";
import fetch from "./fetch";

app.use('*', (req, res, next) => {

    const { promises, store } = fetch(req);

    Promise.all(promises).then(data => {
        const html = render(req, res, store);
        res.send(html);
    }).catch(err =>{
        console.log('err');
        console.log(err);
        res.end('server error,please visit later')
    })

});

核心在于路由部分:这里匹配全部路由(也能够是首页路由),使用fetch方法获取到了promisesstore,而后再用render方法生成了html返回给客户端,这两个方法都是封装过的,

公用Action,Reducer

咱们在SPA开发中,请求通常都封装成actionCreator,方便调用与修改,SSR中就共用了actionCreator和reducer。

fetch方法以下:

import 'isomorphic-fetch';

import { createStore } from "redux";
import {actions} from '../src/actions/';

import reducer from "../src/reducers";

const fetchHomeList = (store) => {
    return fetch('http://localhost:9000/api/aaa')
        .then((response)=>{
            console.log('then response------');
            return response.json();
        })
        .then((res)=>{
            console.log(res.data.length);
            store.dispatch(actions.updateHomeList(res.data));

            return res;
        })
        .catch((res)=>{
            console.log('catch res------');
            console.log(res);
        });
};

export default function (req) {
    const store = createStore(reducer);

    const promises = [
        fetchHomeList(store)
    ];

    return {
        promises,
        store
    }
}

fetch文件中,咱们将首页须要获取的数据经过isomorphic-fetch来获取,而后跟SPA同样,dispatchstore中,而后暴露出去。

Render页面

SSR返回的是首次渲染事后的html,首次渲染就是在render方法中实现的:

import fs from 'fs'
import path from 'path'

import React from 'react';
// import ReactDOM from 'react-dom';
import { StaticRouter as Router } from "react-router-dom"
import { renderToString } from "react-dom/server"
import { Provider } from "react-redux"
import Routes from '../src/route';

function getAssets() {
    return getAssets.assets || (() => {
            getAssets.assets = JSON.parse(fs.readFileSync(path.join(__dirname, '../webpack-assets.json')));
            return getAssets.assets
        })()
}

export default function render(req, res, store) {
    const context = {};

    const html = renderToString(
        <Provider store={store}>
            <Router location={req.baseUrl} context={context}>
                <Routes />
            </Router>
        </Provider>
    );

    // <Route>中访问/,重定向到/home路由时
    if (context.url) {
        res.redirect('/home');
        return;
    }

    const main = getAssets();
    const app = main.javascript.app;
    const vendor = main.javascript.vendor;
    const style = main.styles.app;

    return `
    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
        <link href=${style} rel="stylesheet"></link>
        <title>SSR</title>
    </head>
    <body>
        <div id="root">
            ${html}
        </div>
    </body>
    <script>
        window.__INITIAL_STATE__ = ${JSON.stringify(store.getState())}
    </script>
    <script src=${vendor}></script>
    <script src=${app}></script>
    </html>
    `
}

这里显而易见,咱们准备一段html模板,跟SPA那个html模板相似,将renderToString的片断塞进去,同时根据webpack-assets.json获取到打包好的js和css,塞进去;最后,将上面刚刚配置好的store注入进去。

效果

咱们制做一个接口,使用setTimeout 500ms模拟网络开销,效果以下:
clipboard.png
(gif上传不上去不知道为啥。。。)

能够看到SSR要比SPA明显的更快速获得首屏效果。

思考

项目完成的同时,也在思考一些问题:

1. 既然SSR首屏速度快,为什么不全部路由全都SSR

全部页面SSR能够作,这样每一个页面的首屏都会很快,同时js也会小不少。可是带来的问题服务器压力会很大,维护起来成本较高。并且服务端毕竟是模拟客户端环境渲染,一些地方仍是不同的好比没有Document,没有window对象,没法进行DOM操做等。因此推荐首页等重要页面进行SSR。

2. 若是接口时间过长,是否是白屏时间较长

确实有这个问题,理论上讲,RPC要比客户端请求快不少,这样能够节省不少时间;可是若是接口很慢会形成白屏时间过长,得不偿失。因此接口很慢的页面不建议作SSR,同时接口也应该有严格的规范控制接口返回时间。

3. 若是项目首页有很重的逻辑,或者Layout中有重逻辑该如何

页面若是有很重的逻辑好比判断不少不一样条件,作出不少相应处理;依次请求不少接口,或者一块儿请求大量数据等状况,这些逻辑处理都须要一同写进SSR中。

4. Node服务器带来的维护及并发压力等问题

使用Node服务器的话,还涉及到服务器的平常维护问题,日志收集,错误报警等问题,以及性能问题。要求前端(SA)有必定的Node服务器的维护经验,这时前端已经不是纯前端了。

5. 什么项目适合SSR

这个问题才是关键的问题。并非全部项目都适合SSR,就好像不是全部项目都适合Redux同样。根据SSR特色适合场景:

  1. 项目要求SEO,SSR就很合适。
  2. 需求项目某页面首屏时间要求很快,SSR能够减小白屏时间。
  3. 通常是首页,列表页等大量数据页面使用比较常见。

欢迎你们提出些不一样意见用以讨论。

参考

  1. xiyuyizhi/movies
  2. 教你如何搭建一个超完美的React.js服务端渲染开发环境
  3. React 同构与极致的性能优化
  4. 从零开始React服务端渲染
  5. 精读先后端渲染之争

项目源码:https://github.com/Aus0049/re...

相关文章
相关标签/搜索