前端發展到現在,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行的程式碼,我們就實現了一個 通用化的、服務化的、單頁應用服務端渲染解決方案。