用 Cloudflare Worker 來佈署靜態網頁
Tags: web, cloudflare, deployment
Published: 2020-07-23 00:54:17

得益於 Cloudflare 在全球超過 200 多個節點，我們可以很輕易的將我們的應用程式快速的推向 Cloudflare 的全球網路邊緣，這可以讓我們的應用程式的 Response Time 變得更低，如一個以 SPA 技術開發的 Web Application 就可以很輕易的佈署至 Cloudflare Worker。

---

### 基礎環境建立

首先我們需要安裝用於佈署 Worker 的工具：@cloudflare/wrangler，我們可以透過 npm 來安裝這個工具：

```bash
~$ sudo npm install -g @cloudflare/wrangler
```

安裝完畢之後我們進入 Cloudflare 的網站，在右欄的部份找到這個區塊，並按下 "取得您的 API Token"。

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/8p7p0eelmo6cqnbz9v6c.png)

進入頁面之後點選 API Token 的 Tab 並按下"建立 Token"

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/gg3t4wwfc00dkpfbdhhz.png)

並且在建立的頁面中點選 "編輯 Cloudflare Worker"，並進入頁面

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/2cv9sjjm4gy67krd3sb1.png)

在權限的區塊，這裡選定的區塊是 "編輯 cloudflare worker 必要的權限"，因此若沒有其他的需求，就不用額外調整權限區塊。在帳戶資源部份請至少選擇 "包含" 自己的帳戶。區域資源的部份可以選擇特定區域，以免不小心異動其他網域的內容，如果沒有這類的疑慮，也可以直接選擇包含所有區域。

確定之後就會跳出摘要頁面，確認資料無誤之後就可以按下"建立 Token"：

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/php6w26o3lz8szd5jmwe.png)

取得上述的 Token 之後我們就可以回到 Terminal，輸入指令後按照要求將 API Token 貼上

```bash
~$ wrangler config

To find your API Token, go to https://dash.cloudflare.com/profile/api-tokens
and create it using the "Edit Cloudflare Workers" template.

If you are trying to use your Global API Key instead of an API Token
(Not Recommended), run `wrangler config --api-key`.
Enter API Token: 
B4jLkMW933P3P5tsDOqLbKZLi9UMLPEkH8xQdOI7
 Validating credentials...
 Successfully configured. You can find your configuration file at: /home/floatflower/.wrangler/config/default.toml
```

看到 Successfully configured 之後就代表環境已經設定好囉。

### 佈署網站

#### 建立一個靜態網頁模板

```bash
~$ wrangler generate --site mysite

Creating project called `mysite`...
 Done! New project created /home/floatflower/mysite
 You will need to update the following fields in the created wrangler.toml file before continuing:
 You can find your account_id in the right sidebar of your account's Workers page, and zone_id in the right sidebar of a zone's overview tab at https://dash.cloudflare.com
- account_id

~$ cd mysite
```

接著我們就會拿到這些檔案：

```
.
├── node_modules
├── package-lock.json
├── public
│   ├── 404.html
│   ├── favicon.ico
│   ├── img
│   │   ├── 200-wrangler-ferris.gif
│   │   └── 404-wrangler-ferris.gif
│   └── index.html
├── workers-site
│   ├── index.js
│   ├── package.json
│   └── package-lock.json
└── wrangler.toml

```

+ public 資料夾就是放置靜態網站的資料夾，你可以把你的靜態網頁放在裡面。
+ worker-site 就是 Cloudflare Worker 的程式碼，你可以在這裡定義自己的 Cloudflare Worker 行為，但是在這裡我們也不用做其他改變，直接用預設的就可以了。

#### 調整 wrangler.toml

我們打開 wrangler.toml 裡面含有這些內容：

```toml
# worker 名稱
name = "mysite" 
type = "webpack"
account_id = ""

# 如果選擇 true 的話，Cloudflare 會提供給你一個 .workers.dev 網域的網址，因為我們要提供網域，所以我們將其設為 false。
workers_dev = false
# 如果 workers_dev 是 false 時，這個 route 一定要提供，我這邊示範的網址是 mysite.fres.host。
route = "mysite.fres.host/*"
zone_id = ""

[site]
bucket = "./public"
```

其中的 `account_id` 以及 `zone_id` 我們就會回到剛才取得 API Token 那個區塊：

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/8p7p0eelmo6cqnbz9v6c.png)

其中的區域識別碼貼至 `zone_id` 以及帳戶識別碼貼至 `account_id`。

#### 佈署上線

接著我們執行佈署的指令

```bash
~$ wrangler publish

...

 Built successfully, built project size is 13 KiB.
 Created namespace for Workers Site "__mysite-workers_sites_assets"
 Success
 Uploading site files
 Deployed to the following routes:
mysite.fres.host/* => created
```

接著我們回到 Cloudflare Worker 的頁面：

![Workers](https://dev-to-uploads.s3.amazonaws.com/i/z0ao9spuvrm9p0ukhb3l.png)

就會看到一個 `mysite.fres.host/* => mysite`，但是還沒結束。我們要進入 DNS 的頁面，新增一個 DNS Record：

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/nu39v5gbkyfotbkxlc75.png)

DNS Record 的值可以為隨意，因為全部都會被 Cloudflare 攔截起來，所以網域不管指向哪裡都可以，對應完畢之後我們就可以打開瀏覽器並訪問網址，在這裡我的網址是 https://mysite.fres.host 。

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/tkshlg94yxp203tn4n6t.png)

