我是从 2009 年开始写博客的,当时出于对技术的无知和对 .NET 的痴迷,采用了 BlogEngine.NET 作为博客站,并且部署到了国外的托管虚拟主机。后来穷得越来越支付不起虚拟主机的月租费,最终在 2014 年将其迁移到了 Azure 的免费托管平台:https://be-net.azurewebsites.net/,似乎不错,然而它在中国的访问速度并不理想,于是放弃了维护,不再在该平台上更新博文。

不是不再写博文,只是换到其他免费平台上了。当然想过将自己的 BlogEngine.NET 迁移到其他平台,但是很不顺利,于是将其归档,准备等自己技术更加成熟后再重启它。

直到今天,觉得自己的技术已经足够成熟,开始着手重启它,过程可以说是相当顺利和高效,最后的效果是 10 年前的自己想像不到的:https://jeff-tian.jiwai.win/

  • 免费
  • 全静态
  • 有强大的 CDN 支持
  • https
  • 和 git 无缝集成

总结


10 年前的基于 SQLite 数据库和 .NET 服务端的单站点动态博客系统,被改造成了 JAMStack 技术栈的全静态拥有 CDN 和 https 支持的单页应用。

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 文件: markdown


stackbiturlpath: posts/url title: 标题 date: 时间 excerpt: >- 摘要... thumbimgpath: >- 头图URL commentscount: 评论数 positivereactions_count: 点赞数 tags:

  • 标签1
  • 标签2 canonical_url: >- 原文链接 template: post


    正文部分

因此在后续就需要将数据导出成上面的结构

数据导出

之前的 BlogEngine.NET,我使用了 SQLite 作为数据库,现在需要将里面的结构化数据导出成为一个 JSON 文件。

工具

以前用过 SQLite Browser、DBeaver 等等桌面软件,这些 GUI 工具都支持 SQLite。但是今天隆重推荐使用 Metabase,自从我使用了它,就爱不释手,再也不想用以前的那些桌面软件了。它是一个 Web 应用,启动非常简单:
先去官网下载一个 jar 包文件 metabase.jar。然后在命令行输入: java -jar metabase.jar 。这样就在本定环境启动了 metabase,打开浏览器,输入 http://localhost:3000 即可打开 metabase,输入 SQLite 文件路径,就进入到主页面。
image.png
点击打开 BlogEngine.NET,可以看到表结构,对于迁移博文来说,比较关注 Be Posts 和 Be Post Tag:
image.png
你可以很方便地浏览一下自己发布博文的频率情况,只需要点击进入 Be Posts 然后选择 Date Created 字段并且点击分布:
image.png
就能立即得到曲线图:
image.png

SQL 查询

我们需要将数据导出成在前面分析过的数据结构,最终的 SQL 查询如下: sql SELECT PostId, src/pages/posts/ || (case when ltrim(Slug) <> then Slug else PostId end) || .md as filePath, posts/ || (case when ltrim(Slug) <> then Slug else PostId end) as urlPath, --- || stackbiturlpath: >- || posts/ || (case when ltrim(Slug) <> then Slug else PostId end) ||

title: || replace(Title, , ) || || date: || DateCreated || || excerpt: >- || replace(Description, , ) || || commentscount: 0 || positivereactions_count: 0 || tags:

  • || ifNull((select tags from (select PostId, GROUP_CONCAT(tag,
  • ) as tags FROM (select distinct PostId, tag from bePostTag) as bePostTag where bePostTag.PostId = POST.PostId group by bePostTag.PostId)), ) || || canonical_url: https://be-net.azurewebsites.net/post/ || strftime(%Y, DateCreated) || / || strftime(%m, DateCreated) || / || strftime(%d, DateCreated) || / || (case when ltrim(Slug) <> then Slug else PostId end) || || template: post ||


    || PostContent AS data FROM ( SELECT bePosts.PostRowID AS PostRowID, bePosts.BlogID AS BlogID, bePosts.PostID AS PostID, bePosts.Title AS Title, bePosts.Description AS Description, bePosts.PostContent AS PostContent, bePosts.DateCreated AS DateCreated, bePosts.DateModified AS DateModified, bePosts.Author AS Author, bePosts.IsPublished AS IsPublished, bePosts.IsCommentEnabled AS IsCommentEnabled, bePosts.Raters AS Raters, bePosts.Rating AS Rating, bePosts.Views AS Views, bePosts.Slug AS Slug, bePosts.IsDeleted AS IsDeleted FROM bePosts WHERE bePosts.IsDeleted = 0 LIMIT 1048576 ) as POST

主要就是从 BePosts 和 BePostTag 两张表中,把数据组合成需要的样子。

标签聚合

标签表结构如下:
image.png
首先需要将同一个 PostId 的 Tag 聚合到一行,然后将他们用 “【回车符】 - ” 分割串联。可以这样做: sql ifNull((select tags from (select PostId, GROUP_CONCAT(tag,

  • ) as tags FROM (select distinct PostId, tag from bePostTag) as bePostTag where bePostTag.PostId = POST.PostId group by bePostTag.PostId)), )

以上考虑到了数据为空的情况,这样就组成了 markdown 文件中的 Tags 部分。

URL 生成

博文 URL 的生成,优先使用 Slug,当 Slug 为空时,回退到使用 PostId: sql posts/ || (case when ltrim(Slug) <> then Slug else PostId end) as urlPath

导出为 JSON

除了以上两个比较特殊的 SQL 处理,其他的 SQL 都是平凡的。将他们运行,得到结果后,选择导出为 JSON 文件:
image.png
得到的 JSON 文件是这样的:
image.png

静态文件生成

如上所示,得到的 JSON 文件是一个数组,需要为其中每一个元素创建一个对应的文件。通过分析 Stackbit 官方的 stackbit-pull 库,可以发现我们只需要将其从远端服务拉取 JSON 响应的过程简化成直接从本地读取即可,剩下的创建文件的逻辑一模一样。所以,我对其稍加改造后,可以这样来运行: shell npx -p @jeff-tian/stackbit-pull stackbit-pull-json --json-file=/path/to/json

这时,项目的 src/pages/posts 目录下已经有了成百上千的文件。通过 npm run develop 本地运行起来,完美! shell ➜ 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 sites 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
image.png

图片 URL 替换

其实没有那么完美,比如图片全部显示不了。原因是图片上传到 BlogEngine.NET 后,其 src 被设置成了一个需要动态处理的 URL:image.axd?picture=xxxx。通过 VSCode 或者 WebStorm 这样的 IDE 打开项目,使用正则表达式做一个替换,就可以解决问题。具体的正则表达式是这样的: bash 查找:(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

以及: bash 查找: (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 shell git commit -am sync blogengine.net git push -u origin master

netlify 自动部署

使用 github 登录 netlify,同步 github 项目。当 GitHub 项目有新的推送时,netlify 会自动生成网站并且部署:
image.png
netlify 发布成功后,就可以访问生产站点了:https://jeff-tian.jiwai.win