從瀏覽器原理出發聊聊Chrome插件

瀏覽器架構演進

單進程瀏覽器時代

單進程瀏覽器是指瀏覽器的所有功能模塊都是運行在同一個進程裏,這些模塊包含了網絡、插件、JavaScript 運行環境、渲染引擎和頁面等。在 2007 年之前,市面上瀏覽器都是單進程的。

單進程瀏覽器的架構

很多功能模塊運行在一個進程裏,是導致單進程瀏覽器不穩定、不流暢和不安全的一個主要因素。

  • 不穩定:早期瀏覽器需要藉助於插件來實現諸如 Web 視頻、Web 遊戲等各種強大的功能,但是插件是最容易出問題的模塊,並且還運行在瀏覽器進程之中,所以一個插件的意外崩潰會引起整個瀏覽器的崩潰。除了插件之外,渲染引擎模塊也是不穩定的,通常一些複雜的 JavaScript 代碼就有可能引起渲染引擎模塊的崩潰。和插件一樣,渲染引擎的崩潰也會導致整個瀏覽器的崩潰。
  • 不流暢:所有頁面的渲染模塊、JavaScript 執行環境以及插件都是運行在同一個線程中的,這就意味着同一時刻只能有一個模塊可以執行。如果一個腳本非常耗時,它就會獨佔整個線程,這樣導致其他運行在該線程中的頁面沒有機會去執行任務,導致整個瀏覽器失去響應,變卡頓。
  • 不安全:當你在頁面運行一個插件時,插件可以操作系統資源,如果是個惡意插件,那麼它就可以釋放病毒、竊取你的賬號密碼,引發安全性問題。

多進程瀏覽器時代

早期架構

2008 年 Chrome 發佈時的進程架構

從圖中可以看出,早期的架構已經對瀏覽器的能力進行了拆分,主要拆分爲三類:瀏覽器進程、插件進程和渲染進程。每個頁面是運行在單獨的渲染進程中的,同時頁面裏的插件也是運行在單獨的插件進程之中,進程之間是通過 IPC 機制進行通信。這就解決了單進程時代瀏覽器的各種問題:

  • 解決不穩定:由於進程是相互隔離的,所以當一個頁面或者插件崩潰時,影響到的僅僅是當前的頁面進程或者插件進程,並不會影響到瀏覽器和其他頁面。
  • 解決不流暢:JavaScript運行在渲染進程中,所以即使JavaScript阻塞了渲染進程,也只會影響當前的渲染頁面,並不會影響瀏覽器和其他頁面,因爲其他頁面的腳本運行在它們自己的渲染進程中。
  • 解決不安全:Chrome把插件進程和渲染進程鎖在沙箱裏面,沙箱裏面的程序可以運行,但是不能在硬盤上寫入任何數據,也不能在敏感位置讀取任何數據,這樣即使在渲染進程或者插件進程裏面執行了惡意程序,惡意程序也無法突破沙箱去獲取系統權限。

近期架構

相較之前,近期的架構又有了很多新的變化。

近期Chrome進程架構

從圖中可以看出,最新的 Chrome 瀏覽器包括:1 個瀏覽器主進程、1 個 GPU 進程、1 個網絡進程、多個渲染進程和多個插件進程。

  • 瀏覽器進程:主要負責界面顯示、用戶交互、子進程管理,同時提供存儲等功能。可以理解瀏覽器進程是一個統一的"調度大師"去調度其他進程,比如我們在地址欄輸入url時,瀏覽器進程首先會調用網絡進程。
  • 渲染進程:核心任務是將HTML、CSS和JavaScript轉換爲用戶可以交互的網頁,排版引擎Blink和JavaScript引擎V8都是運行在該進程中,默認情況下,Chrome會爲每個 Tab 標籤創建一個渲染進程。出於安全考慮,渲染進程都是運行在沙箱模式下。
  • GPU進程:其實,Chrome 剛開始發佈的時候是沒有 GPU 進程的。而 GPU 的使用初衷是爲了實現 3D CSS 的效果,只是隨後網頁、Chrome 的 UI 界面都選擇採用 GPU 來繪製,這使得 GPU 成爲瀏覽器普遍的需求。最後,Chrome 在其多進程架構上也引入了 GPU 進程。
  • 網絡進程:主要負責頁面的網絡資源加載,之前是作爲一個模塊運行在瀏覽器進程裏面的,直至最近才獨立出來,成爲一個單獨的進程。
  • 插件進程:主要是負責插件的運行,因插件易崩潰,所以需要通過插件進程來隔離,以保證插件進程崩潰不會對瀏覽器和頁面造成影響。

