はじめに
LaravelとNuxt.jsを使って開発したいと思っても最初に何を作ったらいいか思いつきませんよね。
どんなサービスを作るにしても基本となる、ログイン機能を作ることで基礎を定着させましょう。
開発環境
基本はDockerで構築しています。
フロントエンド側では、composition-apiやTypeScriptなどを導入しています。
Docker
・php:7.4-fpm-buster
・composer:1.10
・nginx:1.18-alpine
・mysql:5.7
・node:14.15.3-alpine
Laravel
・Laravel Passport
Nuxt.js
・TypeScript
・@nuxtjs/axios
・@nuxtjs/composition-api/module
・vee-validate
完成コード
それでは一緒にやってみましょう!
Githubからクローン
Dockerから解説となると長くなるので省略します。
GitHubにやり方は書いていますが、以下を実行することで環境構築ができます。
$ git clone git@github.com:ssk9597/Docker-Laravel-Nuxt-Nginx-MySQL.git
$ cd Docker-Laravel-Nuxt-Nginx-MySQL
$ make nuxt
$ make backend
$ make typescript
$ make composition-api
make typescript
とmake composition-api
の2つを実行時にはファイルに追記が必要となります。
こちらは以下の記事を参考にしてください。
まずはHTMLとCSS部分を作成
<template>
<div class="register-container">
<form class="register-wrapper">
<h2 class="register-title">新規登録</h2>
<!-- 姓 -->
<label class="register-label">
<span class="register-label-check">必須</span>
<span class="register-label-name">姓</span>
</label>
<input class="register-input-area" type="text" placeholder="例)田中" />
<!-- 名 -->
<label class="register-label">
<span class="register-label-check">必須</span>
<span class="register-label-name">名</span>
</label>
<input class="register-input-area" type="text" placeholder="例)太郎" />
<!-- メールアドレス -->
<label class="register-label">
<span class="register-label-check">必須</span>
<span class="register-label-name">メールアドレス</span>
</label>
<input class="register-input-area" type="email" placeholder="例)taro@example.com" />
<!-- パスワード -->
<label class="register-label">
<span class="register-label-check">必須</span>
<span class="register-label-name">パスワード</span>
</label>
<input class="register-input-area" type="password" placeholder="例)taroTanaka" />
<!-- 新規追加 -->
<button class="register-button" type="submit">新規登録</button>
</form>
</div>
</template>
<style lang="scss" scoped>
.register {
&-container {
position: relative;
height: 100vh;
width: 100%;
background-color: #eeeeee;
}
&-wrapper {
position: absolute;
top: 50%;
right: 50%;
transform: translateX(50%) translateY(-50%);
max-width: 400px;
width: 100%;
background-color: #fff;
border-radius: 10px;
padding: 50px 50px;
}
&-title {
text-align: center;
padding-bottom: 20px;
}
&-alert-red {
padding: 5px 10px;
background: #ffebee;
margin-bottom: 10px;
font-size: 12px;
}
&-label {
&-check {
background-color: #ee4056;
color: #fff;
padding: 3px 10px;
border-radius: 10%;
font-size: 0.75rem;
font-weight: 600;
}
&-name {
font-size: 0.8rem;
padding: 5px;
}
}
&-input {
padding-top: 10px;
padding-bottom: 15px;
display: block;
&-area {
box-sizing: border-box;
width: 100%;
padding: 10px;
font-size: 14px;
color: #303030;
border: solid 1px #eee;
border-radius: 6px;
outline: 0;
transition: 0.3s;
}
&-error {
padding-top: 5px;
font-size: 12px;
color: #ee4056;
font-weight: 600;
}
}
&-button {
margin-top: 20px;
width: 100%;
height: 40px;
color: #fff;
background: #40c7c1;
text-decoration: none;
border: none;
outline: none;
border-radius: 8px;
cursor: pointer;
appearance: none;
transition: 0.2s;
&:hover {
background: #78cec8;
}
}
}
</style>
フロントエンド側にバリデーションを追加
フロントエンドとバックエンドの両方にバリデーションをつけることが必須です。
フロントエンドにバリデーションがない場合、HTTP通信が走ってしまうのでエラーページへ遷移してしまいます。
また万が一、APIを直接叩かれたときにバックエンド側にバリデーションがないとDBに直接値が入ってしまう可能性があります。
以上の理由からフロントエンドにバリデーションを設定しましょう。
Nuxt.jsには、VeeValidateがあるのでこちらを使って実装していきましょう。
インストール方法
①VeeValidateをインストールする
$ docker-compose exec front npm install vee-validate --save
②プラグインファイルを作成する
import Vue from 'vue';
// 使用する機能
import { ValidationProvider, ValidationObserver, localize, extend } from 'vee-validate';
// エラーメッセージの日本語
import ja from 'vee-validate/dist/locale/ja.json';
// 使用するルール
import { required, email, min, alpha_dash } from 'vee-validate/dist/rules';
extend('required', required);
extend('email', email);
extend('min', min);
extend('alpha_dash', alpha_dash);
Vue.component('ValidationProvider', ValidationProvider);
Vue.component('ValidationObserver', ValidationObserver);
localize('ja', ja);
③nuxt.config.jsに追記
{
plugins: [{ src: '@/plugins/vee-validate' }],
build: {
transpile: ['vee-validate/dist/rules'],
},
}
これで使えるようになります。
使い方
// まずは全体をValidationObserverで囲もう。
// :disabledにinvalidが付くことでバリデーションすべてクリアしないとボタンがクリックできません。
<ValidationObserver v-slot="{ invalid }">
<button type="submit" :disabled="invalid">新規登録</button>
</ValidationObserver>
// バリデーション対象をValidationProviderで囲もう。
// nameはバリデーションエラー時のメッセージで使われます。
// rulesにどんどんルールを入れていきます。
<ValidationProvider name="パスワード" rules="required|min:8|alpha_dash" v-slot="{ errors }">
<input type="password" placeholder="例)taroTanaka" v-model="password" />
<p>{{ errors[0] }}</p>
</ValidationProvider>
ルールに関しては以下の公式サイトが網羅されているのでこちらをご確認ください。
それでは実装していきましょう
<template>
<div class="register-container">
<ValidationObserver v-slot="{ invalid }">
<form class="register-wrapper">
<h2 class="register-title">新規登録</h2>
<!-- 姓 -->
<label class="register-label">
<span class="register-label-check">必須</span>
<span class="register-label-name">姓</span>
</label>
<ValidationProvider class="register-input" name="姓" rules="required" v-slot="{ errors }">
<input class="register-input-area" type="text" placeholder="例)田中" />
<p class="register-input-error">{{ errors[0] }}</p>
</ValidationProvider>
<!-- 名 -->
<label class="register-label">
<span class="register-label-check">必須</span>
<span class="register-label-name">名</span>
</label>
<ValidationProvider class="register-input" name="名" rules="required" v-slot="{ errors }">
<input class="register-input-area" type="text" placeholder="例)太郎" />
<p class="register-input-error">{{ errors[0] }}</p>
</ValidationProvider>
<!-- メールアドレス -->
<label class="register-label">
<span class="register-label-check">必須</span>
<span class="register-label-name">メールアドレス</span>
</label>
<ValidationProvider
class="register-input"
name="メールアドレス"
rules="required|email"
v-slot="{ errors }"
>
<input class="register-input-area" type="email" placeholder="例)taro@example.com" />
<p class="register-input-error">{{ errors[0] }}</p>
</ValidationProvider>
<!-- パスワード -->
<label class="register-label">
<span class="register-label-check">必須</span>
<span class="register-label-name">パスワード</span>
</label>
<ValidationProvider
class="register-input"
name="パスワード"
rules="required|min:8|alpha_dash"
v-slot="{ errors }"
>
<input class="register-input-area" type="password" placeholder="例)taroTanaka" />
<p class="register-input-error">{{ errors[0] }}</p>
</ValidationProvider>
<!-- 新規追加 -->
<button class="register-button" type="submit" :disabled="invalid">新規登録</button>
</form>
</ValidationObserver>
</div>
</template>
スクリプトを追加
それでは次に動作を担うスクリプトの部分を追加していきましょう。
分けて考えた方が分かりやすいと思いますので今回は、data
, computed
, methods
の3つのパートに分けて説明していきます。
data
特に難しいところはないですね。
lastName
、firstName
、email
、password
の4つは、DBへ登録するために使う値です。
errors
はエラー文を表示する際に使用します。
<template>
<div class="register-container">
<ValidationObserver v-slot="{ invalid }">
<form class="register-wrapper">
<h2 class="register-title">新規登録</h2>
<!-- 姓 -->
<label class="register-label">
<span class="register-label-check">必須</span>
<span class="register-label-name">姓</span>
</label>
<ValidationProvider class="register-input" name="姓" rules="required" v-slot="{ errors }">
<input
class="register-input-area"
type="text"
placeholder="例)田中"
v-model="lastName"
/>
<p class="register-input-error">{{ errors[0] }}</p>
</ValidationProvider>
<!-- 名 -->
<label class="register-label">
<span class="register-label-check">必須</span>
<span class="register-label-name">名</span>
</label>
<ValidationProvider class="register-input" name="名" rules="required" v-slot="{ errors }">
<input
class="register-input-area"
type="text"
placeholder="例)太郎"
v-model="firstName"
/>
<p class="register-input-error">{{ errors[0] }}</p>
</ValidationProvider>
<!-- メールアドレス -->
<label class="register-label">
<span class="register-label-check">必須</span>
<span class="register-label-name">メールアドレス</span>
</label>
<ValidationProvider
class="register-input"
name="メールアドレス"
rules="required|email"
v-slot="{ errors }"
>
<input
class="register-input-area"
type="email"
placeholder="例)taro@example.com"
v-model="email"
/>
<p class="register-input-error">{{ errors[0] }}</p>
</ValidationProvider>
<!-- パスワード -->
<label class="register-label">
<span class="register-label-check">必須</span>
<span class="register-label-name">パスワード</span>
</label>
<ValidationProvider
class="register-input"
name="パスワード"
rules="required|min:8|alpha_dash"
v-slot="{ errors }"
>
<input
class="register-input-area"
type="password"
placeholder="例)taroTanaka"
v-model="password"
/>
<p class="register-input-error">{{ errors[0] }}</p>
</ValidationProvider>
<!-- 新規追加 -->
<button class="register-button" type="submit" :disabled="invalid">新規登録</button>
</form>
</ValidationObserver>
</div>
</template>
<script lang="ts">
import { defineComponent, ref } from '@nuxtjs/composition-api';
export default defineComponent({
setup() {
// data
const lastName = ref<string>('');
const firstName = ref<string>('');
const email = ref<string>('');
const password = ref<string>('');
const errors = ref<string>('');
return {
// data
lastName,
firstName,
email,
password,
errors,
};
},
});
</script>
computed
こちらはHTMLの部分が何も変わらないのでスクリプト部分のみ記載します。
ポイントは2つです。
computedは、@nuxtjs/composition-api内で読み込みが必要
refはvalueで値を取得するため、lastName.value
, firstName.value
となる
<script lang="ts">
import { defineComponent, ref, computed } from '@nuxtjs/composition-api';
export default defineComponent({
setup(props, context) {
// data
const lastName = ref<string>('');
const firstName = ref<string>('');
// computed
const name = computed(() => {
return `${lastName.value} ${firstName.value}`;
});
return {
// data
lastName,
firstName,
// computed
name,
};
},
});
</script>
methods
こちらは重要なポイントがオンパレードです。
なので順に説明していきます。
<template>
<div class="register-container">
<ValidationObserver v-slot="{ invalid }">
<form class="register-wrapper" @submit.prevent="registerUser()">
<h2 class="register-title">新規登録</h2>
<div v-if="errors" class="register-alert-red">
<p v-if="errors.name">{{ errors.name[0] }}</p>
<p v-if="errors.email">{{ errors.email[0] }}</p>
<p v-if="errors.password">{{ errors.password[0] }}</p>
</div>
<!-- 姓 -->
<label class="register-label">
<span class="register-label-check">必須</span>
<span class="register-label-name">姓</span>
</label>
<ValidationProvider class="register-input" name="姓" rules="required" v-slot="{ errors }">
<input
class="register-input-area"
type="text"
placeholder="例)田中"
v-model="lastName"
/>
<p class="register-input-error">{{ errors[0] }}</p>
</ValidationProvider>
<!-- 名 -->
<label class="register-label">
<span class="register-label-check">必須</span>
<span class="register-label-name">名</span>
</label>
<ValidationProvider class="register-input" name="名" rules="required" v-slot="{ errors }">
<input
class="register-input-area"
type="text"
placeholder="例)太郎"
v-model="firstName"
/>
<p class="register-input-error">{{ errors[0] }}</p>
</ValidationProvider>
<!-- メールアドレス -->
<label class="register-label">
<span class="register-label-check">必須</span>
<span class="register-label-name">メールアドレス</span>
</label>
<ValidationProvider
class="register-input"
name="メールアドレス"
rules="required|email"
v-slot="{ errors }"
>
<input
class="register-input-area"
type="email"
placeholder="例)taro@example.com"
v-model="email"
/>
<p class="register-input-error">{{ errors[0] }}</p>
</ValidationProvider>
<!-- パスワード -->
<label class="register-label">
<span class="register-label-check">必須</span>
<span class="register-label-name">パスワード</span>
</label>
<ValidationProvider
class="register-input"
name="パスワード"
rules="required|min:8|alpha_dash"
v-slot="{ errors }"
>
<input
class="register-input-area"
type="password"
placeholder="例)taroTanaka"
v-model="password"
/>
<p class="register-input-error">{{ errors[0] }}</p>
</ValidationProvider>
<!-- 新規追加 -->
<button class="register-button" type="submit" :disabled="invalid">新規登録</button>
</form>
</ValidationObserver>
</div>
</template>
<script lang="ts">
import { defineComponent, ref, computed, useContext } from '@nuxtjs/composition-api';
export default defineComponent({
setup(props, context) {
// axios
const { $axios } = useContext();
// router
const router = context.root.$router;
// data
const lastName = ref<string>('');
const firstName = ref<string>('');
const email = ref<string>('');
const password = ref<string>('');
const errors = ref<string>('');
// computed
const name = computed(() => {
return `${lastName.value} ${firstName.value}`;
});
// methods
const registerUser = async () => {
try {
// 新規登録
await $axios.post('/users/register', {
name: name.value,
email: email.value,
password: password.value,
});
// ログイン
const res = await $axios.post('/users/login', {
email: email.value,
password: password.value,
});
// token
await $axios.setToken(res.data.token, 'Bearer');
router.push('/');
} catch (err) {
errors.value = err.response.data.errors;
}
};
return {
// data
lastName,
firstName,
email,
password,
errors,
// computed
name,
// methods
registerUser,
};
},
});
</script>
①axiosの使い方
// axios
const { $axios } = useContext();
②routerの使い方
// router
const router = context.root.$router;
③バックエンドからのエラーメッセージの取得方法
err.response.data.errors
で取得できます。
<script lang="ts">
export default defineComponent({
setup(props, context) {
const registerUser = async () => {
try {}
catch (err) {
errors.value = err.response.data.errors;
}
};
},
});
</script>
では次にバックエンド側を作っていきましょう!
まずはLaravel Passportを導入しよう
LaravelにはデフォルトでトークンベースのシンプルなAPI認証の機能が備わっています。
しかし公式マニュアルにある通り、Laravel Passportを使用することを強く推奨されています。
なので今回も準拠した形で作っていきましょう。
インストール方法
①パッケージをインストール
$ docker-compose exec app composer require laravel/passport
②DBをマイグレーションしてキーを発行する
$ docker-compose exec app php artisan migrate
$ docker-compose exec app php artisan passport:install
③Models/User.phpを編集
<?php
namespace App\Models;
use Laravel\Passport\HasApiTokens; // 追記
class User extends Authenticatable
{
use HasApiTokens, Notifiable; // 追記
}
④Providers/AuthServiceProvider.phpを編集
<?php
namespace App\Providers;
use Laravel\Passport\Passport; // 追記
class AuthServiceProvider extends ServiceProvider
{
public function boot()
{
$this->registerPolicies();
Passport::routes(); // 追記
}
}
⑤config/auth.phpを編集
<?php
return [
'guards' => [
'web' => [
'driver' => 'session',
'provider' => 'users',
],
'api' => [
'driver' => 'passport',
'provider' => 'users',
'hash' => false,
],
],
}
これで使えるようになりました。
Authを使うのでCORSを修正しよう
header情報にtokenを設置するのでCORSの修正が必要になります。
<?php
namespace App\Http\Middleware;
use Closure;
class Cors
{
public function handle($request, Closure $next)
{
return $next($request)
->header('Access-Control-Allow-Origin', 'http://localhost:3000')
->header('Access-Control-Allow-Methods', 'GET, POST')
->header('Access-Control-Allow-Headers', 'Content-Type, X-XSRF-TOKEN, Authorization')
->header('Access-Control-Allow-Credentials', 'true');
}
}
バリデーションを作成する
$ docker-compose exec app php artisan make:request StoreRegister
// ①trueに変更(これをしないとうまく作用しない)
public function authorize() {
return true;
}
public function rules()
{
return [
"name" => "required|string",
"email" => "required|email|unique:users|max:255",
"password" => "required|min:8"
];
}
コントローラーを作成する
$ docker-compose exec app php artisan make:controller LoginController
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
// Models
use App\Models\User;
// Hash
use Illuminate\Support\Facades\Hash;
// Auth
use Illuminate\Support\Facades\Auth;
// Validation
use App\Http\Requests\StoreRegister;
class LoginController extends Controller
{
// 新規登録
public function register(StoreRegister $request)
{
// instance
$user = new User;
// value_save
$user->fill(array_merge($request->all(), ["password" => Hash::make($request->password)]))->save();
}
// ログイン
public function login(Request $request)
{
// validation
$login = $request->validate([
"email" => "required|email",
"password" => "required|string|min:8"
]);
// validationエラー
if (!Auth::attempt($login)) {
return response(["message" => "メールアドレスもしくはパスワードが間違っています"]);
};
// token発行
$token = Auth::user()->createToken('authToken')->accessToken;
// tokenとユーザー情報を受け取る
return response(["user" => Auth::user(), "token" => $token]);
}
}
ルーティング
Route::middleware(['cors'])->group(function () {
Route::options('accounts', function () {
return response()->json();
});
Route::post("/users/register", "LoginController@register");
Route::post("/users/login", "LoginController@login");
});
これで新規登録画面が作成できました。
それでは次にログイン画面を作っていきましょう。
バックエンドの処理に関してはすべて記載したので、フロントエンド部分のみ作ります。
ログイン画面
<template>
<div class="login-container">
<ValidationObserver v-slot="{ invalid }">
<form class="login-wrapper" @submit.prevent="loginUser()">
<h2 class="login-title">ログイン</h2>
<div v-if="errors" class="login-alert-red">
<p>{{ errors }}</p>
</div>
<!-- メールアドレス -->
<label class="login-label">
<span class="login-label-name">メールアドレス</span>
</label>
<ValidationProvider
class="login-input"
name="メールアドレス"
rules="required|email"
v-slot="{ errors }"
>
<input
class="login-input-area"
type="email"
placeholder="例)taro@example.com"
v-model="email"
/>
<p class="login-input-error">{{ errors[0] }}</p>
</ValidationProvider>
<!-- パスワード -->
<label class="login-label">
<span class="login-label-name">パスワード</span>
</label>
<ValidationProvider
class="login-input"
name="パスワード"
rules="required|min:8|alpha_dash"
v-slot="{ errors }"
>
<input
class="login-input-area"
type="password"
placeholder="例)taroTanaka"
v-model="password"
/>
<p class="login-input-error">{{ errors[0] }}</p>
</ValidationProvider>
<!-- 新規追加 -->
<button class="login-button" type="submit" :disabled="invalid">ログイン</button>
</form>
</ValidationObserver>
</div>
</template>
<script lang="ts">
import { defineComponent, ref, useContext } from '@nuxtjs/composition-api';
export default defineComponent({
setup(props, context) {
// axios
const { $axios } = useContext();
// router
const router = context.root.$router;
// data
const email = ref<string>('');
const password = ref<string>('');
const errors = ref<string>('');
// methods
const loginUser = async () => {
try {
const res = await $axios.post('/users/login', {
email: email.value,
password: password.value,
});
// error_message
errors.value = res.data.message;
// errorがない時TOPページへ移動する
if (!errors.value) {
router.push('/');
}
// token
await $axios.setToken(res.data.token, 'Bearer');
} catch (err) {
errors.value = err.response.data.errors;
}
};
return {
// data
email,
password,
errors,
// methods
loginUser,
};
},
});
</script>
<style lang="scss" scoped>
.login {
&-container {
position: relative;
height: 100vh;
width: 100%;
background-color: #eeeeee;
}
&-wrapper {
position: absolute;
top: 50%;
right: 50%;
transform: translateX(50%) translateY(-50%);
max-width: 400px;
width: 100%;
background-color: #fff;
border-radius: 10px;
padding: 50px 50px;
}
&-title {
text-align: center;
padding-bottom: 20px;
}
&-alert-red {
padding: 5px 10px;
background: #ffebee;
margin-bottom: 10px;
font-size: 12px;
}
&-label {
&-name {
font-size: 0.8rem;
}
}
&-input {
padding-top: 10px;
padding-bottom: 15px;
display: block;
&-area {
box-sizing: border-box;
width: 100%;
padding: 10px;
font-size: 14px;
color: #303030;
border: solid 1px #eee;
border-radius: 6px;
outline: 0;
transition: 0.3s;
}
&-error {
padding-top: 5px;
font-size: 12px;
color: #ee4056;
font-weight: 600;
}
}
&-button {
margin-top: 20px;
width: 100%;
height: 40px;
color: #fff;
background: #40c7c1;
text-decoration: none;
border: none;
outline: none;
border-radius: 8px;
cursor: pointer;
appearance: none;
transition: 0.2s;
&:hover {
background: #78cec8;
}
}
}
</style>
終わりに
特に難しいと思うポイントも少なく実装できるかと思います。
この後は掲示板サイトを作ってみても、何かしらのアプリケーションを作ってみてもいいと思います。
こちらをスタートに良質なポートフォリオをたくさん作っていってくださいね。
何かわからないところや間違っているところがありましたらご指摘よろしくお願いいたします。