昨天一個實習生同事來找我:“哥們,我的Chrome插件遇到個奇怪問題,為什麼我插入到頁面的內容腳本content.js,重寫頁面腳本的方法沒有生效?”
其實類似的問題我也經常碰到,比如:“為什麼popup頁面調用不了content.js的函數?”、“插入到頁面的內容腳本為啥訪問不到Vue實例?”、“background腳本為啥不能訪問頁面的dom節點”?………這些問題在插件開發裡真的挺常見的,大家都容易踩坑。
所以今天,我想用最通俗的方式,把自己遇到的問題和一些小經驗分享出來:
放心,沒有複雜的術語,用圖解的方式來梳理脈絡,希望這些內容能幫到大家,也歡迎大家一起交流!
要搞懂插件通信,首先得了解瀏覽器的架構。現在的Chrome瀏覽器採用的是“多進程架構”,什麼意思呢?直接看圖:

上圖中,我們瀏覽器架構是由渲染進程、插件進程、網絡進程、瀏覽器主進程、gpu進程等組成的。
瀏覽器主進程: 相當於公司的大老闆,負責整個公司的運轉。比如你要開新窗口、切換標籤、下載文件、彈出權限提示,都是瀏覽器主進程安排的。其他進程有啥事也都得跟主進程報備,主進程來調度。
渲染進程:平時我們使用瀏覽器打開的每一個網頁,都是由渲染進程進行渲染的。插件注入的內容腳本也在這裡運行,不過處於“隔離世界”,與頁面腳本相互隔離,這個後面我們會詳細講。
網絡進程:處理所有頁面與擴展的請求,我們網頁中或者插件中的接口請求這種脏活累活都由它來幹。
GPU進程:網頁中有炫酷動畫、視頻、3D效果啥的,GPU進程就會幫你畫得又快又漂亮。
插件進程:運行我們平時所裝的瀏覽器插件,我們裝的不同的插件會分配到不同的插件進程中,互不干擾,誰家插件出了問題,最多自己崩,不會影響到其他插件和頁面的運行。
Chrome瀏覽器其實就是把各種工作分開來做,誰負責啥都很清楚。主進程管大局,渲染進程負責把網頁內容展示出來,網絡進程專門搞數據傳輸,GPU進程讓動畫和視頻更流暢,插件進程則讓你裝的各種擴展各自獨立運行。大家各幹各的,互不影響,這樣瀏覽器用起來才又快又穩還安全。
大家看到我的插件專欄裡的插件目錄,大致是這樣:
demo
├── manifest.json # 擴展的"身份證"
├── background.js # 插件後台腳本
├── injected.js # 注入頁面腳本
├── content.js # 內容腳本
└── popup.html # 彈窗頁面
我們來簡單聊聊每個文件的作用:
這個文件就像插件的身份證,裡面寫明了插件的名字、版本、權限、各個腳本的入口。比如你這個插件是彈窗類型,還是需要內容腳本、後台腳本,都要在這裡聲明清楚。沒有它,瀏覽器都不認你這個插件。
這個文件就是你點瀏覽器右上角插件圖標彈出來的小窗口。寫法跟普通網頁一樣,可以放按鈕、輸入框、結果展示區。
如下紅色框住的就是popup頁面:

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

後台腳本是插件的大腦,負責處理各種“後台任務”。比如監聽消息、和伺服器通信、管理數據。它不直接操作頁面內容,但能幫你做很多“脏活累活”,比如跨域請求、定時任務、權限控制。彈窗頁面、內容腳本都可以和它發消息,讓它幫忙幹活。
Service Worker 也是運行在插件進程中,與Popup頁面共享同一個進程。Service Worker在進程內部運行在一個獨立的服務工作線程(Off-Main Thread)上,而Popup則運行在渲染進程的主線程上。service worker的執行環境是 ServiceWorkerGlobalScope,沒有 window 和 document 對象,因此無法直接操作DOM。
彈窗頁面(Popup)可以通過 chrome.runtime.sendMessage 向Service Worker發送消息,讓它幫忙處理後台任務。Service Worker會一直監聽這些消息,並在需要時被瀏覽器喚醒執行。
Popup和Service Worker在同一個插件進程中,但是在不同的線程和執行環境/上下文Context中,所以仍然需要通過消息傳遞機制通信,如下圖:

