🔧 阿川の電商水電行
Shopify 顧問、維護與客製化
💡
小任務 / 單次支援方案
單次處理 Shopify 修正/微調
⭐️
維護方案
每月 Shopify 技術支援 + 小修改 + 諮詢
🚀
專案建置
Shopify 功能導入、培訓 + 分階段交付

大部分人都錯了!這才是Chrome插件多腳本通信的正確姿勢

昨天一個實習生同事來找我:“哥們,我的Chrome插件遇到個奇怪問題,為什麼我插入到頁面的內容腳本content.js,重寫頁面腳本的方法沒有生效?”

其實類似的問題我也經常碰到,比如:“為什麼popup頁面調用不了content.js的函數?”、“插入到頁面的內容腳本為啥訪問不到Vue實例?”、“background腳本為啥不能訪問頁面的dom節點”?………這些問題在插件開發裡真的挺常見的,大家都容易踩坑。

所以今天,我想用最通俗的方式,把自己遇到的問題和一些小經驗分享出來:

  • 為什麼Chrome要把插件分成這麼多腳本?它們之間到底啥關係?
  • 那些“看起來應該能用,但就是不行”的通信方式,背後的原因是什麼?
  • 怎麼讓插件的各個部分配合得更順暢?

放心,沒有複雜的術語,用圖解的方式來梳理脈絡,希望這些內容能幫到大家,也歡迎大家一起交流!

前置知識:

要搞懂插件通信,首先得了解瀏覽器的架構。現在的Chrome瀏覽器採用的是“多進程架構”,什麼意思呢?直接看圖:

瀏覽器架構

上圖中,我們瀏覽器架構是由渲染進程、插件進程、網絡進程、瀏覽器主進程、gpu進程等組成的。

  • 瀏覽器主進程: 相當於公司的大老闆,負責整個公司的運轉。比如你要開新窗口、切換標籤、下載文件、彈出權限提示,都是瀏覽器主進程安排的。其他進程有啥事也都得跟主進程報備,主進程來調度。

  • 渲染進程:平時我們使用瀏覽器打開的每一個網頁,都是由渲染進程進行渲染的。插件注入的內容腳本也在這裡運行,不過處於“隔離世界”,與頁面腳本相互隔離,這個後面我們會詳細講。

  • 網絡進程:處理所有頁面與擴展的請求,我們網頁中或者插件中的接口請求這種脏活累活都由它來幹。

  • GPU進程:網頁中有炫酷動畫、視頻、3D效果啥的,GPU進程就會幫你畫得又快又漂亮。

  • 插件進程:運行我們平時所裝的瀏覽器插件,我們裝的不同的插件會分配到不同的插件進程中,互不干擾,誰家插件出了問題,最多自己崩,不會影響到其他插件和頁面的運行。

Chrome瀏覽器其實就是把各種工作分開來做,誰負責啥都很清楚。主進程管大局,渲染進程負責把網頁內容展示出來,網絡進程專門搞數據傳輸,GPU進程讓動畫和視頻更流暢,插件進程則讓你裝的各種擴展各自獨立運行。大家各幹各的,互不影響,這樣瀏覽器用起來才又快又穩還安全。

插件各部分的角色與能力

瀏覽器插件開發實踐 - 不一樣的少年_的專欄 - 掘金

大家看到我的插件專欄裡的插件目錄,大致是這樣:

demo
├── manifest.json # 擴展的"身份證"
├── background.js # 插件後台腳本
├── injected.js # 注入頁面腳本
├── content.js # 內容腳本
└── popup.html # 彈窗頁面

我們來簡單聊聊每個文件的作用:

manifest.json —— 插件的“身份證”

這個文件就像插件的身份證,裡面寫明了插件的名字、版本、權限、各個腳本的入口。比如你這個插件是彈窗類型,還是需要內容腳本、後台腳本,都要在這裡聲明清楚。沒有它,瀏覽器都不認你這個插件。

popup.html —— 彈窗頁面

這個文件就是你點瀏覽器右上角插件圖標彈出來的小窗口。寫法跟普通網頁一樣,可以放按鈕、輸入框、結果展示區。

如下紅色框住的就是popup頁面:

popup頁面

這個popup頁面,瀏覽器會分配一個插件進程來進行渲染,插件進程的本質就是一個渲染進程,如下圖:

popup進程

background.js —— 後台腳本

後台腳本是插件的大腦,負責處理各種“後台任務”。比如監聽消息、和伺服器通信、管理數據。它不直接操作頁面內容,但能幫你做很多“脏活累活”,比如跨域請求、定時任務、權限控制。彈窗頁面、內容腳本都可以和它發消息,讓它幫忙幹活。

Service Worker 也是運行在插件進程中,與Popup頁面共享同一個進程。Service Worker在進程內部運行在一個獨立的服務工作線程(Off-Main Thread)上,而Popup則運行在渲染進程的主線程上。service worker的執行環境是 ServiceWorkerGlobalScope,沒有 windowdocument 對象,因此無法直接操作DOM。

