前后端同構(gòu),作為針對單頁應(yīng)用 SEO 優(yōu)化乏力、首屏速度瓶頸等問題而產(chǎn)出的解決方案,近來在 react、vue 等前端技術(shù)棧中都得到了支持。當(dāng)我們正打算拋棄傳統(tǒng)的純服務(wù)端渲染模式,擁抱前后端分離的最佳實踐時,有些人卻已經(jīng)從單頁應(yīng)用的格局里跳出,重新去思考和定義服務(wù)端渲染。
前后端同構(gòu)是指在前后端維護同一份代碼。它是在SPA的基礎(chǔ)上,利用服務(wù)端渲染(SSR)直出首屏,解除單頁面應(yīng)用(SPA)在首屏渲染上面臨的窘境。明確地說,同構(gòu)是將傳統(tǒng)的純服務(wù)端直出的首屏優(yōu)勢和SPA的站內(nèi)體驗優(yōu)勢結(jié)合起來,以取得最優(yōu)解的解決方案。

next.js 是基于 react 的優(yōu)秀的同構(gòu)直出方案,結(jié)合 webpack 和自身提供的路由機制,可以說是開箱即用。近期由于項目技術(shù)棧遷移,接觸到了 next,順便也把 next 的源碼看了一遍,所以抽空記一記。對 next 或者同構(gòu)不太了解的建議先移步 next.js。next 的核心代碼在于定制了一套 react 的服務(wù)端渲染方案,所以以下也是按照這個流程分步剖析源碼。
目錄結(jié)構(gòu)一覽


next 服務(wù)端渲染的核心代碼位于 server 目錄下,完成了請求監(jiān)聽、路由分發(fā)、組件渲染和請求回饋等多個環(huán)節(jié),lib 中的模塊一部分是 next 自身使用(如app.js),另一部分是暴露給開發(fā)者的可用模塊(如dynamic.js)。
如何啟動服務(wù)
next 提供了它自己的 CLI,開發(fā)模式下我們直接通過 next或next dev 執(zhí)行 bin 目錄下的 bin/next腳本。它通過你傳入的參數(shù)去判別不同的操作,然后分發(fā)執(zhí)行同目錄下對應(yīng)的其他腳本,當(dāng)我們執(zhí)行next時,bin/next會對應(yīng)執(zhí)行bin/next-dev。

在
bin/next-dev中,根據(jù)代碼我們可以猜想 next 完成了一個關(guān)鍵的操作:實例化一個服務(wù)對象,然后啟動監(jiān)聽 http 請求。而同樣的,在 bin/next-start中我們也能看到相同的代碼。


document 請求時,如何完成服務(wù)端渲染
注意到啟動服務(wù)時引入的的模塊server/index.js,我們發(fā)現(xiàn)這個模塊 export 了一個擁有許多方法的豐富的 server 類,不難意識到就是這個類完成了大部分的渲染工作。實際上不僅如此,這個 server 類兼具組件渲染、路由匹配、錯誤機制、緩存處理等等多個環(huán)節(jié)的實現(xiàn),服務(wù)端的核心功能都濃縮在這個類上。

而在
bin/next-dev執(zhí)行的start()方法中,this.prepare()會根據(jù)實例化 server 對象傳入的dev字段來判斷是否啟動hot-reloader,這個不做深入。關(guān)鍵的,我們可以看到之前的猜測是正確的,這里 next 利用node原生內(nèi)置的http模塊啟動了一個服務(wù),并傳入了監(jiān)聽的回調(diào)函數(shù)。

追溯這個回調(diào)函數(shù)的本體,我們定位到
handleRequest這個函數(shù),它首先對請求的 Url 做了一層處理,然后調(diào)用了run()方法,并將處理后的參數(shù)傳了進去。

而在
run()方法里邊,除了根據(jù) dev 去運行hot-reloader和錯誤處理之外,next 做了一個路由的匹配。那么這個路由又是哪來的呢?
基于 page 目錄路徑的路由匹配是如何實現(xiàn)的
定位一下不難發(fā)現(xiàn),在實例化 server 對象(construtor())的時候,next 調(diào)用了它的defineRoutes()方法,在這個方法中定義了一個 routes 對象。這個對象是一系列路由和回調(diào)函數(shù)的映射。

而定義這個對象后,next 又為這個對象增加多一個路由處理,并且將這個映射集合一并添加到
this.router上。增加的這個路由實際上就是我們的頁面 URL 匹配(劃重點,這里是頁面請求的入口),而this.router是 next 自身實現(xiàn)的路由器,這里只是做一個簡單的路由登記。
看回
run()方法,正是服務(wù)啟動時做了路由登記,在這里才能執(zhí)行路由查詢,并且執(zhí)行了相應(yīng)的回調(diào)。當(dāng)然如果不存在回調(diào),說明 URL 無效,自然是返回404咯。
為了弄清楚 URL 的 path 是如何對應(yīng)到文件路徑,我們繼續(xù)深入??椿貏倓偽覄澚酥攸c的地方,我們可以看到頁面請求時執(zhí)行的回調(diào)中,調(diào)用了一個
render()方法。在其中,next 除了一些常規(guī)的錯誤處理之外,最重要的是:1、調(diào)用了renderToHTML()方法; 2、調(diào)用了sendHTML()函數(shù)??上攵@個步驟分別完成了頁面渲染和請求回饋兩個環(huán)節(jié),而追溯這兩個函數(shù),可以定位到都來自于同目錄下的server/render.js模塊。

未完,后續(xù)請看 next.js 的服務(wù)端渲染機制(二)