azain4645 / zennThread

0 stars 0 forks source link

useFetchがCORSによりエラーとなる #1

Closed azain4645 closed 2 years ago

azain4645 commented 2 years ago

nuxt3+vuetifyで、zennの非公式APIからデータを取得して表示するサンプルを見つけたので、参考にして実装しています。 元の記事では、script setup 以前の内容でしたので、部分的に変更しながら進めています。

https://zenn.dev/satonopan/articles/eca27c1b4f0b93

問題は、元はaxiosを使っており、useFetchに書き換えたのですが、CORSエラーになりデータが取得出来ていないようです。 あまりわかっていない部分もあるんですが、こちら側の設定の問題でしょうか?

https://github.com/azain4645/zennThread/blob/e4435d18b6885eb6ba4e8c4d13a9c72135cc0883/pages/index.vue#L23-L28

また、元の記事では const response = await $axios.$get<Article[]>(url) state.articles = response というようにArticleの型で取得しているようですが、同じように書くことが出来ますか?

すみませんがお願いします

gel1123 commented 2 years ago

@azain4645 結論を先に述べると、CORSエラーが発生しないよう問い合わせを行いたい場合には、「サーバ側でコールする」のが妥当ですね!

CORSエラーとは何か、という部分の理解が必要なのでざっくりご説明しますね!

CORSエラー とは

まずCORSってなに?

オリジン間リソース共有 (Cross-Origin Resource Sharing, CORS) とは、Webページが自身とは異なる オリジン のURLにアクセスを行うことができる機能ですね!

https://developer.mozilla.org/ja/docs/Web/HTTP/CORS

じゃあCORS「エラー」っていうのは?

上記の「CORS」の機能は、サーバ側で設定を行うものなのですが、

特にサーバ側でCORSの設定がされていない場合、ブラウザは「異なるオリジンのURLと通信できない」仕様になっています。

なので、そういったCORS未設定の Web-API などにブラウザから通信を試みた場合、ブラウザの仕様に従って、アクセスを拒絶されるのが 「CORSエラー」 です。

CORSエラーを回避するには?

ブラウザの仕組みであり、CORSを許可するには Web-API 提供元のサーバ側での設定が必要な以上、第三者が CORS未許可の Web-API を 「ブラウザから」 コールすることはできません。

つまり、逆に言えば「ブラウザ以外からであれば」CORSの仕組みは機能しないので、コールできます。

というわけで、話をNuxt3アプリケーションに戻すと、現状のコードではフロントエンドから直接 useFetch で Web-API から情報を取得しようとされていますが、

これを下記のような構成に直すと、うまく動くようになります(サーバ側の機能を使うので、ビルドモードは SSGではなく、SSR である必要があります)

こうすると、あくまでフロントエンドが通信しているのは自分自身が所属する Nuxt3 アプリケーションのサーバなので、CORS未許可のWeb-APIであっても問い合わせを行うことができます!

なお Server Routes server/api/任意の名前.ts のサーバ処理は、URL /api/任意の名前 で公開されますので、 $fetch('/api/任意の名前') や、 useFetch('/api/任意の名前') でフロントエンドから問い合わせできます!

※ Nuxt3 Server Routes のドキュメント https://v3.nuxtjs.org/guide/features/server-routes/


補足:オリジンってなに?

origin === protocol + domain + port です!

http://localhost:8080/hoge/fuge というURLがあるとしたら、このうちの http://localhost:8080 がオリジンですね!

このオリジンの文字列が違う場合には、CORS設定がない限り、ブラウザから直接リソースを取得することはできません。

※ MDNのオリジン解説ドキュメント https://developer.mozilla.org/ja/docs/Glossary/Origin


余談:Nuxt3 Server Routes って要はなに?

Nuxt3 アプリケーション内で「簡易的なWeb-API を作成&公開できる機能」です。 便利なのでよく使いますね

