10 年前老部落格以 JAM Stack 方式滿血復活!
我是從 2009 年開始寫部落格的,當時出於對技術的無知和對 .NET 的癡迷,採用了 http://BlogEngine.NET 作為部落格站,並且部署到了國外的託管虛擬主機。 後來窮得越來越支付不起虛擬主機的月租費,最終在 2014 年將其遷移到了 Azure 的免費託管平臺:https://be-net.azurewebsites.net/,似乎不錯,然而它在台灣的訪問速度並不理想,於是放棄了維護,不再在該平臺上更新博文。
不是不再寫博文,只是換到其他免費平臺上了。 當然想過將自己的 http://BlogEngine.NET 遷移到其他平臺,但是很不順利,於是將其歸檔,準備等自己技術更加成熟后再重啟它。
直到今天,覺得自己的技術已經足夠成熟,開始著手重啟它,過程可以說是相當順利和高效,最後的效果是 10 年前的自己想像不到的:https://jeff-tian.jiwai.win/ 。
- 免費
- 全靜態
- 有強大的 CDN 支援
- https
- 和 git 無縫集成
總結
10 年前的基於 SQLite 資料庫和 .NET 服務端的單站點動態部落格系統,被改造成了 JAMStack 技術疊的全靜態擁有 CDN 和 HS 支援的單頁應用。
JAMStack
JAMStack 是一種讓網站更快、更安全、並且更易於伸縮的架構。 它構建在開發者熱愛的眾多工具鏈和工作流基礎之上,使得生產力最大化。
其核心原則是預渲染和解耦,從而讓網站和應用的發佈擁有前所未有的信心和彈性。
JAM 最早可能是 JAVAScript、API 和 Markup 的縮寫,即使用標記語言寫好網站視圖,數據和交互通過 JAVAScript 和 API 技術進行增強。 但是現在成為了一個架構範式。
特性
預渲染
在 JAMStack 中,整個前端都是在構建過程中預生成為高度優化過的靜態頁面和資源。 這個預渲染的過程使得網站可以直接從 CDN 載入,節省了成本、減少了複雜度和風險,不再依賴關鍵的基礎設施和動態伺服器。
因為有很多流行的工具用來生成網站,像 Gatsby,Hugo,Jekyll,Eleventy,NextJs 等等,很多網頁開發者已經熟悉了所需要的這些工具,所有轉變成更有生產力的 JAMStack 開發者是很容易的。
利用 JAVAScript 增強
通過標記語言和直接從 CDN 載入的 JAMStack 網站中的其他使用者介面資源,這些網站可以很快並且安全的發佈。 在這個基礎上,JAMStack 網站可以使用 JAVAScript 和 API 和後端服務溝通,從而允許增強和個人化用戶體驗。
利用服務更進一步增強
豐富的 API 生態成為了 JAMStack 網站的顯著賦能者。 由於擁有利用功能變數名稱專家的能力,這些專家可以通過 API 來提供產品和服務,從而團隊可以構建出更加複雜的應用,相比自己實現這樣的能力需要承擔更多的風險和額外的負擔。 現在我們可以將諸如身份認證、支付、內容管理、數據服務、搜索以及更多的事情外包出去了。
JAMStack 網站可以在構建時利用這些服務,並且在運行時直接通過 JavaScript 在瀏覽器裡直接利用這些服務。 清晰解耦的這些服務帶來了更大的可移植性和靈活性,同時還顯著降低了風險。
好處
採用 JAMStack 架構的網站和專案工作流擁有各種好處,其中的關鍵是:
安全
JAMStack 從託管基礎設施中移除了變動的部分和系統,從而只需要更少的伺服器和系統,減少了攻擊面。 頁面和資源都是預生成的文件,從而支援唯讀託管,這更進一步減少了可能的攻擊向量。 同時支援由提供者提供動態的工具和服務,這些專業供應商有專門的團隊來對其專業的產品做安全加固,並且提供高標準的服務。
可擴展
流行的處理高流量負載的架構是通過額外添加緩存熱門視圖和資源的邏輯來實現的,而在 JAMStack 架構中,這是 預設提供的:它不再需要額外的複雜邏輯和工作流來決定哪些資源何時需要緩存,因為整個網站是完全通過 CDN 來提供服務的。
採用 JAMStack 的網站,所有的一切都被緩存在內容分發網路中。 擁有更簡單的部署,天然內置的冗餘,以及難以置信的高負載能力等特性。
高性能
頁面載入速度對用戶的體驗和交互影響非常大。 JAMStack 網站不再需要伺服器在請求時生成頁面視圖,而是在構建時提前生成頁面。
因為所有的頁面已經在離使用者最近的 CDN 節點中可獲取,所以沒有昂貴和複雜的基礎設施,也能達到極高的性能。
可維護
託管複雜性降低后,維護性任務也減少了。 一個預生成的網站,無論直接從單主機還是直接從 CDN 載入都不再需要專家團隊來護航。
這些維護工作都在構建時完成了,所以現在是一個生成好的網站,它非常穩定並且可以無伺服器託管,從而不存在打補丁、升級或者運維的工作。
可移植
JAMStack 網站是預生成的。 這意味著你可以使用各種託管服務來託管它們,並且可以在你喜歡的託管服務中自由轉移。 任何簡單的靜態託管方案就足夠了。
基礎設施綁定,再見。
開發體驗
JAMStack 網站可以使用各種工具構建。 它們不依賴特定技術或者奇怪的小眾框架。 相反,它們構建在被廣泛使用的工具之上並且遵守被廣泛使用的約定。 這樣的結果就是,尋找具有激情和天賦的開發者就沒那麼困難了。
效率和效益相得益彰。
最佳實踐
如果你堅持使用一點點最佳實踐,那麼在構建 JAMStack 專案時,你就可以真的從這個技術棧里得到最大的收益。
將整個網站部署在CDN
因為 JAMStack 專案不依賴服務端代碼,所以可以分散式部署而不是存活在單台伺服器上。 直接將整站託管在 CDN 解鎖了無可匹敵的速度和性能。 你的應用推到邊緣的東西越多,用戶體驗就越好。
現代化的構建工具鏈
充分利用現代化的構建工具。 瀏覽器的世界變化太快就像一片難以適應的叢林,但是你仍然希望不必等到明天的瀏覽器問世而在今天就用上明天的網頁標準。 那麼目前這意味著你要使用 Babel、PostCSS、Webpack 以及相關的工具。
自動化構建
因為 JAMStack 標記是預生成的,所以改變內容後只有在下一次構建才可能發不到生產環境。 自動化這個過程將節省你很多精力。 你可以利用 webhooks,或者使用一個包含自動化服務的發佈平臺。
原子化部署
由於 JAMStack 項目增長得非常之大,新的改變可能需要重新部署成百上千的檔。 一個一個的文件上傳方式會導致在這個過程結束前系統處於一個不一致的狀態。 你可以利用讓你實現「原子化部署」的系統來避免這個情況的發生,這種平臺只在所有改變的檔全部更新後才會發佈到生產環境。
即時緩存失效方案
當構建-部署的循環變成一個常規行為后,你需要確保當一個部署上線后,它就真的上線了。 通過確保你的 CDN 能夠處理即時緩存清空來打消你的任何疑慮。
將所有東西都放在 Git 裡
在 JAMStack 專案中,任何人都能夠通過 git 克隆,然後使用標準流程安裝需要的依賴(比如 npm install)之後在本地運行整個專案。 不需要資料庫克隆,不需要複雜的安裝。 這減少了貢獻者的難題,也簡化了 staging 和 testing 工作流。
復活過程
在瞭解到了 JAMStack 架構后,我感覺自己的技術儲備準備好了。 其實部落格文章一旦寫好,就應該是一份靜態檔,畢竟沒有什麼太多的動態訊息。 靜態檔的好處是沒有伺服器端性能損耗,並且可以快取到 CDN 的終端節點。 所以完全沒有必要去使用 .NET 之類的伺服器端程式。 整個復活過程如下:
靜態網站生成工具的選擇
這樣的工具非常多,我最終選擇了 Stackbit。 通過創建一個 Stackbit 網站,就能看到專案目錄結構,這個專案其實是一個典型的 Gatsby JS 專案加上一個 Stackbit 工具鏈。 部落格文章主要在 src/pages/posts 目錄下,並且每篇部落格都是一個如下結構的 markdown 檔:
---
stackbit_url_path: posts/url
title: 标题
date: '时间'
excerpt: >-
摘要...
thumb_img_path: >-
头图URL
comments_count: 评论数
positive_reactions_count: 点赞数
tags:
- 标签1
- 标签2
canonical_url: >-
原文链接
template: post
---
正文部分
因此在後續就需要將數據匯出成上面的結構
數據匯出
之前的 http://BlogEngine.NET,我使用了 SQLite 作為資料庫,現在需要將裡面的結構化數據匯出成為一個 JSON 檔。
工具
以前用過 SQLite Browser、DBeaver 等等桌面軟體,這些 GUI 工具都支援 SQLite。 但是今天隆重推薦使用 Metabase,自從我使用了它,就愛不釋手,再也不想用以前的那些桌面軟體了。 它是一個 Web 應用,啟動非常簡單: 先去官網下載一個 jar 包檔 metabase.jar。 然後在命令列輸入: java -jar metabase.jar
。 這樣就在本定環境啟動了 metabase,打開瀏覽器,輸入 http://localhost:3000 即可打開 metabase,輸入 SQLite 檔路徑,就進入到主頁面。

