10
Help us understand the problem. What are the problem?

More than 1 year has passed since last update.

posted at

Netlify Functions を使って CORS エラーを回避する

はじめに

現在 Web アプリを Nuxt で開発していて、nuxt generateで静的コンテンツを作成し、Netlify にホスティングしている
そのアプリでは、別ドメインの API にリクエストを投げてデータを取得しているので、CORS ポリシーでブロックされないようにproxy-moduleを使って API と同一ドメインになるようにプロキシしている

nuxt.config.js
modules: ['@nuxtjs/axios', '@nuxtjs/proxy'],
proxy: {
  '/atnd': {
    target: 'http://api.atnd.org/events',
    pathRewrite: {
      '^/atnd': '/'
    }
  },
  '/connpass': {
    target: 'https://connpass.com/api/v1/event',
    pathRewrite: {
      '^/connpass': '/'
    }
  }
}

ただし、上記の proxy モジュールによる書き換えはサーバーが必要で、generate 機能を使って静的コンテンツをホスティングしている場合は機能しない
ちなみにローカルでは webpack-dev-server などを使って、Node サーバー上で動かしていれば、ちゃんとプロキシされる

この辺のことは以下に詳しく書いてある
https://github.com/nuxt-community/proxy-module/issues/1

Nuxt の generate 機能を使っていても、ちゃんと別ドメインの API が叩けるようにするというのが今回の開発の目的です

作業リポジトリはこちら
https://github.com/kurosame/event-search

Netlify Functions を使う

ブラウザには別ドメインへの通信を拒否する仕組みが標準で備わっている
Nuxt のプロキシ機能はサーバーが無いと使えないし、API 側にオリジンを許可させることもできない

これらのことを踏まえて、今回は Netlify Functions を使って Lambda 関数内で API リクエストを実行することで CORS エラーを回避することにした

Netlify Functions の実行環境は AWS Lambda だが、AWS 側の設定は一切やらなくて良い
てか AWS アカウントも不要

Netlify のアカウントがあれば使えるが、フリープランだと以下の制限がある

  • 125000 リクエスト/月
  • 100 時間/月

言語は JS と Go をサポートしている
(本記事では JS で実装している)

ローカルで動作確認

以下のモジュールをインストール

yarn add -D netlify-lambda

設定ファイルを用意する

netlify.toml
[build]
  functions = "functions/dist"

Lambda にやらせたい処理を書いて、handler 関数を export

functions/src/sample.ts
export async function handler(event, context) {
  return {
    statusCode: 200,
    body: 'Hello, World'
  }
}
package.json
"scripts": {
  "lambda": "netlify-lambda serve functions/src"
},
yarn lambda

以下のようにリクエストする
http://localhost:9000/sample

Functions から connpassAPI を叩いてみる

以下のコードが Functions 関数の実装
API の仕様上、100 件以上レスポンスが存在していても、MAX100 件しかデータが取れないので再帰関数にしているが、やっていることは API から取ってきたデータを body に JSON.stringify にして渡しているだけです
IConnpassEventResponse と IConnpassResponse の型インターフェースは後述してます

yarn add -D @types/aws-lambda
functions/src/connpass.ts
import { IConnpassEventResponse, IConnpassResponse } from '@/store/connpass'
import axios, { AxiosResponse } from 'axios'
import { APIGatewayProxyEvent } from 'aws-lambda'

export async function handler(event: APIGatewayProxyEvent) {
  const ymd = (event.queryStringParameters || { period: '' }).period
  const count = 100
  const getEvents = (
    events: IConnpassEventResponse[] = [],
    start: number = 1
  ): Promise<IConnpassEventResponse[]> =>
    axios
      .get('https://connpass.com/api/v1/event', {
        params: { ymd, start, count }
      })
      .then((res: AxiosResponse<IConnpassResponse>) =>
        res.data.results_returned === count
          ? getEvents([...events, ...res.data.events], start + count)
          : [...events, ...res.data.events]
      )

  const events: IConnpassEventResponse[] = await getEvents()

  return {
    statusCode: 200,
    body: JSON.stringify(events)
  }
}

TS で書いた場合は、以下を functions ディレクトリに追加

