IBM Cloud Docs
エッジ機能のユース・ケース

エッジ機能のユース・ケース

以下のユース・ケースは例としてのみ提供されており、ご使用の環境に正確に同じであることを意図したものではありません。

以下のコードをテストする際には、サービスの中断を引き起こす可能性があるため、注意してください。

A/B テスト

CIS エッジ機能を作成して、A/B テストを制御できます。

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

async function fetchAndApply(request) {
  const name = 'experiment-0'
  let group          // 'control' or 'test', set below
  let isNew = false  // is the group newly-assigned?

  // Determine which group this request is in.
  const cookie = request.headers.get('Cookie')
  if (cookie && cookie.includes(`${name}=control`)) {
    group = 'control'
  } else if (cookie && cookie.includes(`${name}=test`)) {
    group = 'test'
  } else {
    // 50/50 Split
    group = Math.random() < 0.5 ? 'control' : 'test'
    isNew = true
  }

  // We'll prefix the request path with the experiment name. This way,
  // the origin server merely has to have two copies of the site under
  // top-level directories named "control" and "test".
  let url = new URL(request.url)
  // Note that `url.pathname` always begins with a `/`, so we don't
  // need to explicitly add one after `${group}`.
  url.pathname = `/${group}${url.pathname}`

  const modifiedRequest = new Request(url, {
    method: request.method,
    headers: request.headers
  })

  const response = await fetch(modifiedRequest)

  if (isNew) {
    // The experiment was newly-assigned, so add a Set-Cookie header
    // to the response.
    const newHeaders = new Headers(response.headers)
    newHeaders.append('Set-Cookie', `${name}=${group}; path=/`)
    return new Response(response.body, {
      status: response.status,
      statusText: response.statusText,
      headers: newHeaders
    })
  } else {
    // Return response unmodified.
    return response
  }
}

応答ヘッダーの追加

応答ヘッダーを変更するには、まず応答のコピーを作成して、変更可能にします。 次に、Headersインターフェースを使用してヘッダーを追加、変更、または削除することができます。

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

/**
 * Set the `x-my-header` header
 * @param {Request} request
 */
async function handleRequest(request) {
  let response = await fetch(request);

  // Make the headers mutable by re-constructing the Response.
  response = new Response(response.body, response);
  response.headers.set('x-my-header', 'custom value');
  return response;
}

複数の要求の集約

この例では、異なるAPIエンドポイントに複数のリクエストを行い、それらの応答をまとめて単一の応答として返します。

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

/**
 * Make multiple requests,
 * aggregate the responses and
 * send it back as a single response
 */
async function fetchAndApply(request) {
    const init = {
      method: 'GET',
      headers: {'Authorization': 'XXXXXX'}
    }
    const [btcResp, ethResp, ltcResp] = await Promise.all([
      fetch('https://api.coinbase.com/v2/prices/BTC-USD/spot', init),
      fetch('https://api.coinbase.com/v2/prices/ETH-USD/spot', init),
      fetch('https://api.coinbase.com/v2/prices/LTC-USD/spot', init)
    ])

    const btc = await btcResp.json()
    const eth = await ethResp.json()
    const ltc = await ltcResp.json()

    let combined = {}
    combined['btc'] = btc['data'].amount
    combined['ltc'] = ltc['data'].amount
    combined['eth'] = eth['data'].amount

    const responseInit = {
      headers: {'Content-Type': 'application/json'}
    }
    return new Response(JSON.stringify(combined), responseInit)
}

条件付きルーティング

使用されているデバイスに応じて異なるコンテンツを配信する最も簡単な方法は、関心のある条件に基づいてリクエストの URL を書き換えることです。 以下の例を参照してください。

装置タイプ

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

async function fetchAndApply(request) {
  let uaSuffix = ''

  const ua = request.headers.get('user-agent')
  if (ua.match(/iphone/i) || ua.match(/ipod/i)) {
    uaSuffix = '/mobile'
  } else if (ua.match(/ipad/i)) {
    uaSuffix = '/tablet'
  }

  return fetch(request.url + uaSuffix, request)
}

