本站已关停,现有内容仅作科研等非赢利用途使用。特此声明。
查看: 1636|回复: 0
打印 上一主题 下一主题

React Server Side Rendering 解决 SPA 应用的 SEO 问题

[复制链接]
跳转到指定楼层
1#
发表于 2016-7-10 22:21:25 | 只看该作者 回帖奖励 |倒序浏览 |阅读模式
本帖最后由 tvvocold 于 2016-7-10 22:23 编辑

原文转自 Coding 博客:https://blog.coding.net/blog/Rea ... ndering-for-SPA-SEO

前端技术的流行,衍生了许多前端框架,如 Angular JS、Polymer、Backbone、Ember.js 等,这些前端框架有些支持创建一个单页 Web 应用(Single Page Web Application)。可是,当需要应用支持良好的 SEO 的时候,你可能就会忧伤了,毕竟普通的搜索引擎可能还不支持 SPA 应用。 听闻 React 支持 Server Side Rendering,顿时激起我的兴趣,想要一探究竟,于是诞生了这篇博文。


刚好 Coding 博客需要做一些调整,而博客支持 SEO 也是首要任务,于是在业余时间,用 React 提升了下 Coding 博客的体验。


写好博客前端的基本组件和页面之后,开始搜索 *React Server Side Rendering* 相关的关键词,Clone 各种项目的源码下来看,找到几个比较好的 React Server Side Rendering 的 Demo:


