Vue.js (12) - 實戰 CRUD 使用 Laravel + Vue

Vue.js

接續前一篇 Vue.js (11) - 在 Laravel 5.4 中使用 Vue 2.1,這一篇將要實戰如何寫出一個 CRUD 的應用,也就是對資料庫做建立、讀取、更新及刪除的動作。
我們將從無到有實際寫一個簡單的文章管理應用,整個打造的流程大致如下:
  1. 設定資料庫:為了示範方便,將使用 SQLite。
  2. 建立 API Routes:我們會透過 API 對資料庫做請求的動作。
  3. 建立 Post 頁面:在 Laravel 頁面中放入 Post.vue 元件。
  4. 建立 Post vue 元件:這裡是和使用者互動的部份,藉由前面建立的 API 來操作資料。
我們開始吧~

設定資料庫

你可以使用其他資料庫管理系統,例如 MySQL,這裡為了示範方便使用 SQLite

1. 建立資料庫檔案

在專案目錄的 database 目錄下,執行:
touch sqlite.db

2. 修改 .env

將資料庫的設定改成以下內容:
DB_CONNECTION=sqlite
DB_DATABASE=/絕對路徑/laravel-vue/database/sqlite.db

3. 產生 Post 模型及遷移檔

在專案根目錄下執行:
php artisan make:model Post -m
使用 make:model 模型名稱 建立資料模型,-m 是同時建立資料庫遷移檔。這裡是為了簡化操作,你也可以分開建立。

執行後會產生 app/Post.phpdatabase/migrations/2017_06_06_073431_create_posts_table.php 兩個檔案。其中的 2017_06_06_073431_ 會因建立的時間而不同。

4. 編輯 2017_06_06_073431_create_posts_table.php:

PHPpublic function up()
{
    Schema::create('posts', function (Blueprint $table) {
        $table->increments('id');
        $table->string('title'); // 文章的標題
        $table->string('body'); // 文章的內容
        $table->timestamps();
    });
}
存檔,然後執行:
php artisan migrate
這樣資料庫的表格及欄位就建好了,你可以用指令或 SQLPro for SQLite 軟體來查看資料庫的內容。

建立 API Routes

註:請先啟動內建伺服器 php artisan serve,確定網站是可以正常運作的。

我們會建立 5 個 API :
  • GET http://127.0.0.1:8000/api/posts:取得全部文章
  • GET http://127.0.0.1:8000/api/posts/{id}:取得單一文章
  • POST http://127.0.0.1:8000/api/posts:建立一篇文章
  • PUT http://127.0.0.1:8000/api/posts/{id}:更新一篇文章
  • DELETE http://127.0.0.1:8000/api/posts/{id}:刪除一篇文章

routes/api.php

Laravel 5.4 已經內建 API 機制,所有在 api.php 建立的 routes,網址都會以 api 開頭,像這樣 http://127.0.0.1:8000/api

這裡有一點要說明,為了示範單純化,我們這裡建立的 API 全都是未受限制的,也就是任何知道網址的人就能存取,這樣其實很危險。在正規的作法下,必須只接受登入後的存取,可以使用中介軟體來達成:
Route::middleware('auth:api')->get(...略);
這樣中介軟體就會過濾掉未登入的操作,記得在正式的產品中,務必加入驗證機制。
取得全部文章
目前資料庫中是空的,我們先用 Tinker 來加入一筆資料:
php artisan tinker 
這樣會進入 Tinker 的互動模式。Tinker 可以和你的應用程式做互動,其中包括 Eloquent。

之前用指令產生的 Post.php 被放在 app 目錄下,因此完整名稱為 App\Post,我們用它來列出所有文章:
App\Post::all();
應該會是空陣列,因為我們還沒新增資料。動手來新增一筆吧:
// 以下輸入一行可以按 Enter 執行
$post = new App\Post;
$post->title = "這是文章標題";
$post->body = "這是文章內容";
$post->save();
// 到這裡如果回傳 true 表示資料新增成功了

