🙆

Cloudflare Workers と KV でTodoListアプリを作る

2023/02/05に公開

Cloudflare Workersはサーバレスエッジコンピューティングサービスです。そしてデータの永続的保存オプションとして以下が用意されています。
    KV : key-value型永続ストレージ
    R2 : AWS S3互換ストレージ
    Durable Objects : オブジェクトストレージ
    D1 : エッジで動作するSQL Liteデータベース(2023/02/05 時点でアルファ版です)
    Queues : ジョブのキューイング

この記事ではKVを使ってJavaScriptで簡単なTodoを作ってみます。記事前半分の土台となっているのはこのブログですが、英語であり、また手順として少し初心者にハードルが高く、Cloudflareのバージョンアップにより画面イメージも変更となっているため再構成しています。
後半部分は少し難易度があがりますが、追加シナリオです。ぜひ挑戦してみてください。

なお、筆者はChromeで開発していますので、意図した通りの動作がしない場合、Chromeを使ってみるかTwitter @kameoncloudまでDMください。

  1. (初めて使う方向け)サブドメインの設定方法
    マネージメントコンソール左ペインの"Workers"をクリックすると、サブドメインの入力が求められます。適当な名前を入力してください。"xxxxxx.workers.dev"が皆さんのアカウントに割り当てられたサブドメインになります。次の画面では料金プランの設定が求められますので、"Continue with Free"を押して初期設定を完了してください。

  2. Workersの設定
    マネージメントコンソールにアクセスします。

    "Create application" を押し、次に出てきた画面で"Create Worker"を押します。

    "todolist"と名前を付けます。

    Hello Worldがそのままセットされていますので、Deployを押します。


    "Edit Code"を押します。
    エディターが出てきましたね。Workersは皆さん任意のEditorで開発したものをCLI経由でアップロードすることができますが、Cloudflare専用編集画面も用意されています。なかなかパワフルな機能が備わっていますので、このハンズオンの前半部分はこの画面で作業を進めます。

  3. コードの挿入と編集
    まずは何も考えず画面下の"Save and deploy"、もしくは"Send"を選んでください。以下のように"Hello world"が表示されていれば動作しています。

以下のコードを張り付けて再度実行してください。

const html = todos => `
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <title>Todos</title>
    <link href="https://cdn.jsdelivr.net/npm/tailwindcss/dist/tailwind.min.css" rel="stylesheet"></link>
  </head>

  <body class="bg-blue-100">
    <div class="w-full h-full flex content-center justify-center mt-8">
      <div class="bg-white shadow-md rounded px-8 pt-6 py-8 mb-4">
        <h1 class="block text-grey-800 text-md font-bold mb-2">Todos</h1>
        <div class="flex">
          <input class="shadow appearance-none border rounded w-full py-2 px-3 text-grey-800 leading-tight focus:outline-none focus:shadow-outline" type="text" name="name" placeholder="A new todo"></input>
          <button class="bg-blue-500 hover:bg-blue-800 text-white font-bold ml-2 py-2 px-4 rounded focus:outline-none focus:shadow-outline" id="create" type="submit">Create</button>
        </div>
        <div class="mt-4" id="todos"></div>
      </div>
    </div>
  </body>

  <script>
    window.todos = ${todos || []}

    var updateTodos = function() {
      fetch("/", { method: 'PUT', body: JSON.stringify({ todos: window.todos }) })
      populateTodos()
    }

    var completeTodo = function(evt) {
      var checkbox = evt.target
      var todoElement = checkbox.parentNode
      var newTodoSet = [].concat(window.todos)
      var todo = newTodoSet.find(t => t.id == todoElement.dataset.todo)
      todo.completed = !todo.completed
      window.todos = newTodoSet
      updateTodos()
    }

    var populateTodos = function() {
      var todoContainer = document.querySelector("#todos")
      todoContainer.innerHTML = null

      window.todos.forEach(todo => {
        var el = document.createElement("div")
        el.className = "border-t py-4"
        el.dataset.todo = todo.id

        var name = document.createElement("span")
        name.className = todo.completed ? "line-through" : ""
        name.innerText = todo.name

        var checkbox = document.createElement("input")
        checkbox.className = "mx-4"
        checkbox.type = "checkbox"
        checkbox.checked = todo.completed ? 1 : 0
        checkbox.addEventListener('click', completeTodo)

        el.appendChild(checkbox)
        el.appendChild(name)
        todoContainer.appendChild(el)
      })
    }

    populateTodos()

    var createTodo = function() {
      var input = document.querySelector("input[name=name]")
      if (input.value.length) {
        window.todos = [].concat(todos, { id: window.todos.length + 1, name: input.value, completed: false })
        input.value = ""
        updateTodos()
      }
    }

    document.querySelector("#create").addEventListener('click', createTodo)
  </script>
</html>
`

const defaultData = { todos: [] }

const setCache = (key, data) => TODO_LIST.put(key, data)
const getCache = key => TODO_LIST.get(key)

async function getTodos(request) {
  const ip = request.headers.get('CF-Connecting-IP')
  const cacheKey = `data-${ip}`
  let data
  const cache = await getCache(cacheKey)
  if (!cache) {
    await setCache(cacheKey, JSON.stringify(defaultData))
    data = defaultData
  } else {
    data = JSON.parse(cache)
  }
  const body = html(JSON.stringify(data.todos))
  return new Response(body, {
    headers: { 'Content-Type': 'text/html' },
  })
}

async function updateTodos(request) {
  const body = await request.text()
  const ip = request.headers.get('CF-Connecting-IP')
  const cacheKey = `data-${ip}`
  try {
    JSON.parse(body)
    await setCache(cacheKey, body)
    return new Response(body, { status: 200 })
  } catch (err) {
    return new Response(err, { status: 500 })
  }
}