點擊打開 http://BlogEngine.NET,可以看到表結構,對於遷移博文來說,比較關注 Be Posts 和 Be Post Tag:

你可以很方便地流覽一下自己發佈博文的頻率情況,只需要點擊進入 Be Posts 然後選擇 Date Created 字段並且點擊分佈:

就能立即得到曲線圖:

SQL 查詢
我們需要將數據匯出成在前面分析過的數據結構,最終的 SQL 查詢如下:
SELECTPostId,'src/pages/posts/'||(casewhenltrim(Slug)<>''thenSlugelsePostIdend)||'.md'asfilePath,'posts/'||(casewhenltrim(Slug)<>''thenSlugelsePostIdend)asurlPath,'---'||'stackbit_url_path: >-'||' posts/'||(casewhenltrim(Slug)<>''thenSlugelsePostIdend)||'title: '''||replace(Title,'''','"')||''''||'date: '''||DateCreated||''''||'excerpt: >- '||replace(Description,'',' ')||''||'comments_count: 0'||'positive_reactions_count: 0'||'tags: - '||ifNull((selecttagsfrom(selectPostId,GROUP_CONCAT(tag,' - ')astagsFROM(selectdistinctPostId,tagfrombe_PostTag)asbe_PostTagwherebe_PostTag.PostId=POST.PostIdgroupbybe_PostTag.PostId)),'')||''||'canonical_url: https://be-net.azurewebsites.net/post/'||strftime('%Y',DateCreated)||'/'||strftime('%m',DateCreated)||'/'||strftime('%d',DateCreated)||'/'||(casewhenltrim(Slug)<>''thenSlugelsePostIdend)||''||'template: post'||'---'||PostContentASdataFROM(SELECT"be_Posts"."PostRowID"AS"PostRowID","be_Posts"."BlogID"AS"BlogID","be_Posts"."PostID"AS"PostID","be_Posts"."Title"AS"Title","be_Posts"."Description"AS"Description","be_Posts"."PostContent"AS"PostContent","be_Posts"."DateCreated"AS"DateCreated","be_Posts"."DateModified"AS"DateModified","be_Posts"."Author"AS"Author","be_Posts"."IsPublished"AS"IsPublished","be_Posts"."IsCommentEnabled"AS"IsCommentEnabled","be_Posts"."Raters"AS"Raters","be_Posts"."Rating"AS"Rating","be_Posts"."Views"AS"Views","be_Posts"."Slug"AS"Slug","be_Posts"."IsDeleted"AS"IsDeleted"FROM"be_Posts"WHERE"be_Posts"."IsDeleted"=0LIMIT1048576)asPOST
主要就是從 BePosts 和 BePostTag 兩張表中,把數據組合成需要的樣子。
標籤聚合
標籤表結構如下:

首先需要將同一個 PostId 的 Tag 聚合到一行,然後將他們用 「【回車符】 - 」 分割串聯。 可以這樣做:
ifNull((selecttagsfrom(selectPostId,GROUP_CONCAT(tag,'
- ')astagsFROM(selectdistinctPostId,tagfrombe_PostTag)asbe_PostTagwherebe_PostTag.PostId=POST.PostIdgroupbybe_PostTag.PostId)),'')
以上考慮到了數據為空的情況,這樣就組成了 markdown 檔中的 Tags 部分。
URL 生成
博文 URL 的生成,優先使用 Slug,當 Slug 為空時,回退到使用 PostId:
'posts/'||(casewhenltrim(Slug)<>''thenSlugelsePostIdend)asurlPath
匯出為 JSON
除了以上兩個比較特殊的 SQL 處理,其他的 SQL 都是平凡的。 將他們運行,得到結果後,選擇匯出為 JSON 檔:

得到的 JSON 檔是這樣的:

靜態檔生成
如上所示,得到的 JSON 檔是一個陣列,需要為其中每一個元素創建一個對應的檔。 通過分析 Stackbit 官方的 stackbit-pull 庫,可以發現我們只需要將其從遠端服務拉取 JSON 回應的過程簡化成直接從本地讀取即可,剩下的創建檔的邏輯一模一樣。 所以,我對其稍加改造后,可以這樣來運行:
npx -p @jeff-tian/stackbit-pull stackbit-pull-json --json-file=/path/to/json
這時,專案的 src/pages/posts 目錄下已經有了成百上千的檔。 通過 npm run develop
本機運行起來,完美!
➜ npm run develop
> @jeff-tian/[email protected] develop /Users/tianjef/jeff-tian/unicms-copy-01
> gatsby develop
success open and validate gatsby-configs - 0.088s
You can now view @jeff-tian/space in the browser.
⠀
http://localhost:8000/
⠀
View GraphiQL, an in-browser IDE, to explore your site's data and schema
⠀
http://localhost:8000/___graphql
⠀
Note that the development build is not optimized.
To create a production build, use gatsby build
打開 http://localhost:8000

圖片網址替換
其實沒有那麼完美,比如圖片全部顯示不了。 原因是圖片上傳到 http://BlogEngine.NET 後,其 src 被設置成了一個需要動態處理的 URL:image.axd?picture=xxxx。 通過 VSCode 或者 WebStorm 這樣的 IDE 打開專案,使用正則運算式做一個替換,就可以解決問題。 具體的正規表示式是這樣的:
查找:(href|src)="[^"]+?zizhujy.com/blog/image.axd?picture=([^"]+?)"
替换成:$1="https://raw.githubusercontent.com/Jeff-Tian/blogeng
ine.net/master/Source/BlogEngine/BlogEngine.NET/App_Data/files/$2"
以及:
(href|src)="[^"]+?zizhujy.com/BlogEngine/BlogEngine/BlogEngine.NET/image.axd?picture=([^"]+?)"
$1="https://raw.githubusercontent.com/Jeff-Tian/blogengine.net/master/Source/BlogEngine/BlogEngine.NET/App_Data/files/$2"
上傳到 github
將整個專案推到 github
git commit -am "sync blogengine.net"
git push -u origin master
netlify 自動部署
使用 github 登錄 netlify,同步 github 專案。 當 GitHub 專案有新的推送時,netlify 會自動產生網站並且部署:

netlify 發佈成功後,就可以存取生產網站了:https://jeff-tian.jiwai.win 。