我如何開發出 Chrome 擴展

這是一個關於如何從一個簡單的想法出發,最終開發出一個日均千次圖片下載點擊的 Chrome 擴展的故事。在這個過程中,我遇到了許多技術挑戰,也學到了很多關於產品開發和用戶體驗的寶貴經驗。

一切都始於一個微小的煩惱

你有沒有過這樣的經歷:在瀏覽 Instagram 或 Pinterest 時看到一張美麗的圖片,想要保存下來,卻發現需要右鍵、選擇「另存圖片」、等待彈窗、選擇路徑… 整個過程繁瑣到讓人失去耐心?

這正是我去年在整理設計靈感時遇到的問題。作為一個開發者,我的第一反應是:「一定有更好的方法。」

當時我正在為一個設計項目收集素材,需要從各個網站下載大量圖片。每次右鍵保存的操作讓我倍感煩躁,特別是當我發現下載的圖片解析度經常不是最高的版本時。

「為什麼不能像手機 App 那樣,長按就能保存圖片呢?」我想著。

從想法到第一行代碼

週末的下午,我決定動手解決這個問題。最初的想法很簡單:做一個 Chrome 擴展,讓用戶可以通過懸停和點擊來快速保存圖片。

我把這個想法分享給了我的朋友 Felix Falkenberg(@ffalkenberg),一位 ML Engineer 和 DevOps 專家。作為數字游牧者的他對效率工具有著敏銳的嗅覺,立刻就認識到了這個想法的潛力。

「這確實是個痛點,」Felix 說,「我們可以一起做這個項目,正好我們都沒有上架過 Chrome 擴展的經驗,這算是個不錯的學習練習。」

雖然我有軟體架構的經驗,但 Chrome 擴展開發對我們來說都是全新的領域。

但當我們開始深入研究時,才發現這個「簡單」的想法背後藏著許多複雜的技術挑戰。

第一個挑戰:如何檢測滑鼠懸停

1
2
3
4
5
6
// 我的第一次嘗試 - 天真的方法
document.addEventListener('mouseover', function(e) {
if (e.target.tagName === 'IMG') {
showDownloadButton(e.target);
}
});

看起來簡單對吧?但實際測試時問題接踵而來:

  • 按鈕閃爍不停
  • 性能問題導致頁面卡頓
  • 有些圖片檢測不到
  • 按鈕位置不正確

這讓我意識到,做好一個看似簡單的功能需要考慮的細節遠比想像中複雜。

深入兔子洞:事件優化

經過幾天的研究和測試,我學到了事件委派和防抖的重要性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 優化後的版本
let hoverTimeout;
let currentImage = null;

document.addEventListener('mouseover', function(e) {
if (e.target.tagName === 'IMG' && e.target !== currentImage) {
clearTimeout(hoverTimeout);
hoverTimeout = setTimeout(() => {
showDownloadButton(e.target);
currentImage = e.target;
}, 200); // 200ms 延遲避免閃爍
}
});

document.addEventListener('mouseout', function(e) {
if (e.target.tagName === 'IMG') {
clearTimeout(hoverTimeout);
setTimeout(() => {
hideDownloadButton();
}, 300); // 給用戶時間移動到按鈕上
}
});

最大的挑戰:找到最高解析度的圖片

當基本功能運作後,我們發現了真正的技術挑戰:很多網站顯示的是縮圖,但用戶想要的是原始大小的圖片。這個看似簡單的需求,實際上是整個項目最複雜的部分。

Google Images 的謎題

Google Images 是我們遇到的最大挑戰。表面上看到的圖片元素,其 src 屬性指向的只是一個低解析度的預覽圖,而真正的高解析度圖片 URL 被深深埋藏在複雜的 DOM 結構和 data 屬性中。

我花了整整一週時間研究 Google Images 的頁面結構,像偵探一樣追蹤每個可能的線索:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// Google Images 高解析度檢測 - 經過無數次試錯後的最終方案
function getGoogleImagesHighRes(img) {
// 方法 1: 檢查各種可能的 data 屬性
const dataUrl = img.getAttribute('data-src') ||
img.getAttribute('data-original-src') ||
img.getAttribute('data-iurl');

// 方法 2: 爬取父元素的 data-ri 屬性(Google的元數據)
const parent = img.closest('[data-ri]');
if (parent) {
try {
const metadata = JSON.parse(parent.getAttribute('data-ri'));
// 'ou' 是 original URL 的縮寫
if (metadata.ou) return metadata.ou;
} catch (e) {
// JSON 解析失敗,繼續嘗試其他方法
}
}

// 方法 3: 分析 srcset 找最高解析度
if (img.srcset) {
const srcsetUrls = parseSrcset(img.srcset);
return srcsetUrls[srcsetUrls.length - 1].url;
}

// 方法 4: 嘗試從 URL 參數中獲取原始圖片
if (img.src.includes('googleusercontent.com')) {
return img.src.replace(/=s\d+/, ''); // 移除尺寸限制參數
}

return img.src; // 最後的回退方案
}

