挨踢部落直播課堂第七期:如何使用React構(gòu)建同構(gòu)(isomorphic)應(yīng)用
原創(chuàng)【51CTO.com原創(chuàng)稿件】隨著前端的發(fā)展,為了用戶體驗(yàn),H5越來越多的使用SPA架構(gòu),導(dǎo)致JS代碼越來越多,體積也變的龐大,這時(shí)傳統(tǒng)的ajax方式在首屏訪問時(shí)就變得慢了,而且ajax在seo方面有天然的弱勢(shì),這時(shí)服務(wù)端渲染又回來了。我們使用React搭配React Router等類庫來實(shí)現(xiàn)服務(wù)端渲染,讓首屏更快,seo更好。那么,如何使用React構(gòu)建同構(gòu)(isomorphic)應(yīng)用呢,我們特此邀請(qǐng)到百安居前端架構(gòu)師陳國興做直播分享。
隨著前端的發(fā)展,為了用戶體驗(yàn),H5越來越多的使用SPA架構(gòu),導(dǎo)致JS代碼越來越多,體積也變的龐大,這時(shí)傳統(tǒng)的ajax方式在首屏訪問時(shí)就變得慢了,而且ajax在seo方面有天然的弱勢(shì),這時(shí)服務(wù)端渲染又回來了。我們使用React搭配React Router等類庫來實(shí)現(xiàn)服務(wù)端渲染,讓首屏更快,seo更好。那么,如何使用React構(gòu)建同構(gòu)(isomorphic)應(yīng)用呢,我們特此邀請(qǐng)到百安居前端架構(gòu)師陳國興做直播分享。
內(nèi)容簡介
1. 移動(dòng)端為什么要用SPA
2. 傳統(tǒng)ajax方式和服務(wù)端渲染加載速度比較
3. 服務(wù)端渲染技術(shù)詳解
4. 同構(gòu)方式的react代碼編寫一些需要注意的地方
我們會(huì)用到的react、react-router、redux這些庫,,代碼示例是之前的項(xiàng)目,react-router是2的版本,和最新的API可能會(huì)有一些差異。
一、移動(dòng)端為什么要用SPA
我們先從為什么用SPA說起。這是因?yàn)橐苿?dòng)互聯(lián)網(wǎng)的發(fā)展。頁面的跳轉(zhuǎn)如果使用傳統(tǒng)鏈接跳轉(zhuǎn)的方式,尤其是在2.5G、3G時(shí)代,網(wǎng)速慢,不穩(wěn)定,很容易點(diǎn)擊鏈接后,然后就看到一片白茫茫的頁面,運(yùn)氣好,等一會(huì)到新的頁面,運(yùn)氣不好,那就一直在白頁面上。所以需要SPA,至少在網(wǎng)絡(luò)不好的時(shí)候,還可以看到頁面,這樣用戶的體驗(yàn)會(huì)比較好。
因?yàn)槭褂肧PA的方式開發(fā),必然導(dǎo)致客戶端JS是富客戶端的JS,那么就帶來一個(gè)問題,代碼量多了如何管理,以及如何可維護(hù)。這就有了早期的BackBone,SpineJs等MVC框架,以及之后的MVP,MVVM等框架,把原來服務(wù)端的架構(gòu)思想逐漸帶到前端。目前,以angular、vue、react最為流行。
有人會(huì)問,為什么不選擇angular或者vue?
二、傳統(tǒng)ajax問題和服務(wù)端渲染加載速度比較
我們今天是講同構(gòu),同構(gòu)首先是服務(wù)端渲染(SSR),一般也稱為首屏優(yōu)化。我盜一張圖,來看傳統(tǒng)的頁面渲染流程。
最早的Web開發(fā)方式其實(shí)是服務(wù)端渲染,但是后來大家覺得體驗(yàn)不好,每一次都是要重新刷新頁面,這就有了ajax。最初,ajax并沒有問題。但是,移動(dòng)時(shí)代來了,JS框架來了。JS變的越來越大了。
從上面的圖可以看出,我們要訪問一個(gè)頁面,首先是渲染一個(gè)沒有數(shù)據(jù)的空白頁面,然后加載資源,比如CSS,JS,一個(gè)打包壓縮好的JS文件甚至有好幾百K。
我發(fā)個(gè)圖,極端情況在慢速3G下的訪問情況。
慢速的3G,沒有調(diào)用接口的情況,到可正常訪問時(shí),總時(shí)間在22.94s(不計(jì)圖片加載)。
如果是使用服務(wù)端渲染,是不需要js即可看到頁面的,也就是時(shí)間是這里的login頁面和css加載完就可以看到真正的頁面。而如果是傳統(tǒng)ajax方式,則是在22s多,兩者有6倍左右的差距,如果再加上接口調(diào)用,我們之前測(cè)試過,用戶看到首屏的的時(shí)間,有8-10倍左右的差距。
服務(wù)端渲染的首屏?xí)r間是:page+api request+css,page已經(jīng)包含數(shù)據(jù)了。
客戶端的首屏?xí)r間是:page+css+js+api request。
除了客戶端需要加載一個(gè)很大的js文件外,API請(qǐng)求在服務(wù)端進(jìn)行一般也是更快的。
三、服務(wù)端渲染技術(shù)詳解
為什么要使用客戶端與服務(wù)端復(fù)用代碼的同構(gòu)方式?維護(hù)性問題。客戶端是不安全的,所以服務(wù)端不能信任客戶端,需要做各種校驗(yàn),包括拉取數(shù)據(jù)后的ui渲染,這樣就需要前后端都要寫一次一樣邏輯的代碼。為了開發(fā)效率、維護(hù)性等,所以需要復(fù)用。
這點(diǎn)上,nodejs有天然的優(yōu)勢(shì)。如果不考慮同構(gòu)的話,光服務(wù)端渲染,其實(shí)很簡單,react提供了一個(gè)方法:renderToString()。只要把它取得的數(shù)據(jù)塞到模版文件里就可以了,比如nodejs的ejs文件。為了代碼復(fù)用,我們會(huì)考慮ui放服務(wù)端渲染,邏輯放服務(wù)端,API請(qǐng)求的代碼也共用一套,路由最好也是只寫一次。
接下來,我們就把具體的代碼大概講一下。
這里,history屬性在瀏覽器端與服務(wù)端是不一樣的,所以需要傳進(jìn)來。瀏覽器端使用browserHistory:
- import { browserHistory } from 'react-router'
服務(wù)器端使用createMemoryHistory:
- import { RouterContext, createMemoryHistory, match } from 'react-router'
我們把服務(wù)器端(nodejs)的路由配置全部貼出來,其實(shí)使用的是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
- }
路由匹配所有請(qǐng)求,當(dāng)訪問時(shí),根據(jù)路由配置,取得對(duì)應(yīng)的react組件,因?yàn)橐诜?wù)端馬上調(diào)用API接口獲取數(shù)據(jù),我們會(huì)在容器組件放一個(gè)靜態(tài)方法:fetchData,調(diào)用這個(gè)方法來取得數(shù)據(jù),然后放在一個(gè)變量傳給ejs模版文件。當(dāng)然,我們這時(shí)頁面已經(jīng)渲染出數(shù)據(jù)了。這個(gè)reduxState變量的數(shù)據(jù)是做為js加載完后 渲染時(shí)使用。
我們看一下客戶端的代碼:
- 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__ 這個(gè)就是我從服務(wù)端傳過來的變量reduxState的值,用來初始化redux的store。
同時(shí),如果為了避免首屏服務(wù)端請(qǐng)求一次數(shù)據(jù),瀏覽器又再請(qǐng)求一次數(shù)據(jù),我們可以把當(dāng)前的container組件的displayName也從服務(wù)端傳回瀏覽器端,這樣在組件里判斷有值,則不發(fā)起fetch請(qǐng)求,而是直接使用的是redux store的值。
fetchData的大概代碼我也貼一下:
- static fetchData ({store}) {
- let cityId = global.currentCityId
- return store.dispatch(actions.getHomeData(cityId))
- }
寫這個(gè)方法的目的也是為了復(fù)用redux的邏輯,不管是action還是store。這樣,我們不需要掌握很多nodejs知識(shí),只需要在server端配置一下路由,即可實(shí)現(xiàn)nodejs與瀏覽器端一套代碼復(fù)用。包括UI、邏輯、redux、路由。后續(xù)只需要正常寫組件,寫數(shù)據(jù)請(qǐng)求、邏輯等即可。
四、同構(gòu)方式的react代碼編寫一些需要注意的地方
最后,講一下一些注意點(diǎn)。
1、在react的初次渲染的周期(constructor\componentWillMount\render),不要寫瀏覽器相關(guān)對(duì)象的代碼,比如window。另外:要注意componentDidMount是在瀏覽器端執(zhí)行,在node端并不會(huì)執(zhí)行。也不要在上面的幾個(gè)生命周期寫setState。
2、用戶首屏渲染后,在沒有加載js的情況下,有可能馬上進(jìn)行操作,比如鏈接跳轉(zhuǎn)或者表單提交,所以要假設(shè)沒有JS的情況也可以正常訪問。比如,表單提交使用form,鏈接使用href(react router的link)而不是onClick。這里,react router的Link,當(dāng)你js加載完后會(huì)自動(dòng)把鏈接變成hash的形式。同時(shí)js加載完成后,就可以把表單事件或者鏈接轉(zhuǎn)給js來處理了,后續(xù)的頁面就全部走ajax的方式跳轉(zhuǎn)。
3、瀏覽器要訪問API地址,這個(gè)涉及到多個(gè)環(huán)境,我這里為了方便,是在我的node做代理中轉(zhuǎn)API請(qǐng)求的,這樣,瀏覽器端的請(qǐng)求的API地址只要是http://localhost 就可以。nodejs端根據(jù)不同的環(huán)境取不同的API接口配置,而且這樣做有額外的好處,可以繞過跨域,API后端服務(wù)不需要去配跨域這么麻煩,瀏覽器的請(qǐng)求也可以少一個(gè)option去校驗(yàn)是否允許跨域訪問。
react同構(gòu),差不多就這些東西了。
以下問題是來自51CTO開發(fā)者社群小伙伴們的提問和分享
Q:Java-workman-北京:如果只用react+ajax的情況效率會(huì)有變化嗎?不是一個(gè)新的應(yīng)用,只是在原有基礎(chǔ)上使用react的dom去展示,和普通的ajax會(huì)有太大的出處嗎?
A:百安居-陳老師:這個(gè)效率就是之前說的,你要數(shù)據(jù)出來,必須得等你的JS文件下載完,然后發(fā)起請(qǐng)求,所以肯定會(huì)比較慢。
A:百安居-陳老師:我自己有弄了一個(gè)startkit,并沒傳到github。
Q:數(shù)據(jù)-unicorn-北京:ant.design是目前最好的react框架嗎?
A:百安居-陳老師: ant.design不是react框架。只是UI。
Q:前端-秋香姐-深圳:node做代理中轉(zhuǎn)API請(qǐng)求 這個(gè)是怎么做的啊?這個(gè)http-proxy是在服務(wù)端做的還是在客戶的做的啊?
A:百安居-陳老師:用http-proxy。
- import httpProxy from 'http-proxy'
- const proxy = httpProxy.createProxyServer({
- target: `${targetUrl}/api`
- })
- server.use('/api', (req, res) => {
- proxy.web(req, res)
- })
Q:前端-秋香姐-深圳:static fetchData 方法是啥時(shí)候怎么調(diào)用的呀?
A:
Q:數(shù)據(jù)-unicorn-北京:react UI框架您推薦那個(gè)呢?
A:百安居-陳老師: 這個(gè)要根據(jù)具體的場(chǎng)景,我們一般都不用UI框架,都是根據(jù)具體設(shè)計(jì)來做。后臺(tái)的話,可以考慮用Ant.Design,這個(gè)聽說比較大,不適合面向終端用戶。
Q:前端-秋香姐-深圳:陳老師,做這個(gè)服務(wù)端渲染我們是不是需要有一個(gè)node服務(wù)器呀?
A:百安居-陳老師:對(duì)的。
Q:前端-秋香姐-深圳:我們對(duì)這個(gè)node服務(wù)器怎么搭建配置呢?
A:百安居-陳老師:一般用node最好,因?yàn)檎Z言一樣,復(fù)用性最高。我是用express,其實(shí)沒幾行代碼,基本都貼了。其實(shí)很簡單。就是配置一個(gè)路由,一個(gè)靜態(tài)的獲取數(shù)據(jù)方法供nodejs端調(diào)用。其他的注意一下一些細(xì)節(jié)就好了。
Q:前端-秋香姐-深圳:對(duì)了,我們做這個(gè)react同構(gòu),需要運(yùn)維同學(xué)幫我們做些什么配置嗎?還是跟之前沒做react同構(gòu)的服務(wù)器一樣嗎?
A:百安居-陳老師:需要跑一個(gè)nodejs服務(wù)。可能你之前的頁面是由Java之類的渲染,現(xiàn)在都交給nodejs就好了。Java之類的只需要提供API接口。
Q:呆丸-搬磚-烏龜:“Java之類的只需要提供API接口。” 這個(gè)意思是,前臺(tái)要自己搞個(gè)node服務(wù)器?
A:百安居-陳老師:nodejs服務(wù)器。同構(gòu),就是服務(wù)端、客戶端復(fù)用一套代碼。那么既然有服務(wù)端了。
Q:Java-workman-北京:陳老師能否簡單的描述一下React的精髓或最優(yōu)美的地方是什么?
A:百安居-陳老師:我最佩服的是React那么復(fù)雜的功能,它暴露出來的API卻非常簡潔,可以說,只要一個(gè)render方法,就入門了,懂props、state就能寫大部分功能了。化繁為簡的功力非常高深。
【51CTO原創(chuàng)稿件,合作站點(diǎn)轉(zhuǎn)載請(qǐng)注明原文作者和出處為51CTO.com】






