當前架構

目前Chrome瀏覽器的架構正在發生一些改變,稱爲面向服務的架構(SOA),目的是將和瀏覽器本身(Chrome)相關的部分拆分爲一個個不同的服務,服務化之後,這些功能既可以放在不同的進程裏面運行也可以合併爲一個單獨的進程運行。這樣做的主要原因是讓Chrome在不同性能的硬件上有不同的表現。當Chrome運行在一些性能比較好的硬件時,瀏覽器進程相關的服務會被放在不同的進程運行以提高系統的穩定性。相反如果硬件性能不好,這些服務就會被放在同一個進程裏面執行來減少內存的佔用。

面向服務的架構

插件運行機制

在運行機制前,我們先來回顧一下打開頁面會發生什麼:

打開頁面發生了什麼

  • 用戶新增一個tab,此時系統瀏覽器進程、渲染進程、GPU 進程、網絡進程會被創建好;
  • 用戶輸入url,瀏覽器進程檢查url,組裝協議,構成完整的url;
  • 瀏覽器進程通過進程間通信(IPC)把url請求發送給網絡進程;
  • 網絡進程接收到url請求後檢查本地緩存是否緩存了該請求資源,如果有則將該資源返回給瀏覽器進程;
  • 如果沒有,網絡進程向web服務器發起http請求(網絡請求);
  • 網絡進程解析響應流程;
    • 檢查狀態碼,非200執行狀態碼對應的處理邏輯;
    • 200響應處理:檢查響應類型Content-Type,如果是字節流類型,則將該請求提交給下載管理器,不再進行後續的渲染,如果是html則通知瀏覽器進程準備渲染進程進行渲染;
  • 準備渲染進程
    • 瀏覽器進程檢查當前url是否和之前打開的渲染進程根域名是否相同,如果相同,則複用原來的進程,如果不同,則開啓新的渲染進程;
  • 傳輸數據、更新狀態
    • 渲染進程準備好後,瀏覽器向渲染進程發起“提交文檔”的消息,渲染進程接收到消息和網絡進程建立傳輸數據的“管道”;
    • 渲染進程接收完數據後,向瀏覽器發送確認消息;
    • 瀏覽器進程接收到確認消息後更新瀏覽器界面狀態:安全、地址欄url、前進後退的歷史狀態、更新web頁面;

打開插件發生了什麼

插件的運行相較於頁面會有簡化

1.我們打開瀏覽器,新增一個空白tab頁

2.tab欄空白處右鍵,選擇任務管理器,打開任務管理器面板

