サーバーとの通信
pingページの作成
接続の練習のためサーバーに/ping
エンドポイントを実装しておきましょう。 サーバーのリポジトリのhandler.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
を作ってみましょう。
<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.ts
にPingPage
を追加するのも忘れず行いましょう。
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
にリンクを追加します。
<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
リクエストを送りたい場合は次のようにやれば良いでしょう。
const data = {
/* ここにサーバーに渡すデータを入れる。 */
}
fetch('送信したい先のURL', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
})
解答
src/pages/LoginPage.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 部分のみ。
<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
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 のオブジェクトに変換しています。
<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
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
リンクを追加します。
<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
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
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 を叩くボタン作る