早幾年前端還處于刀耕火種、JQuery獨樹一幟的時代,前後端代碼的耦合度很高,一個web頁面檔案的代碼可能是這樣的:

這意味着後端的工程師往往得負責一部分修改HTML、編寫腳本的工作,而前端開發者也得了解頁面上存在的服務端代碼含義。
有時候某處頁面邏輯的變動,鑒于代碼的混搭,可能都不确定應該請後端還是前端來改動(即使他們都能處理)。
前端架構熱潮
有句俗話說的好——“人啊,要是擅于開口‘關我屁事’和‘關你屁事’這倆句,可以節省人生中的大部分時間”。
随着這兩年被 angular 牽頭帶起的各種前端MV*架構的風靡,後端可以毋須再于靜态頁面耗費心思,隻需要專心開發資料接口供前端使用即可。得益于此,前後端終于可以安心地互相道一聲“關我屁事”或“關你屁事”了。
以 avalon 為例,前端隻需要在頁面加載時發送個ajax請求取得資料綁定到vm,然後做view層渲染即可:
靜态頁面的代碼也由前端一手掌握,原本服務端的代碼換成了 avalaon 的專用屬性與插值表達式:
前後端代碼隔離的形式大大提升了項目的可維護性和開發效率,已經成為一種web開發的主流模式。它解放了後端程式員的雙手,也将更多的控制權轉移給前端人員(當然前端也是以需要多學習一些架構知識)。
弊端
前後端隔離的模式雖然給開發帶來了便利,但相比水乳交融的舊模式,頁面首屏的資料需要在加載的時候向服務端發去請求才能取得,多了請求等候的時間(RTT)。
這意味着使用者通路頁面的時候,這段“等待後端傳回資料”的時延會處于白屏狀态,如果使用者網速差,那麼這段首屏等候時間會是很糟糕的體驗。
當然拉到資料後,還得做 view 層渲染(用戶端引擎的處理還是很快的,忽略渲染的時間),這又依賴于架構本身,即架構要先被下載下傳下來才能處理這些視圖渲染操作。那麼好家夥,一個 angular.min.js 就達到了 120 多KB,用着渣信号的使用者得多等上一兩秒來下載下傳它。
這麼看來,單純前後端隔離的形式存在首屏時間較長的問題,除非未來平均網速達到上G/s,不然都是不理想的體驗。
另外使用前端架構的頁面也不利于SEO,其實應該說不利于國内這些渣搜尋引擎的SEO,谷歌早已能從記憶體中去抓資料(用戶端渲染後的DOM資料)。
so 怎麼辦?相信很多朋友猜到了——用 node 來助陣。
直出和同構
直出說白了其實就是“服務端渲染并輸出”,跟起初我們提及的前後端水乳交融的開發模式基本類似,隻是後端語言我們換成了 node 。
09年開始冒頭的 node 現在成了當紅炸子雞,包含阿裡、騰訊在内的各大公司都廣泛地把 node 用到項目上,前後端整而為一,如果 node 的特性适用于你的項目,那麼何樂而不為呢。
我們在這邊也提及了一個“同構”的概念,即前後端(這裡的“後端”指的是直出端,資料接口不一定由node開發)使用同一套代碼方案,友善維護。
目前 node 在服務端有着許多主流抑或非主流的架構,包括 express、koa、thinkjs 等,能夠較快上手,利用各種中間件得以進行靈活開發。
另外諸如 ejs、jade 這樣的渲染模闆能讓我們輕松地把首屏内容(資料或渲染好的DOM樹)注入頁面中。
這樣使用者通路到的便是已經帶有首屏内容的頁面,大大降低了等候時間,提升了體驗。
示例
在這裡我們以 koa + ejs + React 的服務端渲染為例,來看看一個簡單的“直出”方案是怎樣實作的。該示例也可以在我的github上下載下傳到。
項目的目錄結構如下:
1. node 端 jsx 解析處理
node 端是不會自己識别 React 的 jsx 文法的,故我們需要在項目檔案中引入 node-jsx ,即使現在可以安裝 babel-cli 後(并添加預設)使用 babel-node 指令替代 node,但後者用起來總會出問題,故暫時還是采納 node-jsx 方案:
View Code
2. 首頁路由('./routes/home')配置
注意這裡我們使用了 ReactDOMServer.renderToString 來渲染 React 元件為純 HTML 字元串,注意 List(props, lis) ,我們還傳入了 props 和 children。
其在 ejs 模闆中的應用為:
就這麼簡單地完成了服務端渲染的處理,但還有一處問題,如果元件中綁定了事件,用戶端不會感覺。
是以在用戶端我們也需要再做一次與服務端一緻的渲染操作,鑒于服務端生成的DOM會被打上 data-react-id 标志,故在用戶端渲染的話,react 會通過該标志位的對比來避免備援的render,并綁定上相應的事件。
這也是我們把所要注入元件中的資料(syncData)傳入 ejs 的原因,我們将把它作為用戶端的一個全局變量來使用,友善用戶端挂載元件的時候用上:
ejs上注入直出資料:
頁面入口檔案(js/page/home.js)挂載元件:
3. 輔助工具
為了玩鮮,在部分子產品裡寫了 es2015 的文法,然後使用 babel 來做轉換處理,在 gulp 和 webpack 中都有使用到,具體可參考它們的配置。
另外鑒于服務端對 es2015 的特性支援不完整,配合 babel-core/register 或者使用 babel-node 指令都存在相容問題,故針對所有需要在服務端引入到的子產品(比如React元件),在koa運作前先做gulp處理轉為es5(這些構模組化塊僅在服務端會用到,用戶端走webpack直接引用未轉換子產品即可)。
ejs檔案中樣式或腳本的内聯處理我使用了自己開發的 gulp-embed ,有興趣的朋友可以玩一玩。
4. issue
說實話 React 的服務端渲染處理整體開發是沒問題的,就是開發體驗不夠好,主要原因還是各方面對 es2015 支援不到位導緻的。
雖然在服務端運作前,我們在gulp中使用babel對相關子產品進行轉換,但像 export default XXX 這樣的文法轉換後還是無法被服務端支援,隻能降級寫為 module.exports = XXX。但這麼寫,在其它子產品就沒法 import XXX from 'X' 了(改為 require('X')代替),總之不爽快。隻能期待後續 node(其實應該說V8) 再疊代一些版本能更好地支援 es2015 的特性。
另外如果 React 元件涉及清單項,正常我們會加上 key 的props特性來提升渲染效率,但即使前後端傳入相同的key值,最終 React 渲染出來的 key 值是不一緻的,會導緻用戶端挂載元件時再做一次渲染處理。
對于這點我個人建議是,如果是靜态的清單,那麼統一都不加 key ,如果是動态的,那麼就加吧,用戶端再渲染一遍感覺也沒多大點事。(或者你有更好方案請留言哈~)
5. 其它
有時候服務端引入的子產品裡面,有些東西是僅僅需要在用戶端使用到的,我們以這個示例中的元件 component/List 為例,裡面的樣式檔案
不應當在服務端執行的時候使用到,但鑒于同構,前後端用的一套東西,這個怎麼解決呢?其實很好辦,通過 window 對象來判斷即可(隻要沒有什麼中間件給你在服務端也加了window接口):
不過請注意,這裡我通過 webpack 把元件的樣式也打包進了用戶端的頁面入口檔案,其實不妥當。因為通過直出,頁面在響應的時候就已經把元件的DOM樹都先顯示出來了,但這個時候是還沒有取到樣式的(樣式打包到入口腳本了),需要等到入口腳本加載的時候才能看到正确的樣式,這個過程會有一個閃動的過程,是種不舒服的體驗。
是以走直出的話,建議把首屏的樣式抽離出來内聯到頭部去。
唠唠磕磕就說了這麼多,歡迎讨論交流,共勉~