React Server Side Rendering 解决 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 Server Side Rendering 的处理方法了。 Server Side Render 需要什么最先想到的肯定是,能够直接把一个 SPA 应用输出成 HTML 字符串吧!嗯,没错,就是它。 React renderToString 和 renderToStaticMarkup 魔法棒React 提供了俩个神奇的方法, 这样搜索爬虫就能爬出一个具有内容的 HTML 页面,而不是一个 SPA 应用的 Initialize HTML 页面。 你可能会奇怪,为什么提供了俩个 React Component To String 的方法,其实它们是有区别的:
可以直接输出 HTML 字符串了,是不是就可以做到服务器端渲染了? 有了魔法还不够React 提供的俩个渲染 HTML 字符串方法,虽然能做到直接渲染出 React Component,但是我们应用中的数据该如何处理、如何管理、如何渲染。 熟悉 React 的都知道 Flux ,使用 Flux 可以更加方便 React 的数据交互,让 React 更专注于 View 的逻辑。(附图:React Flux 应用交互过程)但是,一个 React Flux 应用即使可以在浏览器端正常的运行,直接在服务器端使用 所以,我们需要解决哪些问题呢?
从上面的回答,可以看到我们暂时需要解决的问题:
带着这些问题,也许会有一个轮子可以解决这个问题,不然就得自己造一个轮子了,好吧,看看有没有好用的轮先。在 GitHub 找找,找到 Redux 和 Fluxible 俩个好轮子,看了下文档,选择了 Fluxible,因为觉得它对于 Component Context 的管理比较好,而且是在 Flux 的基础上实现的。(最关键可以用酷酷的 Decorator Pattern)。 怎么用 Fluxible 完成 Server Side Rendering 的魔法1. 前端路由的选择作为 SPA 应用,都需要一个前端路由来处理不同的渲染。Fluxible 提供了自己的 router 组件,当然,使用 react-router 也可以。本文就是使用 react-router 来作为路由组件的。新建一个 Router.jsx ,作为博客的路由入口: import....;//polyfillif(!Object.assign) Object.assign=React.__spread;//eslint-disable-lineno-underscore-danglevar{ Route,DefaultRoute,NotFoundRoute,RouteHandler,Link }=Router;module.exports=( <Routepath="/"handler={App}> <DefaultRoutehandler={Demo}/> <NotFoundRoutehandler={PageNotFound}/> </Route>); react-router 具体使用方法,请参考文档(v1.3.x) 2. Store 、Action、Service使用 Fluxible 之后,Store 最好只作为数据存储使用,Store 中不要加入数据请求之类的方法,数据请求方法可以使用 Fluxible Plugins 来管理,也可以自己封装 service 类来管理。Fluxble 提供了 createStore 方法和 BaseStore 基类来创建 Store,可以根据自己的需求选择,下面创建一个 Blog.store.js : import...;varCHANGE_EVENT='change';classDemoStoreextendsBaseStore{ constructor(dispatcher){ super(dispatcher); this.dispatcher=dispatcher;//ProvidesaccesstowaitForandgetStoremethods this.data=null; } loadData(data){ this.data=data; this.emitChange(); } getState(){ return{ data:this.data}; } //Forsendingstatetotheclient dehydrate(){ returnthis.getState(); } //Forrehydratingserverstate rehydrate(state){ this.data=state.data; }}DemoStore.storeName='DemoStore';DemoStore.handlers={ "LOAD_DATA":"loadData",};exportdefaultDemoStore; 可以注意到 BlogStore 中有几个比较重要的方法和属性:
有了 Store 之后,接下来创建一个 Action 。 import...;classDemoAction{ /** *@param{string}text */ staticloadData(actionContext,payload,done){ DemoService.loadData(payload,function(data){ actionContext.dispatch('LOAD_DATA',data); done&&done(); }); }}exportdefaultDemoAction; Action 在 Fluxible 中使用 importRequestfrom'superagent';classBlogService{ staticloadData(payload,done){ varreq=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){ varresult=res.body; done&&done(result,done); }); }}exportdefaultBlogService; 有了 Store、Action、Service ,数据和事件的绑定也就有了,下面只需要把数据跟 React Component 交互处理好就可以了。 ###3. 增加一个 Route Page 使用 react-router 作为路由组件,它为每一个 url 正则都指定了一个 Handler,这个 Handler 就是一个 React Component,react-router 会直接渲染这个 React Component 以及它的子节点。 importReactfrom'react';importDemoStorefrom"Demo.store.js";importDemoActionfrom"Demo.action.js";import{connectToStores}from'fluxible-addons-react';@connectToStores([DemoStore],(context)=>({ DemoStore:context.getStore(DemoStore).getState()}))classDemoextendsReact.Component{ staticcontextTypes={ 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); vardata=this.props.DemoStore.data||[]; varitemContent=data.map(function(item,i){ return(<p>{item.content}</p>); }); return( <div> {itemContent} <divclassName="align-center"> <aclassName="button"onClick={this.reload.bind(this)}>重新加载</a> </div> </div> ); }}Demo.loadAction=[DemoAction.loadData];exportdefaultDemo; 从上面的代码中可以看到几个比较重要的地方:
鉴于 react-router 的使用,需要为 App 提供一个 RouteHandler 的入口(App.jsx)。 ...import{connectToStores,provideContext}from'fluxible-addons-react';var{RouteHandler}=Router;@provideContextclassAppextendsReact.Component{ staticcontextTypes={ getStore:React.PropTypes.func,executeAction:React.PropTypes.func}; constructor(props,context){ super(props,context); } /** *@return{object} */ render(){ return( <divclassName="main-container"> <RouteHandler{...this.props}/> </div> ); }}exportdefaultApp; 可以看到在 App Class 前面加入了一个 provideContext 的 Decorator Pattern。
4. 服务器端入口和客户端入口Store、Action、Service、Route 和 React Component 都有了之后,接下来就需要为 App 的入口做一些准备工作了。我们需要为 Server 端和 Client 端分别创建渲染入口。在 Server 端预渲染好 HTML 页面(这次渲染只是生成 HTML 字符串),Client 端接收到 HTML 之后从预存储的数据中再次渲染页面(这次渲染可以初始化一些 Dom 和 Dom 事件)。 Client 端处理相对来说比较简单,只需要把 Store 的数据反序列化,然后渲染出页面即可: vardehydratedState=window.App; app.rehydrate(dehydratedState,function(err,context){ if(err){ throwerr; } window.context=context; varmountNode=document.getElementById(app.uid); Router.run(app.getComponent(),Router.HistoryLocation,function(Handler,state){ varComponent=React.createFactory(Handler); React.render( React.createElement( FluxibleComponent,{context:context.getComponentContext()},Component() ),mountNode,function(){ } ); }); Fluxible 提供 rehydrate 方法,将 Store 数据反序列化到 context 中。然后再使用 react-router 的 Server 端处理相对比较复杂,基本过程是:
参考代码: render(req,res){ varcontext=app.createContext({ api:process.env.API||('http://127.0.0.1:'+process.env.PORT),env:{ NODE_ENV:process.env.NODE_ENV} }); varactions=this.actions||[]; Router.run(app.getComponent(),req.url,state){ if(state.routes.length===0){ res.status(404); } async.filterSeries( state.routes.filter(function(route){ returnroute.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({},req:Lodash.extend({},req),res:Lodash.extend({},res),state:Lodash.extend({},state),route:Lodash.extend({},route) },callback); //在ServerSide执行Action的时候,传入一些App上下文参数 },result){ done(); }); },function(){ conststate="window.App="+serialize(app.dehydrate(context))+";"; varComponent=React.createFactory(Handler); varHtmlComponent=React.createFactory(Html); varmarkup=React.renderToString( React.createElement( FluxibleComponent,Component() )); varhtml=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 成为可能,但是还有很多其他的过程,会根据个人业务不同会有区别,还是需要开发者自己熟悉这个过程,以及根据自身业务做出不同的方案。
(编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |