Bài 16: Sử dụng axios để gọi Laravel API trong VueJS

Chào mừng các bạn quay trở lại với series học VueJS với Laravel của mình, ở bài trước mình đã hướng dẫn các bạn cách sử dụng Scoped CSS trong VueJS, ở bài này chúng ta sẽ tìm hiểu về cách gọi API từ backend là Laravel sử dụng axios nhé.

Thiết lập

Đầu tiên chúng ta tạo một component Vue mới đặt tên là ApiCalling.vue với nội dung như sau:

<template>
    <div class="api-calling">
        API CALLING
    </div>
</template>

<script>
    export default {

    }
</script>

<style lang="scss" scoped>
</style>

Sau đó các bạn khai báo component này trong app.js như sau:

Vue.component('api-calling', require('./components/ApiCalling.vue'));

Cuối cùng là thêm nó vào file welcome.blade.php:

<body>
    <div id="app">
        <api-calling></api-calling>
    </div>
    <script src="/js/app.js"></script>
</body>

Tiếp theo chúng ta sẽ setup backend Larave nhé.

Đầu tiên các bạn tạo một database vue_laravel, sau đó chỉnh sửa thông tin db trong file .env cho chính xác nhé.

Ở bài này chúng ta sẽ dùng axios để gọi API thêm, sửa, xoá, get danh sách sản phẩm từ backend. Để làm điều đó đầu tiên ta tạo một model Product trong laravel bằng command sau:

php artisan make:model Product -m

(option -m để tạo luôn 1 migration cho model Product)

Sau đó chúng ta vào database/migrations/create_products_table.php và sửa lại hàm up() như sau:

public function up()
{
    Schema::create('products', function (Blueprint $table) {
        $table->increments('id');
        $table->text('name');
        $table->double('price');
        $table->timestamps();
    });
}

Các bạn có thể thấy ta sẽ tạo ra một bảng tên là products, với các trường như mã sản phẩm (id), tên sản phẩm (name), giá (price), và biến thời gian biểu thị cho ngày tạo/chỉnh sửa sản phẩm.

Tiếp theo chúng ta vào App/Product.php và sửa lại như sau:

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Product extends Model
{
    protected $fillable = [
    	'name', 'price'
    ]
}

Nhân tiện đây mình cũng muốn giải thích cho các bạn một số điều như sau:

  • Mặc định trong Laravel sẽ mapping model Product với bảng products (thêm 's' ở cuối), nếu các bạn sử dụng tên bảng khác thì ta phải khai báo thêm như sau:
protected $table = '<table_name>';
  • Nếu ở migration mà các bạn không có timestamps() thì bên model Product ta khai báo như sau:
public $timestamps = false;
  • Với các field mà chúng ta muốn kiểm soát khi thay đổi giá trị (ví dụ như các giá trị này được post từ form html về chẳng hạn), thì ta cứ bỏ vào biến array $fillable để báo cho Laravel biết là cá field này có thể thay đổi giá trị bởi người dùng.
  • Ngược lại với $fillable là biến array $guarded, biến này sẽ chứa các field mà người dùng không được phép thay đổi. Ở đây các bạn thấy giá trị của field ‘id’ sẽ tự động tăng khi insert một record mới (MySQL tự động làm việc này).
  • Trong một số trường hợp, khi chúng ta lấy tất cả các field của các record, và trong đó, chúng ta không muốn hiển thị một số field nào đó, ví dụ ở đây mình muốn là không hiển thị 2 field là passwordremember_token, khi đó tui sẽ đặt 2 field này vào trong biến array $hidden. Điều này có nghĩa là tui báo với Laravel rằng tui sẽ lấy hết các field trừ 2 field passwordremember_token.

Ok thế là ổn rùi đó nhỉ, tiếp theo chúng ta chạy command:

php artisan migrate

Chú ý nếu ở bước này các bạn gặp lỗi error:...key too long. Thì ta mở file App/Providers/AppServiceProvider và sửa lại như sau (sau đó migrate lại là được nhé):

use Illuminate\Support\Facades\Schema;
//
public function boot()
{
    Schema::defaultStringLength(191);
}

Ở bài này ta làm các thao tác đơn giản như thêm, sửa, xoá, get,...ta sử dụng Route::resource cho tiện nhé. Các bạn mở file routes/web.php và thêm vào như sausau:

