現行的服務裡,有 prerender.io 可以使用,不過由於他是使用 PhantomJS 來當作背後的引擎,而,偏偏我自己開機器來做的時候,常常遇到一個畫面要處理超過 1 分鐘的窘境。
所以,只好拿 Google Chrome 新推出的 --headless
來試一下水溫。
安裝
目前是使用 Ubuntu 16.04LTS 來進行安裝,在伺服器上裝 Google Chrome 挺弔詭的就是。最簡單的方式就是透過 apt-get
來安裝,如果你要去下載 deb
來裝也可以。
sudo apt-get install google-chrome-stable
然後,你需要一點 NodeJS 的套件來輔助你,
cd /opt
mkdir prerender
cd prerender
npm install chrome-remote-interface
nom install koa@next
這裡用 Koa2 沒有什麼特別原因 因為潮,你如果 express 順手的話,用 express 也是可以的。
設定
Chrome --headless
要啟動成背景服務,所以你可以用 supervisor 來跑,或是其他你用的順手的工具。啟動的參數這邊給大家參考一下,
/usr/bin/google-chrome --headless --disable-gpu --window-size=1366,768 --remote-debugging-port=9222 --user-data-dir=/mnt/tmp http://127.0.0.1
由於你是在伺服器環境,所以 --disable-gpu
請不要忘記,然後 --window-size
是指定你要的話面大小,你也可以自己指定。
接著 --remote-debugging-port=9222
就用預設的 9222
埠就好了,他預設會將這個服務啟動在 localhost:9222
底下,最後 --user-data-dir
可以不給倒是沒關係。
啟動之後,其實你就可以用 chrome-remote-interface
套件裡面的 bin/client.js
來玩玩看這個 Chrome Headless 的功能,基本上是完全參照 Chrome DevTools Protocol Viewer 的項目的,有興趣可以參考官方文件,
方法
我們需要寫一個 Prerender 的服務來跑在背景,當主機發出請求的時候,利用這個服務來渲染出資料,吐回去給主機。例如,Facebook Bot 來爬頁面的時候,主機就對 Prerender 服務發出請求,然後我們將畫面處理好送回去。
之所以需要 Prerender 的目的應該不用贅述,用 JavaScript 產生的資料,我們就一樣用 JavaScript 產生一份送回去。
首先我們要寫一個 server.js
用來處理請求,所以這裡會起一個 Koa2,用來接收資料進來,
const http = require('http')
const Koa = require('koa')
const PORT = 3000
const ADDRESS = '0.0.0.0'
const app = new Koa()
app.use(async (ctx, next) => {
ctx.body = '<html></html>'
await next()
})
http.createServer(app.callback()).listen(PORT, ADDRESS, () => {
console.log('Starting prerender server on http://%s:%s', ADDRESS, PORT)
})
接著我們要把 chrome-remote-interface
放進去,
const http = require('http')
const Koa = require('koa')
const CDP = require('chrome-remote-interface')
然後我們得先取出 URL,然後交給 CDP 處理,
app.use(async (ctx, next) => {
const URL = ctx.request.url.substr(1)
await next()
CDP((client) => {
// 這裡要處理頁面資料
}).on('error', (err) => {
console.error(err)
ctx.body = '<html></html>'
})
})
CDP 裡面要做幾件事情,我們需要使用到的工具,可以從 client
當中提取出來,需要用的有 Network
, Page
與 Runtime
三個工具。
CDP((client) => {
const { Network, Page, Runtime } = client
// 設定網路,送出該 URL
Network.requestWillBeSent(params => params.request.url)
// 當頁面完成讀取後,取出頁面資料
Page.loadEventFired(() => {
})
// 做一下 Promise
Promise.all([Network.enable(), Page.enable()])
.then(() => {
// 啟用 Network, Page 然後回傳 Page.navigate 的結果
return Page.navigate({ url: URL })
}).catch((err) => {
console.error(err)
client.close()
})
}).on('error', (err) => {
console.error(err)
ctx.body = '<html></html>'
})
取出頁面資料後,需要靠 Runtime
這個工具來將畫面運算出來,
Runtime.evaluate({expression: 'document.documentElement.outerHTML'}).then(result => {
// 因為你是拿出 outerHTML 所以 DOCTYPE 在這邊加回去
ctx.body = '<!DOCTYPE html>' + result.result.value
})
client.close()
這樣就完成畫面渲染,然後把資料吐回請求的主機了。
地雷
然後你會發現,Facebook Bot 過來抓的東西好像都半殘,例如 JavaScript 執行到一半,或是你的畫面本身有 async
的程序需要執行的時候,然後 DOM 的資料就會東缺一點西缺一點。
為何?
因為 Page
交給 Runtime
在執行的時候,並不會等待你畫面上的任何程序,會將結果直接返回,所以你所拿到的 result
的資料,等於是第一次渲染頁面的結果,後續的變動並不會傳回來。
所以,加一點 setTimeout
可以解決這個狀況,但是前提是,你的主機反應時間必須要比 setTimeout
的時間要快,不然也是沒輒。
// 當 Page.loadEventFired 發生後
// 不要馬上將畫面做 Runtime.evaluate
// 稍微等一下再做,讓畫面上的非同步傳輸資料都做完
// 例如你用 API 跟後端拿資料,然後修改 DOM 這件事
// 或是你修改了 meta tag 之類的事情
setTimeout(() => {
Runtime.evaluate({expression: 'document.documentElement.outerHTML'}).then(result => {
ctx.body = '<!DOCTYPE html>' + result.result.value
})
client.close()
// 讓子彈飛一會
}, 3000)
這樣回傳的頁面資訊就會比較完整,當然,如果你的 API 反應時間超過 3000 ms
的話,這邊的時間就要再拉長一點,請自己斟酌一下。
另外,由於他是真的跑一個 Chrome 來處理你的畫面,所以你畫面上有 JavaScript 的錯誤,他也是會 真實呈現 的!所以,如果你發現你渲染出來的話面沒東西或是半殘,也有可能是 JavaScript 有錯誤所導致。
所以,在使用這個東西之前,請先確保你的 JavaScript 運行正常,不然你可能就真的用得上 Remote Debugging 的工具了(笑
其實他本來就是用來做 Remote Debugging 的 XD
結語
完整的程式碼在這裡,可以拿去參考,
https://gist.github.com/hinablue/da18161b503fbb0833fe637d58a70e7e
目前來說,處理速度還算是不錯的,只是你要確保 Google Chrome 與你的 server.js
都運行良好就是,或者是你也可以自己寫個快取機制,也是不錯的。