カスタム・ヘッダー

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

async function fetchAndApply(request) {
  let suffix = ''
  //Assuming that the client is sending a custom header
  const cryptoCurrency = request.headers.get('X-Crypto-Currency')
  if (cryptoCurrency === 'BTC') {
    suffix = '/btc'
  } else if (cryptoCurrency === 'XRP') {
    suffix = '/xrp'
  } else if (cryptoCurrency === 'ETH') {
    suffix = '/eth'
  }

  return fetch(request.url + suffix, request)
}

起点のない応答

直接エッジから応答を戻すことができます。 発信元に要求を送信する必要はありません。

POST と PUT の HTTP 要求の無視

POST と PUT の HTTP 要求を無視します。 以下のスニペットを使用すると、その他の要求をすべて起点にパススルーできます。

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

async function fetchAndApply(request) {
  if (request.method === 'POST' || request.method === 'PUT') {
    return new Response('Sorry, this page is not available.',
        { status: 403, statusText: 'Forbidden' })
  }

  return fetch(request)
}

スパイダーまたはクローラーの拒否

望ましくないスパイダーまたはクローラーから起点を保護します。 以下のケースでは、ユーザー・エージェントが「うるさいロボット」の場合、エッジ機能は要求を起点に送信する代わりに応答を戻します。

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

async function fetchAndApply(request) {
  if (request.headers.get('user-agent').includes('annoying_robot')) {
    return new Response('Sorry, this page is not available.',
        { status: 403, statusText: 'Forbidden' })
  }

  return fetch(request)
}

特定の IP の接続の防止

IP アドレスを拒否リストに登録します。 このコードの断片は、特定のIP(この場合は 225.0.0.1 )がオリジンに接続するのを防ぎます。

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

async function fetchAndApply(request) {
  if (request.headers.get('cf-connecting-ip') === '225.0.0.1') {
    return new Response('Sorry, this page is not available.',
        { status: 403, statusText: 'Forbidden' })
  }

  return fetch(request)
}

Post 要求

HTTP POST 要求の内容を読み取ります。

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

/**
 * Making a curl request that looks like
 * curl -X POST --data 'key=world' example.com
 * or
 * curl -X POST --form 'key=world' example.com
 */
async function fetchAndApply(request) {
  try {
    const postData = await request.formData();
    return new Response(`hello ${postData.get('key')}`)
  } catch (err) {
    return new Response('could not unbundle post data')
  }
}

エッジ機能から HTTP POST 要求を作成します。

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

/**
 * Create a POST request with body 'key=world'
 * Here, we are assuming that example.com acknowledges the POST request with body key=world
 */
async function fetchAndApply(request) {
  let content = 'key=world'
  let headers = {
    'Content-Type': 'application/x-www-form-urlencoded'
  }
  const init = {
    method: 'POST',
    headers: headers,
    body: content
  }
  const response = await fetch('https://example.com', init)
  console.log('Got response', response)
  return response
}

Cookie の設定

CIS Edge機能を使用してクッキーを設定することができます。

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

async function fetchAndApply(request) {
  let response = await fetch(request)

  const randomStuff = `randomcookie=${Math.random()}; Expires=Wed, 21 Oct 2018 07:28:00 GMT; Path='/';`

  // Make the headers mutable by re-constructing the Response.
  response = new Response(response.body, response)
  response.headers.set('Set-Cookie', randomStuff)

  return response
}

署名済み要求

Web 暗号 API の助けを借りて、要求の署名という一般的な URL 認証方式をエッジ機能に実装できます。