彈窗頁面(Popup)可以通過 chrome.runtime.sendMessage 向Service Worker發送消息,讓它幫忙處理後台任務。Service Worker會一直監聽這些消息,並在需要時被瀏覽器喚醒執行。

Popup和Service Worker在同一個插件進程中,但是在不同的線程和執行環境/上下文Context中,所以仍然需要通過消息傳遞機制通信,如下圖:

popup與service worker

舉例popup頁面和插件後台的通信

比如你做了一個“天氣查詢”插件,用戶在彈窗頁面輸入城市名,點擊“查詢”按鈕,彈窗頁面就會通過 chrome.runtime.sendMessage把城市名發給後台腳本,後台腳本收到後去請求天氣接口,然後把結果返回給彈窗頁面顯示。

圖解

  • 用戶操作popup頁面
  • popup頁面用chrome.runtime.sendMessage發消息給後台腳本
  • 後台腳本處理請求,返回結果給popup頁面

代碼示例:

popup.htm

<!DOCTYPE html>
<html>
  <body>
    <input id="city" placeholder="城市名" />
    <button id="btn">查天氣</button>
    <div id="result"></div>
    <script>
      const btn = document.getElementById('btn')
      btn.onclick = function() {
        const city = document.getElementById('city').value;
        // popup向background.js發送查詢消息
        chrome.runtime.sendMessage({ type: 'getWeather', city }, function(response) {
            // background.js返回的消息
          const data =  response.weather 
          document.getElementById('result').textContent = data || '查詢失敗';
        });
      };
    </script>
  </body>
</html>

background.js

chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
  if (msg.type === 'getWeather') {
    // 這裡只做演示,實際開發可以用fetch去請求真實接口
    const fakeWeather = `${msg.city}:晴,25°C`;
    sendResponse({ weather: fakeWeather });
    // 如果是異步操作(比如fetch),需要return true
  }
});

這樣,彈窗頁面和後台腳本就能通過消息機制實現通信,彈窗頁面只負責收集用戶輸入和展示結果,後台腳本負責處理數據和業務邏輯,分工明確,開發起來也很清晰!

我們打開chrome的任務管理器看剛剛上面提到的popup插件,來看看service worker和popup是否在同一個進程中運行。

如下圖:

任務管理器

可以看出service worker和popup確實是在同一個進程中運行的。

content.js —— 內容腳本

內容腳本是插件派到網頁裡的“臥底”,所以這個content.js是在頁面的渲染進程中運行的,不是在插件中運行的。

我們畫個圖來解釋下:

內容腳本

當我們用瀏覽器打開掘金網站時,瀏覽器會啟動一個渲染進程來負責頁面的展示。掘金網站自己的JS變量和運行環境都在Main World(頁面腳本環境)裡,比如你用React/Vue寫的代碼、window上掛的變量,都是屬於這個世界。

如果你這時候裝了一個護眼插件,想讓頁面變成護眼模式(比如把背景色調成綠色),插件會通過內容腳本來操作頁面的DOM,比如直接修改body的樣式。這段內容腳本其實是在另一個獨立的JS環境裡運行,也就是圖裡的Isolated World(內容腳本環境)。

雖然內容腳本和頁面腳本的執行環境是隔離開的,互相訪問不到對方的變量,但它們可以一起操作和共享頁上的DOM(比如document.body),所以內容腳本能幫你改頁面樣式、加按鈕、彈提示,但沒法直接拿到頁面裡的JS變量。如果內容腳本真要和頁面腳本通信,可以用window.postMessage這種方式來“搭橋”。

這樣設計既保證了安全,又能讓插件靈活地擴展頁面功能。

內容腳本是怎麼進到頁面裡的呢?

內容腳本是插件派到網頁裡的“臥底”,其實它的“臥底”過程也是有講究的:

有兩種方式,第一種是聲明式的,另一種是編程式的。

聲明式:

瀏覽器根據插件的manifest.json配置,自動幫你注入到指定網頁的渲染進程裡。比如你在manifest.json裡寫了:

"content_scripts": [
  {
    "matches": ["https://juejin.cn/*"],
    "js": ["content.js"]
  }
]

只要你打開掘金網站,瀏覽器就會自動把content.js注入到頁面的渲染進程裡,讓它在Isolated World(隔離環境)裡運行。

編程式(按需注入)

這裡有個很重要的概念:瀏覽器裡不同進程之間(比如主進程、渲染進程、插件進程)要互相傳遞消息,靠的就是IPC(Inter-Process Communication)機制,翻譯過來就是“進程間通信”。

  • 比如你在後台腳本(background.js)或彈窗頁面(popup)裡調用chrome.scripting.executeScript,就能按需把內容腳本注入到指定網站頁面裡。

  • 這個過程其實是:插件發起注入請求後,瀏覽器主進程會先受理這個請求,然後通過IPC機制,把要注入的內容腳本安全地傳遞給目標頁面的渲染進程。

  • 渲染進程收到腳本後,就會在頁面的隔離環境(Isolated World)裡執行內容腳本,這樣內容腳本就真正“落地”到頁面中了。

所以,整個流程就是:插件調用chrome.scripting.executeScript → 瀏覽器主進程受理並通過IPC轉發 → 頁面渲染進程執行內容腳本。

