0

我的帖子

个人中心

设置

  发新话题
作者:陈国兴

【摘要】
随着前端的发展,为了用户体验,H5越来越多的使用SPA架构,导致JS代码越来越多,体积也变的庞大,这时传统的ajax方式在首屏访问时就变得慢了,而且ajax在seo方面有天然的弱势,这时服务端渲染又回来了。我们使用React搭配React Router等类库来实现服务端渲染,让首屏更快,seo更好。那么,如何使用React构建同构(isomorphic)应用呢,我们特此邀请到百安居前端架构师陈国兴做直播分享。

【正文】


随着前端的发展,为了用户体验,H5越来越多的使用SPA架构,导致JS代码越来越多,体积也变的庞大,这时传统的ajax方式在首屏访问时就变得慢了,而且ajax在seo方面有天然的弱势,这时服务端渲染又回来了。我们使用React搭配React Router等类库来实现服务端渲染,让首屏更快,seo更好。那么,如何使用React构建同构(isomorphic)应用呢,我们特此邀请到百安居前端架构师陈国兴做直播分享。

内容简介

1. 移动端为什么要用SPA
2. 传统ajax方式和服务端渲染加载速度比较
3. 服务端渲染技术详解
4. 同构方式的react代码编写一些需要注意的地方



我们会用到的react、react-router、redux这些库,,代码是之前的项目,react-router是2的版本,和最新的API可能会有一些差异。

一、移动端为什么要用SPA


我们先从为什么用SPA说起。这是因为移动互联网的发展。页面的跳转如果使用传统链接跳转的方式,尤其是在2.5G、3G时代,网速慢,不稳定,很容易点击链接后,然后就看到一片白茫茫的页面,运气好,等一会到新的页面,运气不好,那就一直在白页面上。所以需要SPA,至少在网络不好的时候,还可以看到页面,这样用户的体验会比较好。
因为使用SPA的方式开发,必然导致客户端JS是富客户端的JS,那么就带来一个问题,代码量多了如何管理,以及如何可维护。这就有了早期的BackBone,SpineJs等MVC框架,以及之后的MVP,MVVM等框架,把原来服务端的架构思想逐渐带到前端。目前,以angular、vue、react最为流行。

有人会问,为什么不选择angular或者vue?
用一句流行的话来说就是:angular(vue)你是个好人,但我们不适合。
当然,真正的原因是React的组件化思想刚好和自己想要的匹配,是技术思想上的认同,而react出来时,vue那时还没出来,angular真是又重又复杂。

二、传统ajax方式和服务端渲染加载速度比较


我们今天是讲同构,同构首先是服务端渲染(SSR),一般也称为首屏优化。我盗一张图,来看传统的页面渲染流程。






最早的Web其实是服务端渲染,但是后来大家觉得体验不好,每一次都是要重新刷新页面,这就有了ajax。最初,ajax并没有问题。但是,移动时代来了,JS框架来了。JS变的越来越大了。
从上面的图可以看出,我们要访问一个页面,首先是渲染一个没有数据的空白页面,然后加载资源,比如CSS,JS,一个打包压缩好的JS文件甚至有好几百K。
等JS加载完了,这时才发起API请求,用户还得继续等,等到请求回来才能看到一个真正的页面。
所以这个时候,反而慢了。同时,传统ajax还有SEO不友好的问题。

