一、背景
虽然 “博客” 已经是很多很多年前流行的东西了,但是时至今日,仍然有一部分人在维护自己的博客站点,输出不少高质量的文章。
我使用过几种博客托管平台或静态博客生成框架,前段时间使用Hono.js+Fauna ,基于 EO 边缘函数搭建了一个博客网站样例,写一篇小小文章进行记录。
二、技术栈
2.1 Hono.js
Hono.js 是一个轻量、快速的 Edge Web 框架,适用于多种 JavaScript 运行时:Cloudflare Workers、Fastly Compute、Deno、Bun、Vercel、Netlify、AWS Lambda 等,同样也可以在 EO 边缘函数 Runtime 中运行起来。
在 EO 边缘函数中,最简单的 Hono 应用写法如下:
import { Hono } from 'hono'
const app = new Hono()app.get('/', (c) => c.text('Hono!'))app.fire();
2.2 Fauna
Fauna 作为 Cloud API 提供的分布式文档关系数据库。Fauna 使用 FQL 进行数据库查询(FQL: A relational database language with the flexibility of documents)。
因此,我准备将博客文章存放在 Fauna 中,在 Fauna JS Driver 的基础上,包装 RESTful API 供边缘函数调用。
注意:
- 目前 EO 边缘函数还不能完全支持 Fauna JS Driver,因此现阶段还不能直接使用 JS Driver 进行数据查询,需要将 Fauna API 服务搭建在其他 JS 环境中。
- EO 边缘函数后续将支持 KV,同时也会兼容 Fauna JS Driver 的写法,因此这里可以进行优化。
import { Client, fql } from 'fauna';
...router.get('/', async c => {const query = fql`Blogs.all()`;const result = await (c.var.faunaClient as Client).query<Blog>(query);return c.json(result.data);
});
...
三、搭建博客
3.1 路由
博客网站的路由比较简单,可以直接使用 Hono.js 进行处理:
import { Hono } from "hono";import { Page } from "./pages/page";
import { Home } from "./pages/home";const app = new Hono();...app.get("/", async (c) => {const blogs = await getBlogList();return c.html(<Home blogs={blogs} />);
});app.get("/blog/:id{[0-9]+}", async (c) => {const id = c.req.param("id");const blog = await getBlogInfo(id);if (!blog) return c.notFound();return c.html(<Page blog={blog} />);
});app.fire();
3.2 页面
Hono.js 中,可以直接按照 jsx 的语法定义页面结构和样式:
import { DateTime } from "luxon";import { Layout } from "./components/layout";...const Item = (props: { blog: Blog }) => {const { ts, id, title } = props.blog;const dt = DateTime.fromISO(ts.isoString);const formatDate = dt.toFormat("yyyy-MM-dd");return (<li><section><p style={{ fontSize: "14px", color: "gray" }}>{formatDate}</p><a href={`/blog/${id}`}>{title}</a></section></li>);
};const List = (props: { blogs: Blog[] }) => (<ul>{props.blogs.map((blog) => (<Item blog={blog} />))}</ul>
);export const Home = (props: { blogs: Blog[] }) => {const title = "Tomtomyang's Blog";return (<Layout title={title}><header><h1>{title}</h1></header><List blogs={props.blogs} /></Layout>);
};
详情页要比列表页稍微复杂一点,一方面需要将 markdown 文件转换,另一方面还需要计算生成文章目录:
import { parse } from "marked";
import { html, raw } from "hono/html";import { DateTime } from "luxon";import type { Blog } from "..";
import { Layout } from "./components/layout";
import { getRenderer } from "../utils/render";const renderer = getRenderer();const Toc = () => { ... };const Info = (props: { author: string; time: string }) => {const { author, time } = props;const dt = DateTime.fromISO(time);const formatDate = dt.toFormat("yyyy-MM-dd hh:mm:ss");return (<div style={{ paddingBottom: "0.6em" }}><span style={{ color: "gray" }}>{formatDate}</span><span style={{ marginLeft: "10px" }}>{author}</span></div>);
};const Content = (props: { content: string }) => {return (<article style={{ fontSize: "16px" }}>{html`${raw(props.content)}`}</article>);
};export const Page = (props: { blog: Blog }) => {const { title, author, content, ts } = props.blog;const parsedContent = parse(content, { renderer });return (<Layout title={title}><header><h1>{title}</h1></header><Info author={author} time={ts.isoString}></Info><Content content={parsedContent} /><Toc content={parsedContent} /></Layout>);
};
3.3 缓存
在边缘构建站点的优势之一是对缓存的控制比较灵活,首先,我准备首先缓存 Fauna API 的响应结果:
首页展示所有文章列表,新增文章后,我需要在首页展示出来,因此列表接口我设置不缓存或者缓存时间很短:
export const fetchWithCache = async (url: string) => {try {return await fetch(url, {eo: {cacheTtlByStatus: {200: 24 * 60 * 60 * 1000,},},});} catch (err) {return new Response(`FetchWithCache Error: ${err.massage}`, {status: 500,});}
};
文章详情页展示文章的具体内容,我个人的习惯是一篇文章写完后,才进行发布,因此后续文章内容发生变动的概率较低,我选择缓存更长的时间:
export const fetchWithCache = async (url: string) => {try {return await fetch(url, {eo: {cacheTtlByStatus: {200: 7 * 24 * 60 * 60 * 1000,},},});} catch (err) {return new Response(`FetchWithCache Error: ${err.massage}`, {status: 500,});}
};
同时,文章详情页还有一个需要注意的点是,通过 API 获取到文章内容后,我还会计算生成 文章目录,文章内容不变,生成的文章目录肯定也不会变,因此这部分重复的计算也可以通过缓存解决掉,方式是使用边缘函数 Runtime 中的 Cache API,将 c.html(<Page blog={blog} />)
生成的 HTML 字符串进行缓存,这样就解决了 Toc 重复计算的问题:
app.get("/blog/:id{[0-9]+}", async (c) => {const id = c.req.param("id");const cache = caches.default;const cacheKey = getCacheKey(id);try {const cacheResponse = await cache.match(cacheKey);if (cacheResponse) {return cacheResponse;}throw new Error(`Response not present in cache. Fetching and caching request.`);} catch {const blog = await getBlogInfo(id);if (!blog) return c.notFound();const html = await c.html(<Page blog={blog} />);html.headers.append("Cache-Control", "s-maxage=xxxx");c.event.waitUntil(cache.put(cacheKey, html));return html;}
});
3.4 部署
使用 Tef-CLI 的 publish 命令,直接将开发好的代码部署到 EdgeOne 边缘节点上;或者可以将 dist/edgefunction.js
文件中的代码,粘贴到 EdgeOne 控制台 - 边缘函数 - 新建函数 编辑框中进行部署。
四、总结
经过上面的步骤,我的博客站点就搭建好了:
列表页:
详情页:
整体来看,在边缘节点上搭建一个博客站点,可以更灵活、更高效的利用和操作 CDN 缓存,对于不同类型的页面,我可以设置不同的缓存策略;边缘 Serverless + Cloud API 的部署方式,让我能足够方便的更新博客,后续随着 EO 边缘函数的不断迭代,这种玩法还有很大的升级空间。