// 再來看一次結果
App\Post::all();
應該會看到我們新增的文章。現在開啟 api.php 加入我們的第一個 API Route:
PHPRoute::get('/posts', function() {
    return response()->json(App\Post::all(), 200);
});
打開瀏覽器進入 http://127.0.0.1:8000/api/posts 看看是否有收到 JSON 格式的資料。
取得單一文章
接著加入取得單一文章的 API Route:
PHPRoute::get('/posts/{id}', function($id) {
    return response()->json(App\Post::find($id), 200);
});
看看 http://127.0.0.1:8000/api/posts/1 應該有資料。這個 Route 之後不會用到,單純示範。
建立一篇文章
註:為了示範單純,這裡都不做任何輸入資料的驗證。

加入一個建立文章的 API Route:
PHPRoute::post('/posts/', function(Request $request) {
    $post = new Post;
    $post->title = $request->input('title', '沒有標題');
    $post->body = $request->input('body', '沒有內文。');
    $ok = $post->save();

    return response()->json(['ok' => $ok], 200);
});
接收 POST (這裡是指表單傳送的方法) 來新增資料,新增後回傳 ok 的值告知是否成功。

接著你可以用 Postman 這個工具來測試,或是用 curl 命令列工具,執行:
curl -d "title=第2篇文章&body=第2篇文章的內容" http://127.0.0.1:8000/api/posts
得到回傳結果 {"ok":true} 表示成功了。回到 http://127.0.0.1:8000/api/posts 看看,應該會多出一筆資料。

-d 選項是指將它之後的字串內容以 POST 方法傳送到指定的網址。前面取得文章的動作也可以用 curl 來測試:
curl http://127.0.0.1:8000/api/posts
curl http://127.0.0.1:8000/api/posts/1
更新一篇文章
加入一個更新文章的 API Route:
PHPRoute::put('/posts/{id}', function(Request $request, $id) {
    $ok = false;
    $msg = '';
    //
    $post = Post::find($id);
    if ($post) {
        $post->title = $request->input('title', '沒有標題');
        $post->body = $request->input('body', '沒有內文。');
        $ok = $post->save();
        if (!$ok) $msg = '更新失敗!';
    } else {
        $msg = '找不到文章';
    }

    return response()->json(['ok' => $ok, 'msg' => $msg], 200);
});
測試更新文章:
curl -X PUT -d "title=第2篇文章修改&body=第2篇文章的內容修改" http://127.0.0.1:8000/api/posts/2
回傳 {"ok":true,"msg":""} 表示成功。你可以試試看更新找不到的 id 是否會回傳錯誤訊息。回到 http://127.0.0.1:8000/api/posts 看看結果。

-X 選項可以讓你自定傳送的方法名稱,其他選項和 POST 相同,但是注意網址要附上文章 id。
刪除一篇文章
PHPRoute::delete('/posts/{id}', function($id){
    $rows = Post::destroy($id);
    $ok = ($rows > 0);
    return response()->json(['ok' => $ok], 200);
});
測試:
curl -X DELETE http://127.0.0.1:8000/api/posts/2
回傳結果 {"ok":true} 表示成功了。回到 http://127.0.0.1:8000/api/posts 看看結果。

PostController.php

前面我們將 API 的執行工作全都寫在 api.php 中,當功能變多時,夾雜大量的處理邏輯將會變得很混亂,以下將重構處理邏輯,將它們移到相對應的 Controller 中。

執行以下指令:
php artisan make:controller PostController
會建立 app/Http/Controllers/PostController.php,然後將我們在 api.php 中建立的處理邏輯移過來:
PHP<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Post;

class PostController extends Controller
{
    // API

    /**
     * 取得全部文章
     */
    function apiAll() {
        return response()->json(Post::all(), 200);
    }

    /**
     * 取得單一文章
     */
    function apiFindPostById($id) {
        return response()->json(Post::find($id), 200);
    }

    /**
     * 建立一篇文章
     */
    function apiCreatePost(Request $request) {
        $post = new Post;
        $post->title = $request->input('title', '沒有標題');
        $post->body = $request->input('body', '沒有內文。');
        $ok = $post->save();

        return response()->json(['ok' => $ok], 200);
    }

    /**
     * 更新一篇文章
     */
    function apiUpdatePostById(Request $request, $id) {
        $ok = false;
        $msg = '';
        //
        $post = Post::find($id);
        if ($post) {
            $post->title = $request->input('title', '沒有標題');
            $post->body = $request->input('body', '沒有內文。');
            $ok = $post->save();
            if (!$ok) $msg = '更新失敗!';
        } else {
            $msg = '找不到文章';
        }

        return response()->json(['ok' => $ok, 'msg' => $msg], 200);
    }

