前言
『终章』从建站开始,就一直托管在自己的 VPS 上,并且通过 GitHub Webhook 以及自制后端实现自动部署。不过最近把网站托管到了 Cloudflare Workers Sites。选择它的主要原因是自己一直使用 Cloudflare 的 DNS 和 CDN,其实类似的静态托管服务 Vercel 以及 Netlify 也可以考虑一下。而这类服务的优点也大多类似:
- 一般会部署到多个网络节点,比如 Workers Sites 就会部署在 Cloudflare 遍布世界的200多个节点上。当然,我自然是不会缺少这区区200个节点的,我缺少的是遍布世界各地的读者。
- 自带 CDN 并且与托管网站的服务器处于同一数据中心,节省了回源时间。
- 较高的 SLA。
- 不需要自己处理证书,虽然现在 Let's Encrypt 已经足够普及并且Certbot 也非常易用,不过能多偷一点懒也是很好的。
由于 Workers Sites 不提供 CI/CD,所以我选择了使用 GitHub Action 来负责生成网站、处理杂物,以及部署到 Workers Sites。
首次部署
先放出官方文档以供参考。
初始化 Workers Sites
Workers Sites 需要使用 wrangler 进行部署,首先安装 Node.js,然后直接使用 npm i @cloudflare/wrangler -g
安装 wrangler。
接下来在项目目录运行 wrangler init
,会生成 wrangler.toml,然后编辑这个文件。
name = "codaworld" # 网站名,不能包含 "."
type = "webpack"
account_id = "915646e8f79239578920a2a6e4c3b0a6" # Cloudflare Account ID
routes = ["coda.world/*", "www.coda.world/*"] # 自定义域名,后面的星号不要省略
zone_id = "1b825f5f5586940c5fa5cf44cfc24a8d" # 域名的 Zone ID
workers_dev = false # 不启用 workers.dev 子域名
[site]
bucket = "./public" # 生成后的网站的位置,我使用的 Hugo 所以是 public
entry-point = "workers-site" # Workers Sites 相关配置的路径,一般保持默认即可。
创建和使用 API Token
- 在 Cloudflare 个人资料页面 就可以创建 API Token,官方已经给出了一个与 Workers 相关的模板,其实里面的 Read Account Settings 和 Read User Details 这两个权限去掉也没问题,另外记得也设置一下账户限制和域名限制。
- 然后运行
wrangler config
后输入 API Token,或者也可以把 API Token 赋予 CF_API_TOKEN 环境变量。
预览和发布
- 可以先运行
wrangler preview --watch
预览一下。 - 而真汉子往往直接运行
wrangler publish
进行发布。
配置 DNS
由于只有存在 DNS 记录的域名才会匹配 Workers Sites,所以我们需要在 Cloudflare 的控制面板中对相应的域名添加 DNS 记录,我直接设置了
AAAA coda.world 100::
AAAA www.coda.world 100::
确保 Proxy Status 设为 Proxied 也就是要把云朵图标点亮,另外只要有 DNS 记录即可,并不需要有效的 IP。
卸磨杀驴
由于已经完成了基本配置并且生成了相应的文件,再加上以后的部署都会交给 GitHub Actions 来进行,所以我直接删除了 workers-site/node_modules
。
再把 wrangler 也删了 npm r @cloudflare/wrangler -g
。
我甚至把 Node.js 都顺便卸载了。
持续集成与自动部署
构建网站
在项目目录新建 .github/workflows/Build.yaml
(其实文件名随意,只要后缀是 yaml/yml 即可)
1name: Automic Deploy
2on:
3 push:
4 branches:
5 - master
6 workflow_dispatch:
7jobs:
8 Automata:
9 runs-on: ubuntu-latest
10 steps:
11 - uses: actions/checkout@v2
12 - name: Generate Website
13 run: |
14 chmod u+x "./hugo_Linux-64bit"
15 ./hugo_Linux-64bit --minify
上述代码只是进行了 Checkout,然后运行了 ./hugo_Linux-64bit
生成网站。没错,我把可执行文件直接扔进 repo 里面了,这种用法不被提倡,但是方便、兼容性高并且构建速度也很快。不喜欢这样做的可以使用 Hugo Setup 这个 Action。先将写好的 workflow push 到 GitHub 测试一下,如果没问题再继续。
部署到 Workers Sites
先到 https://github.com/{username}/{repo}/settings/secrets/actions
添加 secret,名称填写 CLOUDFLARE_WORKERS_TOKEN
,值自然是前面创建的 API Token (建议 roll 一个新的)。然后在 Build.yaml
里补上下面这一段,因为我不喜欢拉取 docker,所以直接把 wrangler 安装到了项目目录下。另外我选择每次都安装最新的 wrangler,所以没有缓存 node_modules。
16 - name: Deploy
17 run: |
18 npm i @cloudflare/wrangler
19 npx wrangler publish
20 env:
21 CF_API_TOKEN: ${{ secrets.CLOUDFLARE_WORKERS_TOKEN }}
如果不介意拉取 docker 可以直接使用 Cloudflare 官方提供的 Action:
16 - name: Publish
17 uses: cloudflare/wrangler-action@1.3.0
18 with:
19 apiToken: ${{ secrets.CLOUDFLARE_WORKERS_TOKEN }}
20
21
处理杂物
这里我通过一串简单的例子讲解一下各种用法。
首先是每次部署完后通知搜索引擎网站已经更新:
22 - name: Ping Google
23 run: curl -fsSL "https://www.google.com/ping?sitemap=xxxxxx"
然后改进一下上述代码,让这个步骤只在 public/sitemap.xml
发生改变时进行。
22 - name: Detect Cache Hit
23 id: cache
24 uses: actions/cache@v2
25 with:
26 path: ~/notexist
27 key: ${{ hashFiles('public/sitemap.xml') }}
28 - name: Ping Google
29 if: steps.cache.outputs.cache-hit != 'true'
30 run: curl -fsSL "https://www.google.com/ping?sitemap=xxxxxx"
上述代码中使用了 cache@v2
这个 Action,主要目的是为了将 public/sitemap.xml
的哈希值记录下来并作为缓存的索引,下次运行时如果缓存命中就表示这个文件没发生改变,所以不需要 Ping Google。当然,使用多个文件或文件夹计算 Hash Key 也是可以的,例如 ${{ hashFiles('content/*.md') }}
。另外由于我们只需要知道缓存是否命中而不在乎缓存的内容,所以代码中直接缓存了一个不存在的文件 ~/notexist
。
当一个步骤失败时,GitHub Action 默认会取消所有后续步骤,所以例如需要通知多个搜索引擎时,需要使用 if: always()
确保当前步骤不会因为其它步骤失败而取消,再配合其它限制例如 steps.cache.outcome == 'success'
保证只会在必要时触发。
22 - name: Detect Cache Hit
23 id: cache
24 uses: actions/cache@v2
25 with:
26 path: ~/notexist
27 key: ${{ hashFiles('public/sitemap.xml') }}
28 - name: Ping Google
29 if: always() && steps.cache.outcome == 'success' && steps.cache.outputs.cache-hit != 'true'
30 run: curl -fsSL "https://www.google.com/ping?sitemap=xxxxxx"
31 - name: Ping Bing
32 if: always() && steps.cache.outcome == 'success' && steps.cache.outputs.cache-hit != 'true'
33 run: curl -fsSL "https://www.bing.com/ping?sitemap=xxxxxx"
在前面的代码中使用了 cache@v2
,而 GitHub Action 会驱逐超过 7 天未被访问的缓存,可以通过一个 Workflow 来定时访问缓存从而防止缓存失效,新建 .github/workflows/AccessCache.yaml
:
name: Access Cache
on:
schedule:
- cron: "0 0 */6 * *"
workflow_dispatch:
jobs:
Access:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- run: chmod u+x "./hugo_Linux-64bit" && ./hugo_Linux-64bit --minify
- uses: actions/cache@v2
with:
path: ~/notexist
key: ${{ hashFiles('public/sitemap.xml') }}
上述代码中 schedule
定义了每 6 天触发一次,而 workflow_dispatch
表示这个 workflow 可以被手动触发。定时触发还可以用来 Keep Alive 各种后台服务,或者记录网站的 uptime 等等。
Workers Sites 配置(进阶篇)
通过编辑 workers-site/index.js 可以实现更多的功能,例如:
- 通过跳转去掉 URL 结尾的 index.html。
- 静态资源设置更长的缓存时间。
- 添加 HTTP Header。
- 使用 Server Push。
下方是我个人的配置,可以作为参考,不过注意由于我的 CSP 设置的非常严格,直接照搬可能导致网站的部分内容无法显示。
import { getAssetFromKV } from '@cloudflare/kv-asset-handler'
async function handleEvent(event) {
const { origin, pathname, search } = new URL(event.request.url)
const cacheExtRegExp = new RegExp(/\.(css|js|ttf|woff|jpg|png|ico|svg)$/)
let response
try {
// 'example.com/*/index.html' -> 'example.com/*/'
if (pathname.endsWith('/index.html')) {
response = new Response(null, {
status: 301,
headers: {
Location: `${origin}${pathname.substring(0, pathname.length - 10)}${search}`,
'Cache-Control': 'max-age=86400',
},
})
// generate_204
} else if (pathname === '/generate_204') {
response = new Response(null, { status: 204 })
// Static content 1 year cache
} else if (cacheExtRegExp.test(pathname)) {
response = await getAssetFromKV(event, {
cacheControl: {
edgeTTL: 31536000,
browserTTL: 31536000,
cacheEverything: true,
},
})
response.headers.set('cache-control', 'public, max-age=31536000, immutable')
// Default 4 hour cdn cache, 1 hour browser cache
} else {
response = await getAssetFromKV(event, {
cacheControl: {
edgeTTL: 14400,
browserTTL: 3600,
cacheEverything: true,
},
})
}
// Server Push & CSP Header
if (response.headers.get('Content-Type') && response.headers.get('Content-Type').includes('text/html')) {
response.headers.append('Link', '</style/style.min.7f90d7ef9a6c6e1f79015475f28cdba49bc95fc4adf49bd909990be3c3f963df.css>; rel=preload; as=style')
response.headers.append('Link', '</style/icomoon.woff?38uz27>; rel=preload; as=font; crossorigin')
response.headers.set('Content-Security-Policy', `default-src 'none'; base-uri 'none'; form-action 'none'; frame-ancestors 'none'; img-src 'self'; media-src 'self'; object-src 'self'; style-src 'self'; font-src 'self'`)
}
} catch (e) {
response = new Response(null, { status: 404 })
}
// Set Headers
response.headers.set('Referrer-Policy', 'same-origin')
response.headers.set('X-Content-Type-Options', 'nosniff')
response.headers.set('X-Frame-Options', 'DENY')
response.headers.set('X-XSS-Protection', '1; mode=block')
return response
}
addEventListener('fetch', event => {
event.respondWith(handleEvent(event))
})
Cloudflare Workers 还有各种用法,例如反代、Header 认证、根据 IP 位置重定向以及重写 HTML 等,可以通过官方提供的各种例子了解。