🔑 關鍵洞察
✦ AI·GEN從「Monaco 只能放連結、不能直接放圖」這個小需求出發,一路牽出圖片處理的整條坑:Base64 為何會讓編輯器卡頓又撐爆資料庫、貼上就自動上傳到 NAS 的設計,以及最容易忽略的一顆地雷——Docker bind mount 其實是一道「傳送門」,只要 HDD 沒掛載成功,檔案就會靜默寫回系統碟。文章也記錄了把靜態圖片牆重寫成 NAS 動態 API 的規劃,以及照片牆改版時「UI 先行、資料後補」的取捨。
最近我開始用 Monaco Editor 當作部落格的後台編輯器。這是 VS Code 的核心、一個為寫程式而生的編輯器,卻被我拿來寫部落格,想想有點硬核。但這篇的主角不是 Monaco 本身,而是「在它裡面塞一張圖」這件看似無聊的小事——結果從一個簡單需求,一路牽出 Base64、背景上傳、NAS 掛載、到圖片牆重寫的一整條坑。
Monaco 不吃圖,只吃連結#
Monaco 和一般所見即所得的編輯器完全是兩個世界。它不像 Word 或 編輯器,拖張圖進去就即時顯示;在它的世界裡,圖片只是一串 Markdown 連結。也就是說,想插入圖片,得先讓圖片變成一個「可用的網址」。當下能想到的路只有兩條:把圖轉成 直接內嵌,或上傳到某處生成 URL。
第一個直覺是 Base64。圖片直接變成一長串字元嵌進 Markdown,文字和圖片就綁死在一起,文章搬來搬去也不會掉圖,聽起來很方便:
但實測就是一個深坑。一張普通圖片轉成 Base64 後,字串會膨脹到數十萬個字元,而 Monaco 一碰到這種超長的單行文本,渲染就卡到懷疑人生;更別說把這麼巨大的文本直接塞進資料庫,查詢效能會直線下滑、容量還暴增。這條路顯然行不通。
於是回到業界標準:上傳到伺服器生成 URL,再用 Markdown 引用。文章文本保持輕量,資料庫只存文字,圖片交給檔案系統、NAS 或 CDN 提供,載入快又好加快取:
TIP
Base64 是「最誘人的一行解」——文圖綁死、零依賴、複製即用。但它把成本藏在你看不到的地方:編輯器的渲染效能、資料庫的體積與查詢。很多「簡單」的方案,只是把帳單延後、記在別的地方而已。
讓「上傳」這件事,在體驗裡消失#
方向定了,但手動上傳圖片、複製連結、再貼回編輯器,這流程太破壞寫作的連貫性。理想狀況是:無論貼上還是拖曳,圖片都能自動完成上傳、生成連結、插進游標處,整個過程「隱形」。邏輯其實很直觀,拆成四步:
- 監聽事件:掛上 Monaco 的 。
- 攔截圖片:檢查剪貼簿或拖曳的內容裡有沒有 ——是純文字就放行,是圖片就攔截預設行為。
- 背景上傳:把這張圖丟給後端
POST /api/upload,後端寫檔後回傳 URL。 - 插回游標:前端拿到 URL,用 在游標當下的位置插入
。
寫成程式大概長這樣(這是當時的設計雛形):
// 貼上時攔截圖片,背景上傳,拿到 URL 後插入游標處
editor.onDidPaste(async () => {
const file = getImageFromClipboard(); // 剪貼簿裡有 File Object 嗎?
if (!file) return; // 純文字就放行
const url = await uploadToNAS(file); // POST /api/upload → 回傳圖片 URL
editor.executeEdits('paste-image', [{ // 在游標處插入 markdown
range: editor.getSelection(),
text: ``,
}]);
});這條上傳線後來是真的做起來了:後端收到圖片會自動壓縮並轉成 WebP,存進 NAS,吐回一個形如 /uploads/2026/02/<時間戳>-<亂數>.webp 的網址——日期分資料夾、時間戳命名。從此貼一張截圖,幾乎是瞬間插入,寫作流完全不斷。
但檔案不會偷偷留在系統碟嗎?#
上傳有了,但這裡我卡了一下,冒出一個很自然的疑問:我的 server 開在系統碟上,就算最後要存進 NAS,圖片經過 Node.js 後端時,不還是會在 web 端(系統槽 SSD)留一份嗎?
答案是不會,而關鍵在於 。我在 NAS 硬碟上開一個專用目錄,然後把它掛進容器:
services:
blog-backend:
volumes:
# 左邊是真實的 HDD 路徑,右邊是容器內的路徑
- /mnt/hdd16tb_01/blog_data/images:/app/public/uploads這樣一來,Node.js 只管把圖存到 /app/public/uploads,檔案就會穿過這道映射、直接落在 NAS 的磁區上。本質上是一道「傳送門」:寫進 /mnt/hdd16tb_01 底下的東西,一個 KB 都不會消耗到系統碟——系統槽在整個過程裡,只是個「路過的通道」。(相對地,如果讓 Docker 自己建 named volume,檔案反而會躺在 /var/lib/docker/volumes/ 底下,那才會吃系統碟。)
但這道傳送門有唯一一個會出包的情況:如果那顆 HDD 其實沒有成功掛載上去,Linux 會把 /mnt/hdd16tb_01 當成一個普通的空資料夾,這時寫進去的檔案就會靜默地落回系統碟——沒有報錯,你甚至不會發現,直到某天系統碟莫名其妙被塞滿。所以每次都得先用 df -h 確認掛載狀態:
檔案系統 容量 已用 可用 已用% 掛載點
/dev/nvme0n1p2 1.9T 255G 1.5T 15% /
/dev/sdb1 15T 28K 15T 1% /mnt/hdd16tb_02
/dev/sda1 15T 22G 15T 1% /mnt/hdd16tb_01看到 /mnt/hdd16tb_01、/mnt/hdd16tb_02 對應的是那兩顆 15T 的 HDD(而不是併回 /),我才敢放心把圖往裡面寫。最後再讓 nginx 把 /uploads/ 開頭的請求直接對應到硬碟上的圖片(這是當時的規劃):
location /uploads/ {
alias /mnt/hdd16tb_01/blog_data/images/;
expires 30d;
add_header Cache-Control "public, max-age=2592000";
}WARNING
「檔案存進 NAS」這件事最陰險的失敗,不是報錯,而是沒報錯。掛載掉了、路徑打錯,系統照樣把檔案乖乖寫進系統碟,一切看起來都正常,直到系統碟爆掉。凡是靠掛載點的儲存,df -h 確認掛載,值得變成一個肌肉記憶。
手機拍的照片,怎麼引用進文章?#
上傳截圖解決了,但還有一個情境:我人在外面用手機拍照,傳進 NAS 之後,要怎麼在文章裡引用?這時我在兩個做法之間權衡:
做法一,nginx 靜態代理。 讓 NAS 上某個相片資料夾透過 nginx 暴露成公開網址,任何丟進去的圖片都能用 https://koimsurai.com/nas-images/檔名.jpg 存取。優點是幾乎零開發、照片一傳進 NAS 就能引用;缺點也很明顯:得自己記住或去 NAS 裡複製檔名,圖片一多,找圖就很痛,寫作體驗會斷。
做法二,圖庫選擇器(當時想做的升級版)。 由 NAS 提供一支 GET /api/photos,回傳某資料夾下所有圖片的檔名與網址 JSON;然後在 Monaco 編輯區上方放一個「從 NAS 插入圖片」按鈕,點擊後彈出一個縮圖網格的 Modal,點一張就自動把  插進游標處。不用再手動複製網址,而是用視覺化的方式選圖。
工具很簡單、體驗差很多。做法一適合當過渡,做法二才是我心裡的目標形態——不過在那個當下,它還停在「規劃」這一格。
既有的圖片牆,要不要打掉重練?#
順著這條線,我看向自己那面既有的圖片牆。它當時是很典型的「靜態構建」:前端透過 manifestLoader.ts 去讀一個寫死的 /photos-manifest.json,裡面列好每張圖的路徑、縮圖、以及 佔位資料;萬一連 manifest 都找不到,還會降級用 Vite 的 import.meta.glob 去硬讀寫死在前端專案 assets/Portfolio/ 裡的圖。我也寫了一支 image-processor.ts,會自動壓縮、轉檔、算出給 Blurhash 用的 thumbHash,但輸出路徑寫死成 /generated/ 這種網頁靜態資料夾。
最大的痛點是:新增一張照片的流程有夠繁瑣。 每加一張圖,我都得把檔案放進專案的靜態目錄、跑一次腳本更新 photos-manifest.json,然後重新部署整個網站。哪怕只是多一張貓的照片。攝影作品本身又很吃空間,擠在 1.9T 的系統碟上也不是辦法。
於是我畫了一套重寫的設計(同樣是當時的規劃):
- 把
image-processor.ts轉成 NAS 背景服務——照片一上傳到 NAS,背景就自動生成高畫質圖、縮圖、算thumbHash,只是輸出路徑改成 nginx 代理的網址。 - 建一支
GET /api/gallery/photos——回傳跟原本PhotosManifestData一模一樣結構的 JSON 陣列。 - 前端無痛切換——
PhotoGallery.tsx只要改一行資料源:
const response = await fetch('/photos-manifest.json');
const response = await fetch('https://koimsurai.com/api/gallery/photos');因為回傳的資料結構完全相同,前端那些 、Blurhash 佔位、hover 互動的組件一行都不用改——只是把資料源從「Web 專案本地」搬到「NAS 動態 API」。改造完,新增照片再也不用重新部署,把圖丟進 NAS 就會自己出現。
UI 先行,資料後補#
照片牆的視覺我倒是先重做了。舊版卡片間距太大、又壓了一張「攝影作品集錦」的大標題卡,看起來更像個「資料夾管理介面」,而不是展示視覺的藝廊。參考 Innei 的 Afilmory,核心是讓照片成為唯一的主角:捨棄大方塊、改用等高自適應網格,標題置中收斂,hover 時把旁邊的照片淡下去、只讓當前這張浮起來。改完像這樣:
然後,美美的牆做完,我才想到一件事:我的照片根本沒有上 tag。 上面那排「全部 / 貓咪 / 風景」的篩選鈕,壓根篩不了東西。
一條線,拉出一整面牆#
回頭看,這整趟從「Monaco 只吃連結」出發,一路拉出 Base64 的陷阱、傳送門與 df -h、再到圖片牆的重寫。把已經跑起來的那條上傳線畫成圖,大概是這樣:
每一塊技術細節,單獨看都很孤立:Monaco 的事件、Base64 的編碼、一行 bind mount、一支 df -h。但它們其實都在為同一件事服務——讓「放一張圖」這個動作,對寫作的人來說變得自然、甚至無感。而這趟最好玩的,反而是那個最樸素的教訓:很多看起來玄的問題(檔案為什麼跑到系統碟?),答案往往就藏在「先搞清楚它實際上到底怎麼運作」的下一步。掛載點是一道傳送門,只要你記得先確認它真的接通了。
還沒有留言
✨ 成為第一個留言的人吧