はじめに
現在 Web アプリを Nuxt で開発していて、nuxt generate
で静的コンテンツを作成し、Netlify にホスティングしている
そのアプリでは、別ドメインの API にリクエストを投げてデータを取得しているので、CORS ポリシーでブロックされないようにproxy-moduleを使って API と同一ドメインになるようにプロキシしている
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
設定ファイルを用意する
[build]
functions = "functions/dist"
Lambda にやらせたい処理を書いて、handler 関数を export
export async function handler(event, context) {
return {
statusCode: 200,
body: 'Hello, World'
}
}
"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
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
{
"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 にパラメータを書いている
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 を使った意味がないので、、)
proxy: {
'/connpass': {
target: 'http://localhost:9000'
}
}
最後にアクションを Dispatch する
this.$store.dispatch('connpass/getConnpassEvents', '20190624,20190625')
動作確認
ローカルで Lambda を起動して、Nuxt でアクションを Dispatch する
yarn lambda -t 100
ホスティングして動作確認
netlify.toml がルートディレクトリに存在する場合、Netlify の管理画面上の設定は無視される
その場合、netlify.toml にデプロイ設定を書く必要がある
[build]
command = "yarn generate"
functions = "functions/dist"
publish = "dist"
Netlify にホスティングすると、Functions はhttps://[Site name].netlify.com/.netlify/functions
というパスになるので、axios の baseURL を Functions の URL にする
axios: { baseURL: '/.netlify/functions' },
proxy: {
'/.netlify/functions/connpass': {
target: 'http://localhost:9000'
}
}
ちなみに netlify-lambda を使ってローカルでビルドした場合はhttp://localhost:9000
とhttp://localhost:9000/.netlify/functions
のどちらでも良いので、上記の proxy 設定でアクセスできる
ただし、axios の baseURL を Functions に固定するのは後々不便になるかもしれない
その場合は、環境変数を使って対応した方がいいかも
後は Netlify 上で GitHub リポジトリを紐づけて、その GitHub リポジトリのブランチに push すればホスティングされて動作するはずです