    /**
     * 刪除一篇文章
     */
    function apiDeletePostById($id) {
        $rows = Post::destroy($id);
        $ok = ($rows > 0);
        return response()->json(['ok' => $ok], 200);
    }
}
接著修改 api.php,改為使用 PostController:
PHPRoute::get('/posts', 'PostController@apiAll');
Route::get('/posts/{id}', 'PostController@apiFindPostById');
Route::post('/posts', 'PostController@apiCreatePost');
Route::put('/posts/{id}', 'PostController@apiUpdatePostById');
Route::delete('/posts/{id}', 'PostController@apiDeletePostById');
這樣是不是清爽多了~

建立 Post 頁面

API 都打造完成之後,接下來就要讓使用者可以使用它們來操作內容。

routes/web.php

在這裡建立的 Route 是可以讓使用者連入的網址,我們一樣將處理邏輯分開:
PHPRoute::get('/posts', 'PostController@index');
然後在 PostController.php 加上一個新方法:

PHP/**
 * 文章首頁
 */
function index()
{
    return view('post');
}

// API
...略
非常簡單的只回傳一個 view。

新增 resources/views/post.blade.php

我們會用到前一篇建立的 Layout ,所以內容如下:
@extends('layouts.default')

@section('title', 'Make CRUD App By Laravel with Vue')

@section('content')
    <Post></Post>
@endsection

@section('script')
    <script src="/js/post.js"></script>
@endsection

關於 CSS

之前的 Layout 檔沒有用到 CSS,現在來補加,首先編輯 webpack.mix.js
JavaScriptmix.js('resources/assets/js/hello.js', 'public/js')
    .extract(['lodash','jquery','axios','vue'])
    .sass('resources/assets/sass/app.scss', 'public/css');
加上 sass 的部分,這是 Laravel 內建的,可以調整成自己想要的。再來是把打包後的 app.css 加入 Layout 中。

編輯 resources/views/layouts/default.blade.php
HTML...略
    <title>@yield('title')</title>
    <link rel="stylesheet" href="/css/app.css">
</head>
...略
以上在 Laravel 中關於頁面的部份就完成,接下來終於進入 Vue 的部分了。

建立 Post vue 元件

新增 resources/assets/js/components/Post.vue

HTML<template>
    <div class="content">
        <div v-for="post in posts">
            <h1>{{ post.title }}</h1>
            <p>{{ post.body }}</p>
            <button class="btn btn-xs btn-primary" @click="modify(post)">修改</button>
            <button class="btn btn-xs btn-danger" @click="remove(post.id)">刪除</button>
            <hr>
        </div>

        <form id="form">
            <div class="form-group" :class="{ 'has-warning': titleWarning }">
                <label class="control-label">標題
                    <span v-if="titleWarning">不能空白</span>
                </label>
                <input class="form-control" v-model="post.title">
            </div>
            <div class="form-group" :class="{ 'has-warning': bodyWarning }">
                <label class="control-label">內容
                    <span v-if="bodyWarning">不能空白</span>
                </label>
                <textarea class="form-control" v-model="post.body"></textarea>
            </div>
            <div class="form-group">
                <div v-if="isSave">
                    <button @click.prevent="save">儲存</button>
                    <button @click.prevent="cancel">取消</button>
                </div>
                <button v-else @click.prevent="publish">發佈</button>
            </div>
        </form>
    </div>
</template>