(Server Routes を AWS Lambda に乗せて、フロントエンドを Amazon S3 に乗せることで SSR版Nuxt3アプリケーションをAWS上でホストしたりします)

実装方法は上記解説中に記載した公式ドキュメントをご覧ください!

azain4645 commented 2 years ago

ありがとうございます。

これを下記のような構成に直すと、うまく動くようになります(サーバ側の機能を使うので、ビルドモードは SSGではなく、SSR である必要があります)

とりあえずお聞きしますが、今回のAPIがCORS未許可なので、サーバ側で取得する必要があり、将来的にBaaSを利用する場合は、自分で設定ができるので、SSGでも問題ないですか?

それとも教えて頂いたように

(Server Routes を AWS Lambda に乗せて、フロントエンドを Amazon S3 に乗せることで SSR版Nuxt3アプリケーションをAWS上でホストしたりします)

という形にするのが一般的ですか?

gel1123 commented 2 years ago

@azain4645

とりあえずお聞きしますが、今回のAPIがCORS未許可なので、サーバ側で取得する必要があり、将来的にBaaSを利用する場合は、自分で設定ができるので、SSGでも問題ないですか?

お、今回ご利用された Web-API はあくまで今回だけのもので、将来的には BaaS で自分用の Web-API を立ち上げられるという話ですかね? そうですね、そういうことでしたら自分自身で CORSの設定をする(あるいはそもそもWeb-APIと同じオリジンでNuxt3 をホスティングする)ことができるはずなので、SSGでも問題ないですよ!

なおSSRとSSGについてどちらが一般的ということは特になく、システムが実現したい要件によって、どちらにすべきかは都度変わりますね!

ですので、azain4645 さんがその時開発したいシステムの構成によって、このあたりは都度良さそうな方を選んでいくといいですよ!

azain4645 commented 2 years ago

公式のドキュメントなど見ながらserver/api/ を用意して、 http://localhost:3000/api/hello 自体でAPIのデータを返却するところまでは出来ました。

ただ、page側で受け取ることがわかりませんでした (ちょっとネット上でもサンプル少ないですね)

https://github.com/azain4645/zennThread/blob/cc072d91b098ef5b099389f8425c93b48808ee4b/pages/index.vue#L23-L31

元の記事では const response = await $axios.$get<Article[]>(url) state.articles = response というようにArticleの型で取得しているようですが、このあたりも含めて教えていただけたら幸いです。

宜しくお願いします

gel1123 commented 2 years ago

@azain4645

結論としてコードから載せるとこんな感じですね!

server/api/getArticle.ts を実装する

type Article = {
  title: string
  likedCount: number
  publishedAt: string
}

export default defineEventHandler(async (e) => {
  // 外部とのやり取りなら型推論が効かないので genericsに型を明示してやる
  const result = await $fetch<Article[]>(`https://zenn-api.netlify.app/.netlify/functions/trendTech`);
  return result;
});

pages/index.vue から上記APIをコールする

<script setup lang="ts">

  type Article = {
    title: string
    likedCount: number // スペルミス訂正
    publishedAt: string // スペルミス訂正
  }

  type State = {
    articles: Article[]
  }

  const headers = [
    { text: '記事名', value: 'title' },
    { text: 'いいね数', value: 'likedCount' },
    { text: '公開日時', value: 'publishedAt' },
  ]

  const state = ref<State>({
    articles: [],
  })

  const getZennArticles = async () => {
    // 型注釈やgenericsを使わずとも、/api/getArticle の応答型が自動で型推論される
    // (なのでこれだけで、resultはArticle[]であるものとVSCodeの側でちゃんと表示される)
    const result = await $fetch("/api/getArticle")
    console.log({result})

    state.value.articles = result;
  }

  onMounted(() => {
    getZennArticles()
  })
</script>

<template>
  <pre>{{
    state.articles.length === 0 ?
      'loading...'
      : JSON.stringify(state.articles, null, 2)
  }}</pre>
</template>
gel1123 commented 2 years ago

@azain4645

Nuxt3公式ドキュメントに、

This composable provides a convenient wrapper around useAsyncData and $fetch. It automatically generates a key based on URL and fetch options, as well as infers API response type. (これはuseAsyncDataと$fetch を使いやすくラップした関数です。URLとパラメータに基づいてキーを自動的に生成し、APIの応答の型を推測します)

https://v3.nuxtjs.org/guide/features/data-fetching/

とあるように、useFetch, useAsyncData, $fetch などは「URLとパラメータに基づいて応答を型推論する」ようになっています。

ですので、Server Routes /server/api/xxx.ts で自らNuxt3アプリケーション内に定義した簡易Web-APIであれば、自動的に応答が型推論されますので、特に明示的な型の記述は不要です。

一方で、上記コードの /server/api/getArticle.ts で使っているような Nuxt3アプリケーションの外部への問い合わせの場合には、型推論が効きません。

ですので、Fetch系メソッドの generics に型を明示してやることで、応答の型を指定してやることができます。

const result = await $fetch<Article[]>(`https://zenn-api.netlify.app/.netlify/functions/trendTech`);
gel1123 commented 2 years ago

@azain4645

ちなみに今回に限らず、TypeScriptで「このメソッドはどうにかして型を補足できそうだ...」と思えるコードがあれば、ひとまずそのメソッドの型情報を見るといいですよ!

例えば $fetch だったら、VSCode 上で $fetch の上にカーソルを置くと、

$Fetch
<unknown, "https://zenn-api.netlify.app/.netlify/functions/trendTech">(request: "https://zenn-api.netlify.app/.netlify/functions/trendTech", opts?: FetchOptions<ResponseType>) => Promise<...>

というような情報がポップアップされます。

この型情報を見れば、

と推測できます。 実際に、今回書いたコードのように $fetch をgenericsで <Article[]> にしてやると、 表示される型情報が次のように変わります。

var $fetch: $Fetch
<Article[], NitroFetchRequest>(request: NitroFetchRequest, opts?: FetchOptions<ResponseType>) => Promise<...>

...と、まあこのような形でTypeScriptならコード見るだけで使い方を推測出来るようになっているので、 もし型の補足が出来そうだと感じたら、ひとまずそのメソッドの型情報を見ると分かりやすいですよ!

gel1123 commented 2 years ago

余談ですが、ここスペルミスから上手に値を連携出来ていなかったのでお知らせです!

   type Article = {
     title: string
-    linkedCount: number
-    publichedAt: string
+    likedCount: number // スペルミス訂正
+    publishedAt: string // スペルミス訂正
   }

(おそらく @azain4645 さんの方で pages/index.vueuseFetch がうまく動いていなかったのだとしたら、それはこのスペルミスが原因だと思います)

gel1123 commented 2 years ago

@azain4645 さらに余談ですが、私の上記動作確認コードでは azainさんのコードをこのように書き換えさせていただきましたが、

-  test
-  <!-- <v-data-table 
-    :headers="headers"
-    :items="state.value.articles"
-    :items-per-page="5"  
-    class="elevation-1"
-  >
-  </v-data-table> -->
+  <pre>{{
+    state.articles.length === 0 ?
+      'loading...'
+      : JSON.stringify(state.articles, null, 2)
+  }}</pre>

そもそも v-data-table は、azainさんがインストールされた "vuetify": "^3.0.0-beta.5" には存在しないコンポーネントですので、ここは Vuetify + Table で表示させたいなら違うコンポーネントを使うのが妥当です!

※ v-data-table は Vuetify2 のコンポーネントで、Vuetify3 には存在しない状態です。Vuetify3 のテーブルコンポーネントの公式ドキュメントを元に、Vuetify3 としての書き方への修正が必要ですね!

https://next.vuetifyjs.com/en/components/tables/