Nuxt + Laravel でとりあえず動くものをサクッと作る
本記事は Nuxt + Laravel で とりあえず動くものをサクッと作ろう をテーマにつらつら書いていきます。
はじめに
記事投稿者はフロント実装者ですが、イチから REST APIを構築してみたいなぁ、ということから手順などを記事にしてみました。
こんな方向けの記事です
- Vue.js + TypeScript の基礎を理解されている方
- PHP / MySQL の基礎を理解されている方
- フロントエンドだけどREST APIでとりあえず動くものを作ってみたい方
- CORSに悩んでいる方
- 環境変数の持たせ方に悩んでいる方
今回のゴール
- 簡単なCRUDを実装
- 今回は書籍管理システム
- バックエンドはLaravelでREST API構築
- Nuxtでフロントの構築
- CORS対策もするよ
環境
- PC: MacBook Pro (Intel Core 2016)
- OS: macOS Montery12.0.1
- フロントエンド:
- Node.js 16.9.1
- npm 7.21.1
- Nuxt.js 2.15.8
- TypeScript 4.5.4
- バックエンド
- PHP 8.0.10
- Laravel Framework 8.76.2
- Laravel Installer 4.1.1
- DB
- MySQL 14.14
フォルダ構成
全体的な構成です。
- sample
- nuxt-sample
- pages
- index.vue
- detail
- _id.vue
- service
- book.ts
- laravel-sample
Nuxt(フロントエンド)の構築
フロントは Nuxt + TypeScriptで構築いたします。
1. create
まずは create コマンドから叩きます。
% mkdir sample
% cd sample
% npx create-nuxt-app <project-name>
npx については以下の記事が参考になったので載せておきます。
コマンドを叩くと対話式でいくつか質問があります。
今回は以下のようにしました。
? Project name: nuxt-sample
? Programming language: TypeScript
? Package manager: Npm
? UI framework: None
? Nuxt.js modules: Axios - Promise based HTTP client
? Linting tools: ESLint, Prettier
? Testing framework: Jest
? Rendering mode: Single Page App
? Deployment target: Static (Static/Jamstack hosting)
? Development tools: jsconfig.json (Recommended for VS Code if you're not using
typescript)
? Continuous integration: None
? Version control system: Git
インストール完了したらこんなメッセージが出ます。
🎉 Successfully created project nuxt-sample
To get started:
cd nuxt-sample
npm run dev
To build & start for production:
cd nuxt-sample
npm run build
npm run start
To test:
cd nuxt-sample
npm run test
For TypeScript users.
See : https://typescript.nuxtjs.org/cookbook/components/
インストールしたNuxt.jsのディレクトリに移動します。
% cd nuxt-sample
一応Nuxt起動してみます。
% npm run dev
うまくいくと以下のように表示されます。
╭───────────────────────────────────────╮
│ │
│ Nuxt @ v2.15.8 │
│ │
│ ▸ Environment: development │
│ ▸ Rendering: client-side │
│ ▸ Target: static │
│ │
│ Listening: http://localhost:3000/ │
│ │
╰───────────────────────────────────────╯
ℹ Preparing project for development 06:34:37
ℹ Initial build may take a while 06:34:37
ℹ Discovered Components: .nuxt/components/readme.md 06:34:37
✔ Builder initialized 06:34:37
✔ Nuxt files generated 06:34:37
✔ Client
Compiled successfully in 8.19s
ℹ Waiting for file changes 06:34:51
ℹ Memory usage: 249 MB (RSS: 355 MB) 06:34:51
ℹ Listening on: http://localhost:3000/ 06:34:51
No issues found.
ここでは http://localhost:3000/ にアクセスします。
はい、動いていますね。
2. 開発準備
ここからDX向上のために色々設定していきます。
VSCodeの設定
まず local 用の設定ファイルを作成します。
% mkdir .vscode
% touch .vscode/settings.json
投稿者がいつもやっている設定は以下の通りです。
{
"[vue]": {
"editor.defaultFormatter": "octref.vetur"
},
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"vetur.experimental.templateInterpolationService": true,
"vetur.format.defaultFormatter.html": "prettier",
"files.autoSave": "afterDelay",
"eslint.alwaysShowStatus": true,
"editor.tabSize": 2,
"editor.formatOnPaste": true,
"editor.formatOnType": true,
"editor.formatOnSave": true,
"files.insertFinalNewline": true,
"files.eol": "\n"
}
ESLintの設定
ESLint は rules の箇所だけ以下の通りにしています。
rules: {
'no-useless-constructor': 'off',
'vue/singleline-html-element-content-newline': 'off',
'no-use-before-define': 'off',
quotes: [
'error',
'single',
{
avoidEscape: true,
allowTemplateLiterals: false,
},
],
},
3. proxy と nuxt.config.js の設定
続いては、proxy のインストールと nuxt.config.js を設定していきます。
まず proxy です。
% npm i @nuxtjs/proxy
次は、nuxt.config.js で proxy の設定を行います。
以下のように修正します。
modules: ['@nuxtjs/axios', '@nuxtjs/proxy'],
axios: { proxy: true },
proxy: {
'/api': 'http://127.0.0.1:8000',
},
今回は実装を軽くしているので、proxy 内の URL を直書きしていますが、
実務では環境変数を用意します。
上記のコードだと上司に怒られます。
環境変数の設定については著者がQiita側で記述した以下の記事をご参考下さい。
4. HTTPClient
今回は axios が入っているので、axios を使います。
外部通信のメソッド作成
外部と通信するメソッドを作成していきます。
軽微なレイヤーにしていますが、
このようなアーキテクチャでは上司に怒られ(ry
% mkdir service
% touch service/book.ts
import axios, { AxiosResponse } from 'axios'
export interface BookResponse {
id: number
title: string
author: string
}
export interface BookRequest {
title: string
author: string
}
export class BookService {
async fetchBooks(): Promise<BookResponse[]> {
const { data } = await axios.get<AxiosResponse<BookResponse[]>>(
'/api/books'
)
return data.data
}
async postBookData(bookRequest: BookRequest) {
await axios.post('/api/books', bookRequest)
}
async fetchBook(bookId: number) {
const { data } = await axios.get<AxiosResponse<BookResponse>>(
`/api/books/${bookId}`
)
return data.data
}
putBook(bookId: number, data: BookRequest) {
axios.put(`/api/books/${bookId}`, data)
}
async deleteBook(bookId: number) {
await axios.delete(`/api/books/${bookId}`)
}
}
5. vueファイルの作成
ここからは、vueファイルを作成していきます。
まずは、「pages/index.vue」 から。
pages/index.vue
<template>
<div>
<h2>List</h2>
<ul v-for="(book, i) in books" :key="i">
<li>{{ book.title }}</li>
<nuxt-link :to="{ name: 'book-detail-id', params: { id: book.id } }"
><button>詳細</button>
</nuxt-link>
<button @click="onClickDelete(book.id)">削除</button>
</ul>
<h3>新規追加</h3>
<input v-model="form.title" type="text" placeholder="title" /><br />
<input v-model="form.author" type="text" placeholder="author" /><br />
<button @click="onClickAdd">追加</button>
</div>
</template>
<script lang="ts">
import Vue from 'vue'
import bookService, { BookResponse } from '@/service/book'
interface Form {
title: string
author: string
}
type Book = BookResponse
interface DataType {
form: Form
books: Book[]
}
export default Vue.extend({
async asyncData() {
const books = await bookService.fetchBooks()
return {
books,
}
},
data(): DataType {
return {
form: { title: '', author: '' },
books: [],
}
},
methods: {
async onClickAdd() {
await bookService.postBook({ ...this.form })
this.books = await bookService.fetchBooks()
this.form = { title: '', author: '' }
},
async onClickDelete(bookId: number) {
await bookService.deleteBook(bookId)
this.books = await bookService.fetchBooks()
},
},
})
</script>
pages/book/detail/_id.vue
続いては、「pages/book/detail/_id.vue」 です。
「詳細」をクリックした時に個別の情報を表示します。
<template>
<div>
<h2>詳細</h2>
<input v-model="book.title" type="text" />
<input v-model="book.author" type="text" />
<button @click="onClickEdit">修正</button>
<nuxt-link :to="{ name: 'index' }"><p>Book List</p></nuxt-link>
</div>
</template>
<script lang="ts">
import Vue from 'vue'
import bookService, { BookResponse } from '@/service/book'
type Book = BookResponse
interface DataType {
book: Book
}
export default Vue.extend({
async asyncData({ route }) {
const bookId = Number(route.params.id)
const book = await bookService.fetchBook(bookId)
return {
book,
}
},
data(): DataType {
return {
book: {
id: 0,
title: '',
author: '',
},
}
},
methods: {
onClickEdit() {
const bookId = Number(this.$route.params.id)
bookService.putBook(bookId, this.book)
this.$router.push({ name: 'index' })
}
}
})
</script>
今回、実装を軽量にするためstoreは使っていません。
はい、ここでフロントは終了です。
Laravel(バックエンド)の構築
続いては、バックエンドを構築していきます。
残念ながら、今回はDockerは使いません。
Dockerでの構築は別記事で紹介したいと思います。
(ちなみに投稿者の mac には phpenv を入れています)
1. create
先ほどまで作成していたフロントのフォルダとは別のフォルダで作っていきます。
% cd sample
% laravel new <project_name>
2. Local に DB を作る
さて、続いては Local に DB を作りましょう。
% mysql -u root
mysql > CREATE DATABASE <db_name>;
mysql > show databases;
上記コマンドで先ほど作成した DB があればOKです。
OKでしたら、以下コマンドで一旦 MySQL から抜けます。
% exit
3. 各種設定
これから、動作のために必要な設定を行なっていきます。
.env
先ほど作成した DB を .env ファイルで設定します。
デフォルトの値はこのようになっています。
L14 あたりです。
DB_DATABASE=laravel_sample
DB_USERNAME=root
DB_PASSWORD=
修正が必要であれば、修正しましょう。
config/app.php
続いては、config/app.php の timezone
と locale
を以下のように修正します。
L70 あたりと、L83 あたりです。
'timezone' => 'Asia/Tokyo'
'locale' => 'ja'
config/database.php
そして、 config/database.php の mysql
内にある charset
callation
を以下のように修正します。
L55 あたりです。
'charset' => 'utf8'
'callation' => 'utf8_unicode_ci'
4. model の作成
さあ、ここから model を artisan
コマンドで作っていきます。
今回は model と migration を同時に作りますので、以下のコマンドを叩きます。
% php artisan make:model Book --migration
完了したら、ホワイトリストを登録しましょう。
自動生成された、app/Models/Book.php の Bookクラス
に以下を追加します。
protected $fillable = [
'title',
'author',
];
続いて、自動生成されたマイグレーションファイルで table 設計を行います。
今回は以下のような設計にします。
Schema::create('books', function (Blueprint $table) {
$table->bigIncrements('id');
$table->string('title');
$table->string('author');
$table->timestamps();
});
5. migrate
では、migration していきます。
Laravel の場合、以下のコマンドを叩くだけです。
% php artisan migrate
.env の DB の設定などを間違えていなければ成功するはずです。
念の為、MySQL を確認してみましょう。
% mysql -u root
mysql > use <database_name>;
mysql > show tables;
ついでに、設計通りに作成されているかチェックしてみます。
mysql > DESC books;
+------------+---------------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+------------+---------------------+------+-----+---------+----------------+
| id | bigint(20) unsigned | NO | PRI | NULL | auto_increment |
| title | varchar(255) | NO | | NULL | |
| author | varchar(255) | NO | | NULL | |
| created_at | timestamp | YES | | NULL | |
| updated_at | timestamp | YES | | NULL | |
+------------+---------------------+------+-----+---------+----------------+
5 rows in set (0.00 sec)
上記のようになっていると思います。
で、一応 mysql から抜けます。
mysql > quit
では、次に進みましょう。
6. controler の作成
続いては、controler を作成します。
こちらも、Laravel であればコマンドで作成できます。
今回は、REST API なので以下のコマンドを叩いてみましょう。
% php artisan make:controller BookController --api
app/Http/Controllers/BookController.php が作成されます。
BookController のメソッドの中身を作る
app/Http/Controllers/BookController.php を見るとメソッドの中身までは
ありませんのでメソッドの中身を作っていきます。
まずは、Book Model を使うので、L6 あたりに use App\Models\Book;
を追加します。
index
public function index()
{
$books = Book::all();
return response()->json([
'message' => 'ok',
'data' => $books
], 200, [], JSON_UNESCAPED_UNICODE);
}
Laravel の ORM(オブジェクト・リレーショナル・マッパー) については以下の公式を参照下さい。
store
public function store(Request $request)
{
$book = Book::create($request->all());
return response()->json([
'message' => 'Book created successfully',
'data' => $book
], 201, [], JSON_UNESCAPED_UNICODE);
}
show
public function show($id)
{
$book = Book::find($id);
if ($book) {
return response()->json([
'message' => 'ok',
'data' => $book
], 200, [], JSON_UNESCAPED_UNICODE);
}
return response()->json([
'message' => 'Book not found',
], 404);
}
update
public function update(Request $request, $id)
{
$update = [
'title' => $request->title,
'author' => $request->author
];
$book = Book::where('id', $id)->update($update);
if ($book) {
return response()->json([
'message' => 'Book updated successfully',
], 200);
}
return response()->json([
'message' => 'Book not found',
], 404);
}
destroy
public function destroy($id)
{
$book = Book::where('id', $id)->delete();
if ($book) {
return response()->json([
'message' => 'Book deleted successfully',
], 200);
}
return response()->json([
'message' => 'Book not found',
], 404);
}
これで簡単な CRUD が実装できました。
7. routes の作成
続いては、routes/api.php を以下のようにします。
Route::apiResource('/books', 'App\Http\Controllers\BookController');
今回は、middlewareなどは使わないのでついでに削除しておきます。
8. laravel-cors のインストール
以下のコマンドを叩きます。
% composer require fruitcake/laravel-cors
% php artisan vendor:publish --tag="cors"
はい、これでバックエンドは準備できましたのでいよいよ動かしてみましょう!
動作確認
では、実際に動かしてみます。
Laravel 起動
最初に、Lravael 側を立ち上げます。
Laravel 直下で以下のコマンドを叩きます。
php artisan serve
成功すると以下のような表示が出ます。
Starting Laravel development server: http://127.0.0.1:8000
[Sat Dec 18 15:03:49 2021] PHP 8.0.10 Development Server (http://127.0.0.1:8000) started
これで、バックエンドが port8000 で立ち上がりました。
Nuxt 起動
Laravel 側は動作させたままで、次は Nuxt 側を起動します。
Nuxt 直下で以下のコマンドを叩きます。
npm run dev
build されたらブラウザで確認してみましょう。
ブラウザで確認
http://localhost:3000/ にアクセスします。
表示されました。
では、CRUD 操作をしてみましょう。
title と author を入力して「追加」ボタンをクリック。
追加できました。
続いて、削除ボタンを押してみましょう。
削除できました。
では最後に更新をしてみましょう。
「詳細」ボタンをクリックします。
内容を変更して「修正」ボタンを押すと、
無事修正もできました。
これで Nuxt + Laravel で CRUD 操作ができました。
せっかくなので最後に DB の値も見てみましょう。
% myqsl -u root
mysql > SELECT * FROM books;
DB の中身も見ることができました。
以上となります。
Discussion
すみません、質問です。 npm run devをしたところ
WARN in ./pages/book/detail/_id.vue?vue&type=script&lang=ts& friendly-errors 17:58:53
"export 'default' (imported as 'bookService') was not found in '@/service/book'
friendly-errors 17:58:53
と出まして、画面も表示されませんでしたがバージョンや設定の問題でしょうか?
と思ったら、qiitaのほうに修正版上がってましたね。https://qiita.com/uehatsu/items/19b1ae0c706993d0e550 お騒がせしました
ここでは『http://localhost:3000/』 にアクセスします。
の部分のリンクが変です