---
# System prepended metadata

title: Chrome Extension 初探

---

## 開始動手

### 最簡單的 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
```