3.可以看到運行了6個進程,分別是瀏覽器進程、GPU進程、網絡進程、存儲進程、渲染進程和擴展進程。

  • 擴展進程中運行Extension Page,主要包括backgrount.html和popup.html;
    • backgrount.html中沒有任何內容,是通過background.js創建生成,當瀏覽器打開時,會自動加載插件的background.js文件,它獨立於網頁並且一直運行在後臺,它主要通過調用瀏覽器提供的API和瀏覽器進行交互;
    • popup.html有內容的,跟我們普通的web頁面一樣,由html、css、Javascript組成,它是按需加載的,需要用戶去點擊地址欄的按鈕去觸發,才能彈出頁面;
  • 渲染進程主要運行Web Page,當打開頁面時,會將content_script.js加載並注入到該網頁的環境中,它和網頁中引入的Javascript一樣,可以操作該網頁的DOM Tree,改變頁面的展示效果;
  • GPU進程主要爲插件界面的渲染提供硬件能力支持;
  • 網絡進程主要處理插件中的外部資源請求,比如nexydy插件依賴到一些外部js;
  • 存儲進程爲插件提供本地存儲能力,比如使用chrome.storage.local進行持久化存儲;
  • 瀏覽器進程在這裏更多起到橋樑作用,作爲中轉可以實現Extension Page和content_script.js之間的消息通信。

插件基本介紹

版本發展

chrome插件存在三個版本,分別是Manifest V1、Manifest V2和Manifest V3。其中MV1版本已經被廢棄了,目前市面上存在MV2和MV3版本,以MV2爲主流,在被MV3慢慢取代。時間線:

Manifest V2新特性

https://developer.chrome.com/docs/extensions/mv2/manifestVersion/#manifest-v1-changes

  • 設置了默認的內容安全策略`script-src 'self'; object-src 'self';`。有關內容安全策略的詳細配置,可以參考MDN文檔;
  • 默認情況下,插件包內的資源不再可供外部網站使用。需要通過清單web_accessible_resources屬性將其顯式列入白名單;
  • browser action API更改;
  • page action API更改;
  • chrome.extension 代替 chrome.self 來指向插件本身;
  • chrome.extension.getTabContentses和chrome.extension.getExtensionTabs廢棄,使用extension.getViews替代;
  • Port.tab廢棄,使用runtime.Port替代;

Manifest V3新特性

  • Service worker替換Background Page;
  • 網絡請求修改廢棄webRequest API使用新的 declarativentrequest API 來處理;
  • 不再允許執行遠程託管的代碼,只能執行擴展包內包含的JS;
  • Promises 已經被添加到許多方法中,但仍支持回調作爲替代方法;
  • Browser Action API 和 Page Action API被統一爲單獨的Action API;
  • Web可訪問的資源,可以只對指定的站點和擴展可用;
  • 內容安全策略(CSP),現在可以爲單個對象中的不同執行上下文指定單獨的CSP;
  • executeScript的變化,不能再執行任意字符串,只能執行腳本文件和函數;

切換MV3會帶來的問題

  • 由於background不再支持page頁面配置background.html,因此也無法調用window對象上的XMLHttpRequest來構建ajax請求,也就是說我們不能像V2版本一樣,在background.html中使用XMLHttpRequest來發送請求了,而是需要使用fetch來獲取接口數據;
  • 由於service workers是短暫的,在不使用時會終止,這意味着它們在整個插件運行期間會不斷的啓動、運行和終止,也就是不穩定的;因此我們可能需要對V2中background.js的代碼邏輯進行一些改造,以往我們會習慣將一些數據直接存儲到全局變量,比如像下面這樣:
// V2 background.js
let saveUserName = "";

// 其他頁面,比如content-script或者popup中存儲數據
chrome.runtime.onMessage.addListener(({ type, name }) => {
  if (type === "set-name") {
    saveUserName = name;
  }
});

// 點擊popup時展示數據
chrome.action.onClicked.addListener((tab) => {
  // 這裏saveUserName可能爲空字符串
  console.log(saveUserName, "saveUserName");
});
  • 因此在V3中,需要對這種全局變量數據進行改造,改造的方式也很簡單,就是將數據持久化保存到storage中,需要用到的地方隨用隨取:
// V3 service worker
chrome.runtime.onMessage.addListener(({ type, name }) => {
  if (type === "set-name") {
    chrome.storage.local.set({ name });
  }
});

