探索Headless Chrome
作者:陳寧,就職於餓了麼先後從事電商、CRM開發,經歷前端從蠻荒到現代的過程,熱愛新技術,推崇自動化。
責編:陳秋歌,關注前端開發領域,尋求報道或者投稿請發郵件chenqg#csdn.net。
導讀:Headless模式,較常應用於自動化測試、網路爬蟲、自動截圖等場景中。本文深入解讀了這一模式,並實戰分享了利用Headless實現預渲染的過程。
Headless簡介
Headless Chrome來了,你現在可以在headless/server環境中執行瀏覽器了。什麼?讓瀏覽器執行在沒有介面的伺服器端環境中,那瀏覽器可以用來幹嘛。
想象一下每次在發版前,測試人員都需要測試系統的功能,重複且乏味。於是你決定讓程式自動測試介面上的功能。你不需要瀏覽器有GUI介面,想通過程式設計的方法來驅動瀏覽器進行各種操作,並且希望能在伺服器端執行,這樣每次發版前就可以自動測試相關功能,提高測試效率。
以上只是一個應用場景,Headless瀏覽器可以理解為沒有GUI介面的瀏覽器程式。由於沒有介面,所以在速度上比普通瀏覽器稍快,它可以在自動化測試、效能檢查、獲取元資料(例如爬蟲)和網頁截圖等方面發揮用途。
對比
在Chrome瀏覽器還沒有原生支援Headless之前,早期瀏覽器可以通過Xvfb服務處理圖形顯示從而實現Headless模式,近期火狐也在積極研發原生支援Headless模式,預計在Firefox 56版本中實現。還有一種方案是通過封裝瀏覽器核心來實現Headless。比較知名的比如PhantomJS(目前僅維護)封裝了QtWebKit核心,SlimerJS封裝了Gecko核心,TrifleJS封裝了IE核心。
而使用這些框架的時候,可能會出現很多奇怪的問題。這些程式是執行在封閉環境中的,所以會導致和外部通訊很繁瑣,並且由於採用的核心比較老,從而很多新特性,新語法不支援,並非真實的使用者環境。所以提倡用Headless模式替代這些框架,從而獲得更好的效果。
使用
Chrome Beta 59開始在Liunx、Mac、Window(Chrome 60)上支援Headless模式。下載並安裝好相應版本的瀏覽器後,可以有多種方式來啟動Chrome Headless模式。
通過命令列引數—headless來啟動:
$ /Applications/Google\ Chrome\ Canary.app/Contents/MacOS/Google\ Chrome\ Canary --headless —remote-debugging-port=9222
另外也可以採用封裝好的Chrome啟動庫來達到多個平臺相容啟動,如Lighthouse用Node.js實現的chrome-launcher庫,自動尋找系統中Chrome程式的安裝位置,然後通過child_process模組來啟動Chrome瀏覽器。
同時Headless也支援被嵌入到C++程式中,從而可以更加底層地控制瀏覽器。
當啟動完Headless瀏覽器後,Mac上會出現Chrome的圖示,但是並不能開啟看到介面。然後我們可以通過瀏覽器訪問相應的遠端除錯埠來參看相應的除錯介面。除了客戶端能通過遠端介面訪問外,還可以通過程式設計實現DevTools協議來和瀏覽器進行相關的通訊,從而實現對頁面的控制。
架構
圖1為Headless Chrome架構圖。 Headless Chrome主要實現了兩個功能,一個是實現了Headless API的Headless shell應用程式,通過命令列引數啟動Headless模式,即啟動Headless shell。一個是Headless library,它實現了嵌入式應用程式能控制瀏覽器並與網頁互動的功能。
圖1 Headless Chrome架構圖
如果你是通過C++程式嵌入的話,就可以用Headless library來和瀏覽器進行通訊。瀏覽器和外界通訊有一套協議稱為DevTools。Client API是基於Chrome DevTools協議實現的一套可以和瀏覽器互動的庫。除了上面說的,還有許多庫實現DevTools,如官方推薦採用Node.js實現的chromeremote-interface庫,或者採用Python實現的chromote庫等。
DevTools協議
Chrome DevTools是一套可以用來和Chrome瀏覽器通訊的協議,平常我們開發除錯Chrome程式用的開發者工具即是基於該協議實現的一個網頁程式。Chrome開發者工具通過Socket和Chrome進行通訊,瀏覽器中的一個Tab頁面即對應一個Socket通道。然後互相進行資料交換,從而實現對網頁的檢查、除錯和監控等功能。
我們可以用命令列引數在客戶端來遠端除錯頁面。在命令列中加入引數“—remote-debuggingport=9222”啟動Chrome後,在瀏覽器進入“localhost:9222”即可看到除錯介面。其中我們可以通過網路面板中的WebSocket連線來檢視除錯程式和Chrome進行的資料收發。我們可以把該協議當成瀏覽器的API,想實現什麼功能,需要傳送固定格式的資訊過去,瀏覽器接收後會返回相應的資料。
Headless應用
在自動化測試和網路爬蟲等領域會經常用到該項新特性。
自動化測試
自動化測試有許多的框架,比較好用的比如Nightmare,這是一款基於Electron的自動化測試庫,語法漂亮好用。最近這個庫也計劃從Electron遷移到Headless Chrome。我們也可以結合Karma來實現UI的自動化測試,這樣可以保證程式碼在真實環境中執行。
網路爬蟲
網路爬蟲應用在以前的方案中,會有較多問題,比如資料抓取不全。現在很多的網站都做成了單頁應用,採用AJAX互動,傳統爬蟲能拿到的資料有限,如果不執行前端程式碼,就拿不到有用的資訊。此時,我們可以用Headless Chrome來執行相關的程式碼,將頁面執行完成後,在對相應的頁面進行分析。這比其他方案能有更好的穩定性,不過由於目前這方面的庫還不是很成熟,導致需要自己去寫一些底層的實現,在開發效率上會比較慢。
自動截圖
自動截圖也可以被應用到Headless中。在前端程式碼報錯後,如果希望能把當前錯誤頁面的截圖併發給監控程式,目前的純前端做法可以採用html2canvas。但用過這個庫後,你會發現有的截圖效果很不理想,和原來的介面差距較大。那可以換個角度,採用後端截圖的方法。由於頁面展示本質是HTML和CSS。我們可以在伺服器端部署Chrome Headless伺服器,裡面載入對應網站的資源,等前端報錯後,只需將前端整個頁面的Dom資料傳送給伺服器,伺服器把相應內容的Dom替換後,由於CSS一般是提取出來的,所以客戶端和服務端樣式表一致,Dom結構一致,資料一致,即可以將伺服器端截圖併發送給監控程式。
實戰預渲染
Prerender和Server-side render(SSR) 兩種技術都是解決首屏渲染問題,以此來提高使用者體驗的方案。Prerender方案不需要後端是Node.js。其實本質上,Prerender只提供一個假的靜態首頁預先給客戶看到樣式,不具備應用的功能。
在目前的SPA網站中,首屏大多會有一個id為app的元素。等框架資源載入完成後,框架會動態替換app元素為真正的應用樣子。而在資源尤其是打包後的JavaScript檔案沒載入完成之前,頁面基本處於白屏的狀態。而Prerender正是希望用一個固定的樣式來代替這個白屏的狀態。
目前實現預渲染的簡單方案可以採用幾張圖片,來給使用者直觀的應用佈局樣式,從而增加使用者等待的時長。複雜一點可以採用webpack將預先寫好的樣式元件打包後內聯寫入首屏頁面,包括寫入JavaScript指令碼,寫入HTML和CSS等。讓使用者可以快速瞭解應用的名字,整體顏色佈局資訊。具體做成什麼樣,需要由應用本身來決定。但不希望在首頁中有過多的內嵌程式碼,否則拖慢初始載入速度導致後續資源載入變慢的話,預渲染效果也會不理想。
我們可以用Headless來實現預渲染,有兩種預渲染方案。
一種是在伺服器端,當請求過來後,把請求動態掛在Headless Chrome裡,然後把Chrome裡面的Dom拿到後返回給客戶端,這個也可以做成SPA應用程式通用的SEO優化方案。
另一種是在程式碼釋出階段將靜態樣式內嵌寫入網站首頁。在打包階段開啟靜態伺服器,然後用Headless Chrome來訪問對應的網站,並得到網站的Dom。和骨架圖不同的是,這時候的Dom應該是網站真實渲染後的Dom。
在實際應用中,會碰到渲染出頁面結構含有開發時髒資料問題。如果把開發時的資料去掉,會影響整體頁面的佈局,因為有的佈局是靠內容撐起來的。所以我們採用了字元替換的方法,把文字資料替換為 ,這樣既保留了佔位,又去掉了髒資料。對於圖片的處理需要把圖片的href更換為預設URL圖片,有的icon如果是內聯資料,需要去掉。總之一個原則,讓頁面初始載入骨架看起來和真實結構一致。可以採用在編碼的時候,在元素的屬性上設定標誌符,來表明文字或者圖片是否需要被替換。處理完後,需要有效果,其前提是把CSS檔案在打包的時候單獨提取出來,這樣在初始載入時才會有效果。然後通過webpack打包把處理後的Dom資料內嵌到首頁中。當用戶首次訪問的時候,首頁就已內嵌有了對應的Dom結構,讓使用者對網站佈局有個大概的感知,減少使用者等待時間。
示例程式碼請見下。
const chromeLauncher = require(‘chrome-launcher');
const CDP = require('chrome-remote-interface');
function delay(time) {
time = time || 0;
return new Promise((resolve, reject) => {
setTimeout(function() {
resolve();
}, time);
})
}
async function preRender() {
// open chrome
const chrome = await chromeLauncher.launch({
port: 9222,
});
const { Page, DOM } = await CDP();
await Promise.all([
Page.enable(),
DOM.enable(),
]);
await Page.navigate({ url: 'https://h5.ele.me/market/#/home' });
await Page.loadEventFired();
// wait for loading data
await delay(3000);
const rootNode = await DOM.getDocument();
const appNode = await DOM.querySelector({ nodeId: rootNode.root.nodeId,selector: '#app' });
// replace product data to clear data
const needReplaceFlag = '#app [shell-replace]';
const defaultImage = 'http://defaultImage.com';
const replaceNode = await DOM.querySelectorAll({ nodeId:
rootNode.root.nodeId, selector: needReplaceFlag });
replaceNode.nodeIds.length && await new Promise((resolve, reject) => {
const tasks = [];
replaceNode.nodeIds.forEach(nodeId => {
try {
const task = DOM.getOuterHTML({ nodeId }).then(html => {
const nodeName = html.outerHTML.split('>')[0].slice(1).split(' ')[0];
if (nodeName === 'img') {
return DOM.setAttributeValue({ nodeId, name: 'src',value: defaultImage });
} else {
return DOM.setOuterHTML({ nodeId, outerHTML: `<${nodeName}> </${nodeName}>` });
}
});
tasks.push(task);
} catch (e) {
reject(e);
};
});
Promise.all(tasks).then(() => {
resolve();
}).catch(e => reject(e));
});
const shellHTML = await DOM.getOuterHTML({ nodeId: appNode.nodeId });
}
處理後shell效果圖,見圖2。
圖2 處理後shell效果圖
實際中還發現幾個問題,一是如果首屏展示在不同的機器上需要對應不同的效果,就需要自己手動寫入JavaScript檔案來動態實現,比較麻煩。二是會發現處理完後還是留有雜的Dom元素,影響效果,所以還需要深度清理下資料才行。
總結
Headless可以幫助開發者更好地進行自動化測試,由於是瀏覽器原生支援,所以比其他方式實現的Headless更加穩定,佔用記憶體小,也不容易出現難以解決的問題。不過目前相關的庫比較少,如果需要新的功能,需要自己寫相應的實現。本文拋磚引玉,Headless還有許多有趣的用法等著大家一起挖掘。
歡迎加入“CSDN前端開發者”群,與更多專家、技術同行進行熱點、難點技術交流。請掃描以下二維碼申請入群。