<script>
export default {
    data() {
        return {
            posts: [],
            post: {
                id: null,
                title: '',
                body: ''
            },
            titleWarning: false,
            bodyWarning: false,
            isSave: false
        }
    },

    methods: {
        init: function () {
            let self = this;
            axios.get('/api/posts')
                .then(function (response) {
                    self.posts  = response.data;
                })
                .catch(function (response) {
                    console.log(response);
                });
        },

        publish: function () {
            this.titleWarning = (this.post.title.trim().length == 0);
            this.bodyWarning = (this.post.body.trim().length == 0);
            if (this.titleWarning || this.bodyWarning) return;
            // 
            let self = this;
            axios.post('/api/posts', this.post)
                .then(function (response) {
                    if (response.data['ok']) {
                        self.init();
                        self.titleWarning = false;
                        self.bodyWarning = false;
                        self.post = {id:null, title: '', body:''};
                    }
                })
                .catch(function (response) {
                    console.log(response)
                });
        },

        modify: function (post) {
            location.href = "#form";
            this.post.id = post.id;
            this.post.title = post.title;
            this.post.body = post.body;
            this.isSave = true;
            console.log(this.post);
        },

        save: function () {
            let self = this;
            axios.put('/api/posts/' + this.post.id, this.post)
                .then(function (response) {
                    if (response.data['ok']) {
                        self.init();
                        self.isSave = false;
                        self.post = {id:null, title: '', body:''};
                    }
                })
                .catch(function (response) {
                    console.log(response);
                });
        },
      
        cancel: function () {
            this.post = {id: null, title: '', body: ''};
            this.isSave = false;
        },

        remove: function (id) {
            let self = this;
            axios.delete('/api/posts/' + id)
                .then(function (response) {
                    if (response.data['ok']) {
                        self.init();
                    }
                })
                .catch(function (response) {
                    console.log(response);
                });
        }
    },

    mounted: function () {
        this.init();
    }
}
</script>

<style scoped>
    .content {
        padding: 20px;
    }
</style>
說明:
  • 頁面的排版是上方顯示文章清單,同時附上「修改」與「刪除」的按鈕。
  • 接著是一個表單,可同時做為「發佈」新文章及「儲存」修改的內容。
  • 在 data() 中,posts 會儲存全部的文章,post 會和表單的欄位綁定,可做為發佈及修改時的資料。其他 3 個是用來得知狀態改變時用的。
  • methods 中,請求都是透過我們建立的 API
    • init 用來初始化資料,所以它會去取得全部的文章。
    • axios 是一個基於 Promise 的 HTTP 請求套件,由於在其內部中無法使用 this 來存取外部的資料,所以透過 let self = this; 轉傳。它是以 get().then().catch() 的方式來請求資料,成功時呼叫 then() 失敗時呼叫 catch()
    • publish 會送出新增文章的請求,成功的話執行 init 來取得全部文章。
    • modify 會將資料送給 post ,由於它和表單綁定,所以使用者就可以直接在表單上看到內容。
    • save 會送出更新文章的請求,成功的話執行 init 來取得全部文章。
    • cancel 會取消表單中的內容,並且隱藏「儲存」及「取消」按鈕。
    • remove 會送出刪除文章的請求,成功的話執行 init 來取得全部文章。
  • mounted 會在網頁載入完成時執行,這裡我們執行 init 方法。

新增 resources/assets/js/post.js

JavaScriptrequire('./bootstrap.js');

import Post from './components/Post.vue';

new Vue({
    el: '#app',
    components: { Post }
})

webpack.mix.js

JavaScriptmix.js('resources/assets/js/hello.js', 'public/js')
    .js('resources/assets/js/post.js', 'public/js')
    .extract(['lodash','jquery','axios','vue'])
    .sass('resources/assets/sass/app.scss', 'public/css');
把我們新建立的 post.js 加入打包的動作中。

關於 CSRF

Laravel 已經內建 CSRF 保護機制,所以我們必須修改 default.blade.php
HTML...略
<meta name="csrf-token" content="{{ csrf_token() }}">
<title>@yield('title')</title>
...略
<head> 中加上相關的 meta 值。它被用於 resources/assets/js/bootstrap.js 中,Laravel 已經幫我們指定給 axios 了。如果你少了這個動作,所有表單的送出請求都會被禁止。

好了,全部完成,執行自動打包來看看結果
npm run watch
或
yarn run watch
在瀏覽器中開啟 http://127.0.0.1:8000/posts 試用看看自己打造的應用程式吧~

以下是執行結果: CRUD App use laravel with vue demo
本文網址:http://blog.tonycube.com/2017/06/vuejs-12-crud-laravel-vue.html
Tony Blog 撰寫,請勿全文複製,轉載時請註明出處及連結,謝謝 😀

我要留言

留言小提醒:
1.回覆時間通常在晚上,如果太忙可能要等幾天。
2.請先瀏覽一下其他人的留言,也許有人問過同樣的問題。
3.程式碼請先將它編碼後再貼上。(線上編碼:http://bit.ly/1DL6yog)
4.文字請加上標點符號及斷行,難以閱讀者恕難回覆。
5.感謝您的留言,您的問題也可能幫助到其他有相同問題的人。