当爬虫来抓页面时,页面还是空白的,可能爬虫离开时,甚至数据还没从服务端拉取过来。同时,也是因为SPA一般在浏览器使用hash来做路由,这个并不会做为http请求的一部分发送给服务器(google会把#!方式的url索引),这也是搜索引擎不友好的一个原因。
我发个图,在慢速3G下的访问情况。





慢速的3G,没有调用接口的情况,总时间在22.94s(不计图片加载)。
可以看到,login页面和main.css加载完是在3.96s。

如果是使用服务端渲染,是不需要js即可看到页面的,也就是时间是这里的login页面和css加载完就可以看到真正的页面。而如果是传统ajax方式,则是在22s多,两者有6倍左右的差距,如果再加上接口调用,我们之前测试过,用户看到首屏的的时间,有8-10倍左右的差距。

服务端渲染的首屏时间是:page+api request+css,page已经包含数据了。
客户端的首屏时间是:page+css+js+api request。



三、服务端渲染技术详解


除了客户端需要加载一个很大的js文件外,API请求在服务端进行一般也是更快的。
这里简单解释一下首屏的概念:非首页。从任何地方进来的那个页面都是首屏。
也就是说,做isomorphic,首先要保证没有js的情况,可以直接从浏览器输入任何一个地址进行访问,也是可访问的。和早期的服务端渲染是一样的。所以,这也是为什么SEO能更友好的原因。

为什么要使用客户端与服务端复用代码的同构方式?维护性问题。客户端是不安全的,所以服务端不能信任客户端,需要做各种校验,包括拉取数据后的ui渲染,这样就需要前后端都要写一次一样逻辑的代码。为了开发效率、维护性等,所以需要复用。

这点上,nodejs有天然的优势。如果不考虑同构的话,光服务端渲染,其实很简单,react提供了一个方法:renderToString()。只要把它取得的数据塞到模版文件里就可以了,比如nodejs的ejs文件。为了代码复用,我们会考虑ui放服务端渲染,逻辑放服务端,API请求的代码也共用一套,路由最好也是只写一次。

react router它就支持服务端路由,并且它也为服务端渲染提供了一些友好的API,比如Link。




接下来,我们就把具体的代码大概讲一下。
首先是路由定义。





这里,history属性在浏览器端与服务端是不一样的,所以需要传进来。浏览器端使用browserHistory:
import { browserHistory } from 'react-router'






服务器端使用createMemoryHistory:
import { RouterContext, createMemoryHistory, match } from 'react-router'


我们把服务器端(nodejs)的路由配置全部贴出来,其实使用的是react-router提供的方法。
server.get('*', (req, res, next) => {
  const history = createMemoryHistory()
  const routes = createRoutes(history)
  let store = configStore()

  match({ routes, location: req.url }, (err, redirectLocation, renderProps) => {
    if (err) {
      res.status(500).send(err.message)
    } else if (!renderProps) {
      res.status(404).send('page not found')
    } else {
      getComponentFetch(renderProps, history, store).then(() => {
        let reduxState = escape(JSON.stringify(store.getState()))
        let html = ReactDOM.renderToString(
          <Provider store={store}>
            {<RouterContext {...renderProps} />}
          </Provider>
          )
        res.render('home', { html, scriptSrcs, cssSrc, reduxState })
      })
      .catch((err) => {
        next(err)
      })
    }
  })
})

function getComponentFetch (renderProps, history, store) {
  let { query, params } = renderProps
  let component = renderProps.components[renderProps.components.length - 1].WrappedComponent
  let promise = component && component.fetchData ? component.fetchData({ query, params, store, history }) : Promise.resolve()
  return promise
}

路由匹配所有,当访问时,根据路由配置,取得对应的react组件,因为要在服务端马上调用API接口获取数据,我们会在组件放一个静态方法:fetchData,调用这个方法来取得数据,然后放在一个变量传给ejs模版文件。


当然,我们这时页面已经渲染出数据了。这个reduxState变量的数据是做为js加载完后 渲染时使用。

我们看一下客户端的代码:
let reduxState = {}
if (window.__STATE__) {
  try {
    reduxState = JSON.parse(unescape(__STATE__))
  } catch (e) {
  }
}
const store = configStore(reduxState)
  ReactDOM.render((
    <Provider store={store}>
      {createRoutes(browserHistory)}
    </Provider>
    ), document.getElementById('container-root'))


window.__STATE__ 这个就是我从服务端传过来的变量reduxState的值,用来初始化redux的store。


同时,如果为了避免首屏服务端请求一次数据,浏览器又再请求一次数据,我们可以把当前的container组件的displayName也从服务端传回浏览器端,这样在组件里判断有值,则不发起fetch请求,而是直接使用的是redux store的值。

fetchData的大概代码我也贴一下:
static fetchData ({store}) {
    let cityId = global.currentCityId
    return store.dispatch(actions.getHomeData(cityId))
  }

写这个方法的目的也是为了复用redux的逻辑,不管是action还是store。这样,我们不需要掌握很多nodejs知识,只需要在server端配置一下路由,即可实现nodejs与浏览器端一套代码复用。包括UI、逻辑、redux、路由。后续只需要正常写组件,写数据请求、逻辑等即可。


四、同构方式的react代码编写一些需要注意的地方
最后,讲一下一些注意点。
1、在react的初次渲染的周期(constructor\componentWillMount\render),不要写浏览器相关对象的代码,比如window。componentDidMount是在浏览器端执行,在node端并不会执行。也不要在上面的几个生命周期写setState。

2、用户首屏渲染后,在没有加载js的情况下,有可能马上进行操作,比如链接跳转或者表单提交,所以要假设没有JS的情况也可以正常访问。比如,表单提交使用form,链接使用href(react router的link)而不是onClick。这里,react router的Link,当你js加载完后会自动把链接变成hash的形式。
补充一下同构方式的渲染流程:用户发起请求—>服务端接收到请求—> 匹配路由—>拉数据—>渲染界面—>拉JS代码—>匹配浏览器路由—>走路由对应的组件的生命周期—>拉数据——>更新组件。
所以,当js都down下来后,这时你的onClick事件才是真正可以生效的。

3、浏览器要访问API地址,这个涉及到多个环境,我这里为了方便,是在我的node做代理中转API请求的,这样,浏览器端的请求的API地址只要是http://localhost 就可以。nodejs端根据不同的环境取不同的API接口配置,而且这样做的好处,还可以绕过跨域,API后端服务不需要去配跨域这么麻烦,浏览器的请求也可以少一个option去校验是否允许跨域访问。

react同构,差不多就这些东西了。


以下问题是来自51CTO开发者社群小伙伴们的提问和分享


Q:Java-workman-北京:如果只用react+ajax的情况效率会有变化吗?不是一个新的应用,只是在原有基础上使用react的dom去展示,和普通的ajax会有太大的出处吗?

A:百安居-陈老师:这个效率就是之前说的,你要数据出来,必须得等你的JS文件下载完,然后发起请求,所以肯定会比较慢。


Q:前端-Jouryjc-深圳:老师麻烦贴一下项目github。

A:百安居-陈老师:我自己有弄了一个startkit,并没传到github。


Q:数据-unicorn-北京:ant.design是目前最好的react框架吗?

A:百安居-陈老师: ant.design不是react框架。只是UI。


Q:前端-秋香姐-深圳:node做代理中转API请求 这个是怎么做的啊?这个http-proxy是在服务端做的还是在客户的做的啊?

A:百安居-陈老师:用http-proxy。
在nodejs端。
import httpProxy from 'http-proxy'
const proxy = httpProxy.createProxyServer({
  target: `${targetUrl}/api`
})
server.use('/api', (req, res) => {
  proxy.web(req, res)
})


Q:前端-秋香姐-深圳:static fetchData 方法是啥时候怎么调用的呀?

A:







Q:数据-unicorn-北京:react UI框架您推荐那个呢?

A:百安居-陈老师: 这个要根据具体的场景,我们一般都不用UI框架,都是根据具体设计来做。后台的话,可以考虑用Ant.Design,这个听说比较大,不适合面向终端用户。


Q:前端-秋香姐-深圳:陈老师,做这个服务端渲染我们是不是需要有一个node服务器呀?

A:百安居-陈老师:对的。


Q:前端-秋香姐-深圳:我们对这个node服务器怎么搭建配置呢?

A:百安居-陈老师:一般用node最好,因为语言一样,复用性最高。我是用express,其实没几行代码,基本都贴了。其实很简单。就是配置一个路由,一个静态的获取数据方法供nodejs端调用。其他的注意一下一些细节就好了。


Q:前端-秋香姐-深圳:对了,我们做这个react同构,需要运维同学帮我们做些什么配置吗?还是跟之前没做react同构的服务器一样吗?

A:百安居-陈老师:需要跑一个nodejs服务。可能你之前的页面是由Java之类的渲染,现在都交给nodejs就好了。Java之类的只需要提供API接口。



Q:呆丸-搬磚-烏龜:“Java之类的只需要提供API接口。” 這個意思是,前台要自己搞個node服務器?
A:百安居-陈老师:nodejs服务器。同构,就是服务端、客户端复用一套代码。那么既然有服务端了。

Q:Java-workman-北京:陈老师能否简单的描述一下React的精髓或最优美的地方是什么?
A:百安居-陈老师:我最佩服的是React那么复杂的功能,它暴露出来的API却非常简洁,可以说,只要一个render方法,就入门了,懂props、state就能写大部分功能了。化繁为简的功力非常高深。



这么好的文章,占沙发啦~!

写后端的表示看不太懂前端,框架太多了。。。



宝剑厉不厉害,要看它的主人是谁,不信来PHP版块看看~!
51CTO论坛有移动端啦!扫码下载体验就送月会员哦!
引用:
原帖由 七彩极 于 2017-12-5 23:23 发表
这么好的文章,占沙发啦~!

写后端的表示看不太懂前端,框架太多了。。。
好文章,值得分享~



51CTO论坛有移动端啦!扫码下载体验就送月会员哦!
专业解封企业支付宝余额交易关闭,冻结投诉。扣:2996287260



‹‹ 上一贴:CSS三级导航 第三级li错位导致无法正常点击 ...   |   下一贴:windows10是否支持OBS直播软件? ››
  发新话题
快速回复主题
关于我们 | 诚聘英才 | 联系我们 | 网站大事 | 友情链接 |意见反馈 | 网站地图
Copyright©2005-2017 51CTO.COM
本论坛言论纯属发布者个人意见,不代表51CTO网站立场!如有疑义,请与管理员联系:bbs@51cto.com