Route::resource('products', 'ProductController');

Tiếp theo ta tạo ProductController bằng cách:

php artisan make:controller ProductController --resource

Sau đó ta mở file ProductController.php lên, ở đó ta thấy đã có sẵn một số phương thức cho việc CRUD.

Gọi API

Thêm mới

Bây giờ chúng ta quay trở lại component ApiCalling.vue và tạo một form tạo sản phẩm mới như sau:

<template>
    <div class="api-calling">
        <div class="create-form">
            <div class="product-name-input">
                <input type="text" v-model="product.name">
            </div>
            <div class="product-name-input">
                <input type="text" v-model.number="product.price">
            </div>
            <div class="button-create">
                <button @click="createProduct">Create</button>
            </div>
        </div>
    </div>
</template>

<script>
    export default {
        data() {
            return {
                product: {
                    name: '',
                    price: 0
                }
            }
        },
        methods: {
            createProduct() {
                axios.post('/products', {name: this.product.name, price: this.product.price})
                .then(response => {
                    console.log(response.data.result)
                })
                .catch(error => {
                    console.log(error)
                })
            }
        }
    }
</script>

<style lang="scss" scoped>
</style>

Ở đây phần code HTML chắc các bạn có thể hiểu được(có gì thắc mắc comment bên dưới cho mình nhé). Mình sẽ giải thích phần code trong script. Ở đó ta có một phương thức là createProduct. Phương thức này sẽ sử dụng axios để tạo một post request đến route /products với 2 tham số là nameprice. Có thể các bạn sẽ thắc mắc:

  • axios??? nói mãi không chỉ cách cài đặt hay import nó, cứ thế phang vào sử dụng hay sao? 😃. Hiện tại khi setup mới project Laravel ở trong file resources/assets/js/bootstrap.js đã có sẵn:
window.axios = require('axios');

Tức là ta có thể sử dụng axios global trên toàn ứng dụng Vue để gọi API rồi nhé (nếu vì lí do nào đó chưa có các bạn tự thêm vào nha).

  • Điều tiếp theo có thể các bạn tự hỏi là tự dưng tạo sản phẩm sao biết route /products với method là post mà gọi? Thì các bạn xem hình bên dưới nhé (mình lấy ở trang chủ Laravel): Ở đây khi tạo sản phẩm ta cần gọi tới phương thức store trong ProductController, không phải phương thức create nhé, cái đó dành cho việc show form tạo sản phẩm thôi (điều này ta tự làm bên Vue được).

Ok khá ổn rồi đó nhỉ, giờ ta vào ProductController.php để lấy dữ liệu từ Vue và insert vào DB nhé. Ta sửa lại hàm store như sau (nhớ use App\Product; nhé):

public function store(Request $request)
{

    $this->validate($request, [
        'name' => 'required|min:5',
        'price' => 'required',
    ]);
    Product::create([
        'name'     => $request->input('name'),
        'price'    => $request->input('price'),
    ]);
    return response([
        'result' => 'success'
    ], 200);
}

Ở trên các bạn có thể thấy mình validate request với một vài điều kiện trước khi thêm nó vào trong DB, nếu thành công thì sẽ trả về mã 200.

Bởi vì mình có validate, nên bên Vue ta sửa lại chút để in ra lỗi nếu có nhé:

ApiCalling.vue

<template>
   <div class="api-calling">
       <div class="error" v-if="errors.length">
           <span v-for="err in errors">
               {{ err }}
           </span>
           <hr>
       </div>
       <div class="create-form">
           <div class="product-name-input">
               <input type="text" v-model="product.name">
           </div>
           <div class="product-name-input">
               <input type="text" v-model.number="product.price">
           </div>
           <div class="button-create">
               <button @click="createProduct">Create</button>
           </div>
       </div>
   </div>
</template>

<script>
   export default {
       data() {
           return {
               product: {
                   name: '',
                   price: 0
               },
               errors: []
           }
       },
       methods: {
           createProduct() {
               axios.post('/products', {name: this.product.name, price: this.product.price})
               .then(response => {
                   console.log(response.data.result)
               })
               .catch(error => {
                   this.errors = []
   				if(error.response.data.errors.name) {
   					this.errors.push(error.response.data.errors.name)
   				}
   				if(error.response.data.errors.price) {
   					this.errors.push(error.response.data.errors.price)
   				}
               })
           }
       }
   }