ここで紹介する例では、 CIS は、 SHA-256 のダイジェストアルゴリズムを使用したHMAC(Hash-based Message Authentication Code)により、 URL のパスと有効期限を示すタイムスタンプを認証します。 認証済みリソースを正常にフェッチするには、ユーザー・エージェントは、照会パラメーターを使用して正しいパス、有効期限タイム・スタンプ、および HMAC を指定する必要があります。 これらの 3 つのパラメーターのいずれかが改ざんされている場合、要求は失敗します。

有効期限のタイムスタンプの信頼性は HMAC によって保証されており、つまり、 URL が期限切れとなった際に、HMAC が正しければ、ユーザーが提供したタイムスタンプが正しいと信頼できるということです。 また、お客様が所有する URL が有効期限切れかどうかを確認することもできます。

署名済み要求の検証

この例では、パス名が /verify/ で始まるすべてのリクエストに対するHMACを検証します。 URL。

デバッグの利便性を考慮して、このEdge機能は、 URL またはHMACが無効である場合、または URL が期限切れである場合に、403メッセージを返します。 実際の実装では 404 を戻すこともできます。

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

async function verifyAndFetch(request) {
  const url = new URL(request.url)

  // If the path doesn't begin with our protected prefix, just pass the request
  // through.
  if (!url.pathname.startsWith("/verify/")) {
    return fetch(request)
  }

  // Make sure we have the minimum necessary query parameters.
  if (!url.searchParams.has("mac") || !url.searchParams.has("expiry")) {
    return new Response("Missing query parameter", { status: 403 })
  }

  // We'll need some super-secret data to use as a symmetric key.
  const encoder = new TextEncoder()
  const secretKeyData = encoder.encode("my secret symmetric key")
  const key = await crypto.subtle.importKey(
    "raw", secretKeyData,
    { name: "HMAC", hash: "SHA-256" },
    false, [ "verify" ]
  )

  // Extract the query parameters we need and run the HMAC algorithm on the
  // parts of the request we're authenticating: the path and the expiration
  // timestamp.
  const expiry = Number(url.searchParams.get("expiry"))
  const dataToAuthenticate = url.pathname + expiry

  // The received MAC is Base64-encoded, so we have to go to some trouble to
  // get it into a buffer type that crypto.subtle.verify() can read.
  const receivedMacBase64 = url.searchParams.get("mac")
  const receivedMac = byteStringToUint8Array(atob(receivedMacBase64))

  // Use crypto.subtle.verify() to guard against timing attacks. Since HMACs use
  // symmetric keys, we could implement this by calling crypto.subtle.sign() and
  // then doing a string comparison -- this is insecure, as string comparisons
  // bail out on the first mismatch, which leaks information to potential
  // attackers.
  const verified = await crypto.subtle.verify(
    "HMAC", key,
    receivedMac,
    encoder.encode(dataToAuthenticate)
  )

  if (!verified) {
    const body = "Invalid MAC"
    return new Response(body, { status: 403 })
  }

  if (Date.now() > expiry) {
    const body = `URL expired at ${new Date(expiry)}`
    return new Response(body, { status: 403 })
  }

  // We've verified the MAC and expiration time; we're good to pass the request
  // through.
  return fetch(request)
}

// Convert a ByteString (a string whose code units are all in the range
// [0, 255]), to a Uint8Array. If you pass in a string with code units larger
// than 255, their values overflow!
function byteStringToUint8Array(byteString) {
  const ui = new Uint8Array(byteString.length)
  for (let i = 0; i < byteString.length; ++i) {
    ui[i] = byteString.charCodeAt(i)
  }
  return ui
}

署名済み要求の生成

通常、署名されたリクエストは、電子メールなど、通常とは異なる方法でお客様に送信されます。または、対称鍵をお持ちの場合は、お客様ご自身で生成することも可能です。 署名付きリクエストは、Edge 機能でも生成できます。

URL へのリクエストで、 /generate/ で始まるものは、 CIS が /generate//verify/ に置き換え、その結果のパスをタイムスタンプで署名し、署名済みの完全な URL をレスポンスボディに返します。

