ChrAlpha's Blog

Thumbnail-%E5%85%89%E9%80%9F%E5%8F%9B%E9%80%83%EF%BC%81%E2%80%94%E2%80%94%E5%B0%86%E7%BD%91%E7%AB%99%E9%83%A8%E7%BD%B2%E8%87%B3%20Cloudflare%20Workers%20Site%20%E4%B8%8A

光速叛逃!——将网站部署至 Cloudflare Workers Site 上

2020-11-29·笔记本

Cloudflare 在不久前发博文宣布 开放一定额度的 Workers KV 免费试用。配合 Cloudflare Workers & Cloudflare Workers Site 更能打出一套强有力的组合拳。1G 的免费键值存储供尝鲜而言足矣,而此空间也能满足一般小站的托管,免费享受 Cloudflare 全球边缘节点部署岂不美哉?

我的博客站点与资源是分开存放的,目前生成的网站静态文件还不及 1G。嗯,前一套方案才落地半年多,我又双叒叕叛逃了……

那好吧,还是把这次迁移的过程记录下来。本文方法高度来源于「将 Hexo 部署到 Cloudflare Workers Site 上的趟坑记录」。有所不同的是,我会不厌其烦地将部分细节——那些原作者认为无需赘述的细节——一一罗列仔细。或许可操作性会更强。

Cloudflare Workers 存在了有些时日,而每次执行后 Workers 都会注销进程以释放性能。这让一些持久化的任务难以实现。而 Workers KV 的出现,通过键值对存储空间让任务状态得以留存、延续。去年 9 月诞生的 Cloudflare Workers Site 将网站存储至 Workers KV、通过 Workers 处理请求,实现了将网站部署至 Cloudflare 全球节点上。对于本来就使用 Cloudflare 托管的域名,将网站通过 Cloudflare Workers Site 部署更是免去了飘忽不定的回源请求,让访问更加稳定。

既然都是预先生成好网站文件存储于 Workers KV,自然只能是静态网站。不过换一个角度,如果你的网站曾经部署在 GitHub Pages、Netlify 等平台上的,迁移到 Cloudflare Workers Site 应该不是问题。

通过 Wrangler 部署

尽管 Cloudflare Workers 可以直接在网页端编辑部署,Workers Site 还是需要本地通过 Wrangler CLI 配置。你可以阅读 Cloudflare 文档 了解 Wrangler 多种安装方式。

安装 Wrangler

可以通过 npm 或 cargo 安装 Wrangler。由于我已经配置了 Node.js 环境,就直接通过 npm 安装了。

npm i @cloudflare/wrangler -g

如果不希望接触 npm 或 carge,你也可以 手动安装

本地初始化

在你需要部署的项目下执行初始化命令:

wrangler init --site my-static-site

最后的 my-static-site 是项目名称,可以自行填写。

随后 Cloudflare 会在项目中生成 workers-site 文件夹和 wrangler.toml 配置文件。

配置文件还是要多注意下,先是一些基本配置:

name = "blog"
type = "webpack"
account_id = ""
workers_dev = false
route = "blog.ichr.me/*"
zone_id = ""

account_idzone_id 在 Cloudflare 面板可以找到。route 是自定义域名,留空则默认使用 Cloudflare 提供的三级域名。

Cloudflare IDs

接下来是网站的配置:

[site]
bucket = "./public"
entry-point = "workers-site"

bucket 是存放生成好的网站静态文件的目录,对于 Hexo 而言默认是 ./public 目录下,其他生成器可能有所不同。

发布!

初始化完成后,生成好网站静态页面目录,就可以通过 Wrangler CLI 将网站部署到 Cloudflare Workers Site 上了。当然,在此之前需要验证身份。你可以通过 Cloudflare 邮箱 + 密码的形式验证,亦可生成一串 API Token 验证。更推荐后者,因为直接通过账号密码验证无法有效掌控权限、泄露后危害更大,并且账号密码验证是会弹出浏览器以授权的,纯命令行环境就比较尴尬了。