chrome.action.onClicked.addListener(async (tab) => {
  const { name } = await chrome.storage.local.get(["name"]);
  chrome.tabs.sendMessage(tab.id, { name });
});
  • webRequest API切換至declarativentrequest API,很多代碼邏輯需要重構;

爲什麼切換MV3?

從Manifest V1到Manifest V2,可以看到Chrome想提高插件的隱私和安全,同時也優化了不少API。而Manifest V3除了安全性更完善外,還在性能上下了功夫。Manifest V3 的核心非常明確,就是限制擴展對系統資源的使用。一直以來高資源佔用都是 Chrome 爲人詬病的痛點,而且擴展由於在後臺運行,如果出現問題,更是難以定位和管理。雖然增加了諸多限制,但Manifest V3還是有優點的:

  • Service Worker 使擴展不再能常駐後臺,讓擴展所佔用的資源可以被回收,降低了瀏覽器整體的開銷;
  • 限制規則的數量,相當於控制了單一擴展在規則計算方面的資源使用上限;

這些變化可以讓 Chrome 變得更加流暢,對於用戶來說是好事。

展示形式

Chrome插件有以下常見的8中展現形式:

browserAction(瀏覽器右上角)

在瀏覽器右上角擴展程序一欄顯示,包含一個圖標、名稱和popup

山海關插件popup

pageAction(地址欄右側)

pageAction指的是在當某些特定頁面打開才顯示的圖標。在早些版本的Chrome是將pageAction放在地址欄的最右邊,左鍵單擊彈出popup,右鍵單擊則彈出相關默認的選項菜單。而新版的Chrome更改了這一策略,pageAction和普通的browserAction一樣也是放在瀏覽器右上角,只不過沒有點亮時是灰色的,點亮了纔是彩色的,灰色時無論左鍵還是右鍵單擊都是彈出選項。

右鍵菜單

通過開發Chrome插件可以自定義瀏覽器的右鍵菜單,主要是通過chrome.contextMenus API實現,右鍵菜單可以出現在不同的上下文,比如普通頁面、選中的文字、圖片、鏈接,等等。

掘金插件右鍵菜單

override(覆蓋特定頁面)

使用override可以將Chrome默認的一些特定頁面替換掉,改爲使用擴展提供的頁面。擴展可以替代如下頁面:

  • 歷史記錄:從工具菜單上點擊歷史記錄時訪問的頁面,或者從地址欄直接輸入 chrome://history
  • 新標籤頁:當創建新標籤的時候訪問的頁面,或者從地址欄直接輸入 chrome://newtab
  • 書籤:瀏覽器的書籤,或者直接輸入 chrome://bookmarks

掘金插件替換了新標籤頁

devtools(開發者工具)

Chrome允許插件在開發者工具(devtools)上開發,主要表現在:

  • 自定義一個和多個和Elements、Console、Sources等同級別的面板;
  • 自定義側邊欄(sidebar),目前只能自定義Elements面板的側邊欄;

React Developer Tools

option(選項頁)

插件的設置頁面,可以在右上角入口右鍵,有一個選項標籤

 

omnibox

omnibox是向用戶提供搜索建議的一種方式,可以在搜索欄輸入特定的標識然後按Tab進入搜索。

JSON Viewer插件

桌面通知

Chrome提供了一個chrome.notificationsAPI以便插件推送桌面通知,暫未找到chrome.notifications和HTML5自帶的Notification的顯著區別及優勢。在後臺JS中,無論是使用chrome.notifications還是Notification都不需要申請權限(HTML5方式需要申請權限),直接使用即可。

核心介紹

manifest.json

這是一個Chrome插件最重要也是必不可少的文件,用來配置所有和插件相關的配置,必須放在根目錄。其中,manifest_version、name、version3個是必不可少的。

Manifest V2