> - [React Starter](https://github.com/webpack/react-starter)
- [isomorphic500](https://github.com/gpbl/isomorphic500)


看完之后基本理解 React Server Side Rendering 的处理方法了。


### Server Side Render 需要什么


最先想到的肯定是,能够直接把一个 SPA 应用输出成 HTML 字符串吧!嗯,没错,就是它。


#### React renderToString 和 renderToStaticMarkup 魔法棒


React  提供了俩个神奇的方法,`renderToString` 和 `renderToStaticMarkup`,它们支持将 React Component 直接输出成 HTML 字符串,有了这俩个方法,我们就可以在服务器端执行 React,直接返回渲染之后的结果。


这样搜索爬虫就能爬出一个具有内容的 HTML 页面,而不是一个 SPA 应用的 Initialize HTML 页面。


你可能会奇怪,为什么提供了俩个 React Component To String 的方法,其实它们是有区别的:


> renderToString 方法,只应用在服务器端,把一个 React Component 渲染成 HTMl ,可以将它通过 response 发送到浏览器端,已达到 SEO 的目的。


> renderToStaticMarkup 方法,和 renderToString 类似,但是它生成的 HTML Dom 不包含类似 data-react-id 这样的额外属性,可以用于生成简单的静态页面,节省大量字符串。


可以直接输出 HTML  字符串了,是不是就可以做到服务器端渲染了?


#### 有了魔法还不够


React 提供的俩个渲染 HTML 字符串方法,虽然能做到直接渲染出 React Component,但是我们应用中的数据该如何处理、如何管理、如何渲染。


熟悉 React 的都知道 Flux ,使用 Flux 可以更加方便 React 的数据交互,让 React 更专注于 View 的逻辑。(附图:React  Flux 应用交互过程)
![React  Flux 应用交互过程]()
但是,一个 React Flux 应用即使可以在浏览器端正常的运行,直接在服务器端使用 `renderToString` 方法也只能渲染出 React Component 不包含数据的 HTML Dom,React 的 ComponentDidMount 不会在服务器端触发,你的 Flux Action 事件也无法触发, Store 中不会存在任何数据,并不能解决根本问题。


所以,我们需要解决哪些问题呢?


>A:嗯,把 Store 里初始化好数据就可以了!
>B:等等,好像还要解决异步数据请求的问题。
>C:处理 Action 事件的时候似乎应该在数据返回之后。
>D:他们说的好像都对,既然做了服务器端渲染,那么在浏览器端首次就不需要在 ComponentDidMount 的时候去请求数据了,这么一来,Store 中就没有数据了,岂不是好多操作都没法做了?是不是应该在浏览器端存一个数据副本?要不要在浏览器端重新渲染一次?


从上面的回答,可以看到我们暂时需要解决的问题:


>
1.  初始数据异步请求的问题。所有需要在服务器端渲染的数据请求,都需要在返回 HTML 之前处理完毕。
2.  Action 和 Store 该如何管理?Action 事件发送是在数据之前还是之后?
3.  服务器端数据需要初始化,浏览器端的数据同样需要初始化。所以,浏览器需要保存一个数据副本。副本如何保存,浏览器端 Store 如何初始化?
4.  客户端端渲染的问题。




带着这些问题,也许会有一个轮子可以解决这个问题,不然就得自己造一个轮子了,好吧,看看有没有好用的轮先。在 GitHub 找找,找到 [Redux](https://github.com/rackt/redux) 和 [Fluxible](https://github.com/yahoo/fluxible) 俩个好轮子,看了下文档,选择了 Fluxible,因为觉得它对于 Component Context 的管理比较好,而且是在 Flux 的基础上实现的。(最关键可以用酷酷的 Decorator Pattern)。


### 怎么用 Fluxible 完成 Server Side Rendering 的魔法


##### 1. 前端路由的选择


作为 SPA 应用,都需要一个前端路由来处理不同的渲染。Fluxible 提供了自己的 router 组件,当然,使用 react-router 也可以。本文就是使用 react-router 来作为路由组件的。新建一个 Router.jsx ,作为博客的路由入口:




```
import ....;
// polyfill
if (!Object.assign)
    Object.assign = React.__spread; // eslint-disable-line no-underscore-dangle


var {
    Route,
    DefaultRoute,
    NotFoundRoute,
    RouteHandler,
    Link
    } = Router;


module.exports = (
    <Route path="/" handler={App}>
        <DefaultRoute handler={Demo}/>
        <NotFoundRoute handler={PageNotFound}/>
    </Route>
);


```
react-router 具体使用方法,请参考[文档(v1.3.x)](https://github.com/rackt/react-router/tree/master/docs)


##### 2.  Store 、Action、Service


使用 Fluxible 之后,Store 最好只作为数据存储使用,Store 中不要加入数据请求之类的方法,数据请求方法可以使用 Fluxible Plugins 来管理,也可以自己封装 service 类来管理。Fluxble 提供了 createStore 方法和 BaseStore 基类来创建 Store,可以根据自己的需求选择,下面创建一个 Blog.store.js :




```
import ...;
var CHANGE_EVENT = 'change';
class DemoStore extends BaseStore {
    constructor(dispatcher) {
        super(dispatcher);
        this.dispatcher = dispatcher; // Provides access to waitFor and getStore methods
        this.data = null;
    }
    loadData(data) {
        this.data = data;
        this.emitChange();
    }
    getState() {
        return {
            data: this.data
        };
    }
    // For sending state to the client
    dehydrate() {
        return this.getState();
    }
    // For rehydrating server state
    rehydrate(state) {
        this.data = state.data;
    }
}
DemoStore.storeName = 'DemoStore';
DemoStore.handlers = {
    "LOAD_DATA": "loadData",
};
export default DemoStore;
```




可以注意到 BlogStore 中有几个比较重要的方法和属性:


> - dehydrate 方法,用于将服务器端载入数据之后的 Store 中的 state ,序列化输出到浏览器端。
> - rehydrate 方法,用于将序列化之后的 Store 中的 state,在浏览器端反序列化成 Store 对象。
> - storeName 属性,可以直接使用 storeName 在 Component Context 中获取 Store 实例。
> - handlers 属性,定义 Store 监听的 Action 事件。


有了 Store 之后,接下来创建一个 Action 。




```
import ...;


class DemoAction {


    /**
     * @param  {string} text
     */
    static loadData(actionContext, payload, done) {
        DemoService.loadData(payload, function (data) {
            actionContext.dispatch('LOAD_DATA', data);
            done && done();
        });
    }




}


export default  DemoAction;
```




Action 在 Fluxible 中使用 `context.executeAction` 方法来执行,会将`actionContext` 上下文作为参数传入 Action 中。
Action 有一个回调函数 `done` 主要用于使用 `async` 处理异步请求时使用(不需要在服务器端加载数据的 Action 可以不传入此函数)。
Action 只负责分发事件,以及处理在不同业务逻辑下的事件分发。具体数据加载交给 Service 来处理。




```
import Request from 'superagent';
class BlogService {


    static loadData(payload, done) {
        var req = Request
            .get("http://127.0.0.1:4011/api/data");
        if (payload.req) {
            req.set("Cookie", payload.req.headers.cookie || "");
        }
        req.query(payload.form)
            .end(function (err, res) {
                var result = res.body;
                done && done(result, done);
            });
    }




}
export default BlogService;
```




有了 Store、Action、Service ,数据和事件的绑定也就有了,下面只需要把数据跟 React Component 交互处理好就可以了。


3. 增加一个 Route Page
使用 react-router 作为路由组件,它为每一个 url 正则都指定了一个 Handler,这个 Handler 就是一个 React Component,react-router 会直接渲染这个 React Component 以及它的子节点。




```
import React from 'react';
import DemoStore from "Demo.store.js";
import DemoAction from "Demo.action.js";


import { connectToStores } from 'fluxible-addons-react';
@connectToStores([DemoStore], (context) => ({
    DemoStore: context.getStore(DemoStore).getState()
})) class Demo extends React.Component {


    static contextTypes = {
        getStore: React.PropTypes.func,
        executeAction: React.PropTypes.func
    };


    constructor(props) {
        super(props);
    }


    reload() {
        this.context.executeAction(DemoAction.loadData, {});
    }


    /**
     * @return {object}
     */
    render() {
        console.info(this.props.DemoStore);
        var data = this.props.DemoStore.data || [];
        var itemContent = data.map(function (item, i) {
            return (<p>{item.content}</p>);
        });
        return (
            <div>
                {itemContent}
                <div className="align-center">
                    <a className="button" onClick={this.reload.bind(this)}>重新加载</a>
                </div>
            </div>
        );
    }




}


Demo.loadAction = [DemoAction.loadData];


export default Demo;
```




从上面的代码中可以看到几个比较重要的地方:


> - connectToStores ,这里使用的是 Decorator 模式,也可以直接作为函数使用,具体请查阅 Fluxible 的文档,这个函数可以让你为 React Component 的 props 执行注入回调函数,如注入 state。
> - contextTypes,为 React Component 提供俩个比较重要的方法,getStore 和 executeAction。
> - loadAction ,此处是我自定义的属性,主要用于在入口处为每个  Page Handler  React Component 加入需要初始化的数据触发事件。


鉴于 react-router 的使用,需要为 App 提供一个 RouteHandler 的入口(App.jsx)。




```
...
import {connectToStores, provideContext} from 'fluxible-addons-react';
var {RouteHandler} = Router;




@provideContext class App extends React.Component {


    static contextTypes = {
        getStore: React.PropTypes.func,
        executeAction: React.PropTypes.func
    };


    constructor(props, context) {
        super(props, context);
    }


    /**
     * @return {object}
     */
    render() {
        return (
            <div className="main-container">
                <RouteHandler {...this.props}/>
            </div>
        );
    }




}


export default App;
```


可以看到在 App Class 前面加入了一个 provideContext 的 Decorator Pattern。


> provideContext ,会为React Component 以及它的子节点加入 executeAction  getStore 方法,当然它也支持为子节点加入新的方法或者属性。


4. 服务器端入口和客户端入口


Store、Action、Service、Route 和 React Component 都有了之后,接下来就需要为 App 的入口做一些准备工作了。我们需要为 Server 端和 Client 端分别创建渲染入口。在 Server 端预渲染好 HTML 页面(这次渲染只是生成 HTML 字符串),Client 端接收到 HTML 之后从预存储的数据中再次渲染页面(这次渲染可以初始化一些 Dom 和 Dom 事件)。
        
Client 端处理相对来说比较简单,只需要把 Store 的数据反序列化,然后渲染出页面即可:


```
         var dehydratedState = window.App;
        app.rehydrate(dehydratedState, function (err, context) {


            if (err) {
                throw err;
            }


            window.context = context;


            var mountNode = document.getElementById(app.uid);
            Router.run(app.getComponent(), Router.HistoryLocation, function (Handler, state) {
                var Component = React.createFactory(Handler);
                React.render(
                    React.createElement(
                        FluxibleComponent,
                        {context: context.getComponentContext()},
                        Component()
                    ),
                    mountNode,
                    function () {
                    }
                );
            });
```




Fluxible  提供 rehydrate 方法,将 Store 数据反序列化到 context 中。然后再使用 react-router 的 `Router.run` 方法渲染 HTML。


Server 端处理相对比较复杂,基本过程是:
> 1.  创建 Fluxible Context 对象
> 2.  使用 react-router 的 `Router.run` 方法根据 Request URL 渲染。
> 3.  渲染之前把 Router Handler 需要进行 SEO 的数据发送 Action 请求(需要处理异步的问题)。
> 4.  待所有数据请求完毕之后,序列化 Fluxible Context 。
> 5.  渲染 Router Handler 对应的 React Component。
> 6.  使用 `React.renderToStaticMarkup`渲染出 html, body,head 等外层标签。并使用 React Component 渲染的结果填充 body 内部内容。
> 7.  发送 HTML 字符串到浏览器端。
        
参考代码:


```
render(req, res) {
        var context = app.createContext({
            api: process.env.API || ('http://127.0.0.1:'+process.env.PORT),
            env: {
                NODE_ENV: process.env.NODE_ENV
            }
        });
        var actions = this.actions || [];
        Router.run(app.getComponent(), req.url, function (Handler, state) {
            if (state.routes.length === 0) {
                res.status(404);
            }
            async.filterSeries(
                state.routes.filter(function (route) {
                    return route.handler.loadAction ? true : false;
                }),
                function (route, done) {
                    async.map(actions.concat(route.handler.loadAction), function (action, callback) {
                        context.getActionContext().executeAction(action, {
                            form: Lodash.merge(state.params, state.query),
                            params: Lodash.extend({}, state.params),
                            query: Lodash.extend({}, state.query),
                            req: Lodash.extend({}, req),
                            res: Lodash.extend({}, res),
                            state: Lodash.extend({}, state),
                            route: Lodash.extend({}, route)
                        }, callback);
                        //在 Server Side 执行 Action 的时候,传入一些 App 上下文参数
                    }, function (err, result) {
                        done();
                    });
                },
                function () {


                    const state = "window.App=" + serialize(app.dehydrate(context)) + ";";
                    var Component = React.createFactory(Handler);
                    var HtmlComponent = React.createFactory(Html);


                    var markup = React.renderToString(
                        React.createElement(
                            FluxibleComponent,
                            {context: context.getComponentContext()},
                            Component()
                        ));


                    var html = React.renderToStaticMarkup(HtmlComponent({
                        context: context.getComponentContext(),
                        state: state,
                        uid: app.uid,
                        markup: markup
                    }));
                    res.send(html);
                }
            );
        });
    }


```


到这一步,React Server Side Rendering 案例已经可以完整运行起来了。


运行环境首推 nodejs,毕竟都是 js,兼容性会很好。


然后使用 curl 命令查看输出的内容,可以看到不再只是简单的输出一个 React App 的入口基本标签,而是整个包含数据的 HTML 页面。 如此一来,搜索爬虫就能爬出一个完整的 HTML 页面了。


### 总结
React 提供原生的 Component To String 支持,使得 React Server Side Rendering 成为可能,但是还有很多其他的过程,会根据个人业务不同会有区别,还是需要开发者自己熟悉这个过程,以及根据自身业务做出不同的方案。


----


-  本文示例 [Demo 地址](https://coding.net/u/kin/p/react-server-side-demo/git) `https://coding.net/u/kin/p/react-server-side-demo/git` 代码中有什么问题,欢迎指正。
-  示例代码是在 React 1.3.x 的基础上编写的,其他使用的 npm 库也都是在  React 1.3.x 的基础上。
-  React 1.4.0 已经更新,并且有一部分调整,React Server Side Rendering 方案也有所调整,Fluxible 已经对 React 1.4.0 有新的版本,React-router 也升级了,并且使用也有比较大的调整。有兴趣的可以研究一下。
-   [React Starter](https://github.com/webpack/react-starter) 项目实现的 Server Side Rendering 也值得看一看。
-   [Redux](https://github.com/rackt/redux)  的服务器端渲染实现,可以参考 [ @hulufei ]( https://coding.net/u/hulufei )  的博客 [《玩转 React 服务器端渲染》](https://blog.coding.net/blog/React-server-rendering)
ChinaGDG.com
回复

使用道具 举报

*滑动验证:
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

快速回复 返回顶部 返回列表