就會看到 Cloudflare 的預設頁面，做到這裡最基本的 Cloudflare 佈署就完成了。

## 番外篇：用 Cloudflare Worker 佈署 History mode 的 Vue SPA Application

首先我們先開啟一個 Vue SPA 的應用程式，並選擇開啟 History Mode，然後我們將剛才產生的 wrangler.toml 以及 worker-site 移到這個 mysite_spa 底下。

```bash
~$ vue create mysite_spa
```

接著在設定之前首先先看一下 Vue.js 官方文件提到當需要佈署 History mode 的應用程式時，所需要做的設定，以 Nginx 做範例：

```
location / {
	try_files $uri $uri/ /index.html;
}
```

概念就是先 try-file 看看有沒有路徑對應的文件，如果沒有則遞送 index.html。

### 更改 wrangler.toml

因為在 Vue Application 中 public 資料夾另有他用，因此我們不能照常將 `wrangler.toml` 的 bucket 設為 `./public` 資料夾，而是要將其改成 `./dist`。

```toml
# 將 site 區塊的設定改成這樣
[site]
bucket = "./dist"
```

### 更改 worker-site/index.js

接著我們要修改 `worker-site/index.js` 檔案的內容，以符合上述提到的 Nginx 遞送 Vue Application 的邏輯。

我們先打開 `worker-site/index.js`

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

/**
 * The DEBUG flag will do two things that help during development:
 * 1. we will skip caching on the edge, which makes it easier to
 *    debug.
 * 2. we will return an error message on exception in your Response rather
 *    than the default 404.html page.
 */
const DEBUG = false

addEventListener('fetch', event => {
    try {
        event.respondWith(handleEvent(event))
    } catch (e) {
        if (DEBUG) {
            return event.respondWith(
                new Response(e.message || e.toString(), {
                    status: 500,
                }),
            )
        }
        event.respondWith(new Response('Internal Error', { status: 500 }))
    }
})

async function handleEvent(event) {
    const url = new URL(event.request.url)
    let options = {}

    /**
   * You can add custom logic to how we fetch your assets
   * by configuring the function `mapRequestToAsset`
   */
    // options.mapRequestToAsset = handlePrefix(/^\/docs/)

    try {
        if (DEBUG) {
            // customize caching
            options.cacheControl = {
                bypassCache: true,
            }
        }

        const page = await getAssetFromKV(event, options)

        // allow headers to be altered
        const response = new Response(page.body, page)

        response.headers.set('X-XSS-Protection', '1; mode=block')
        response.headers.set('X-Content-Type-Options', 'nosniff')
        response.headers.set('X-Frame-Options', 'DENY')
        response.headers.set('Referrer-Policy', 'unsafe-url')
        response.headers.set('Feature-Policy', 'none')

        return response

    } catch (e) {
        // if an error is thrown try to serve the asset at 404.html
        if (!DEBUG) {
            try {
                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 })
            } catch (e) {}
        }

        return new Response(e.message || e.toString(), { status: 500 })
    }
}

/**
 * Here's one example of how to modify a request to
 * remove a specific prefix, in this case `/docs` from
 * the url. This can be useful if you are deploying to a
 * route on a zone, or if you only want your static content
 * to exist at a specific path.
 */
function handlePrefix(prefix) {
    return request => {
        // compute the default (e.g. / -> index.html)
        let defaultAssetKey = mapRequestToAsset(request)
        let url = new URL(defaultAssetKey.url)

        // strip the prefix from the path for lookup
        url.pathname = url.pathname.replace(prefix, '/')

        // inherit all other props from the default request
        return new Request(url.toString(), defaultAssetKey)
    }
}
```

程式有點長，不過我們並沒有要修改太多，找到以下這個區塊，這個區塊定義了當應用程式找不到指定檔案的時候就返回 404.html 並將 Http Status Code 設為 404 Not Found。

```js
// if an error is thrown try to serve the asset at 404.html
if (!DEBUG) {
    try {
        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 })
    } catch (e) {}
}
```

因此我們將這段程式修改成這樣：

```js
// if an error is thrown try to serve the asset at 404.html
if (!DEBUG) {
    try {
        let notFoundResponse = await getAssetFromKV(event, {
            mapRequestToAsset: req => new Request(`${new URL(req.url).origin}/index.html`, req),
        })

        return new Response(notFoundResponse.body, { ...notFoundResponse, status: 200 })
    } catch (e) {}
}
```

這樣就可以在找不到檔案時遞送 index.html 給客戶端了，完成之後我們做一次打包，然後我們透過 `wrangler dev` 指令打開測試伺服器並訪問 http://localhost:8787 ，來看看設定是否正確。

```bash
~$ npm run build
~$ wrangler dev
```

在 Vue Application 的 Demo 頁面中就可以直接嘗試是否 History Vue Router 可以正常運作：

**http://localhost:8787**

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/92zf2oaesdgug3lcsnsd.png)

**http://localhost:8787/about**

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/y95gqfjkqtu3zcgz7i7v.png)

確定無誤之後我們就可以將其佈署至 Cloudflare Worker，最後我們訪問一下佈署的頁面：https://mysite.fres.host

```bash
~$ wrangler publish
```

就可以看到結果頁面：

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/c6koe75wyooicx6sxmmd.png)