在「Cloudflare Profile - API Tokens」新建一个 API Token,选择「Edit Cloudflare Workers」模板。并将不需要的两个权限——「Account Settings」和「User’s Details」删去。

cf remove unused permissions

之后也应只开放准备部署的域的权限。

cf specific zone

将生成的 API Token 保存好,这串字符之后不会再出现。

重新在终端中运行 wrangler config,并将 API Token 输入进随后的提示框中,稍加等待便会提示验证成功。

好像很简单的一步,却卡了我一阵子。如果设备开启的代理,很可能会被代理转发流量而导致验证失败,报错 unexpected EOF during handshake

可以借助代理工具的 log 监视请求再单独编写规则绕过。同样的,诸如 AdGuard 等广告屏蔽工具最好也关闭以免影响验证。

验证通过后即可通过命令预览、发布:

# 上传并预览
wrangler preview --watch

# 上传并发布
wrangler publish

自定义 Workers 处理请求行为

Cloudflare Workers Site 通过 Workers 处理请求并返回相应 HTML 页面,自然可以通过编写 Workers 代码以自定义其部分行为,例如根据 URL 实施不同的缓存配置、添加特定响应头、返回 404 页面、实现 Server Push 等。

此时你打开 Cloudflare Workers 面板,点入该为网站创建的项目,试图向其他 Workers 一样直接「Quick Edit」,扑面而来的确是一板被压缩过的代码。如果你以前只直接通过 Workers 面板编辑一些简单的程序,现在可能会突然疑惑。Workers Site 不能在面板编辑,至少被压缩后面目全非的代码也很难再察觉哪是哪了。

正确的姿势是在修改项目 workers-site 目录下的 index.js。打开这个文件顿时觉得亲切很多,这不过这里可以方便地索引 npm 依赖。

最终完整配置

当然,你也可以直接参考我修改后的最终 Router。

import { getAssetFromKV, mapRequestToAsset } from '@cloudflare/kv-asset-handler';

addEventListener('fetch', (event) => {
    try {
        event.respondWith(handleEvent(event));
    } catch (e) {
        event.respondWith(new Response('Internal Error', { status: 500 }));
    }
});

async function handleEvent(event) {
    const url = new URL(event.request.url);
    const { origin, pathname: path, search } = new URL(event.request.url);
    let options = {};

    try {
        // 将 `/index.html` 结尾的请求重定向至 `/`
        if (path.endsWith('/index.html')) {
            return new Response(null, {
                status: 301,
                headers: {
                    Location: `${origin}${path.substring(0, path.length - 10)}${search}`,
                    'Cache-Control': 'max-age=3600',
                },
            });
        }

        // 手动提高 RSS 页面的缓存 TTL
        if (path === '/atom.xml') {
            return getAssetFromKV(event, {
                cacheControl: {
                    edgeTtl: 6 * 60 * 60,
                    browserTtl: 12 * 60 * 60,
                    cacheEverything: true,
                },
            });
        }

        // CSS 文件超长时间缓存
        if (path.startsWith('/css/')) {
            const response = await getAssetFromKV(event, {
                cacheControl: {
                    edgeTtl: 365 * 24 * 60 * 60,
                    browserTtl: 365 * 24 * 60 * 60,
                    cacheEverything: true,
                },
            });
            response.headers.set('cache-control', `public, max-age=${365 * 24 * 60 * 60}, immutable`);
            return response;
        }

        // 其余默认 4 小时 CDN 缓存、1 小时浏览器缓存
        const response = await getAssetFromKV(event, {
            cacheControl: {
                edgeTtl: 4 * 60 * 60,
                browserTtl: 60 * 60,
                cacheEverything: true,
            },
        });

        response.headers.set('X-XSS-Protection', '1; mode=block');

        // Server Push 样式文件
        if (response.headers.get("Content-Type").includes('text/html')) {
            response.headers.append('Link', '</css/main.css>; rel=preload; as=style');
        }

        return response;
    } catch (e) {
        // 未找到资源,返回 404 页面
        let notFoundResponse = await getAssetFromKV(event, {
            mapRequestToAsset: (req) => new Request(`${new URL(req.url).origin}/404.html`, req),
        });

        return new Response(notFoundResponse.body, { ...notFoundResponse, status: 404 });
    }
}

