
最近想在部落格的文章頁面加上瀏覽數統計,雖然已經有用Google Analytics,但想讓前台使用者也可以看到數字,就決定自己寫一個輕量的 PV 組件
唯一難處只有資料該怎麼儲存呢?剛好最近在看Cloudflare Worker KV
為什麼選 Cloudflare Workers KV?
Cloudflare Workers KV 是 Cloudflare 提供的全球分散式 Key-Value 儲存服務,專門為 Workers 設計,適合用在:
- 計數器(PV / UV / like)
- 設定值、feature flag
- 快取型資料(讀多寫少)
它的特點是:
- 無需自行架設資料庫
- 讀取速度快,延遲低
- 與 Worker 原生整合
- 低用量免費!
在這次 PV 統計的場景中,每個「文章 URL」就是一個 key,瀏覽數就是對應的 value,非常符合 KV 的使用模型
需要注意的是,KV 屬於最終一致性(eventual consistency),不適合高精度即時統計,但對部落格 PV 這種「大概準就好」的用途來說,效能與成本的平衡非常理想
需求整理
需求很簡單:
- 在文章的 post-info 區塊(日期、分類、標籤那一排)加上「眼睛 icon + 數字」
- 打 API 取得當前頁面的瀏覽數,API 會自動 +1
做成組件還是插件?
在 VitePress 架構下,這種小功能做成 Vue 組件 比較合適,原因:
- 現有的
PostDate、PostCategory、PostTag都是小型SFC(Single File Components) - 只需要在文章頁顯示,不用跨多個地方共用
- 不需要插件的生命週期控制
決定就做成與現有組件風格一致的 PostPageView.vue
API 規格
自架的 PV API 規格很簡單:
GET /pv
功能:取得當前頁面的瀏覽數並自動 +1
請求:
- Method: GET
- Headers:
- Referer: 必填,完整 URL(含 domain + path)
- Origin: 選填,用於 CORS 驗證
回應格式:
{
"count": 42
}API 透過 Referer header 識別不同頁面,每次 GET 都會自動計數 +1
實作步驟
1. 在 config.js 加入 API URL 設定
在 .vitepress/config.js 頂部新增常數:
const PV_API_URL = 'https://api.logicat.tw/pv/'並加進 themeConfig:
themeConfig: {
pvApiUrl: PV_API_URL,
// ...其他設定
}這樣在 Vue 組件中就能透過 useData() 取得 theme.value.pvApiUrl
2. 建立 PostPageView.vue 組件
參考 PostDate.vue 的結構,做一個類似的組件:
<template>
<span v-if="count !== null" class="post-pv">
<svg class="pv-icon" viewBox="0 0 20 20" fill="currentColor">
<!-- 眼睛 icon 的 SVG path -->
</svg>
{{ count }}
</span>
</template>
<script lang="ts" setup>
import { useData } from 'vitepress'
import { onMounted, ref } from 'vue'
const count = ref<number | null>(null)
const emit = defineEmits<{
ready: []
}>()
const { theme } = useData()
onMounted(async () => {
const apiUrl = theme.value.pvApiUrl
if (!apiUrl) {
emit('ready')
return
}
try {
const response = await fetch(apiUrl, {
method: 'GET',
referrerPolicy: 'unsafe-url',
})
if (response.ok) {
const data = await response.json()
count.value = data.count
} else {
count.value = 0
}
} catch {
count.value = 0
}
emit('ready')
})
</script>
<style scoped>
.post-pv {
display: inline-flex;
align-items: center;
gap: calc(var(--inline-gap) / 2);
font-weight: 500;
font-size: 0.75rem;
}
.pv-icon {
width: 14px;
height: 14px;
}
</style>重點設計:
- count 初始為
null:用來區分「未啟用」與「已啟用但載入中」 - 未設定 API URL 時:直接 emit
ready並 return,不發請求 - API 失敗時顯示 0:本地開發時 API 可能驗證來源失敗,顯示 0 比隱藏更清楚
referrerPolicy: 'unsafe-url':讓瀏覽器帶完整 URL 作為 Referer header
3. 註冊組件
在 .vitepress/theme/index.js 中 import 並註冊:
import PostPageView from './components/PostPageView.vue'
export default {
// ...
enhanceApp({ app }) {
app.component('PostPageView', PostPageView)
// ...
},
}4. 插入到 ThemeLayout.vue
在 post-info 區塊加入 PostPageView:
<div
v-if="!frontmatter.page"
class="post-info"
:class="{ 'post-info-hidden': !postInfoReady }"
>
<PostDate :date="frontmatter.date" />
<PostPageView @ready="postInfoReady = true" />
<PostCategory v-if="frontmatter.category" ... />
<PostTag v-for="item in frontmatter.tags" ... />
</div>在 script 部分加上狀態管理:
const postInfoReady = ref(false)
// 路由切換時重置,避免顯示舊的 PV 數字
watch(
() => route.path,
() => {
postInfoReady.value = false
},
)處理載入體驗
一開始遇到的問題:PV組件載入完成後才出現,會造成post-info區塊的高度跳動(layout shift)。
解決方式:
- 預留高度:用
opacity: 0取代v-show,讓 post-info 一開始就佔據空間但不可見 - 淡入動畫:加上
transition: opacity 0.3s ease-out,讓出現更平滑
CSS 調整:
.post-info {
transition: opacity 0.3s ease-out;
}
.post-info-hidden {
opacity: 0;
}這樣 post-info 區塊會:
- 一開始就存在並佔據空間(
opacity: 0) - API 完成後移除
.post-info-hiddenclass - 透明度從 0 漸變到 1,呈現平滑淡入效果
- 完全沒有 layout shift
心得
這次實作很順利,VitePress的組件架構很清楚,只要:
- 參考現有組件的結構
- 在
themeConfig加設定 - 用
useData()取得設定值 - 處理好載入狀態就完成了
如果你也想在VitePress加入自訂功能,這套模式可以直接套用:
- 小型展示功能 → Vue 組件
- 設定值放
themeConfig - 載入體驗用
opacity+transition處理
完整內容可以看部落格的commit
參考資料:
