前言
自從上次將部落格專案的圖片從 七牛雲 遷到了 Cloudflare R2 之後就發現,Cloudflare
這個賽博菩薩的產品是真的不錯,非常的適合白嫖,DevNow
專案作為一個開源部落格,整體來說是希望越少依賴一些服務越好,使整個構建、部署流程更加的 輕便 和 快捷 ,讓對於前端不是很熟的同學也能快速的搭建一個自己的部落格。
這篇文章其實完全是個人愛好,只要是實現如何整合 Cloudflare D1 來給文章詳情頁增加一個瀏覽量。目前還沒想好是否要往 DevNow
專案上同步這個功能,就現在自己的 blog 上試試水,如果大家有需求的話,後邊在看是不是可以寫一個指令碼整合到 DevNow
專案中。
方案
方案整體來說是很多的,這裡簡單說下我考量的一些因素:
- 自己有伺服器,可以自己搭建一個資料庫實現。
- 透過一些比較成熟的資料儲存服務,可能需要訂閱功能。
- 藉助類似
Cloudflare
、Vercel
這樣的服務商上的一些服務來實現,資料量少的話就是白嫖。severless
的實現方案對前端來說也比較友好。
這裡還有一個考量是後續可能會考慮透過 Cloudflare Page 部署前端的方案,完全從 Vercel
切到 Cloudflare
,減少多個服務商帶來的管理和整合的複雜度。
先簡單介紹一些 Cloudflare D1 ;
官方的介紹:
使用 D1 短短數秒內即可建立一個無伺服器的關聯式資料庫。透過熟悉的查詢語言、時間點恢復功能和經濟實惠的定價,賦能您構建下一個重大專案。
其實這裡主要看 Free 版的服務,這裡其實有兩個服務可以選擇, KV
和 D1
都可以用來儲存資料,這裡選擇 D1
主要考量因素就是 KV
的每日寫入操作只有 1000 次,整理來對於一些好的部落格網站可能會超過,所以直接上 D1
。
整合到 DevNow 專案中
:::tip[注意]
整體來說所有的操作都有兩種實現方式,一個是在 Cloudflare D1 的官網裡邊操作,一個是透過命令列來實現。
我這裡主要記錄一下命令列的實現,透過一些命令和程式碼來建立和部署 D1 的服務,可以更好的理解整個的流程。
前置條件 :已經完成 Cloudflare 賬號的註冊和信用卡💳的繫結,否則無法後續的流程
:::
參考文件:Cloudflare D1
1. 建立一個Worker
pnpm create cloudflare@latest d1-tutorial
// d1-tutorial 是 <WORDER_NAME>
對於設定,請選擇以下選項:
- For What would you like to start with? choose
Hello World example
. - For Which template would you like to use? choose
Hello World Worker
. - For Which language do you want to use? choose
TypeScript
. - For Do you want to use git for version control? choose
Yes
. - For Do you want to deploy your application? choose
No
(we will be making some changes before deploying).
然後專案中將會得到以下的一個目錄。
2. 建立資料庫
D1 資料庫在概念上與許多其他資料庫類似:資料庫可能包含一個或多個表、查詢這些表的能力以及可選索引。 D1 使用熟悉的SQL 查詢語言↗ (與 SQLite 使用的一樣)。
切換到您剛剛為 Workers 專案建立的目錄:
cd d1-tutorial
執行以下命令併為您的資料庫命名。在本教程中,資料庫名為 devnow
npx wrangler d1 create devnow
# 然後會輸出以下內容:
# [[d1 databases]]
# binding ="DB" # available in your Worker on env.DB
# database name ="prod-d1-tutorial"
# database id ="<unigue-ID-for-your-database>"
這將建立一個新的 D1 資料庫並輸出下一步所需的繫結配置。
wrangler
命令列介面是 Cloudflare 的工具,用於在終端中管理和部署 Workers 應用程式和 D1 資料庫。它是在您使用npm create cloudflare@latest
初始化新專案時安裝的。
3. 將 Worker 繫結到您的 D1 資料庫
您必須為 Worker 建立繫結才能連線到 D1 資料庫。繫結允許您的 Workers 訪問 Cloudflare 開發者平臺上的資源,例如 D1。您可以透過更新wrangler.toml
檔案來建立繫結。
複製第二步輸出的結果到 wrangler.toml
檔案中。
[[d1_databases]]
binding = "DB" # available in your Worker on env.DB
database_name = "prod-d1-tutorial"
database_id = "<unique-ID-for-your-database>"
到這裡一個本地的 D1
資料庫就建好了
4. 對 D1 資料庫進行查詢
4.1 更新 schema.sql 檔案:
// d1-turorial/schema.sql
DROP TABLE IF EXISTS PageView;
CREATE TABLE IF NOT EXISTS PageView (
PageId INTEGER PRIMARY KEY AUTOINCREMENT,
Path TEXT UNIQUE NOT NULL,
Count INTEGER,
LastVisited INTEGER
);
INSERT INTO PageView (PageId, Path, Count, LastVisited)
VALUES
(1, '/posts/1', 0, 1725673703),
(2, '/posts/2', 0, 1725673703),
(3, '/posts/3', 0, 1725673703);
4.2 初始化數本地據庫
npx wrangler d1 execute devnow --local --file=./schema.sql
4.3 驗證資料是否在資料庫中
npx wrangler d1 execute devnow --local --command="SELECT * FROM PageView"
看到如下圖結果,即表示本地資料庫建立成功:
5 在 Worker 中編寫查詢方法
這裡實現了根據 /d1/pageview/
來請求,然後 獲取
和 更新
頁面資料的介面。
// page: d1-tutorial/src/index.ts
export interface Env {
DB: D1Database;
}
export default {
async fetch(request, env): Promise<Response> {
const url = new URL(request.url);
const pathname = url.pathname; // 獲取頁面路徑
// 檢查是否以 https://d1.laughingzhu.cn/d1/pageview 開頭
if (!pathname.startsWith('/api/pageview')) {
return new Response('Not Found', { status: 404 });
}
// 繼續處理 https://d1.laughingzhu.cn/d1/pageview 路徑下的請求
const trimmedPath = pathname.replace('/api/pageview/', '/'); // 去除開頭部分,獲取具體頁面路徑
if (request.method === 'GET') {
try {
// 查詢指定 path 的瀏覽量
const result = await env.DB.prepare(`SELECT Count FROM PageView WHERE Path = ?`)
.bind(trimmedPath)
.first();
// 如果沒有記錄,返回 0
const count = result ? result.Count : 0;
return new Response(JSON.stringify({ path: trimmedPath, count }), {
headers: { 'Content-Type': 'application/json' }
});
} catch (error: any) {
console.error('Database operation failed:', error);
return new Response(`Failed to fetch page views for ${trimmedPath}: ${error.message}`, {
status: 500
});
}
}
if (request.method === 'POST') {
// 使用 UPSERT 來嘗試更新 count,如果沒有該頁面則插入新的記錄
try {
// UTC 轉成北京時間
const utcTimestamp = Math.floor(Date.now() / 1000);
const { results } = await env.DB.prepare(
`
INSERT INTO PageView (Path, Count, LastVisited)
VALUES (?, 1, ?)
ON CONFLICT(Path)
DO UPDATE SET Count = Count + 1, LastVisited = ?
`
)
.bind(trimmedPath, utcTimestamp, utcTimestamp)
.run();
// return Response.json(results);
if (results) {
return Response.json({
message: `Page views for ${trimmedPath} updated.`,
data: results
});
} else {
return new Response(`Page views for ${trimmedPath} updated, but no results returned.`, {
status: 200
});
}
} catch (error: any) {
console.log(error);
return new Response(`Failed to update page views for ${trimmedPath}: ${error.message}`, {
status: 500
});
}
}
// 如果是其他方法,不允許操作
return new Response('Method Not Allowed', { status: 405 });
}
} satisfies ExportedHandler<Env>;
配置 Worker
後,您可以在全域性部署之前在本地測試您的專案。
6. 本地開發執行 Worker
npx wrangler dev
當您執行 wrangler dev
時,Wrangler 會提供一個 URL(很可能是 localhost:8787)來檢視您的 Worker。
示例:
我們透過瀏覽器訪問 http://localhost:8787/d1/pageview/posts/hello-word
查詢返回以下資料,這是因為資料庫中沒有,預設是0。
到這裡本地除錯就完事了。
7.部署線上 D1 資料庫
建立遠端線上資料庫:
npx wrangler d1 execute devnow --remote --file=./schema.sql
查詢遠端線上資料庫:
npx wrangler d1 execute prod-d1-tutorial --remote --command="SELECT * FROM Customers"
這裡和步驟 4.2 、 4.3 的區別就是 --local
換成了 --remote
.
7.1 部署 worker 方法
npx wrangler deploy
這裡主要是部署我們在 worker
中實現的方法,不如上述步驟 4.4 在 d1-turorial/src/index
中實現的查詢、更新頁面資料的方法。
到這裡就可以整個 wokrer
和 資料庫就部署完成了,我們可以到線上去看一下。
我們可以看到 Cloudflare 給我們提供了預設的 Worker 的訪問路由,為了方便,我們也可以新增一個自定義路由,透過自定義路由訪問,如下。
到這裡整個 Cloudflare D1
資料庫這裡就接入完成了。
接下來我們在前端專案中來增加相關的請求方法。
DevNow 前端專案中增加查詢、更新瀏覽量方法
1. 實現方案:
整體來說這裡由於期望 瀏覽量 這個資料比較實時,所以大概方案我能想到的是兩種方案:
- 在
CSR
去做,可以透過整合React
來實現在執行時去請求api
來完成(最簡單); - 渲染方案從
SSG
切換到SSR
,這樣頁面會在每次訪問的時候在服務端動態的生成,資料可以保持實時。(這裡遇到一些坑,下邊會提)
最終我選擇了 SSR
方案,主要考量的因素如下:
使用 CSR
執行時請求會導致介面暴露,由於沒有使用者身份體系,很容易讓一些無聊的人刷介面,主要我們白嫖每天是限額,超出是要收錢的。
所以這裡採用了 SSR
的方案,這樣就可以不暴露介面來實現,不過這裡可能會犧牲掉 SSG
帶來的更好快取效果。
這裡主要是刪除、替換適用於 SSG
方案中的一些 API
:
// page: src/posts/[...slug].astro
// 刪除 export const prerender = true;
// 刪除 getStaticPaths() 相關方法
//可以透過 getEntry 來獲取對應的文章內容
import { getEntry } from 'astro:content';
const { slug } = Astro.params;
const post = await getEntry('doc', slug);
2. Astro 中如何接入
開始使用了 Astro
提供的 API 端點 ,後來仔細瞭解完這個是 SSG
的方案,之後在構建的時候請求,後邊就不會走了,所以基於 API 端點 的實現方式 PASS
。
其實 Astro 中 資料請求 的文件中有提到,可以使用 Fetch 來實現。
官閘道器於 fetch 的
fetch 呼叫將會在構建時執行,並且資料都可用於元件模板中來生成動態 HTML。如果啟用 SSR 模式,任何 fetch 呼叫都將在執行時執行。
// page: src/posts/[...slug].astro
//增加如下來實現動態獲取訪問量
const { slug } = Astro.params;
const data = await fetch(`https://xxx.d1.hosts/d1/pageview/posts/${slug}`);
const result = await data.json();
這裡可以提出到一個指定的檔案來維護,示例我就不改了。
最佳化空間:只有線上上環境才請求,線上不請求,來增加資料的準確度。
更新的實現同上。
3. 頁面快取的問題
部署到線上環境時發現個問題,只有部署完第一次訪問會增加請求對應的方法,後邊就不請求了,猜測是 Vercel
這邊有快取,然後在 Response
體裡看到了 x-vercel-cache: HIT
。第一次是 MISS
以後所有的都是 HIT
,這裡其實可以理解成部署完第一次訪問的時候 Vercel
幫我們生成了一份快取,後續所有的人訪問都走的是快取,這樣在速度上就會有更好的體驗,本來是個好東西,還給我卡主了。
然後就各種開始找方法來避免快取生效。給請求加 禁止快取欄位,給 vercel.json header
增加對應路由不快取的配置發現都沒有用。
這裡可以透過開啟 D1
的實時日誌來除錯:
然後突然想到會不會是 ISR
導致的問題,之前預設開啟了,搜尋了一會發現,ISR
模式會自動快取我們的檔案。解決方案就是 將 astro.config.js
中的 ISR
配置刪除即可。
完事,到這裡專案裡就可以展示 瀏覽量了。效果圖如下:
原文連結:Cloudflare D1 - 免費資料儲存