</script>

<style lang="scss" scoped>
.error {
   span {
       color: red;
   }
}
</style>

Sau đó các bạn thử load lại trang và xem kết quả nhé (thử nhập tên sản phẩm ít hơn 5 kí tự xem sao 😉). Thực sự là vì viblo viết bài dài thì dẫn đến tình trang web bị giật lag, nên các bạn tự test mình không chụp ảnh kết quả nữa nhé, nếu có gì thắc mắc các bạn cứ để lại dưới comment nhé.

Lấy danh sách sản phẩm

Để lấy danh sách sản phẩm từ DB ra ta sửa lại hàm index trong ProductController như sau:

public function index()
{
   return Product::get();
}

Ở trên ta đơn giản là lấy ra tất cả các sản phẩm.

Sau đó ở bên Vue ta sửa lại một chút như sau để load ra danh sách sản phẩm nhé:

<template>
   <div class="api-calling">
       <div class="error" v-if="errors.length">
           <span v-for="err in errors">
               {{ err }}
           </span>
           <hr>
       </div>
       <div class="create-form">
           <div class="product-name-input">
               <input type="text" v-model="product.name">
           </div>
           <div class="product-name-input">
               <input type="text" v-model.number="product.price">
           </div>
           <div class="button-create">
               <button @click="createProduct">Create</button>
           </div>
       </div>
       <hr>
       <div class="list-products">
           <h2>LIST PRODUCT</h2>
           <div class="product-table">
               <table class="table table-bordered">
                   <thead>
                       <tr>
                           <th>ID</th>
                           <th>Name</th>
                           <th>Price</th>
                           <th>Date created</th>
                       </tr>
                   </thead>
                   <tbody>
                       <tr v-for="prod in list_products">
                           <td>{{ prod.id }}</td>
                           <td>{{ prod.name }}</td>
                           <td>{{ prod.price }}</td>
                           <td>{{ prod.created_at }}</td>
                       </tr>
                   </tbody>
               </table>
           </div>
       </div>
   </div>
</template>

<script>
   export default {
       data() {
           return {
               product: {
                   name: '',
                   price: 0
               },
               errors: [],
               list_products: []
           }
       },
       created() {
           this.getListProducts()
       },
       methods: {
           createProduct() {
               this.errors = []
               axios.post('/products', {name: this.product.name, price: this.product.price})
               .then(response => {
                   console.log(response.data.result)
               })
               .catch(error => {
                   this.errors = []
   				if(error.response.data.errors.name) {
   					this.errors.push(error.response.data.errors.name)
   				}
   				if(error.response.data.errors.price) {
   					this.errors.push(error.response.data.errors.price)
   				}
               })
           },
           getListProducts() {
               axios.get('/products')
               .then(response => {
                   this.list_products = response.data
               })
               .catch(error => {
                   this.errors = []
   				if(error.response.data.errors.name) {
   					this.errors.push(error.response.data.errors.name)
   				}
   				if(error.response.data.errors.price) {
   					this.errors.push(error.response.data.errors.price)
   				}
               })
           }
       }
   }
</script>

<style lang="scss" scoped>
.error {
   span {
   	color: red;
   }
}
</style>

Ở đây ta tạo hàm getListProducts trong đó sử dụng axios gọi đến route /products với phương thức get, route này sẽ gọi đến hàm index và trả về danh sách sản phẩm, sau đó ta chỉ việc load danh sách này ra khi component created bằng cách sử dụng v-for. Các bạn thử load lại trang và xem kết quả nhé (nhớ insert trước một vài sản phẩm nha).

Ở đây có một chỗ chưa hợp lý, đó là khi ta thêm sản phẩm mới thì danh sách hiển thị sản phẩm chưa được cập nhật lại, vì nó chỉ được làm mới một lần duy nhất mỗi khi component created. Nhưng ta cũng không nên gọi hàm getListProducts liên tục mỗi khi insert thành công một bản ghi, vì điều đó sẽ làm cho ứng dụng của chúng ta trở nên chậm hơn do mất thời gian query từ DB. Do đó để hiển thị sản phẩm ngay lập tức khi ta vừa insert vào DB thành công, ta làm như sau. Sửa lại một chút ở hàm createProduct:

createProduct() {
    this.errors = []
    axios.post('/products', {name: this.product.name, price: this.product.price})
    .then(response => {
        console.log(response.data.result)
        this.list_products.push({
            id: this.list_products.length + 1,
            name: this.product.name,
            price: this.product.price,
            created_at: moment().format('YYYY-MM-DD HH:mm:ss')
        })
    })
    .catch(error => {
        this.errors = []
        if(error.response.data.errors.name) {
            this.errors.push(error.response.data.errors.name)
        }
        if(error.response.data.errors.price) {
            this.errors.push(error.response.data.errors.price)
        }
    })
},

Ở đây mỗi khi thêm sản phẩm thành công (hoàn tất insert vào DB), ta sẽ thêm ngay 1 bản ghi vào mảng list_products. Mình có sử dụng moment để format datetime cho dễ, các bạn cài moment bằng cách chạy npm install moment --save, sau đó ở component ApiCalling.vue, ta import moment vào bằng cách import moment from 'moment'.

Sau đó các bạn thử load lại trang và thử insert một bản ghi và có thể thấy danh sách đã được cập nhật ngay lập tức, do list_products mỗi khi thay đổi thì Vue sẽ re-render lại DOM.

Sửa thông tin sản phẩm

Tiếp theo để sửa thông tin sản phẩm, đầu tiên ta sửa lại hàm update trong ProductController như sau:

public function update(Request $request, $id)
{
    $this->validate($request, [
        'name' => 'required|min:5',
        'price' => 'required',
    ]);

    $product = Product::find($id);

    $product->name = $request->input('name');
    $product->price = $request->input('price');
    
    $product->save();

    return response([
        'result' => 'success'
    ], 200);
}

Bên Vue ta sửa lại như sau:

<template>
    <div class="api-calling">
        <div class="error" v-if="errors.length">
            <span v-for="err in errors">
                {{ err }}
            </span>
            <hr>
        </div>
        <div class="create-form">
            <div class="product-name-input form-group">
                <input class="form-control" type="text" v-model="product.name">
            </div>
            <div class="product-name-input form-group">
                <input class="form-control" type="text" v-model.number="product.price">
            </div>
            <div class="button-create form-group">
                <button class="btn btn-primary" @click="createProduct">Create</button>
            </div>
        </div>
        <hr>
        <div class="list-products">
            <h2>LIST PRODUCT</h2>
            <div class="product-table">
                <table class="table table-bordered">
                    <thead>
                        <tr>
                            <th>ID</th>
                            <th>Name</th>
                            <th>Price</th>
                            <th>Date created</th>
                            <th></th>
                        </tr>
                    </thead>
                    <tbody>
                        <tr v-for="prod in list_products">
                            <td>{{ prod.id }}</td>
                            <td v-if="!prod.isEdit">
                                {{ prod.name }}
                            </td>
                            <td v-else>
                                <input type="text" class="form-control" v-model="prod.name">
                            </td>
                            <td v-if="!prod.isEdit">
                                {{ prod.price }}
                            </td>
                            <td v-else>
                                <input type="text" class="form-control" v-model.number="prod.price">
                            </td>
                            <td>{{ prod.created_at }}</td>
                            <td v-if="!prod.isEdit"><button class="btn btn-success" @click="prod.isEdit = true">Edit</button></td>
                            <td v-else>
                                <button class="btn btn-primary" @click="updateProduct(prod)">Save</button>
                                <button class="btn btn-danger" @click="prod.isEdit = false">Cancel</button>
                            </td>
                        </tr>
                    </tbody>
                </table>
            </div>
        </div>
    </div>
</template>