舉例: popup頁面和插件後台的通信
比如你做了一個“天氣查詢”插件,用戶在彈窗頁面輸入城市名,點擊“查詢”按鈕,彈窗頁面就會通過 chrome.runtime.sendMessage把城市名發給後台腳本,後台腳本收到後去請求天氣接口,然後把結果返回給彈窗頁面顯示。
圖解:
代碼示例:
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是在頁面的渲染進程中運行的,不是在插件中運行的。
我們畫個圖來解釋下:

當我們用瀏覽器打開掘金網站時,瀏覽器會啟動一個渲染進程來負責頁面的展示。掘金網站自己的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轉發 → 頁面渲染進程執行內容腳本。

前面說過,頁面的JS環境和內容腳本的JS環境是互相隔離的,彼此訪問不到對方的變量和方法,但它們共享同一個DOM樹。如果你想直接改頁面裡的JS環境,比如重寫fetch或XMLHttpRequest,實現像mock接口這樣的功能,就不能只靠內容腳本了。
這時候就需要用“注入腳本”的方式:我們可以在內容腳本裡創建一個script標籤,把要改寫的代碼(比如新的fetch實現)寫進去,然後把這個標籤插入到頁面的DOM裡。這樣,頁面會像加載普通JS文件一樣執行這段代碼,最終就能覆蓋頁面原生的fetch和xhr方法。
這種做法的好處是:
比如我最近寫的一個基礎的mock插件(juejin.cn/post/7570984257666056238),就是用這種方式,在頁面加載前偷偷把fetch和XMLHttpRequest替換成我重寫的,讓所有網絡請求都能被插件攔截和處理。
有時候,我們的注入頁面腳本(injected.js)需要用到插件進程裡的數據,比如判斷某個fetch請求是否命中插件配置的規則。但頁面環境是拿不到插件進程裡的配置的,不過內容腳本可以幫忙“中轉”。
比如我們在injected.js裡重寫了fetch方法,頁面發起請求後,需要查一下插件裡有沒有對應的mock規則。整個數據流可以這樣走:
這樣一來,頁面腳本、內容腳本、插件進程就能通過消息鏈路把數據安全地串聯起來,實現複雜的功能擴展。整個過程就像“接力傳話”,每個環節各司其職,既保證了安全,又讓插件和頁面能靈活協作。

看到這裡,相信你已經對Chrome插件中的多腳本通信有了清晰的認識。讓我們簡單回顧一下今天學到的關鍵知識:
為什麼需要多腳本?
各腳本的角色:
通信秘訣:
還記得開頭那位苦惱的同事嗎?現在你不僅知道為什麼他的變量“死活拿不到”,更重要的是,掌握了正確的解決方法!
希望這篇文章能幫你少踩坑、多收穫。如果覺得有用,歡迎點讚、收藏,你的支持是我持續分享的動力!
有任何問題,歡迎在評論區討論。下期見!👋
如果覺得對您有幫助,歡迎點讚 👍 收藏 ⭐ 关注 🔔 支持一下!
往期實戰推薦:
- Vue3後台分頁寫膩了?我用1個Hook刪掉90%重複代碼(附源碼)
- ⚡一個Vue自定義指令搞定絲滑拖拽列表,告別複雜組件封裝
- 🔥這才是Vue驅動的Chrome插件工程化正確打開方式
- 老闆問我:AI真能一鍵畫廣州旅遊路線圖?我用MCP現場開圖
- 【前端效率工具】:告別右鍵另存,不到50行代碼一鍵批量下載網頁圖片
- 女朋友炸了:剛打開的網頁怎麼又沒了?我反手甩出一鍵恢復按鈕!
- 你家孩子又偷玩網頁遊戲? 試試這個防沉迷工具
- 她說想要浪漫,我把瀏覽器鼠標換成了柴犬,點一下就有煙花(附源碼)
- 女朋友被鏈接折磨瘋了,我寫了個工具一鍵解救
- 上班摸魚看掘金,老闆突然出現在身後...
- 【前端效率工具】再也不用APIfox聯調!零侵入Mock,全程不改代碼、不开代理