每個網站都是一個新謎題

接下來我們發現,每個主流網站都有自己的圖片存儲邏輯:

  • Instagram:高解析度圖片藏在 data-* 屬性中,但屬性名會變化
  • Twitter:需要修改 URL 參數來獲取大圖版本(:large vs :small
  • Pinterest:原始圖片 URL 隱藏在複雜的 JSON 結構中
  • Facebook:多層嵌套的圖片版本,需要遞歸查找

我們需要為每個網站開發專門的解析邏輯:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
class ImageResolver {
constructor() {
this.siteHandlers = {
'instagram.com': this.handleInstagram,
'twitter.com': this.handleTwitter,
'x.com': this.handleTwitter, // Twitter 改名後的域名
'images.google.com': this.handleGoogleImages,
'pinterest.com': this.handlePinterest,
'facebook.com': this.handleFacebook
};
}

async resolveHighResUrl(img) {
const hostname = window.location.hostname;
const handler = this.siteHandlers[hostname];

if (handler) {
try {
const result = await handler(img);
if (result && result !== img.src) return result;
} catch (error) {
console.warn(`Handler failed for ${hostname}:`, error);
}
}

// 通用回退策略:嘗試常見的高解析度模式
return this.genericResolve(img);
}

handleTwitter(img) {
if (img.src.includes('pbs.twimg.com')) {
// 將 :small 或其他尺寸替換為 :large
return img.src.replace(/:(small|medium|thumb)/, ':large');
}
return img.src;
}
}

意想不到的用戶體驗挑戰

技術問題解決後,我們開始面對各種意想不到的用戶體驗問題。

可拖曳按鈕的定位難題

最初我們把下載按鈕固定在圖片的右上角,但很快發現問題:

  • 在小圖片上按鈕過於突兀
  • 有時會遮擋重要內容(如水印、文字)
  • 不同網站的 CSS 會影響按鈕顯示

我們決定讓按鈕可拖曳,但這帶來了新的技術挑戰:如何確保按鈕始終保持在圖片邊界內?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 限制按鈕在圖片邊界內的拖曳邏輯
function constrainButtonPosition(button, image, newX, newY) {
const imgRect = image.getBoundingClientRect();
const btnRect = button.getBoundingClientRect();

// 計算有效拖曳範圍
const minX = 0;
const minY = 0;
const maxX = imgRect.width - btnRect.width;
const maxY = imgRect.height - btnRect.height;

// 限制在邊界內
const constrainedX = Math.max(minX, Math.min(maxX, newX));
const constrainedY = Math.max(minY, Math.min(maxY, newY));

return { x: constrainedX, y: constrainedY };
}

裝飾性圖片的過濾問題

測試過程中我們發現了一個意想不到的問題:很多網站都有用來裝飾的小圖片,比如:

  • 1x1 像素的追蹤圖片
  • 用於背景裝飾的重複圖案
  • 載入中的佔位符圖片
  • CSS 背景圖片

這些圖片上出現下載按鈕既沒用又影響體驗。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// 智能圖片過濾系統
function shouldShowDownloadButton(img) {
// 過濾太小的圖片(可能是裝飾性的)
if (img.width < 50 || img.height < 50) return false;

// 過濾明顯的追蹤圖片
if (img.width === 1 && img.height === 1) return false;

// 過濾常見的佔位符和圖標
const src = img.src.toLowerCase();
const suspiciousPatterns = [
'placeholder', 'loading', 'spinner', 'icon-',
'logo-small', 'avatar-default', '1x1', 'pixel'
];

if (suspiciousPatterns.some(pattern => src.includes(pattern))) {
return false;
}

// 檢查是否為 CSS 背景圖片
const computedStyle = window.getComputedStyle(img);
if (computedStyle.display === 'none' ||
computedStyle.visibility === 'hidden') {
return false;
}

// 檢查圖片的長寬比是否異常(可能是分隔線等裝飾元素)
const aspectRatio = img.width / img.height;
if (aspectRatio > 20 || aspectRatio < 0.05) return false;

return true;
}

視覺反饋的重要性

我添加了細緻的動畫和狀態指示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
.download-button {
opacity: 0;
transform: scale(0.8);
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
}

.download-button.show {
opacity: 1;
transform: scale(1);
}

.download-button:hover {
transform: scale(1.1);
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
}

這些微小的細節讓整個體驗變得流暢和愉悅。

第一個用戶的反饋改變了一切

當我們把第一個版本分享給朋友時,收到的反饋讓我們重新思考產品定位。

「這個很棒!但是我經常需要下載整個頁面的所有圖片,能不能添加批量下載功能?」

這個建議打開了我們的思路。Felix 和我意識到,這不僅僅是一個單張圖片下載工具,而是一個完整的圖片管理解決方案。

「批量下載涉及複雜的並發控制和錯誤處理,」Felix 提醒我,「我們需要仔細設計這個功能的架構。」

批量下載的技術挑戰

實現批量下載看起來簡單,但實際上涉及複雜的異步處理和錯誤處理。我們需要考慮:

  • 避免同時發起太多下載請求(會被瀏覽器限制)
  • 處理部分下載失敗的情況
  • 給用戶提供進度反饋
  • 避免下載重複圖片
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class BatchDownloader {
async downloadAll(images) {
const results = [];
const concurrency = 3; // 限制並發數量

for (let i = 0; i < images.length; i += concurrency) {
const batch = images.slice(i, i + concurrency);
const promises = batch.map(img => this.downloadSingle(img));

try {
const batchResults = await Promise.allSettled(promises);
results.push(...batchResults);

// 更新進度
this.updateProgress(i + batch.length, images.length);

// 避免過於頻繁的請求
await this.delay(500);
} catch (error) {
console.error('Batch download error:', error);
}
}

return results;
}
}

意外的成功

上線一個月後,我們收到了第一封用戶郵件:

「謝謝你們開發了這個擴展!我是一名設計師,每天需要收集大量的視覺素材。你們的工具為我節省了大量時間。能不能添加一個 AI 放大功能?」

這讓我們意識到,我們無意中解決了一個比想像中更普遍的問題。Felix 開玩笑說:「看來我們不小心做對了什麼。」

數據告訴我的故事

  • 第一週:47 個用戶
  • 第一個月:1,247 個用戶
  • 用戶類型:60% 設計師/創作者,25% 學生,15% 其他專業人士
  • 最常用功能:單張下載 (78%),批量下載 (45%),格式轉換 (23%)

技術債務與重構

隨著功能增加,代碼開始變得複雜和難以維護。作為有軟體架構經驗的我,決定進行一次大規模的重構:

1
2
3
4
5
6
7
8
9
// 重構前:一個巨大的文件,500+ 行
// 重構後:模塊化架構
const ClickSave = {
core: new CoreModule(),
ui: new UIModule(),
downloader: new DownloadModule(),
settings: new SettingsModule(),
analytics: new AnalyticsModule()
};

這次重構教會了我們:

  • 單一職責原則的重要性
  • 早期投資於良好架構的價值
  • 測試驅動開發的必要性
  • 團隊合作中代碼審查的價值

學到的最重要的教訓

1. 用戶體驗勝過技術複雜性

最初我過度關注技術實現的優雅性,但用戶只關心功能是否好用。一個簡單直觀的界面比複雜的技術架構更重要。

2. 持續的用戶反饋是產品成功的關鍵

每個版本發布後,我們都會認真閱讀每一條用戶反饋。這些反饋幫助我們了解真正的用戶需求,而不是我們假想的需求。我們建立了一套簡單的系統來追蹤和分析用戶反饋,這對產品迭代至關重要。

3. 性能優化永遠不能忽視

在不同的網站和設備上測試讓我深刻理解了性能優化的重要性。一個功能再強大,如果影響頁面性能,用戶也會毫不猶豫地卸載。

4. 隱私和安全是基本要求

在設計之初就考慮隱私和安全,而不是事後添加。用戶的信任是最寶貴的資產。

5. 合作學習的價值

這個項目證明了合作學習的價值。雖然我們都沒有 Chrome 擴展開發經驗,但通過互相討論和代碼審查,我們學習得比單獨研究要快得多。Felix 從 ML 和 DevOps 的角度提出問題,我從軟體架構的角度思考解決方案,這種互補讓我們避免了很多彎路。

下一步的計劃

現在 ClickSave 已經有了穩定的用戶基礎,我們正在思考下一步的發展方向:

  1. AI 集成:基於用戶請求,Felix 正在探索集成圖片 AI 處理功能
  2. 跨平台支持:考慮開發 Firefox 和 Safari 版本
  3. 雲端同步:讓用戶的設定和下載歷史可以跨設備同步

寫在最後

這個項目教會了我們,好的產品不僅僅是好的代碼,更是對用戶真正需求的理解和滿足。從一個個人的小煩惱出發,最終創造出每天處理數千次圖片下載的工具,這個過程充滿了挑戰,但也帶來了巨大的滿足感。

與 Felix 的合作讓我深刻體會到,兩個人一起學習新技術比單打獨鬥要有效得多。這個 Chrome 擴展開發的小實驗,不僅讓我們學會了擴展開發,更重要的是體驗了從零開始構建產品的完整過程。

如果你也有類似的想法但還沒有開始行動,我們的建議是:不要想太多,先動手做一個最簡單的版本。找一個能力互補的夥伴,用戶的反饋會告訴你們接下來該往哪個方向走。

技術會變化,工具會更新,但解決真實問題的初心和團隊合作的價值不會改變。這就是我們從 ClickSave 這個項目中學到的最重要的一課。


項目相關連結

如果這篇文章對你有幫助,或者你也在開發類似的工具,歡迎在評論區分享你的經驗和想法。