<script>
    import moment from 'moment'
    export default {
        data() {
            return {
                product: {
                    name: '',
                    price: 0
                },
                errors: [],
                list_products: []
            }
        },
        created() {
            this.getListProducts()
        },
        methods: {
            formatDate(d) {
                var dformat = [ d.getFullYear(), (d.getMonth()+1),
                    d.getDate()
                    ].join('/')+
                    ' ' +
                  [ d.getHours(),
                    d.getMinutes(),
                    d.getSeconds()].join(':');
                    return dformat
            },
            createProduct() {
                this.errors = []
                axios.post('/products', {name: this.product.name, price: this.product.price})
                .then(response => {
                    console.log(response.data.result)
                    this.list_products.push({
                        id: this.list_products.length + 1,
                        name: this.product.name,
                        price: this.product.price,
                        created_at: moment().format('YYYY-MM-DD HH:mm:ss'),
                        isEdit: false
                    })
                })
                .catch(error => {
                    this.errors = []
					if(error.response.data.errors.name) {
						this.errors.push(error.response.data.errors.name)
					}
					if(error.response.data.errors.price) {
						this.errors.push(error.response.data.errors.price)
					}
                })
            },
            getListProducts() {
                axios.get('/products')
                .then(response => {
                    this.list_products = response.data
                    this.list_products.forEach(item => {
                        Vue.set(item, 'isEdit', false)
                    })
                })
                .catch(error => {
                    this.errors = error.response.data.errors.name
                })
            },
            updateProduct(product) {
                axios.put('/products/' + product.id, {name: product.name, price: product.price})
                .then(response => {
                    console.log(response.data.result)
                    product.isEdit = false
                })
                .catch(error => {
                    this.errors = error.response.data.errors.name
                })
            }
        }
    }
</script>

<style lang="scss" scoped>
.error {
    span {
        color: red;
    }
}
</style>

Một số sự thay đổi mình mới thêm vào như sau:

  • Chúng ta thêm vào hàm updateProduct để update thông tin của sản phẩm, sử dụng route put
  • Một điều cần chút ý là các bạn có thể thấy mình sửa lại thêm một thuộc tính là isEdit cho tất các bản ghi trong danh sách sản phẩm. Hàm getListProducts khi lấy được danh sách mình set thêm cho nó thuộc tính isEdit, ở đây ta dùng Vue.set để thêm vì như thế thì thuộc tính isEdit mới là reactive data, tức là sau này nó thay đổi thì DOM sẽ được re-render, còn không là ta click button Edit hoài mà không thấy gì xảy ra đâu nhé 😃
  • Tương tự ở hàm createProduct ta cũng thêm isEdit khi push vào list_products
  • Phần HTML phía trên thì đơn giản là khi isEdit bằng true thì ta show ra input để nhập liệu giá trị mới, còn false thì là trạng thái mặc định.

Khá ổn rồi đó, các bạn load lại trang và thử edit một sản phẩm bất kì xem kết quả thế nào nhé 😉

Xoá sản phẩm

Để xoá một sản phẩm đầu tiên ta cần sửa lại hàm destroy trong ProductController như sau:

public function destroy($id)
{
    $product = Product::find($id);
    $product->delete();
    return response([
        'result' => 'success'
    ], 200);
}

Sau đó ở bên Vue ta sửa lại như sau:

<template>
    <div class="api-calling">
        <div class="error" v-if="errors.length">
            <span v-for="err in errors">
                {{ err }}
            </span>
            <hr>
        </div>
        <div class="create-form">
            <div class="product-name-input form-group">
                <input class="form-control" type="text" v-model="product.name">
            </div>
            <div class="product-name-input form-group">
                <input class="form-control" type="text" v-model.number="product.price">
            </div>
            <div class="button-create form-group">
                <button class="btn btn-primary" @click="createProduct">Create</button>
            </div>
        </div>
        <hr>
        <div class="list-products">
            <h2>LIST PRODUCT</h2>
            <div class="product-table">
                <table class="table table-bordered">
                    <thead>
                        <tr>
                            <th>ID</th>
                            <th>Name</th>
                            <th>Price</th>
                            <th>Date created</th>
                            <th></th>
                        </tr>
                    </thead>
                    <tbody>
                        <tr v-for="(prod, index) in list_products">
                            <td>{{ prod.id }}</td>
                            <td v-if="!prod.isEdit">
                                {{ prod.name }}
                            </td>
                            <td v-else>
                                <input type="text" class="form-control" v-model="prod.name">
                            </td>
                            <td v-if="!prod.isEdit">
                                {{ prod.price }}
                            </td>
                            <td v-else>
                                <input type="text" class="form-control" v-model.number="prod.price">
                            </td>
                            <td>{{ prod.created_at }}</td>
                            <td v-if="!prod.isEdit">
                                <button class="btn btn-success" @click="prod.isEdit = true">Edit</button>
                                <button class="btn btn-danger" @click="deleteProduct(prod, index)">Delete</button>
                            </td>
                            <td v-else>
                                <button class="btn btn-primary" @click="updateProduct(prod)">Save</button>
                                <button class="btn btn-danger" @click="prod.isEdit = false">Cancel</button>
                            </td>
                        </tr>
                    </tbody>
                </table>
            </div>
        </div>
    </div>
