Skip to content

サーバーとの通信

pingページの作成

接続の練習のためサーバーに/pingエンドポイントを実装しておきましょう。 サーバーのリポジトリのhandler.rsを書き換えます。

rs
use axum::{
    routing::{get, post},
    Router,
};

use crate::repository::Repository;

mod auth;
mod country;

pub fn make_router(app_state: Repository) -> Router {
    let city_router = Router::new()
        .route("/cities/:city_name", get(country::get_city_handler))
        .route("/cities", post(country::post_city_handler));

    let auth_router = Router::new()
        .route("/signup", post(auth::sign_up))
        .route("/login", post(auth::login));

    let ping_router = Router::new().route("/ping", get(|| async { "pong" })); 

    Router::new()
        .nest("/", city_router)
        .nest("/", auth_router)
        .nest("/", ping_router) 
        .with_state(app_state)
}

次にフロントエンドから/pingエンドポイントにアクセスしてみるコードを書いてみましょう。

src/page以下にPingPage.vueを作ってみましょう。

vue
<script setup lang="ts">
import { onMounted, ref } from 'vue'

const pong = ref<string>('no data')

onMounted(async () => {
  const res = await fetch('/api/ping')
  if (res.ok) {
    pong.value = await res.text()
  }
})
</script>
<template>
  {{ pong }}
</template>

onMountedの引数として渡された関数は、コンポーネントが描画されたときに実行されます。今回はサーバーの/pingエンドポイントにアクセスしてそのデータをpongという変数に代入しています。

関数の定義の前にasyncとついているのは、この関数は非同期に実行するということを宣言しています。非同期に実行するというのは、この関数の処理が終わる前に次の式を評価できるという理解で良いでしょう。サーバーとの通信処理は長時間かかることがあり、待機時間も長いので他の処理ができないと非効率であるために非同期処理となっています。

また、関数実行の前にawaitとついているのはこの関数の処理が終わるまで次の処理を待つということを表しています。これをやらないと処理が終わる前に次の処理に進んでしまうことがあります。

7 行目で変数resにサーバーからのレスポンスを格納しています。 8 行目で http ステータスが ok であるかを確認して、 9 行目でレスポンスの中身をテキストとして取得しています。

router.tsPingPageを追加するのも忘れず行いましょう。

typescript
import { createRouter, createWebHistory } from 'vue-router'
import HomePage from './pages/HomePage.vue'
import NotFound from './pages/NotFound.vue'
import PingPage from './pages/PingPage.vue' //[!code ++]

const routes = [
  { path: '/', name: 'home', component: HomePage },
  { path: '/ping', name: 'ping', component: PingPage }, //[!code ++]
  { path: '/:path(.*)*', component: NotFound }
]

const router = createRouter({
  history: createWebHistory(),
  routes
})

export default router

また併せて、src/App.vueにリンクを追加します。

vue
<template>
  <main>
    <div :class="$style.container">
      <header :class="$style.header">
        <router-link to="/">Home</router-link>
        |
        <router-link to="/ping">Ping</router-link>
      </header>

      <router-view />
    </div>
  </main>
</template>

<style module>
.container {
  max-width: fit-content;
  margin: auto;
}
.header {
  display: flex;
  justify-content: center;
}
</style>

http://localhost:5173/pingにアクセスすると以下のような画面が表示されれば OK です。

参考:
Composition API: ライフサイクルフック | Vue.js
フェッチAPI - Web API | MDN
非同期 JavaScript 入門 - ウェブ開発を学ぶ | MDN
async function - JavaScript | MDN

ログインページの作成

ログインページを作成してみましょう! 上と同じようにページを分けて進めていきましょう。

  • ユーザー名とパスワードが入力できる
    • input タグを使いましょう
    • v-model とかをうまく使いましょう
  • ログインボタンを押すと/api/loginに POST する
    • POST の JSON はサーバーのコードに合わせて書きましょう

fetchでサーバーに対してPOSTリクエストを送りたい場合は次のようにやれば良いでしょう。

typescript
const data = {
  /* ここにサーバーに渡すデータを入れる。 */
}
fetch('送信したい先のURL', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json'
  },
  body: JSON.stringify(data)
})
解答

src/pages/LoginPage.vue

新規作成するファイルです。

vue
<script setup lang="ts">
import { ref } from 'vue'

const username = ref<string>('')
const password = ref<string>('')
const login = () =>
  fetch('/api/login', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({ username: username.value, password: password.value })
  })