{
// 清單文件的版本,這裏先使用2演示
"manifest_version": 2,
// 插件的名稱
"name": "...",
// 插件的版本
"version": "1.0.0",
// 插件描述
"description": "...",
// 圖標,一般偷懶全部用一個尺寸的也沒問題
"icons": {
"16": "img/icon.png",
"48": "img/icon.png",
"128": "img/icon.png"
  },
// 會一直常駐的後臺JS或後臺頁面
"background": {
"scripts": ["js/background.js"]
  },
// 瀏覽器右上角圖標設置,browser_action、page_action、app必須三選一
"browser_action": {
"default_icon": "img/icon.png",
"default_title": "...",
"default_popup": "popup.html"
  },
// 當某些特定頁面打開才顯示的圖標
"page_action": {
"default_icon": "img/icon.png",
"default_title": "...",
"default_popup": "popup.html"
  },
// 需要直接注入頁面的JS
"content_scripts": [{
"matches": ["<all_urls>"],
"js": ["js/content-script.js"],
"css": ["css/custom.css"],
// 代碼注入的時機,document_start, document_end, document_idle,默認document_idle
"run_at": "document_start"
    },
  ],
// 權限申請
"permissions": [
"contextMenus", // 右鍵菜單
"tabs", // 標籤
"notifications", // 通知
"webRequest", // web請求
"webRequestBlocking",
"storage", // 插件本地存儲
"https://*/*" // 可以通過executeScript或者insertCSS訪問的網站
  ],
// 普通頁面能夠直接訪問的插件資源列表,如果不設置是無法直接訪問的
"web_accessible_resources": ["js/inject.js"],
"homepage_url": "...", // 插件主頁
"chrome_url_overrides": { // 覆蓋瀏覽器默認頁面
"newtab": "newtab.html"
  },
"options_ui": { // 插件選項頁
"page": "options.html",
"chrome_style": true
  },
"omnibox": { "keyword" : "..." }, // 向地址欄註冊一個關鍵字以提供搜索建議,只能設置一個關鍵字
"default_locale": "zh_CN", // 默認語言
"devtools_page": "devtools.html", // devtools頁面入口,注意只能指向一個HTML文件,不能是JS文件
"content_security_policy": "...", // 安全策略
"web_accessible_resources": [ // 可以加載的資源
    RESOURCE_PATHS
  ]
}

Manifest V3(僅展示與V2版本的不同點)

{
"manifest_version": 3,
"background": {
"service_worker": js/background.js"
  },
  "action": { //browser_action 和 page_action,統一爲 Action
    "default_icon": "img/icon.png",
    "default_title": "這是一個示例Chrome插件",
    "default_popup": "popup.html"
  }
  "content_security_policy": {
    "extension_pages": "...",
    "sandbox": "..."
  },
  "web_accessible_resources": [{
    "resources": [RESOURCE_PATHS]
  }]
}

content-scripts

是Chrome插件中向頁面注入腳本的一種形式(雖然名爲script,其實還可以包括css的),藉助content-scripts我們可以實現通過配置的方式輕鬆向指定頁面注入JS和CSS。content-scripts和原始頁面共享DOM,但不共享JS。如要訪問頁面JS(例如某個JS變量),只能通過injected js來實現。content-scripts不能訪問絕大部分chrome API,除了下面這4種:

  • chrome.extension
  • chrome.i18n
  • chrome.runtime
  • chrome.storage

這些API絕大部分時候都夠用了,有需要調用其它API的話,可以通過通信讓background或service worker來幫忙調用

background

後臺是一個常駐的頁面,它的生命週期是插件中所有類型頁面中最長的,它隨着瀏覽器的打開而打開,隨着瀏覽器的關閉而關閉,所以通常把需要一直運行的、啓動就運行的、全局的代碼放在background裏面。background的權限非常高,幾乎可以調用所有的Chrome擴展API(除了devtools),而且它可以無限制跨域,可以跨域訪問任何網站而無需要求對方設置CORS。background的概念在MV3版本中變爲了service worker,區別在於生命週期變短了,service worker是短暫的基於事件的腳本,所以不適合用來保存全局變量。