</template>

<script>
    import moment from 'moment'
    export default {
        data() {
            return {
                product: {
                    name: '',
                    price: 0
                },
                errors: [],
                list_products: []
            }
        },
        created() {
            this.getListProducts()
        },
        methods: {
            formatDate(d) {
                var dformat = [ d.getFullYear(), (d.getMonth()+1),
                    d.getDate()
                    ].join('/')+
                    ' ' +
                  [ d.getHours(),
                    d.getMinutes(),
                    d.getSeconds()].join(':');
                    return dformat
            },
            createProduct() {
                this.errors = []
                axios.post('/products', {name: this.product.name, price: this.product.price})
                .then(response => {
                    console.log(response.data.result)
                    this.list_products.push({
                        id: this.list_products.length + 1,
                        name: this.product.name,
                        price: this.product.price,
                        created_at: moment().format('YYYY-MM-DD HH:mm:ss'),
                        isEdit: false
                    })
                })
                .catch(error => {
                    this.errors = []
					if(error.response.data.errors.name) {
						this.errors.push(error.response.data.errors.name)
					}
					if(error.response.data.errors.price) {
						this.errors.push(error.response.data.errors.price)
					}
                })
            },
            getListProducts() {
                axios.get('/products')
                .then(response => {
                    this.list_products = response.data
                    this.list_products.forEach(item => {
                        Vue.set(item, 'isEdit', false)
                    })
                })
                .catch(error => {
                    this.errors = error.response.data.errors.name
                })
            },
            updateProduct(product) {
                axios.put('/products/' + product.id, {name: product.name, price: product.price})
                .then(response => {
                    console.log(response.data.result)
                    product.isEdit = false
                })
                .catch(error => {
                    this.errors = error.response.data.errors.name
                })
            },
            deleteProduct(product, index) {
                axios.delete('/products/' + product.id)
                .then(response => {
                    console.log(response.data.result)
                    this.list_products.splice(index, 1)
                })
                .catch(error => {
                    this.errors = error.response.data.errors.name
                })
            }
        }
    }
</script>

<style lang="scss" scoped>
.error {
    span {
        color: red;
    }
}
</style>

Ở phần này mình thêm một số chỗ như sau:

  • v-for ta sửa lại một chút để lấy thêm vị trí(index) của một product trong mảng list_products
  • Bên dưới một chút ta thêm button Delete, với sự kiện click vào sẽ gọi đến hàm deleteProduct
  • Ta có hàm deleteProduct nhận 2 tham số là productindex là vị trí của product trong mảng product_lists. Trong hàm này ta đơn giản là gọi đến route delete để gọi đến hàm destroy trong ProductController. Sau khi delete xong ta cần phải hiển thị lại danh sách sản phẩm cho chính xác. Nhưng ta sẽ không gọi hàm getListProducts vì như thế sẽ tốn thời gian query lại vào DB, mà ta đơn giản sử dụng hàm của JS là splice. Vì list_productsreactive data (tất cả những gì khai báo trong data đều reactive), nên mỗi khi nó thay đổi thì Vue sẽ re-render lại DOM và ta có thể thấy danh sách đã được thay đổi ngay lập tức

Kết luận

Phù...cuối cùng chúng ta đã kết thúc một bài khá dài, mong rằng các bạn đã xem từ đầu đến cuối và có thể biết được cách gọi API từ Vue sang backend Laravel như thế nào từ đó áp dụng vào thực tế. Toàn bộ code các bạn có thể xem ở đây nhé.

Bài dài nên có thể có chỗ sai sót, hoặc các bạn có thắc mắc gì thì comment bên dưới cho mình nhé. Cám ơn các bạn đã theo dõi. Nếu các bạn có yêu cầu mình làm nội dung về một vấn đề nào đó thì cũng comment bên dưới nhé 😉.

Cám ơn các bạn đã theo dõi ^^!