</script>

<template>
  <div class="login">
    <h1>This is an login page</h1>
    <div>
      <input type="text" v-model="username" />
      <input type="password" v-model="password" />
    </div>
    <div>
      <button @click="login">login</button>
    </div>
  </div>
</template>

src/App.vue

template 部分のみ。

vue
<template>
  <main>
    <div :class="$style.container">
      <header :class="$style.header">
        <router-link to="/">Home</router-link>
        |
        <router-link to="/ping">Ping</router-link>
        |
        <router-link to="/login">Login</router-link>
      </header>

      <router-view />
    </div>
  </main>
</template>

src/router.ts

typescript
import { createRouter, createWebHistory } from 'vue-router'
import HomePage from './pages/HomePage.vue'
import NotFound from './pages/NotFound.vue'
import PingPage from './pages/PingPage.vue'
import LoginPage from './pages/LoginPage.vue' //[!code ++]

const routes = [
  { path: '/', name: 'home', component: HomePage },
  { path: '/ping', name: 'ping', component: PingPage },
  { path: '/login', name: 'login', component: LoginPage }, //[!code ++]
  { path: '/:path(.*)*', component: NotFound }
]

const router = createRouter({
  history: createWebHistory(),
  routes
})

export default router

ログイン済みページの作成

前回作った都市の情報を返す API を表示するページを作成します。

src/pages/CityPage.vue

新規に作成するファイルです。 res.json()としている部分では、サーバーからのレスポンスを json として解釈して JavaScript のオブジェクトに変換しています。

vue
<script setup lang="ts">
import { ref, onMounted } from 'vue'
const props = defineProps<{ cityName: string }>()
const cityInfo = ref()
onMounted(async () => {
  const res = await fetch('/api/cities/' + props.cityName)
  if (res.ok) {
    cityInfo.value = await res.json()
  }
})
</script>

<template>
  <div>
    <h1>
      {{ cityName }}
    </h1>
    <div v-if="cityInfo">{{ cityInfo }}</div>
    <div v-else>街が見つかりませんでした</div>
  </div>
</template>

src/router.ts

CityPage.vueを読み込み登録します。 echo と同じように、path:始まりで書くと、PathParameter として値を取得できます。

参考: Dynamic Route Marching | Vue Router

typescript
import { createRouter, createWebHistory } from 'vue-router'
import HomePage from './pages/HomePage.vue'
import NotFound from './pages/NotFound.vue'
import PingPage from './pages/PingPage.vue'
import LoginPage from './pages/LoginPage.vue' //[!code ++]
import CityPage from './pages/CityPage.vue'

const routes = [
  { path: '/', name: 'home', component: HomePage },
  { path: '/ping', name: 'ping', component: PingPage },
  { path: '/login', name: 'login', component: LoginPage },
  { path: '/city/:cityName', name: 'city', component: CityPage, props: true }, //[!code ++]
  { path: '/:path(.*)*', component: NotFound }
]

const router = createRouter({
  history: createWebHistory(),
  routes
})

export default router

src/App.vue

リンクを追加します。

vue
<template>
  <main>
    <div :class="$style.container">
      <header :class="$style.header">
        <router-link to="/">Home</router-link>
        |
        <router-link to="/ping">Ping</router-link>
        |
        <router-link to="/city/Tokyo">Tokyo</router-link>
        |
        <router-link to="/login">Login</router-link>
      </header>

      <router-view />
    </div>
  </main>
</template>

<style module>
.container {
  max-width: fit-content;
  margin: auto;
}
.header {
  display: flex;
  justify-content: center;
}
</style>

確認

完成するとこんな感じ。

発展課題

HomePage.vue に任意の都市について表示できるような仕組みを作ってみましょう。

  • input タグで都市名を指定
  • 「表示する」のようなボタンを押すことで/city/{その都市名}というリンクに飛ばす

参考: Programmatic Navigation | Vue Router

ログインしていない場合に、ログインページに遷移させる

ブラウザのシークレットウィンドウを起動し、先程の/city/Tokyoを開いてみます。

本来は上のスクリーンショットのように東京の情報が表示されてほしいですが、表示されません。

ブラウザの開発者ツールから見てみるとログインしていないため、ダメだということがわかりました。

そこで、ログインしていない場合にはログインページにリダイレクトするように変更してみます。

meエンドポイントの作成