addEventListener('fetch', event => {
  const url = new URL(event.request.url)
  const prefix = "/generate/"
  if (url.pathname.startsWith(prefix)) {
    // Replace the "/generate/" path prefix with "/verify/", which we
    // use in the first example to recognize authenticated paths.
    url.pathname = `/verify/${url.pathname.slice(prefix.length)}`
    event.respondWith(generateSignedUrl(url))
  } else {
    event.respondWith(fetch(event.request))
  }
})

async function generateSignedUrl(url) {
  // We'll need some super-secret data to use as a symmetric key.
  const encoder = new TextEncoder()
  const secretKeyData = encoder.encode("my secret symmetric key")
  const key = await crypto.subtle.importKey(
    "raw", secretKeyData,
    { name: "HMAC", hash: "SHA-256" },
    false, [ "sign" ]
  )

  // Signed requests expire after one minute. Note that you could choose
  // expiration durations dynamically, depending on, e.g. the path or a query
  // parameter.
  const expirationMs = 60000
  const expiry = Date.now() + expirationMs
  const dataToAuthenticate = url.pathname + expiry

  const mac = await crypto.subtle.sign(
    "HMAC", key,
    encoder.encode(dataToAuthenticate)
  )

  // `mac` is an ArrayBuffer, so we need to jump through a couple hoops to get
  // it into a ByteString, then a Base64-encoded string.
  const base64Mac = btoa(String.fromCharCode(...new Uint8Array(mac)))

  url.searchParams.set("mac", base64Mac)
  url.searchParams.set("expiry", expiry)

  return new Response(url)
}

応答のストリーミング

Edge 関数スクリプトは、 event.respondWith() にレスポンスを送信する前に、レスポンス本文全体を用意しておく必要はありません。 TransformStream, を使用すると、レスポンスのフロントマター(例えば、 HTTP ステータス行やヘッダー)を送信した後にレスポンスボディをストリームすることができます。 この合理化により、 CIS は、Edge機能スクリプトで実行される最初のバイトまでの時間とバッファリングの量を最小限に抑えることができます。

バッファリングを最小にすることは、エッジ機能のメモリー制限より大きい応答本体を処理したり変換したりしなければならない場合に特に重要です。 このようなケースでは、ストリーミングが唯一の実現可能な実装戦略になります。

デフォルトでは、CIS エッジ機能サービスは可能な場合には必ずストリーミングしています。 これらの API は、ストリーミング動作を維持しながら、何らかの方法で応答本体を変更する場合にのみ必要です。 エッジ機能スクリプトが、本体を読み取らずにサブリクエスト応答をクライアントに逐語的に戻す場合、本体の処理は既に最適です。

パススルーのストリーミング

以下の最小限のパススルーの例から始めます。

addEventListener("fetch", event => {
  event.respondWith(fetchAndStream(event.request))
})

async function fetchAndStream(request) {
  // Fetch from origin server.
  let response = await fetch(request)

  // Create an identity TransformStream (a.k.a. a pipe).
  // The readable side becomes our new response body.
  let { readable, writable } = new TransformStream()

  // Start pumping the body. NOTE: No await!
  streamBody(response.body, writable)

  // ... and deliver our Response while that's running.
  return new Response(readable, response)
}

async function streamBody(readable, writable) {
  let reader = readable.getReader()
  let writer = writable.getWriter()

  while (true) {
    const { done, value } = await reader.read()
    if (done) break
    // Optionally transform value's bytes here.
    await writer.write(value)
  }

  await writer.close()
}