async function handleRequest(request) {
  if (request.method === 'PUT') {
    return updateTodos(request)
  } else {
    return getTodos(request)
  }
}

addEventListener('fetch', event => {
  event.respondWith(handleRequest(event.request))
})

こんなエラーが出るはずです。ストレージであるKVを作成し、スクリプトから呼び出す設定(バインディングと言います)をしていないためです。

ではここから、KVを設定していきます。

  1. KVの設定
    マネージメントコンソールからKVを選びます。

    "Create namespace"を選んで"TODO_LIST"と入力し"Add"を押します

    これでKVが作成されました。ただしこれだけではスクリプトから今作成されたKVは認識できないため、バインディングを行う必要があります。
    Workersの詳細画面へ移動し(todolistをクリックします)、"Settings"タブを押します。

    "Variables"をクリックすると、画面中段に"KV Namespace Bindings"が出てきますので、"Add binding"を押します。

    以下のように入力し"Save and deploy"を押します。左がスクリプト上で呼ぶ出す際の名前、右がシステム上認識されるKVの名前です。

    エディタから再度スクリプトを実行しますと以下のようにTodoが出力されるはずです。

    適当な文字列を"Create"したり、チェックボックスで削除したりしてみてください。
    (2023/08/20 Quick EditorだとTODO_LISTのバインディングを正しく認識しないケースがあるようです。その場合Editorからの作業ではなく以下のブラウザからの作業に進んで下さい)

最後にブラウザから、WorkersにアサインされているURL(エディタの画面Todoの上あたりにURLが表示されています)にアクセスして表示されていれば完了です。これでエッジ環境で動作する簡易Webサイト、データベースが完了しました。(表示されているtumai.workers.devは私の環境の値ですので皆さんごとに異なることに注意してください)

  1. (Option)様々なデータ操作
    今までのStepで簡単にデータ操作を行うHTMLを作成可能であることを体験いただきましたが、これから様々な方法でデータを記載していきます。
    その前に、より簡単にデータを出力できるようにエディターでスクリプトを書き換えておきます。
export default {
  async fetch(request, env, ctx) {
    const value = await env.TODO_LIST.get("test");

    if (value === null) {
      return new Response("Value not found",  { status:  404})
    }
    return new Response(value);
  },
};

内容は単純です。"test"という文字列に合致したKEYがあった場合のみ、Valueの値を出力します。
最初は実行すると"Value not found"が出力されます。
では、マネージメントコンソールのKV画面から"TODO_LIST"を特定して"View"をクリックします。
"test"をKeyとして適当な値を"Value"に書き込み、"Add Entry"を押してみてください。

値が一つ追加されます。テスト用にとても便利な機能です。

再度スクリプトを実行すると、ひきつづき"Value not found"が表示されます。これはKVを利用するうえでとても重要なことですが、KVは世界中の275を超えるエッジロケーションで動作する代わりにデータの伝搬が最大60秒かかります。このため、全エッジロケーションで一貫した低レイテンシの処理が求められる場合には別のアーキテクチャを考慮する必要があるので注意してください。
何度か実行すると値が書き換わったことがわかります。
では次のテストに備えて3つの黒ポチを押して"Delete"を選び、データを削除しておきます。

またcURLからAPI経由でのデータ操作ももちろん可能です。API操作のためにはAPI用キーの発行などが必要になります。また別の機会にブログを上げたいと思います。

  1. (Optional)WranglerからのKV環境設定
    WranglerはWorkersの開発環境を構築する専用CLIです。このページを参考に"Hello world"まで実行してください。ページはWindowsとタイトルがついていますがMacでも同じ手順です。(Nodeやnpmのインストール手順が違うだけ)

では最初に以下を実行します。
wrangler init todowrangler
求められる設定はHello worldと同じです。
cd todowranglerを行った後以下のスクリプトで
src/index.jsの内容を置き換えておきます。

export default {
  async fetch(request, env, ctx) {
    const value = await env.TODO_WRANGLER.get("test");

    if (value === null) {
      return new Response("Value not found",  { status:  404})
    }
    return new Response(value);
  },
};

次に以下を実行しKVを再度作成します。
wrangler kv:namespace create "TODO_WRANGLER"
マネージメントコンソールで新しいKVができてることを確認します。
Authenticationエラーが出る人はwrangler loginでログインをしてください。


ターミナルで以下をコピーしておきます。
{ binding = "TODO_WRANGLER", id = "your namespace id" }
(your namespace id は出力されているIDをそのまま使ってください。皆さん毎に固有です。)
wrangler.tomlを開き以下の行を一番最後に追記します。

kv_namespaces = [
{ binding = "TODO_WRANGLER", id = "your namespace id" }
]

(your namespace id は出力されているIDをそのまま使ってください。皆さん毎に固有です。)
wrangler publishを実行しDeployしてください。
Workersが新しく作成されています。Settingsを見ると新しくKVがバインディングされていることも分かります。

ブラウザでアクセスすれば"Value not found"と表示されれば成功です。
KVのマネージメントコンソールで以下の値を書き込み、しばらくしてブラウザから表示されれば完了です。

最後にwrangler経由でのデータ読み書きを試してみます。
再度マネージメントコンソールで"test"のアイテムを削除したのち以下を実行します。
wrangler kv:key put --binding=TODO_WRANGLER "test" "data from wrangler"
データが書き込まれていますので
wrangler kv:key get --binding=TODO_WRANGLER "test"
を実行するとデータを呼び出すことができます。

いかがでしたでしょうか。
WorkersとKVの手軽さとパワフルさを体験いただけたかと思います。私もまだまだ勉強中ですので一緒にがんばりましょう!

Discussion