如何在純前端的網站上安全做出觀看人數
在網站初期,就像我第一篇文章講的,我暫時不想把整體架構的層級搞得太複雜。搞架構這種事很容易搞一搞就卡住,最後連文章都寫不下去。但即使維持在這麼精簡的狀態,有些功能我還是想試著做出來,因為它們對我來說確實重要,觀看人數就是其中一個。所以這篇想聊的是:在一個純前端的網站上,我們要怎麼把觀看人數做出來,而且是安全地做出來。以下是我目前的想法跟實作。
先決定用什麼抓流量
如果不打算自己從頭去計算來訪者的足跡跟相關的東西,現階段最普遍、也最好用的工具,大概就是 Google Analytics,也就是大家熟知的 GA。做法是在網站裡埋入一段 Google 提供的代碼(現在通常就是所謂的 gtag,Google tag);當有人來訪,對應的流量就會被送到 Google,讓它知道有多少人、到了哪個頁面。
對我們這種寫部落格的人來說,這些資料能回答很實際的問題:什麼題目受歡迎、大家喜歡看什麼。我自己覺得,對大部分有在創作的人來說,了解你的讀者、了解你的觀眾,都是一件重要的事。既然有現成又好用的工具,我自然會偏向直接用它,所以在抓流量這件事情上,第一步我就選 GA。
前端不能直接抓,但我們可以退一步想
GA 本身有開放 API,讓我們能把資料打回來。問題是,任何放進瀏覽器的憑證,等於是公開的。而流量是相對隱私的資料,你不會想把這種東西直接暴露在外面,讓任何人都能拿去查。
這時候與其硬要即時,不如先退一步,問自己兩個問題:這個資訊,我能容忍它有延遲嗎?還有,我做這個功能,主要的目的到底是什麼?
對我來說目的有兩個。第一,讓來看的讀者知道哪些文章大家比較常看,他可以先從那幾篇開始;第二,讓他大概知道這個網站來過多少人,心裡對這個站的可信度有個底。這兩個目的,都不需要數字即時跳動。所以我的結論是:這個資訊是可以延遲的,即使讀者看到的人數慢了一天,我也不覺得那是需要優先解決的事。
既然可以延遲,那就週期性更新
一旦接受了「可以延遲」,事情就簡單很多。我們不需要在每次有人打開頁面時,都即時去問一次 GA;我們只要定時去更新一份資料就好。
而在「定時更新」這個前提下,呼叫 GA、把資料拉回來的這一段,就完全不需要放在網頁裡面跑。我們可以讓它在背景定時執行,把結果寫成一份靜態資料。這份資料裡面只有算好的數字,沒有任何憑證或秘密,所以就算它是公開的,也沒有洩露的問題。這也就成了我最後採用的方案。
這個站跑在 Cloudflare Pages 上
這個網站本身是用 Cloudflare Pages 架的。如果你對 Cloudflare Pages 有興趣,可以去翻它的官方文件,或是之後有機會我會寫一篇,我架了很多小工具在上面;基本上只要你的網域是在 Cloudflare 買的,就會附帶相當多的免費功能可以用。像 Cloudflare Pages,我覺得拿來架一些公開的網站就非常合適。
這裡先講一個貫穿整篇的原則:這個功能我用到的每一個零件,都是刻意挑「免費額度就跑得動」的。因為它終究只是一個部落格的小功能,我不希望它變成每個月要付錢養的東西。後面提到每個服務時,我都會順帶講一下它免費方案的限制,以及我的用量離那條線有多遠。
打通 GA4 的 Data API
自動化流程沒辦法走互動式的 OAuth 登入,而 GA 讀取需要的 scope 又屬於 Google 認定的敏感範圍,用共用的 client 會被擋。所以正解是建一個 service account,拿它的金鑰去打 API。
這裡有一個容易漏掉的點:在 GCP 建好 service account,不等於它就能讀你的 GA。GA 的存取權限跟 GCP 的 IAM 是兩套系統,你還要回到 GA 後台的 Property Access Management,把那個 service account 的 email 加成 Viewer。少了這一步,API 會回 PERMISSION_DENIED,而且訊息不會直接告訴你是漏加了這個。