上のように何らかのエンドポイントを叩いた結果、401 が返ってきたらリダイレクトするようにしてもいいですが、今回は traQ やその他 traP のアプリケーションでの書き方に習って、meのエンドポイントを使ってログインされているかの確認をします。

このエンドポイントはログインしているユーザー自身の情報を取得するエンドポイントです。なぜこんなエンドポイントが必要かというと、クライアント自身は自分が何というユーザーでログインしているかをサーバーに問い合わせることなく知ることができないからです。 traQ でも一番始めに me と同じ働きをするエンドポイントを叩き自分の情報を取得しています。

router.tsでログインの確認を行う

Vue Router のbeforeEachという機能を使って、各 Routing の前に特定の関数を呼び出すことができます。 このようにログイン状態を確認する方法はパターンとして覚えてしまってもいいでしょう。

beforeEachに関して詳しくは: Navigation Guards | Vue Router

src/router.ts

typescript
import { createRouter, createWebHistory } from 'vue-router'
import HomePage from './pages/HomePage.vue'
import NotFound from './pages/NotFound.vue'
import PingPage from './pages/PingPage.vue'
import LoginPage from './pages/LoginPage.vue'
import CityPage from './pages/CityPage.vue'

const routes = [
  { path: '/', name: 'home', component: HomePage },
  { path: '/ping', name: 'ping', component: PingPage },
  { path: '/login', name: 'login', component: LoginPage },
  { path: '/city/:cityName', name: 'city', component: CityPage, props: true },
  { path: '/:path(.*)*', component: NotFound }
]

const router = createRouter({
  history: createWebHistory(),
  routes
})

/*[!code ++]*/ router.beforeEach(async (to) => {
  /*[!code ++]*/ if (to.path === '/login') {
    return true //[!code ++]
  } //[!code ++]
  const res = await fetch('/api/me') //[!code ++]
  if (res.ok) return true //[!code ++]
  return '/login' //[!code ++]
}) //[!code ++]

export default router

これでログインしていない場合には、/loginへリダイレクトされるようになりました。 しかし、/login以外の全てのページへアクセスできません(シークレットウィンドウなどで開いて確認してみましょう)。

特定のページだけログイン不要にする

素朴な実装としては、beforeEachの中の条件分岐を増やして許可するという方法が思いつきますが、同じような処理を何度も書くのは面倒ですし、読みにくくなります。 ここでは、Vue Router の meta という機能を使って Route にメタ情報を付与し、それを用いてログイン不要かどうかを判断します。

参考: Route Meta Field | Vue Router

ルーティング設定にmetaを追加

ログインしていなくてもアクセスしたいページにはmeta: { isPublic: true }というプロパティを追加します。

リダイレクト設定の変更

if (to.path === '/login')で分岐していたところをif (to.meta.isPublic)に置き換えます。 最終的なコードは以下のようになるはずです。

router.ts

typescript
import { createRouter, createWebHistory } from 'vue-router'
import HomePage from './pages/HomePage.vue'
import NotFound from './pages/NotFound.vue'
import PingPage from './pages/PingPage.vue'
import LoginPage from './pages/LoginPage.vue'
import CityPage from './pages/CityPage.vue'

const routes = [
  { path: '/', name: 'home', component: HomePage, meta: { isPublic: true } },
  { path: '/ping', name: 'ping', component: PingPage, meta: { isPublic: true } },
  {
    path: '/login',
    name: 'login',
    component: LoginPage,
    meta: { isPublic: true }
  },
  { path: '/city/:cityName', name: 'city', component: CityPage, props: true },
  { path: '/:path(.*)*', component: NotFound, meta: { isPublic: true } }
]

const router = createRouter({
  history: createWebHistory(),
  routes
})

router.beforeEach(async (to) => {
  if (to.meta.isPublic) {
    return true
  }
  const res = await fetch('/api/me')
  if (res.ok) return true
  return '/login'
})

export default router

クライアントの見本:https://github.com/traPtitech/naro-template-frontend/tree/city

サーバー・クライアント両方で API を利用できるようになりました これからは必要な API を考え、実装していくことになります。

最重要課題

国一覧を表示するページを作り、その国名をクリックすると、その国の都市一覧が表示され、その都市名をクリックすると都市の情報が表示されるようにしてみましょう。

発展課題

ログアウト機能を作りましょう。

ヒント

  • サーバープログラムに/logoutを作る
  • API を叩いた人のセッションをセッションストアから破棄する
  • クライアントプログラムに/logoutの API を叩くボタン作る