yarn add -D @babel/preset-typescript
functions/.babelrc
{
  "presets": ["@babel/preset-typescript", "@babel/preset-env"],
  "plugins": [["@babel/plugin-transform-runtime", { "regenerator": true }]]
}

基本的に presets だけでコンパイルエラーは回避できると思うが、plugins はエラーになったらそのエラー名で検索するとたぶん必要なプラグインが分かると思うので、必要に応じて設定する
作った後に気づいたけど、ここに書いてた

yarn lambda

正常に起動したら、以下にアクセス
http://localhost:9000/connpass

たぶん Lambda 側でタイムアウトする(デフォルト 10 秒)ので、日付で絞るかタイムアウト時間を伸ばすかしてみてください
http://localhost:9000/connpass?period=20190624
or
yarn lambda -t 20

Nuxt から Functions にリクエストする

Vuex のアクションで axios を使って Functions にリクエストしている

?period=${period}の書き方について
axios.getの param オプションを使うと、Functions 側の event 引数に入らなかったので、直接 URL にパラメータを書いている

store/connpass.ts
import { IEventState } from '@/store/events'

export interface IConnpassEventResponse {
  title: string
  catch: string
  description: string
  event_url: string
  started_at: string
  ended_at: string
  limit: number
  address: string
  place: string
}

export interface IConnpassResponse {
  results_returned: number
  events: IConnpassEventResponse[]
}

export const actions = {
  async getConnpassEvents({ commit }, period: string) {
    const events: IConnpassEventResponse[] = await (this as any).$axios.$get(
      `/connpass?period=${period}`
    )

    // Functionsのレスポンスを色々加工してStoreに入れている
    // (ここから以下は本記事の目的とは関係ない部分)
    commit(
      'events/setEvents',
      events
        .filter((e: IConnpassEventResponse) => e.limit >= 30)
        .map(
          (e: IConnpassEventResponse) =>
            ({
              title: e.title,
              catch: e.catch,
              description: e.description,
              eventUrl: e.event_url,
              startedAt: (this as any)
                .$moment(e.started_at)
                .format('YYYY-MM-DD HH:mm:ss (ddd)'),
              endedAt: (this as any)
                .$moment(e.ended_at)
                .format('YYYY-MM-DD HH:mm:ss (ddd)'),
              address: `${e.address} ${e.place}`
            } as IEventState)
        ),
      { root: true }
    )
  }
}

ローカルで netlify-lambda を使って起動するとポート 9000 で実行される
Nuxt はポート 3000 で起動しているので、以下のようにプロキシしないと CORS に引っかかる

Netlify にホスティングして動かす場合は、Nuxt と Functions は同じドメイン上で動くので、プロキシは不要
(そうじゃないと今回 Functions を使った意味がないので、、)

nuxt.config.js
proxy: {
  '/connpass': {
    target: 'http://localhost:9000'
  }
}

最後にアクションを Dispatch する

components/Summary.vue
this.$store.dispatch('connpass/getConnpassEvents', '20190624,20190625')

動作確認
ローカルで Lambda を起動して、Nuxt でアクションを Dispatch する

yarn lambda -t 100

ホスティングして動作確認

netlify.toml がルートディレクトリに存在する場合、Netlify の管理画面上の設定は無視される
その場合、netlify.toml にデプロイ設定を書く必要がある

netlify.toml
[build]
  command = "yarn generate"
  functions = "functions/dist"
  publish = "dist"

Netlify にホスティングすると、Functions はhttps://[Site name].netlify.com/.netlify/functionsというパスになるので、axios の baseURL を Functions の URL にする

nuxt.config.js
axios: { baseURL: '/.netlify/functions' },
proxy: {
  '/.netlify/functions/connpass': {
    target: 'http://localhost:9000'
  }
}

ちなみに netlify-lambda を使ってローカルでビルドした場合はhttp://localhost:9000http://localhost:9000/.netlify/functionsのどちらでも良いので、上記の proxy 設定でアクセスできる

ただし、axios の baseURL を Functions に固定するのは後々不便になるかもしれない
その場合は、環境変数を使って対応した方がいいかも

後は Netlify 上で GitHub リポジトリを紐づけて、その GitHub リポジトリのブランチに push すればホスティングされて動作するはずです

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
10
Help us understand the problem. What are the problem?