注意すべき重要な点を以下に示します。

  • streamBody() は非同期関数ですが、 await を呼び出すと、 fetchAndStream() 関数の呼び出しの進行がブロックされてしまうため、呼び出したくありません。 関数は、未解決の reader.read() または writer.write() 操作がある期間、非同期で実行され続けます。
  • バックプレッシャー: await 書き込み操作を呼び出す前の読み取り操作。 同様に、次の読み取り操作を呼び出す前に、 await 書き込み操作を実行してください。 このパターンに従うことで、バック・プレッシャーは起点に伝搬されます。
  • 完了:最後に writer.close() にコールすることで、Edge関数実行にこのレスポンスボディの書き込みが完了したことを通知します。 呼び出された後、 streamBody() は終了します。この動作が望ましくない場合は、返されたプロミスを FetchEvent.waitUntil() に渡します。 ご使用のスクリプトで writer.close() が呼び出されることがない場合は、ランタイムには本体が切り捨てられたように見えますが、引き続き想定どおりに機能している可能性があります。

複数の要求の集約とストリーム

このユースケースは、複数のリクエストをまとめるレシピと似ていますが、今回はすべてのサブリクエストが成功したことを確認すると同時にレスポンスの作成を開始します。レスポンスボディを待つ必要はありません。

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

/**
 * Make multiple requests,
 * aggregate the responses and
 * stream it back as a single response.
 */
async function fetchAndApply(request) {
  const requestInit = {
    headers: { "Authorization": "XXXXXX" }
  }
  const fetches = [
    "https://api.coinbase.com/v2/prices/BTC-USD/spot",
    "https://api.coinbase.com/v2/prices/ETH-USD/spot",
    "https://api.coinbase.com/v2/prices/LTC-USD/spot"
  ].map(url => fetch(url, requestInit))

  // Wait for each fetch() to complete.
  let responses = await Promise.all(fetches)

  // Make sure every subrequest succeeded.
  if (!responses.every(r => r.ok)) {
    return new Response(null, { status: 502 })
  }

  // Create a pipe and stream the response bodies out
  // as a JSON array.
  let { readable, writable } = new TransformStream()
  streamJsonBodies(responses.map(r => r.body), writable)

  return new Response(readable)
}

async function streamJsonBodies(bodies, writable) {
  // We're presuming these bodies are JSON, so we
  // concatenate them into a JSON array. Since we're
  // streaming, we can't use JSON.stringify(), but must
  // instead manually write an initial '[' before the
  // bodies, interpolate ',' between them, and write a
  // terminal ']' after them.

  let writer = writable.getWriter()
  let encoder = new TextEncoder()

  await writer.write(encoder.encode("[\n"))

  for (let i = 0; i < bodies.length; ++i) {
    if (i > 0) {
      await writer.write(encoder.encode(",\n"))
    }
    writer.releaseLock()
    await bodies[i].pipeTo(writable, { preventClose: true })
    writer = writable.getWriter()
  }

  await writer.write(encoder.encode("]"))

  await writer.close()
}

ランタイムは TransformStream の読み取り可能な側で TypedArray を受け取ることを予期しています。 そのため、 writer.write() にストリングを渡すことはなく、Uint 8Arraysのみを渡します。 ストリングを書き込む必要がある場合は、TextEncoder を使用します。

エッジ機能によるカスタム・ロード・バランサー

ロードバランシングは、ホストするウェブサイトの拡張性と信頼性を維持するのに役立ちます。 エッジ機能を使用して、特定のニーズに対応するカスタムロードバランサーを作成することができます。

const US_HOSTS = [
  "0.us.example.com",
  "1.us.example.com",
  "2.us.example.com"
];

const IN_HOSTS = [
  "0.in.example.com",
  "1.in.example.com",
  "2.in.example.com"
];