popup

popup是點擊右上角圖標時打開的一個小窗口網頁,焦點離開網頁就立即關閉,一般用來做一些臨時性的交互。權限級別和background差不多,就是生命週期比較短。

injected-script

chrome插件中其實沒有injected-script這一概念,這是開發者們在開發過程中衍生出來的一種概念,指的是通過DOM操作的方式向頁面注入的一種JS。因爲content-script無法訪問頁面中的JS,雖然可以操作DOM,但是DOM卻不能調用它,也就是無法在DOM中通過綁定事件的方式調用content-script中的代碼。但是在網頁中增加一個按鈕來調用插件的能力是一個比較常見的需求,所以誕生了injected-script。

插件通信機制

講通信機制之前,先回顧一下插件中存在的腳本類型。Chrome插件的JS主要可以分爲這5類:injected script、content-script、popup js、background js和devtools js。

權限對比

JS種類 可訪問的API DOM訪問情況 JS訪問情況 直接跨域
injected 和普通JS無任何差別,不能訪問任何擴展API 可以訪問 可以訪問 不可以
content 只能訪問 extension、runtime等部分API 可以訪問 不可以 不可以
popup 可訪問絕大部分API,除了devtools系列 不可直接訪問 不可以 可以
background 可訪問絕大部分API,除了devtools系列 不可直接訪問 不可以 可以
devtools 只能訪問 devtools、extension、runtime等部分API 可以 可以 不可以

通過權限對比可以看到,每一種腳本在權限上都不相同,所以各種腳本間的相互通信就非常重要,這也是插件能夠實現衆多功能的基礎。

通信概覽

  injected content popup background
injected - window.postMessage - -
content window.postMessage - chrome.runtime.sendMessage chrome.runtime.connect chrome.runtime.sendMessage chrome.runtime.connect
popup - chrome.tabs.sendMessage chrome.tabs.connect - chrome.extension. getBackgroundPage
background - chrome.tabs.sendMessage chrome.tabs.connect chrome.extension.getViews -
devtools chrome.devtools. inspectedWindow.eval - chrome.runtime.sendMessage chrome.runtime.sendMessage

一些常見插件的實現思路

埋點日誌檢測

一般業務中都會進行一些埋點上報,埋點的本質就是發送一些帶特定參數的請求,前端本地調試的時候想實時查看埋點信息通常需要去查看上報接口的入參,或者去對應的埋點平臺查看,這樣非常不方便。基於這個,我們可以使用插件來幫助我們快速的可視化查看埋點信息:

頁面注入小工具

插件的另一個常見用法就是往頁面注入一些工具代碼,比如去除頁面廣告工具。

總結

  • 隨着瀏覽器不斷的發展,Chrome逐漸把一些基礎服務獨立出來,類似於一個跨平臺的線上操作系統。
  • Chrome插件提供的能力很豐富,比如代碼注入、跨域請求、持久化方案、各種通信機制等,開發者可以發揮想象,組裝不同能力以適應不同場景的需求,基本可以實現現代web所能支持的所有功能。
  • Chrome插件MV2版本將在24年1月全面廢棄,需要儘快遷移至MV3版本。

參考資料:

《瀏覽器工作原理與實踐》:https://time.geekbang.org/column/intro/100033601?tab=catalog

《Inside look at modern web browser》:https://developer.chrome.com/blog/inside-browser-part1/

《圖解瀏覽器的基本工作原理》:https://zhuanlan.zhihu.com/p/47407398

《Welcome to Manifest V3》:https://developer.chrome.com/docs/extensions/mv3/intro/

MDN文檔:https://developer.mozilla.org/zh-CN/docs/Web/HTTP/CSPweb_accessible_resources:https://developer.chrome.com/docs/extensions/mv2/manifest/web_accessible_resources/

作者|閔子

原文鏈接

本文爲阿里雲原創內容,未經允許不得轉載。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章