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