• React整理总结


    同构原理
    什么是同构

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

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

        性能: 通过Node直出, 将传统的三次串行http请求简化成一次http请求,降低首屏渲染时间
        SEO: 服务端渲染对搜索引擎的爬取有着天然的优势,虽然阿里电商体系对SEO需求并不强,但随着国际化的推进, 越来越多的国际业务加入阿里大家庭,很多的业务依赖Google等搜索引擎的流量导入,比如Lazada.
        兼容性: 部分展示类页面能够有效规避客户端兼容性问题,比如白屏。

    同构与SPA流程对比

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

    SSR:服务端Node也可以运行React解析出页面内容,并且要比客户端更快;客户端通常要在render一次之后请求数据,数据返回之后再render一次,服务端渲染可以解决客户端重复渲染问题。
    同构与SPA时间对比

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

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

    SPA:

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

    SSR:

        客户端请求页面,服务端去请求数据,请求返回后渲染页面,将渲染好的html返回给客户端,此时页面可视;(request&response)
        html加载完之后,去加载页面中的js;(processing)
        js加载完成之后开始执行;(rendering)
        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项目,但是问题也很明显:

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

    所以没有选用该框架进行尝试,不过该框架凭借着简单易上手,未来还是很有市场的。
    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方法获取到了promises和store,然后再用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一样,dispatch到store中,然后暴露出去。
    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特点适合场景:

        项目要求SEO,SSR就很合适。
        需求项目某页面首屏时间要求很快,SSR可以减少白屏时间。
        一般是首页,列表页等大量数据页面使用比较常见。

    Keep moving forwards~
  • 相关阅读:
    Linux命令记录-Tomcat(七)
    Linux命令记录-PostgreSql(六)
    Linux命令记录-Mysql(五)
    Linux Crontab实现定时任务
    Linux命令记录-Java环境(四)
    Linux命令记录-防火墙(三)
    Linux命令记录-服务相关(二)
    Linux命令记录-环境准备(一)
    Linux 系统安装,磁盘分区要点
    java之Collection类
  • 原文地址:https://www.cnblogs.com/-X-peng/p/13706934.html
Copyright © 2020-2023  润新知