挨踢部落直播課堂第七期:如何使用React構建同構(isomorphic)應用


隨着前端的發展,爲了用戶體驗,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問題和服務端渲染加載速度比較

我們今天是講同構,同構首先是服務端渲染(***),一般也稱爲首屏優化。我盜一張圖,來看傳統的頁面渲染流程。

1

最早的Web開發方式其實是服務端渲染,但是後來大家覺得體驗不好,每一次都是要重新刷新頁面,這就有了ajax。最初,ajax並沒有問題。但是,移動時代來了,JS框架來了。JS變的越來越大了。

從上面的圖可以看出,我們要訪問一個頁面,首先是渲染一個沒有數據的空白頁面,然後加載資源,比如CSS,JS,一個打包壓縮好的JS文件甚至有好幾百K。等JS加載完了,這時才發起API請求,用戶還得繼續等,等到請求回來才能看到一個真正的頁面。所以這個時候,反而慢了,這時服務端渲染的方式又回來了。

我發個圖,極端情況在慢速3G下的訪問情況。

2

慢速的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。

接下來,我們就把具體的代碼大概講一下。首先是路由定義。

3

這裏,history屬性在瀏覽器端與服務端是不一樣的,所以需要傳進來。瀏覽器端使用browserHistory:

import { browserHistory } from 'react-router'

4

服務器端使用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, cs***c, 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來處理了,後續的頁面就全部走ajax的方式跳轉。補充一下同構方式的渲染流程:用戶發起請求—>服務端接收到請求—> 匹配路由—>拉數據—>渲染界面—>拉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:百安居-陳老師:

5

6


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就能寫大部分功能了。化繁爲簡的功力非常高深。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章