## 開始動手 ### 最簡單的 manifest.json ```json { "manifest_version": 3, "name": "Extension Lab", "version": "0.0.1" } ``` ### default popup 用來指定 popup 的 UI ```json { "action": { "default_popup": "index.html" } } ``` ### 其他可設置的 action #### default_icon 設定擴充功能按鈕的圖示。可以是一個檔案路徑或物件,像這樣: ```json { "default_icon": { "128": "icon-128.png" } } ``` > 要注意的是,這只設定 tabbar 上的按鈕 icon。還要設定`icons` 才能給擴充功能管理頁(`chrome://extensions/`)、通知、權限提示、Chrome Web Store 這些地方作顯示。 #### default_title 當滑鼠懸停在擴充功能按鈕上時,顯示的提示文字(tooltip)。 #### browser_style 控制彈出視窗是否使用瀏覽器的樣式。 ### icon 的尺寸 #### 16x16 工具列上的擴充功能按鈕。 #### 32x32 高 DPI 顯示器上的工具列按鈕。 #### 48x48 擴充功能管理頁面的圖示(`chrome://extensions/`)。 #### 128x128 Chrome 應用程式啟動器或 Web Store 的圖示。 > 至少設定 128x128,如果可以還是四個尺寸都設定。另外 chrome://extensions/ 要先 remove 再重 load 才會看到新的 icon。 ### Options Page 讓使用者設定擴充功能的行為或偏好。 ```json { "options_ui": { "page": "options.html", "open_in_tab": false } } ``` ### Side Panel > 要移除 extension 重新載入。 ![image](https://hackmd.io/_uploads/rJORoQqcke.png) ![image](https://hackmd.io/_uploads/B1TW3795Jx.png) ```json { "permissions": ["sidePanel"], "side_panel": { "default_path": "sidepanel.html" } } ``` > 如果你想用程式打開 Side Panel,可以在擴充功能的背景腳本或 popup 裡呼叫這個 API: > ```js > chrome.sidePanel.open({ tabId: YOUR_TAB_ID }); > ``` > > 或確保在當前分頁中 Side Panel 可用: > ```js > chrome.sidePanel.setOptions({ > tabId: YOUR_TAB_ID, > path: "sidepanel.html", > enabled: true > }); > ``` ### chrome_url_overrides.newtab 把 Chrome 預設的新分頁改成你的擴充功能頁面。 ```json { "chrome_url_overrides": { "newtab": "newtab.html" } } ``` ### chrome_url_overrides.bookmarks 用你的 UI 替代 Chrome 的書籤管理介面。 ```json { "chrome_url_overrides": { "bookmarks": "bookmarks.html" } } ``` ### chrome_url_overrides.history 自訂 Chrome 的瀏覽歷史頁面。 ```json { "chrome_url_overrides": { "history": "history.html" } } ``` ### chrome_url_overrides.history 自訂 Chrome 的瀏覽歷史頁面。 ```json { "chrome_url_overrides": { "bookmarks": "bookmarks.html" } } ``` ### devtools_page 自訂 Chrome 的瀏覽歷史頁面。 ```json { "devtools_page": "devtools.html" } ``` ### 通知 ```js chrome.notifications.create({ type: "basic", iconUrl: "icon.png", title: "通知標題", message: "這是通知內容" }) ``` ### Content Scripts 這些是在網頁上插入的 UI 元件,例如浮動按鈕、側邊欄等。 ```json { "content_scripts": [ { "matches": [ "https://*.example.com/*" ], "js": [ "content.js" ], "css": [ "styles.css" ], "run_at": "document_end", "all_frames": false } ] } ``` 解說各欄位: matches:定義哪些網址會執行這個腳本,支援通配符(*)。 - js:執行的 JavaScript 腳本檔。 - css(選用):要插入的 CSS 樣式表。 - run_at(選用):腳本執行時機(預設是 "document_idle"): - "document_start":在網頁開始載入時執行(DOM 尚未生成)。 - "document_end":在網頁 DOM 生成後執行。 - "document_idle"(預設):在頁面資源大致載入後執行。 - all_frames(選用):是否要在所有 iframe 裡執行(預設 false)。 注意事項: 1. content_scripts 無法直接存取擴充功能的背景腳本或 popup。如果需要溝通,請使用 `chrome.runtime.sendMessage()` 和 `chrome.runtime.onMessage.addListener()`。 2. content_scripts 只能在符合 matches 條件的網頁執行,無法直接操作擴充功能內部頁面。 3. Chrome 擴充功能只認得 JavaScript,所以 content_scripts 無法直接執行 .ts 檔案,但你可以用 TypeScript 開發,透過編譯產出 JavaScript,最後讓擴充功能執行。 4. content_scripts 中的 CSS 是在網頁 DOM 加載前就會被插入,不受 run_at 的影響。如果你需要延遲插入 CSS(像是在某些互動後才動態加樣式),就不能用 manifest.json。這時要靠你的 content.js 動態加載。 5. 引用的內容必須在本地端,不能是 CDN 版本。 ### 除了 content scripts 以外還有什麼 scripts 可以寫? #### background script 用來處理長時間執行的邏輯,像事件監聽、API 呼叫、狀態管理等。背景腳本可以一直存在(persistent),或在需要時啟動(非 persistent)。 ```json { "background": { "service_worker": "service_worker.js" } } ``` > 背景腳本不能直接操作網頁 DOM,但可以與 content_scripts 溝通。 #### popup 在 popup.html 裡寫的 script。 它只在 popup 開啟時執行,關閉後就會被卸載。 > 還有諸如 Options Page、Side Panel、Devtools 以及其他各種 chrome_url_overrides 頁面的 scripts --- ### 哪些腳本可以呼叫 `chrome.*` API? 除了 content_scripts 以外,大部分都可以直接呼叫 chrome api(當然是為了安全性考量)。 | **腳本類型** | **可以呼叫 Chrome API?** | **備註** | |-------------------|-------------------------|-------------------------------| | `background.js` | ✅ 全部 | 最完整權限,常用來管理邏輯 | | `service_worker.js`| ✅ 全部 | Manifest V3 的背景腳本 | | `popup.js` | ✅ 大部分 | 必須在 `manifest.json` 中聲明 | | `options.js` | ✅ 大部分 | 用於儲存設定、互動 UI | | `sidepanel.js` | ✅ 大部分 | 側邊欄畫面的互動腳本 | | `content_scripts` | 🚫 **部分 API** | 只能呼叫少數 API(像 `chrome.storage`),無法直接呼叫背景腳本專屬 API(如 `chrome.tabs`) | | `newtab.js` | ✅ 大部分 | 新分頁覆蓋的 UI 腳本 | | **網頁上的 script**| 🚫 無法直接呼叫 | 需要用 `chrome.runtime.sendMessage()` 跟擴充功能溝通 | --- ### Chrome API 分類 & 功能(最常用的 API) | **API 名稱** | **用途** | **可用腳本類型** | |------------------------|-------------------------------|-------------------------------| | `chrome.tabs` | 控制、管理分頁(建立、關閉、切換)| `background.js`, `popup.js` | | `chrome.runtime` | 擴充功能生命周期、訊息傳遞 | **全部腳本** | | `chrome.storage` | 擴充功能內部儲存資料 | **全部腳本** | | `chrome.action` | 控制擴充功能圖示(圖示、badge) | `background.js`, `popup.js` | | `chrome.scripting` | 在網頁中動態注入腳本和樣式 | `background.js`, `service_worker.js` | | `chrome.windows` | 控制瀏覽器視窗(建立、管理) | `background.js`, `popup.js` | | `chrome.bookmarks` | 存取與管理書籤 | `background.js`, `popup.js` | | `chrome.history` | 存取與管理瀏覽記錄 | `background.js`, `popup.js` | | `chrome.notifications` | 發送系統通知 | `background.js`, `popup.js` | | `chrome.contextMenus` | 建立右鍵選單 | `background.js` | | `chrome.alarms` | 定時執行程式 | `background.js`, `service_worker.js` | | `chrome.webRequest` | 攔截、修改網路請求 | `background.js`, `service_worker.js`(需要權限) | | `chrome.devtools` | 自訂 Chrome DevTools 面板 | `devtools.js` | ### hooks #### chrome.runtime - onInstalled - onMessage - onStartup 當瀏覽器啟動時觸發。 - onSuspend 當擴充功能的背景頁面即將被卸載時觸發。 - onUpdateAvailable - onConnect #### chrome.action - onClicked chrome.action 主要提供了 onClicked 事件,當使用者點擊擴充功能的圖示時觸發。 ### chrome.scripting chrome.scripting API 允許開發者動態地將 JavaScript 或 CSS 注入到網頁中,這與在 manifest.json 中定義的 content_scripts 功能相似。主要區別在於,chrome.scripting 允許在程式執行期間根據需要動態注入腳本,提供更大的靈活性: ```js chrome.action.onClicked.addListener((tab) => { chrome.scripting.executeScript({ target: { tabId: tab.id }, files: ['content.js'] }) }) ``` ### popup、content_scripts、side pannel 和 chrome_url_overrides 它們的 origin 是不一樣的嗎?他們能共用 cookie / session storage / local storage 嗎? popup、content_scripts、side panel 和 chrome_url_overrides 的 origin(來源)確實是不一樣的,這會直接影響它們能不能共用像 cookie、localStorage、sessionStorage 這類的瀏覽器儲存。 - Popup / Side Panel / URL Overrides 都是擴充功能自己的網域(`chrome-extension://<extension-id>`),所以彼此之間可以共用 cookie、localStorage、sessionStorage。 - Content Script 是跑在網頁的 context(環境),所以它用的是該網頁自己的 origin。因此: - Cookie、localStorage、sessionStorage 都是屬於該網頁的(像 google.com 就是 google.com 的儲存) - 無法直接共用擴充功能自己的儲存 ### origin 比較 | 元件 | Origin | 能不能共用 Cookie? | localStorage / sessionStorage 共用? | |----------------------|-----------------------------------------|--------------------|------------------------------------| | **Popup(彈出視窗)** | `chrome-extension://<extension-id>` | ✅(擴充功能自己的)| ✅(共用) | | **Side Panel(側邊欄)**| `chrome-extension://<extension-id>` | ✅(擴充功能自己的)| ✅(共用) | | **chrome_url_overrides(像新分頁)** | `chrome-extension://<extension-id>` | ✅(擴充功能自己的)| ✅(共用) | | **Content Script(內容腳本)** | **注入到網頁的 origin**(像 `https://google.com`) | 🌐(看網頁的 cookie) | 🌐(看網頁的儲存) | ### 那要怎麼讓 Content Script 跟擴充功能共用資料? 因為 Content Script 和擴充功能有不同的 origin,所以不能直接共用 localStorage、sessionStorage、cookie。 不過你可以: #### 靠 `chrome.runtime` API 做溝通橋樑 用訊息傳遞(`chrome.runtime.sendMessage()`)來存取擴充功能儲存 ```javascript chrome.runtime.sendMessage({ action: "getStorageData" }, (response) => { console.log(response.data) }) ``` ```javascript chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { if (message.action === "getStorageData") { const data = localStorage.getItem("myData") sendResponse({ data }) } }) ``` #### 或用 `chrome.storage` 這個擴充功能 API ```javascript chrome.storage.local.set({ key: "value" }) chrome.storage.local.get("key", (result) => { console.log(result.key); // "value" }) ``` > chrome.storage 是即便在 content_scripts 裡都可以使用的。但有一個小小的前提是,擴充功能須要在 manifest.json 中宣告權限(permissions)。 ### chrome.storage 的兩種儲存區 - chrome.storage.local:只儲存在本機,速度快、用量大(最大約 10MB,但可在選項中要求無限儲存)。 - chrome.storage.sync:同步到 Google 帳號,在多台裝置間共用,但容量較小(最大約 100KB,並有限制每小時寫入次數)。 ### chrome.storage 支援監聽 ```js chrome.storage.onChanged.addListener((changes, areaName) => { if (areaName === "local" && changes.theme) { console.log("Theme changed:", changes.theme.newValue) } }) ``` ### 測試 html 注入 ```js const span = document.createElement("span") span.textContent = "here we go" span.style.cssText = "color: red; font-size: 20px; font-weight: bold; margin: 10px;" document.body.appendChild(span) ``` --- ## 用 vite 組織程式碼 ### 初始化專案 ```bash npm create vite@latest . -- --template vue npm install ``` ### 安裝依賴 ```bash npm install --save jquery # content scripts 可以用到 npm install --save-dev @types/jquery npm install --save tailwindcss @tailwindcss/vite # 記得在 assets/css/style.css 先 import npm install --save-dev @crxjs/vite-plugin@beta ``` ### 設定範例 #### vite.config.js ```js import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' import { fileURLToPath, URL } from "url" import tailwindcss from '@tailwindcss/vite' import { crx } from '@crxjs/vite-plugin' import manifest from './manifest.json' // https://vite.dev/config/ export default defineConfig({ plugins: [ vue(), tailwindcss(), crx({ manifest }), ], resolve: { alias: [ { find: '@', replacement: fileURLToPath(new URL('./src', import.meta.url)) }, ] }, build: { rollupOptions: { input: { popup: "popup.html", options: "options.html", }, }, } }) ``` #### tsconfig.json ```json { "compilerOptions": { "baseUrl": "./", "paths": { "@/*": ["src/*"] }, "esModuleInterop": true, "lib": ["ES2020", "DOM"], "target": "ES2020", "module": "ESNext", "moduleResolution": "Node", "strict": true, "jsx": "preserve", "sourceMap": true, "resolveJsonModule": true, "isolatedModules": true, "skipLibCheck": true }, "include": [ "src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue", "content.ts", "service-worker-loader.ts" ], "exclude": ["node_modules"] } ``` ### 目錄結構 ```txt . ├── README.md ├── content.ts ├── dist │ ├── assets │ │ ├── content.ts-DoTJBfw_.js │ │ ├── content.ts-loader-CD2_Y7Py.js │ │ └── service-worker-loader.ts-l0sNRNKZ.js │ ├── icons │ │ ├── extension_lab_icon_128x128.png │ │ ├── extension_lab_icon_16x16.png │ │ ├── extension_lab_icon_32x32.png │ │ └── extension_lab_icon_48x48.png │ ├── manifest.json │ ├── options.html │ ├── popup.html │ └── service-worker-loader.js ├── manifest.json ├── options.html ├── package-lock.json ├── package.json ├── popup.html ├── public │ └── icons │ ├── extension_lab_icon_128x128.png │ ├── extension_lab_icon_16x16.png │ ├── extension_lab_icon_32x32.png │ └── extension_lab_icon_48x48.png ├── service-worker-loader.ts ├── src │ ├── assets │ │ ├── main.ts │ │ └── style.css │ ├── components │ │ └── content.vue │ └── pages │ ├── options.vue │ └── popup.vue ├── tsconfig.json └── vite.config.js ```