一個變相的服務端渲染系統

相學長發表於2017-09-30

前端發展到現在,SPA應該已經被應用的非常廣了。可惜的是,我們前進的是快,而人家搜尋引擎爬蟲跟使用者的瀏覽器裝置還跟不上腳步。辛辛苦苦寫好的單頁應用,結果到了SEO跟瀏覽器相容這一步懵逼了。

很多同學肯定都想過服務端渲染的問題。然而一看vue、react關於服務端渲染的文件,可能就被唬住了。之前寫好的並不能無縫遷移。而且,每當有個專案,就需要去run一套node服務。當然,架構能力好些的朋友,可以做好集中化管理。

所以,當我想在專案中,採用vue或者react的時候,就遇到這些非常大的阻力。正當我頭疼腦熱的時候呢,我發現了一條新途徑。

在前不久呢,同事在群裡分享了puppeteer,它GitHub的介紹如下:

Puppeteer is a Node library which provides a high-level API to control headless Chrome over the DevTools Protocol. It can also be configured to use full (non-headless) Chrome.

大意就是說,一個提供操作Headless Chrome的API的node庫。

再具體的說,就是能在node環境中,通過一些API,來“模擬”真實chrome訪問頁面,並對其進行模擬使用者操作、獲取DOM等。

那既然它能夠像真實Chrome那樣去訪問頁面並且輸出渲染後的html,我為什麼不能通過它來給我們做服務端渲染呢?

設想一下,我們有這樣一個服務A,它能夠像chrome一樣訪問指定頁面,並把最終頁面上的dom返回給你。

而你原本的業務伺服器B,只需要判斷是爬蟲,或者低版本IE來訪問時,調取該服務,得到html,將html返回給使用者,這就實現了服務端渲染。大致流程圖如下:

有這樣一個思路後,我們就想辦法來實踐它。實踐的過程,就是解決問題的過程。仔細想想,我們會遇到如下幾個問題:

Q1: 即使是模擬Chrome去請求頁面,很多時候檢視也是非同步渲染的。比如先請求列表介面,得到資料再渲染出列表DOM。這個時間,我們並沒有辦法把控。那這個服務,到底時候才應該把載入完成的HTML返回呢?

遇到問題時,首先可以看看人家的文件 Puppeteer API。欣喜的是,我們找到了如下幾個方法:

page.waitFor(selectorOrFunctionOrTimeout[, options[, ...args]])
page.waitForFunction(pageFunction[, options[, ...args]])
page.waitForNavigation(options)
page.waitForSelector(selector[, options])複製程式碼

我們可以通過一些設定,讓頁面在某種情況下才返回。比如我們通過設定 page.waitForSelector('#app'), 讓頁面出現 id="app" 的元素時,才把html內容返回。

或者通過設定 page.waitForFunction('window.innerWidth < 100'),當頁面寬度小於100px時,才將此時的html內容返回。

通過這些方法,我們就能有辦法控制,想要輸給爬蟲的,是什麼時候、什麼樣的頁面。

Q2: 如果IE使用者訪問量比較大怎麼辦。我們雖然通過這樣的系統,讓本渲染不出頁面的部分瀏覽器(IE9以下)能夠渲染出頁面了。但這樣的請求過程相對而言會更耗時,這不是很合理。

那我們只要做一個快取系統便好。每次請求,都會去判斷此請求是否存在未過期的快取HTML,如果存在,則直接返回快取HTML,否則再去請求頁面,儲存快取。

Q3: 雖然頁面是出來了,IE使用者還是沒辦法做一些JS的互動。

這個我們沒辦法在服務層上去解決了,但我們可以在前端上做更友好的互動提示。如果判斷使用者是低版本IE,則出現一個小Tip,提示使用者下載更好的瀏覽器,獲取更好的體驗。

Q4: 單頁應用的路由多是用錨點(雜湊模式)來做的,而雜湊引數,服務端無法獲取,那就沒辦法請求正確的頁面了。

這個有辦法解決,可以採用HTML History模式的路由,如vue-router,然後路由連結最好以生成a標籤+href的模式寫在頁面中,而不是onclick後js跳轉,這樣爬蟲能最好的爬取整站頁面。

當問題都想到辦法解決後,我們就能開始真正coding了。

啪啪啪,啪啪啪 => SSR-SERVICE

好,然後就好了,不到200行的程式碼,我們就實現了一個 通用化的、服務化的、單頁應用服務端渲染解決方案。

相關文章