下面列举几个我采取了的配置。

自定义缓存配置

@cloudflare/kv-asset-handler 的 README 中就给出了 缓存配置的样例。你可以在用 getAssetFromKV 时将这个参数传入。

const response = await getAssetFromKV(event, {
    cacheControl: {
        browserTtl: 5 * 60,  // 5 min for browser
        edgeTtl: 60 * 60,    // 1 hour for edge network
        bypassCache: false,  // do not bypass Cloudflare's cache
    }
});

而对于 CSS 文件,由于我在每次修改都会更新一个随机后缀。所以这些直接设置 1 年的 TTL,并手动配置 cache-control

const { pathname: path } = new URL(event.request.url);
if (path.startsWith('/css/')) } {
    const response = await getAssetFromKV(event, {
        cacheControl: {
            browserTtl: 365 * 24 * 60 * 60,
            edgeTtl: 365 * 24 * 60 * 60,
            bypassCache: false,
        }
    });
    response.headers.set('cache-control', `public, max-age=${365 * 24 * 60 * 60}, immutable`);
}

Server Push 样式文件

除了 404 页面,我的网站所有 HTML 页面都需要同一个 CSS 文件。那不妨为所有 HTML 请求添加一个响应头推送该样式文件。

if (response.headers.get("Content-Type").includes('text/html')) {
    response.headers.append('Link', '</css/main.css>; rel=preload; as=style');
}

404 页面

对于指向不存在资源的请求,可以返回特定的 404 页面告知用户「页面不存在」。Cloudflare 默认以 /404.html 作为 404 页面,你也可以编写代码自定义这个行为。

try {
    // 正常请求
} catch (e) {
    // 404
    const notFoundPath = '/custom/404.html';
    const notFoundResponse = await getAssetFromKV(event, {
        mapRequestToAsset: req => new Request(`${new URL(req.url).origin}${notFoundPath}`, req),
    });
    return new Response(notFoundResponse.body, { ...notFoundResponse, status: 404 });
}

GitHub Actions 自动化部署

在迁移至 Cloudflare Workers Site 之前,我就已经借助 GitHub Actions 自动构建并部署至静态页面托管平台。这次不过是把 Actions 中部署部分更新为部署至 Cloudflare Workers Site 上。为验证身份,你需要将刚刚生成的 Cloudflare API Token 添加至 GitHub 仓库的 Secret 中。

具体代码就不多解释了,有兴趣也可看看我之前写的一篇「无后端静态博客自动化部署」。

name: Hexo Deploy Automatically

on: [push]

jobs:
  build:

    runs-on: ubuntu-latest
    
    strategy:
      matrix:
        node-version: [12.x]

    steps:
      - name: Checkout
        uses: actions/checkout@v2
        
      - name: Use Node.js ${{ matrix.node-version }}
        uses: actions/setup-node@v1
        with:
          node-version: ${{ matrix.node-version }}
      
      - name: Generate
        run: |
          npm i && npx hexo g
      
      # 仅仅是 Deploy 部分有所不同而已
      - name: Deploy
        uses: cloudflare/wrangler-action@1.3.0
        with:
          apiToken: ${{ secrets.CF_WORKERS_TOKEN }}  # CF_WORKERS_TOKEN 为保存 Token 的 Secret 名

更新迭代好一段时间,Cloudflare Workers Site 也没有那么多坑留给后人踩了。而将网站直接部署至 Cloudflare 全球边缘加速节点上,免去几百毫秒的回源,还能通过 Workers 根据自定义规则处理请求、控制响应。而这些,熟练的话,半小时就能部署好。为何不试试呢?

光速叛逃!——将网站部署至 Cloudflare Workers Site 上
本文作者
ChrAlpha
发布日期
2020-11-29
更新日期
2020-11-29
转载或引用本文时请遵守 CC BY-NC-SA 4.0 许可协议,注明出处、不得用于商业用途!