var COUNTRIES_MAP = {
  IN: IN_HOSTS,
  PK: IN_HOSTS,
  BD: IN_HOSTS,
  SL: IN_HOSTS,
  NL: IN_HOSTS
}
addEventListener('fetch', event => {
  var url = new URL(event.request.url);

  var countryCode = event.request.headers.get('CF-IPCountry');
  var hostnames = US_HOSTS;
  if (COUNTRIES_MAP[countryCode]) {
    hostnames = COUNTRIES_MAP[countryCode];
  }
  // Randomly pick the next host
  var primary = hostnames[getRandomInt(hostnames.length)];

  var primaryUrl = new URL(event.request.url);
  primaryUrl.hostname = hostnames[primary];

  // Fallback if there is no response within timeout
  var timeoutId = setTimeout(function() {
    var backup;
    do {
        // Naive solution to pick a backup host
        backup = getRandomInt(hostnames.length);
    } while(backup === primary);

    var backupUrl = new URL(event.request.url);
    backupUrl.hostname = hostnames[backup];

    event.respondWith(fetch(backupUrl));
  }, 2000 /* 2 seconds */);

  fetch(primaryUrl)
    .then(function(response) {
        clearTimeout(timeoutId);
        event.respondWith(response);
    });
});

function getRandomInt(max) {
  return Math.floor(Math.random() * max);
}

フェッチを使用したキャッシング

フェッチ要求に TTL、カスタム・キャッシュ・キー、およびキャッシュ・ヘッダーを設定することで、リソースのキャッシュ方法を決定します。

async function handleRequest(request) {
  const url = new URL(request.url)

  // Only use the path for the cache key, removing query strings
  // and always store using HTTPS, for example, https://www.example.com/file-uri-here
  const someCustomKey = `https://${url.hostname}${url.pathname}`

  let response = await fetch(request, {
    cf: {
      // Always cache this fetch regardless of content type
      // for a max of 5 seconds before revalidating the resource
      cacheTtl: 5,
      cacheEverything: true,
      //Enterprise only feature, see Cache API for other plans
      cacheKey: someCustomKey,
      },
    })
    // Reconstruct the Response object to make its headers mutable.
    response = new Response(response.body, response)

    //Set cache control headers to cache on browser for 25 minutes
    response.headers.set("Cache-Control", "max-age=1500")
    return response
}

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

HTML リソースのキャッシング

// Force CIS to cache an asset
fetch(event.request, { cf: { cacheEverything: true } })

キャッシュ・レベルを「すべてキャッシュ」に設定すると、アセットのデフォルトの "cacheability" がオーバーライドされます。 TTL については、CIS は起点によって設定されたヘッダーを引き続き使用します。

カスタム・キャッシュ・キー

この機能はエンタープライズのお客様にのみ提供されています。

キャッシュの目的で2つのリクエストが「同じ」かどうかを決定するのは、リクエストのキャッシュキーです。 要求のキャッシュ・キーが過去の要求と同じであれば、どちらにも、キャッシュされた同じ応答を提供できます。

// Set cache key for this request to "some-string".
fetch(event.request, { cf: { cacheKey: "some-string" } })

CIS リクエストのキャッシュキーをリクエストの に基づいて計算しますが、キャッシュ目的で異なるURLを同じように扱いたい場合もあるでしょう。 URL 例えば、お客様のウェブサイトコンテンツが Amazon S3 と Google Cloud Storage の両方からホストされている場合(両方に同じコンテンツがある)、エッジ機能を使用して2つの間でランダムにバランスを取る。 しかし、コンテンツのコピーを2つキャッシュするのは避けたいものです。 カスタムキャッシュキーを使用すると、サブリクエスト URL ではなく、オリジナルリクエスト URL に基づいてキャッシュすることができます。

addEventListener("fetch", (event) => {
  let url = new URL(event.request.url)
  if (Math.random() < 0.5) {
    url.hostname = "example.s3.amazonaws.com"
  }
  else {
    url.hostname = "example.storage.googleapis.com"
  }

  let request = new Request(url, event.request)
  event.respondWith(
    fetch(request, {
      cf: { cacheKey: event.request.url },
    })
  )
})

異なるゾーンを代表して動作するエッジ機能は、互いのキャッシュに影響を与えることはないことを覚えておいてください。 キャッシュキーを上書きできるのは、自身のゾーン内(先の例では event.request.url が保存されたキー)へのリクエスト、または CIS にないホストへのリクエストの場合のみです。 他の CIS ゾーン(例えば、異なる CIS 顧客に属するゾーン)にリクエストを送信する場合、そのゾーンは、 CIS 内で自身のコンテンツがどのようにキャッシュされるかを完全に制御します。これを上書きすることはできません。

