迁移到 Cloudflare Pages
我是如何将在 Vercel 的站点迁移到 Cloudflare Pages 的,以及为什么
原因闲谈
这是一些对此次迁移背景的介绍,如果你想看关于迁移的具体过程来做参考,请跳转到下方的 迁移过程 章节。
从上周开始,这个一直以来部署在 Vercel 的博客就迁移到 Cloudflare Pages 上了。
虽然自己对访问速度比较苛求,最近一段时间一直在做优化速度的事情,甚至拖了一周才在博客更新也是因为想找一个优化图片的方案。
不过这次对于访问速度什么的并没指望能有提升,实际也没有。 对于有着多年经验和用着 AWS Cloudfront 的 Vercel 来说,Cloudflare 其实略显差一些。
不过为什么要迁移呢?原因大概是近期一直对 Serverless 比较感兴趣。
关于 Serverless
如果你对 Serverless (FaaS) 没有概念的话,这里简单概括一下:
Serverless 指的就是将后端代码简化到“从接受请求到发回响应”的最简过程。 剩下的如 HTTP 相关配置,依据负载的动态调整,甚至可能路由等都由提供商负责。
这有点像提供商为你搭好了 nginx,配好了 SSL, 用数据中心的机器池做好了动态调整,甚至帮你选好了路由框架。 你只需要对着收到的请求写代码,为服务器处理到的每个请求付费就可以了。
这样精简的架构写代码需要操心的运维工作一般是最简化的。 对于我这种大部分时间在写前端,完全不想操心各种服务器配置什么的人来说还是很有吸引力的。(Vim 快捷键记不住啊,查找替换都不会,谁来教教我啊)
不过这个看上去近乎完美的服务也不是并没有坑的,其实坑也是有很多的。
(这里暂且不提 vendor lock-in,不同平台提供的等级不同,迁移难度也不同,但这也是选择时你应当考虑的)
冷启动延迟
对于一个以可动态拓展为特点的服务来说,Serverless 和 Docker 等服务也有着同样的问题 —— 冷启动延迟,也就是对于突发负载的响应延迟。
冷启动延迟可以说是 Serverless 最大的问题。
当一个请求进入到了服务器但并没有已经准备好的代码实例在运行时,这个请求就需要等待一个实例的启动的时间。
你可以在 Comparison of Cold Starts in Serverless Functions across AWS, Azure, and GCP 中看到现在三大云提供商的启动延迟时间统计图(感谢原作者的测试)。
即使是最快的 AWS 也时常需要半秒左右来启动,也就是说如果没有实例在运行的话,访问的人就需要多花半秒的时间等实例启动。 这对于一般以毫秒为标准优化的后端来说是极长的等待时间。
这么长的时间是为什么出现的,其实对于云提供商也是很没办法的事情。
Node.js 的运行时中,开发者是可以引入原生包的,也有不少包引用了原生库。 比如著名的 sharp,可以说是“是个服都会有”级别的包了,就引入了 libvips
库来做图片相关处理。
然而引入原生包就意味着对 Node.js 进程本身会有些许修改, 也许写的不好的库还可能会内存溢出,甚至垮掉整个 Node.js 进程。
于是对于每个实例来说,云提供商都不得不启动一个全新的 Node.js 进程,通常就会消耗上百毫秒的时间。 这就解释了为什么 AWS 几年都没优化出可以忽略的启动时间。
那么,Cloudflare 是如何解决这个问题的呢?
答案是直接放弃 Node.js。
Node.js 在设计上就没有考虑急速启动的情况,也没有内置的隔离系统,再怎么在优化上下功夫大多也无济于事。 不过有一个在设计之初就考虑到急速启动和隔离系统的东西,你每天都在用 —— 你的浏览器。
这也就是 Cloudflare Workers 的设想, 像浏览器标签页一样把 Serverless 的代码装进类似 ServiceWorker 的环境中, 这样就可以快速的在 V8 内开启一个处理请求的实例,实例间也做到了互不影响。
于是 Cloudflare 就可以真的声称自己没有冷启动时间了。 因为一般的代码载入都不到十毫秒,再加上保热机制,几乎就不会遇到需要冷启动的情况了。
不过因为我的网络不算好,不能去验证这个延迟是否是真的很低,可能要留给浏览博客的各位测试了。
数据库
对于将后端迁移到 Serverless 来说,有一个东西一直不大算可以做到迁移到无服务器,那就是数据库。
大部分的 SQL 数据库都是会有闲时的整理工作的,也就是读写数据库结束后,数据库本身仍需要去时常整理数据。 市面上的大多数可动态拓展的数据库服务都是按小时收费的,而且至少需要保持一个实例在运行。
虽然现在确实存在无服务器的数据库产品,如之前提过的 Fauna 之类。 但是连接一个外网的数据库总觉得怪怪的,安全性又不是很行,延迟又高。
不过 Cloudflare Workers 带一个 Worker KV 的键值对数据库, 和一个大概是面向高写入和一致性的 Durable Objects。
虽然不是比较复杂的数据库,不过感觉可以满足存点基本数据的需求了。 也是 Cloudflare 内网的,感觉作为个 redis 替代什么的也不错。
也许博客的评论就会用 KV 做呢,再加个 Durable Objects 做的流量统计什么的。
不知道什么时候能有比较复杂的数据库上线,SQL 之类的,document 也不错。
在写这篇博文的时候 Cloudflare 发布了 D1,看起来比较合适。 不过是 Private Beta,感觉自己能进的可能性也不大,要等公开测试估计要几个月,还是要用 KV 了。
如果你想把已有的后端搬上 Cloudflare Workers 或者 Pages 这类的平台,目前只建议搬不需要怎么动数据库的一部分。 现在的 Serverless 数据库还是没什么好方案,如果有真好用的方案冒出来了的话,大概会再写一篇博文什么的。
目前的话,暂时只搬不需要数据库的计算上 Serverless 体验无缝的动态调整倒是很不错。 比如上面提到的图片压缩什么的,不过 Cloudflare Images 这种 SaaS 已经存在了,也蛮便宜的,不如直接用。
迁移过程
这里的迁移过程我并不打算写这个博客的迁移过程,因为这个博客暂时并没有用到 Serverless 的东西(纯静态的)。
准备写的是我将 Platinum 从 Vercel 迁移到 Cloudflare Pages 的过程, 完整的代码 diff 可以在 103e1bc 查看。
Platinum 也没有数据库相关的操作,所以这里不会涵盖 Workers KV 这类的内容, 之后可能做评论区的时候会单写一篇博文。
代码调整
虽然 Vercel 的 Functions 和 Cloudflare Pages 的功能没什么差别,不过由于运行时不同,使用方式也不尽相同。
代码上的迁移总体来说并没有太繁琐,毕竟是类似的服务,差别还是不大的。
TypeScript 类型
首先是为 TypeScript 装上 @cloudflare/workers-types
。
这个到简单,用包管理装上,然后塞进 tsconfig.json
的 types
里即可。
pnpm i -D @cloudflare/workers-types
{
"compilerOptions": {
"types": ["@cloudflare/workers-types"]
}
}
Function 样式修改
接下来是对函数样式的修改。
Vercel 和 Pages 的还是有一些差别的。Vercel 直接给予了一个 response
对象做响应, 而 Pages 需要自己建一个 Response
对象。
如果你原有的 Vercel Function 带着一个传输的压缩(因为 Vercel 并不管压缩响应), 这时候就可以删掉了,Pages 会负责响应的压缩。
Pages 是可以区分请求的方法的,如果你需要接收所有类型的请求,可以将下面的函数名修改为 onRequest
。
// import type { VercelRequest, VercelResponse } from '@vercel/node'
// export default async function handler(request: VercelRequest, response: VercelResponse) {
export const onRequestGet: PagesFunction = async ({ request }) => {
// response.end('Hello World!')
return new Response('Hello World!')
}
如果想在 Pages 拿到当前部署的域名的话也很简单,直接解析一下 request.url
即可。
export const onRequestGet: PagesFunction = async ({ request }) => {
// const { host } = request.headers
const { hostname: host } = new URL(request.url)
}
如果项目内引用有仅在 Node.js 环境下工作的包,就需要找一个替代品换上, 毕竟 Cloudflare Workers 的运行环境是基于 ServiceWorker 的。
路由匹配
之后便是对路由规则不同的修改。
首先把 api
放到项目根目录(注意不是输出目录)的 functions
文件夹中。
如果你之前是通过重写规则使其它目录指向 api
的话,现在可以直接写在对应目录内了。
/api/foo/[name].ts -> /functions/api/foo/[name].ts
Pages 的文件命名是区分深层匹配和浅层匹配的。
如果需要深层的匹配,可以把 [name].ts
重命名到 [[name]].ts
。
/api/deep/[name].ts -> /functions/api/deep/[[name]].ts
之后通过接到的 params
拿到文件名的参数。
export const onRequestGet: PagesFunction = async ({ request, params }) => {
// const { name } = request.query
const { name } = params
}
Vercel 采用的是优先静态文件规则,如果对应目录存在静态文件,则 Vercel 会优先返回文件内容而不是调用 Functions。 而 Pages 采用的是 Functions 优先规则,只要路由符合就会调用,无论同名文件是否存在。
如果想优先返回静态文件内容的话,先尝试调用一下 next
即可。
export const onRequestGet: PagesFunction = async ({ request, next }) => {
try {
// 先尝试匹配静态文件
return await next()
}
catch (e) {
// 忽略未匹配到的
}
// 下面就可以正常写 Function 代码了
}
Headers
Pages 的 Headers 设置还是很简单的。
这里以 manifest
举个例子,更详细的可以看看文档。
在输出目录建一个 _headers
文件,然后对着文件名填上需要的 header。
/manifest.webmanifest
Content-Type: application/manifest+json
这样代码调整就基本完成了,可以尝试部署一下了。
部署到 Pages
下面就尝试将项目部署到 Cloudflare Pages。
代码已经传到 GitHub 的 cf-pages
分支下了,进到 Dashboard 建一个新项目。
然后连接到 GitHub 授权一下,选上对应的库,然后跳转回来勾选,开始设置。
之后就是配置构建环境和构建指令了。
因为我在用 pnpm,部署的话还蛮麻烦的,这里分享一下我的构建指令。
npm install -g pnpm && pnpm i --frozen-lockfile --strict-peer-dependencies && pnpm build
不知道为什么,Pages 的构建环境默认是 Node.js 12,都 EOL 一个月了。 不过好在可以用 NODE_VERSION
一键换一下。
对于设置 NPM_FLAGS
可不可以跳过 install 这个暂时没测试过,不过一直写着,希望可以。
NODE_VERSION=16
NPM_FLAGS=--version
以及配置后的样子。
之后等待部署完成即可,你就在 Cloudflare Pages 上有一个新项目了。
对于 CI 部署等就不涉及了,Cloudflare 在昨天才刚公布的允许使用 CLI 部署。
其实如果不需要必须先 test 的话,直接在 Pages 选择部署相比 GitHub Actions 可能稍微好一些。 毕竟不像 Vercel 的分钟限制,Cloudflare 是按部署次数限制的。
绑定域名也很简单,在自定义域界面加上即可,自带配置教程,感觉没必要截图了。
默认的 pages.dev
域名在大陆早就被污染了,所以如果需要大陆访问的话就必须绑定个自己的域名。 不过 IP 倒是正常,毕竟是 Cloudflare 的。
前天的更新中加上了选择预览分支的功能。 如果你不想所有分支都部署个预览的话,可以设置一下。(默认是部署全部分支)
后记
写这篇博文的时候基本是一边修博客造轮子一边写,花了整整两天才写完。
不过其实部署个项目什么的并不是枯燥的事情,写博文也不是。
第一次写这么长的博文,之前的博文都是几百字搞定,最多填几张图。
不过这次真的想介绍的详细一些,希望自己以后的博文也能如此。
自己的文笔向来都不好,攥字算是很费劲的事情,不过多练或许可以好一些。
感谢 Cloudflare 提供的 Workers 和 Pages,发布的产品我一向都很感兴趣。 Cloudflare 一直像是不想赚钱一样做低价体验又好的产品。
希望有生之年可以把评论区和流量统计做上。
2022-05-12 ~ 2022-05-13,一直是晴天。
CC-BY-SA 4.0 2020-2024 © QiroNT. Powered by VitePress.