另外,Data API 用的是 property 的數字 ID(在 GA 後台 Admin → Property Settings 可以看到),不是 G- 開頭那個 Measurement ID。兩個是不同的東西。
有了憑證,就能拉 Report
有了憑證,剩下的就是呼叫 runReport。我們拉兩份報表:一份帶 pagePath 維度,對應每一頁的瀏覽數;一份不帶維度,拿全站的總量。
const [byPage] = await client.runReport({
property: `properties/YOUR_PROPERTY_ID`,
dateRanges: [{ startDate: '2020-01-01', endDate: 'today' }],
dimensions: [{ name: 'pagePath' }],
metrics: [{ name: 'screenPageViews' }],
});
(實際在 Worker 裡,是用 REST 直接打同一個 runReport 端點,請求內容跟上面一樣,只是沒有透過這個 Node 版的 client。)
GA 回來的 pagePath 是不含網域、不含查詢字串的路徑,把它正規化成站上實際的網址格式(補上結尾斜線之類),就能跟每篇文章對上。整理完寫成一份 stats.json,大致長這樣:
{
"generatedAt": "2026-07-19T...Z",
"site": 65,
"posts": { "/posts/some-post/": 42 }
}
怎麼定時更新,又不把資料弄丟
定時更新最直覺的做法,是排一個每天固定時間跑的工作。這裡有兩件事要顧。一是時間點:把它排在流量來源時區的凌晨,那時前一天的資料通常已經處理完了。二是資料的安全性,萬一某次更新失敗,不能讓它把原本好好的資料蓋掉;失敗的時候,寧可維持上一份舊資料,也不要生出一份空的、全部歸零的檔案。
至於這份資料要放在哪、由誰去更新,一開始我想過最省事的路:讓每天那個工作,直接把算好的檔案 commit 進版本庫就好。但我很快就否決了這個做法:每天一筆 commit,會把整個 blog 的 commit 歷史洗得很亂;而這份東西說到底只是一份 statistic 資料,而且完全可以隨時從 GA 重新拉回來,沒必要為它天天在版控裡留一筆紀錄。版本庫我想留給「內容」,不是留給這種會自己長的數字。
所以我改成把資料放在一個固定的路徑上,由背景工作定時更新那個路徑,網站單純去讀它,全程不碰 git。在 Cloudflare 上,這件事剛好有現成的兩個零件:一個帶 Cron Trigger 的 Worker(一段每天定時醒來的程式,去抓 GA、算好數字),還有 KV 這個存放的地方。
什麼是 KV
KV(Key-Value)是 Cloudflare 提供的一個很單純的鍵值儲存:你給它一個 key、存一包資料,之後用同一個 key 就能把它讀回來,讀取快、而且分佈在全球節點上。它不適合拿來做複雜的關聯查詢,但拿來放「一份算好的 JSON、給大家讀」這種用途,剛剛好。
在這個功能裡,Worker 每天把算好的數字用一個固定的 key 寫進 KV,然後對外開一條路徑 /stats.json,有人來讀就把 KV 裡那包吐回去。前端要的只是「去 /stats.json 拿最新一份數字」,至於它是什麼時候、被誰更新的,前端不需要知道。
而這也正好接回前面說的免費原則。Cloudflare 的 Worker 免費方案,每天有十萬次請求、Cron Trigger 也能用;KV 免費方案是每天十萬次讀取、一千次寫入、一 GB 儲存。我這個功能每天只寫一次(就是那個定時工作),只有在有人開頁面時才讀一次,用量離上限還很遠。加上 GA 的 Data API 本身免費、Cloudflare Pages 也免費,這個觀看數功能的長期成本,實際上是零。
權限設置:這種架構最常卡住的地方
做這種「串很多服務」的功能,會卡住的地方,往往不是程式邏輯,通常反而是每一層各自的權限邊界。它們是彼此獨立的系統,只要有一層沒開通,整條就不會通,而且錯誤訊息常常不會直接告訴你是漏了哪一層。光是這個功能就踩到好幾個:
- GA 的存取,跟 GCP 的 IAM 是兩套。 前面提過,在 GCP 建好 service account,不等於它就能讀你的 GA,你得另外回 GA 後台把它加成 Viewer。
- 金鑰要當成秘密來存,不能進版控。 service account 的金鑰是一把能讀你資料的鑰匙,它只能放在部署環境的 secret 裡(例如 Worker 的 secret),絕對不該出現在 repo 或前端。
- 連部署工具權限細分。 我在建立 KV 的時候就被擋了一次:手上那把 Cloudflare 的 token 有部署 Worker 的權限,卻沒有建立 KV 的權限,回了一個看起來很模糊的 authentication error,補上對應的授權才過。
這些東西單獨看都很小,但沒一個個確認過去,功能就是不會動。
在做類似的服務串聯時,想清楚自己需要的權限會是架構的第一層,值得優先討論後再動手,萬一出錯了,也更能掌握具體段落。
能拉回來,不代表要全部顯示
還有一個是關於「顯示什麼」的決定。GA 能給的資料其實不少,但我最在意、也覺得對讀者最有用的,就是觀看次數這一個;有了它,對我來說要鋪的資訊就夠了。其他的即使 GA 都能拉回來,我也不打算全部攤在畫面上。
這是一個技術上的取捨:當你決定要去拉一份 API 資料,你並不需要、也不應該把它能給的東西全部倒出來。你要拿多少、顯示多少,本身就是設計的一部分。這件事不只適用在這個功能,任何一次程式或架構的設計,都要先想清楚。
這整套,其實是跟 AI 一起做出來的
講到這裡,順帶談一件跟這篇「做法」有關的事:這一整套,從用 gcloud 把 GCP 那側(service account、開 API)弄好、寫抓資料的程式、架 Worker、到部署上線,其實都是我跟 AI coding agent(Claude Code)一起做出來的。
而我在這段協作裡的角色,其實就是這篇文章前面那些東西:做決定。要不要用 GA、資料能不能延遲、要不要 commit、哪些數字該露出來、每一層權限要開什麼。這些「做什麼、不做什麼、為什麼」是我的;至於「怎麼做」,把想法變成能跑的程式、把命令一個個下下去、撞牆了回報,那部分交給 agent,速度快很多。
我覺得這才是現在 AI 對這種「串很多服務」的工作真正的改變。以前要把 GA、gcloud、Cloudflare 這幾套兜在一起,光讀文件、試錯就會耗掉大半天;現在它變成一段對話,我負責判斷跟拍板,agent 負責執行跟驗證。那些坑(權限、傳播延遲)都還在,但文件這種繁雜的東西、服務的配置,AI 會做得比人類更快更好,而我們要做的就是透過好的流程跟權限控管確保他不會有過高的權限、且能對他做的是有了解、知道什麼時候該幫他踩剎車。
Agent 放大的是執行力,不是判斷力。它可以幫你把一份 API 資料一股腦全拉回來、全部顯示出來,但「該不該這樣做」還是得你自己想。這篇裡的程式碼完全不重要,也許你想做直接問 AI,寫出來的流程更適合你的專案,這篇想傳達的是中間一連串的判斷過程,每一個決定背後的那個「為什麼」。(當然如果有人要用,我相信你把連結都給 AI,他肯定能以接近這篇的流程協助你做出你要的設置,這也是留下參考程式碼的一個目的)
小結
整件事其實沒有什麼特別的魔法。它的核心,是接受「這個資訊可以延遲」,然後把一個原本要即時、要後端才能做的功能,退化成一份定時更新的靜態資料。剩下要顧的,就是幾條邊界:憑證不落到前端、更新失敗不要蓋掉好資料、還有想清楚哪些數字才需要顯示。對一個刻意保持精簡的純前端網站來說,這樣就能在不長出後端的前提下,把想要的功能補回來。