起点応答コードに基づくオーバーライド

この機能はエンタープライズのお客様にのみ提供されています。

// Force response to be cached for 86400 seconds for 200 status
// codes, 1 second for 404, and do not cache 500 errors.
fetch(request, {
  cf: { cacheTtlByStatus: { "200-299": 86400, 404: 1, "500-599": 0 } },
})

このオプションは、cacheTtl 機能のバージョンの 1 つです。これは応答の状況コードに基づいて TTL を選択し、自動的には cacheEverything: true を設定しません。 この要求に対する応答の状況コードが一致した場合、CIS は、指定された時間にわたってキャッシュし、起点から送信されたキャッシュに関するディレクティブをオーバーライドします。

TTL の解釈

CIS では、以下の TTL 値が解釈されます。

  • 正の値: CIS がアセットをキャッシュする時間 (秒単位) を示します。
  • 0: アセットはキャッシュされますが、すぐに期限切れになります (毎回起点から再検証します)。
  • -1 または任意の負の値: CIS に対して何もキャッシュしないように指示します。

キャッシュ API

CIS キャッシュ API を使用したキャッシュ。 この例では POST 要求もキャッシュできます。

const someOtherHostname = "my.herokuapp.com"

async function handleRequest(event) {
  const request = event.request
  const cacheUrl = new URL(request.url)

  // Hostname for a different zone
  cacheUrl.hostname = someOtherHostname

  const cacheKey = new Request(cacheUrl.toString(), request)
  const cache = caches.default

  // Get this request from this zone's cache
  let response = await cache.match(cacheKey)

  if (!response) {
    //If not in cache, get it from origin
    response = await fetch(request)

    // Must use Response constructor to inherit all of response's fields
    response = new Response(response.body, response)

    // Cache API respects Cache-Control headers. Setting max-age to 10
    // will limit the response to be in cache for 10 seconds max
    response.headers.append("Cache-Control", "max-age=10")

    // Store the fetched response as cacheKey
    // Use waitUntil so computational expensive tasks don"t delay the response
    event.waitUntil(cache.put(cacheKey, response.clone()))
  }
  return response
}

async function sha256(message) {
  // encode as UTF-8
  const msgBuffer = new TextEncoder().encode(message)

  // hash the message
  const hashBuffer = await crypto.subtle.digest("SHA-256", msgBuffer)

  // convert ArrayBuffer to Array
  const hashArray = Array.from(new Uint8Array(hashBuffer))

  // convert bytes to hex string
  const hashHex = hashArray.map(b => ("00" + b.toString(16)).slice(-2)).join("")
  return hashHex
}

async function handlePostRequest(event) {
  const request = event.request
  const body = await request.clone().text()
  const hash = await sha256(body)
  const cacheUrl = new URL(request.url)

  // Store the URL in cache by prepending the body's hash
  cacheUrl.pathname = "/posts" + cacheUrl.pathname + hash

  // Convert to a GET to be able to cache
  const cacheKey = new Request(cacheUrl.toString(), {
    headers: request.headers,
    method: "GET",
  })

  const cache = caches.default

  //Find the cache key in the cache
  let response = await cache.match(cacheKey)

  // Otherwise, fetch response to POST request from origin
  if (!response) {
    response = await fetch(request)
    event.waitUntil(cache.put(cacheKey, response.clone()))
  }
  return response
}

addEventListener("fetch", event => {
  try {
    const request = event.request
    if (request.method.toUpperCase() === "POST")
      return event.respondWith(handlePostRequest(event))
    return event.respondWith(handleRequest(event))
  } catch (e) {
    return event.respondWith(new Response("Error thrown " + e.message))
  }
})