注入流程

injected.js —— 注入頁面腳本

前面說過,頁面的JS環境和內容腳本的JS環境是互相隔離的,彼此訪問不到對方的變量和方法,但它們共享同一個DOM樹。如果你想直接改頁面裡的JS環境,比如重寫fetch或XMLHttpRequest,實現像mock接口這樣的功能,就不能只靠內容腳本了。

這時候就需要用“注入腳本”的方式:我們可以在內容腳本裡創建一個script標籤,把要改寫的代碼(比如新的fetch實現)寫進去,然後把這個標籤插入到頁面的DOM裡。這樣,頁面會像加載普通JS文件一樣執行這段代碼,最終就能覆蓋頁面原生的fetch和xhr方法。

這種做法的好處是:

  • 能直接影響頁面自己的JS環境,實現更強的功能擴展。
  • 只要DOM是共享的,插入script標籤就能讓頁面執行我們的代碼。
  • 很適合做mock、日誌劫持、性能監控等插件功能。

比如我最近寫的一個基礎的mock插件(juejin.cn/post/7570984257666056238),就是用這種方式,在頁面加載前偷偷把fetch和XMLHttpRequest替換成我重寫的,讓所有網絡請求都能被插件攔截和處理。

跨環境通信流程舉例

有時候,我們的注入頁面腳本(injected.js)需要用到插件進程裡的數據,比如判斷某個fetch請求是否命中插件配置的規則。但頁面環境是拿不到插件進程裡的配置的,不過內容腳本可以幫忙“中轉”。

比如我們在injected.js裡重寫了fetch方法,頁面發起請求後,需要查一下插件裡有沒有對應的mock規則。整個數據流可以這樣走:

  1. 注入頁面腳本(injected.js)攔截到fetch請求,發現需要插件裡的規則配置。
  2. 注入頁面腳本用window.postMessage向內容腳本發消息,請求規則數據。
  3. 內容腳本收到消息後,再用chrome.runtime.sendMessage向插件進程(比如後台腳本)發起請求,獲取最新規則。
  4. 插件進程通過IPC機制把規則數據返回給內容腳本。
  5. 內容腳本拿到數據後,再用window.postMessage把結果傳回注入的頁面腳本injected.js。
  6. 注入頁面腳本拿到規則,判斷請求是否命中,然後做後續處理(比如mock、攔截、日誌等)。

這樣一來,頁面腳本、內容腳本、插件進程就能通過消息鏈路把數據安全地串聯起來,實現複雜的功能擴展。整個過程就像“接力傳話”,每個環節各司其職,既保證了安全,又讓插件和頁面能靈活協作。

通信流程

總結:插件多腳本通信,沒那麼難!

看到這裡,相信你已經對Chrome插件中的多腳本通信有了清晰的認識。讓我們簡單回顧一下今天學到的關鍵知識:

  1. 為什麼需要多腳本?

    • Chrome的多進程架構是為了安全和穩定性,不是為了故意為難開發者。
  2. 各腳本的角色:

    • background.js :負責大部分邏輯和權限,是插件的大腦。在V3版本中,它平時會休眠,有事件時才會被喚醒,所以不要用全局變量存數據哦。
    • content.js :嵌入頁面,能操作DOM,但和頁面JS是隔離的,像是派駐到頁面的特工。
    • popup.html :彈窗頁面,主要負責UI交互,關閉就“失憶”,但用起來很方便。
    • injected.js :直接注入頁面,能訪問和修改頁面環境,類似臥底。
  3. 通信秘訣:

    • background ↔ popup :用chrome.runtime.sendMessage。
    • background ↔ content :用chrome.tabs.sendMessage或chrome.runtime.sendMessage。
    • content ↔ 頁面JS :用window.postMessage。

還記得開頭那位苦惱的同事嗎?現在你不僅知道為什麼他的變量“死活拿不到”,更重要的是,掌握了正確的解決方法!

希望這篇文章能幫你少踩坑、多收穫。如果覺得有用,歡迎點讚、收藏,你的支持是我持續分享的動力!

有任何問題,歡迎在評論區討論。下期見!👋

如果覺得對您有幫助,歡迎點讚 👍 收藏 ⭐ 关注 🔔 支持一下!

往期實戰推薦:


原文出處:https://juejin.cn/post/7573521516324732955


精選技術文章翻譯,幫助開發者持續吸收新知。

共有 0 則留言


精選技術文章翻譯,幫助開發者持續吸收新知。
🏆 本月排行榜
🥇
站長阿川
📝17   💬10   ❤️5
431
🥈
我愛JS
📝2   💬8   ❤️4
92
評分標準:發文×10 + 留言×3 + 獲讚×5 + 點讚×1 + 瀏覽數÷10
本數據每小時更新一次
🔧 阿川の電商水電行
Shopify 顧問、維護與客製化
💡
小任務 / 單次支援方案
單次處理 Shopify 修正/微調
⭐️
維護方案
每月 Shopify 技術支援 + 小修改 + 諮詢
🚀
專案建置
Shopify 功能導入、培訓 + 分階段交付