[Prerender] Chrome headless 應用

現行的服務裡,有 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 的項目的,有興趣可以參考官方文件,

https://chromedevtools.github.io/devtools-protocol/tot/

方法

我們需要寫一個 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, PageRuntime 三個工具。

  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 都運行良好就是,或者是你也可以自己寫個快取機制,也是不錯的。

Hina Chen
偏執與強迫症的患者,算不上是無可救藥,只是我已經遇上我的良醫了。
Taipei