(Demo) Xây dựng blog Single Page Application với Laravel và Vue.js

Trong bài viết này, chúng ta sẽ cùng nhau xây dựng một trang blog Single Page Application (SPA) sử dụng là Laravel framework và Vue.js. Bạn có thể dọc docs của 2 framework này tại trang chủ của nó: LaravelVue.js Nói đơn giản, với 1 SPA các tài nguyên (css, js...) của ứng dụng đó đã được tải xuống ở lần truy cập đầu tiên, các lần tiếp theo chỉ thực hiện gọi ajax để load về dữ liệu mới nên tốc độ ứng dụng của chúng ta sẽ tăng lên rất nhanh so với ứng dụng web truyền thống.

Mục tiêu Sử dụng
https://nobi.dev ( Chỉ là demo thôi, đừng chém e 😄 ) Laravel + Vuejs

Tài khoản demo là: blaysku/123123

Start thôi, trong bài mình sẽ tạm gọi phần code Laravel là backend (BE) và phần code Vue.js là frontend (FE) cho tiện nhé.

1.Cấu trúc

Ở đây mình sẽ sử dụng cấu trúc xây dựng sẵn của Laravel, tức là Vuejs sẽ là 1 phần nằm trong ứng dựng Laravel ở thư mục resources/assets/js, có một cách cool hơn là chúng ta tách riêng hai thằng này ra, sẽ dễ quản lý hơn, đây là cấu trúc thư mục: Phần BE sẽ chịu trách nhiệm tạo api cho ứng dụng, phần FE sẽ gọi api và render ra view, thằng nào ko hoàn thành nhiệm vụ thì (cat) thằng dev.

2. Xây dụng API

Mục tiêu của chúng ta là xây dựng 1 blog có các chức năng thêm sửa xóa post, có tag, có category, comment trong post... Chúng ta sẽ sử dụng Laravel Eloquent API Resources để xây dựng API cho tiện hơn, mình có viết 1 bài ở đây hoặc xem đocument chính chủ tại đây Đây là cấu trúc: Chúng ta sẽ có model Post với các relationship: author, category, tags, comments, likes.... Tạo 1 PostResource kiểu kiểu như này, các Resource khác xây dựng tương tự

    public function toArray($request)
    {
        return [
            'title' => $this->title,
            'slug' => $this->slug,
            'excerpt' => $this->excerpt,
            'meta_description' => $this->meta_description,
            'meta_keywords' => $this->meta_keywords,
            'content' => $this->content,
            'image' => $this->image,
            'thumb' => $this->thumb,
            'featured' => (boolean) $this->featured,
            'is_public' => (boolean) $this->is_public,
            'comments_count' => $this->comments_count,
            'likes_count' => $this->likes_count,
            'created_at' => (string)$this->created_at,
            'updated_at' => (string)$this->updated_at,
            'author' => new UserResource($this->whenLoaded('author')),
            'category' => new CategoryResource($this->whenLoaded('category')),
            'tags' => TagResource::collection($this->whenLoaded('tags')),
            'likes' => LikeResource::collection($this->whenLoaded('likes'))
        ];
    }

Sau đó là 1 Post Resource Controller với các phương thức: index, store, show, update, destroy..., "kiểu kiểu" như vầy :man_vampire:

    /**
     * Display a listing of the resource.
     *
     * @return \App\Http\Resources\PostResource
     */
    public function index(Request $request)
    {
        return PostResource::collection(
            Post::with(['category', 'author', 'tags'])->latest()->paginate()
        );
    }
    /**
     * Display the specified resource.
     * @param  \App\Models\Post  $post
     * @return \App\Http\Resources\PostResource
     */
    public function show(Post $post)
    {
        abort_unless($post->is_public || $this->user->id === $post->user_id, 403);
        return new PostResource($post->load(['author', 'category', 'tags', 'likes']));
    }
    ...

Xong thì vào routes/api.php thêm vào dòng

Route::resource('post', 'PostController');

và truy vập vào http://blog.blaysku.com/api/post thế api đã chạy vù vù rồi 😙 Để xây dựng một api hoàn chỉnh, chúng ta cũng cần tìm hiểu thêm về Policy, Middleware, Model events... Ok, tạm coi như xong với phần BE chúng ta sẽ tiếp tục với Vue.js

3.Vue.js

Để xây dựng được 1 SPA với Vue.js chúng ta cần có kiến thức cơ bản về những thứ sau:

  • Javascript: tất nhiên (ES5, ES6, ES7...)
  • Vue.js: tất nhiên :slight_smile:
  • Routing: Vue-router
  • Mô hình quản lý state: Vuex (quản lý state với các ứng dụng phức tạp, ko thích có thể ko dùng, ở đây mình có dùng)
  • Server-Side Rendering: Vue SSR hoặc nuxt.js (không dùng trong ứng dụng này, mình sẽ xậy dựng một app khác dùng cái này, các bạn nhớ đón xem nhé 😙)
  • ... Để quản lý được việc API Authentication, chúng ta có thể sử dụng các package như jwt-auth hay Laravel Passport, các bạn tìm hiểu thêm nhé, mình thấy trên này viết nhiều rồi. Chúng ta sẽ quan tâm đến file package.json, webpack.mix.js và folder resources/assets/js nữa Đây là cấu trúc phần vue.js của app này: Quy định là ~ sẽ đại diện cho resources/assets/js nhé các bạn.

File ~/app.js sẽ là file gom tất cả những thứ chúng ta code như vue component, các plugin... thành 1 cục, sẽ có nội dung như sau:

import Vue from 'vue'
import store from '~/store' //vuex 
import router from '~/router' //vue-router
import { i18n } from '~/plugins' //plugin: axios, markdown-editor, pusher.js...
import App from '~/components/App' //component tổ tiên :smile:

import '~/components'

new Vue({
  i18n,
  store,
  router,
  ...App
})

Vue-router

Khi sử dụng vue-router thì vấn đề routing sẽ do vue-router xử lý, router của Laravel chỉ cần thêm đoạn sau vào routes/web.php

// Other routes

// Tất cả các routes còn lại đều sẽ chạy index.blade.php
Route::get('{path}', function () {
    return view('index');
})->where('path', '(.*)');

File index.blade.php sẽ có dạng như sau:

<!DOCTYPE html>
<html lang="{{ app()->getLocale() }}">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
  <link rel="shortcut icon" href="{{ asset('favicon.ico') }}" type="image/x-icon">
  <link rel="icon" href="{{ asset('favicon.ico') }}" type="image/x-icon">
  <title>{{ config('app.name') }}</title>
  <link rel="stylesheet" href="{{ mix('css/app.css') }}">
</head>
<body>
  <div id="app"></div>

  @if (app()->isLocal())
    <script src="{{ mix('js/app.js') }}"></script>
  @else
    // đọc thêm ở đây nhé https://laravel.com/docs/5.5/mix#working-with-scripts
    <script src="{{ mix('js/manifest.js') }}"></script>
    <script src="{{ mix('js/vendor.js') }}"></script>
    <script src="{{ mix('js/app.js') }}"></script>
  @endif
</body>
</html>

Nếu bạn nghĩ, link **o nào cũng xài chung một đoạn mã như trên thì SEO cái beep gì thì bạn đúng rồi đấy, tất nhiên là SEO ko tốt bằng website kiểu truyền thống rồi, nhưng cũng đừng lo, giờ bot nó đọc được javascript rồi hơn nữa còn có cả SSR chúng ta sẽ cải thiện ở các bài tiếp theo. :slight_smile: Chúng ta sẽ tạo một file routes.js trong folder ~/router có nội dung (rút gọn) như sau, file này sẽ chứa tất cả routes của app:

// tìm hiểu lazy loading: https://router.vuejs.org/en/advanced/lazy-loading.html
// giả định là những component dưới đây chúng ta đã tạo rồi nhé
const PostIndex = () => import('~/pages/posts/index')
const PostShow = () => import('~/pages/posts/show')
const PostCreate = () => import('~/pages/posts/create')
const PostEdit = () => import('~/pages/posts/edit')

const CategoryShow = () => import('~/pages/categories/show')
const TagShow = () => import('~/pages/tags/show')
const UserShow = () => import('~/pages/users/show')

const Welcome = () => import('~/pages/welcome')

export default {
  { path: '/', name: 'welcome', component: Welcome },
  {
    path: '/category/:slug/:page(\\d+)?', name: 'post.category', component: CategoryShow,
    meta: { tab: 'category' }
  },
  {
    path: '/user/:username/drafts/:page(\\d+)?', name: 'user.drafts', component: UserShow,
    props: { params: { scope: 'drafts' }},
    meta: { tab: 'user' }
  },
  {
    path: '/user/:username/:page(\\d+)?', name: 'user.show', component: UserShow,
    meta: { tab: 'user' }
  },
  {
    path: '/tag/:slug/:page(\\d+)?', name: 'tag.show', component: TagShow,
    meta: { tab: 'tag' }
  },
  {
    path: '/post/featured/:page(\\d+)?', name: 'post.featured', component: PostIndex,
    props: { params: { scope: 'featured' }},
    meta: { tab: 'post' }
  },
  {
    path: '/post/:page(\\d+)?', name: 'post.list', component: PostIndex,
    meta: { tab: 'post' }
  },
  { path: '/post/:slug', name: 'post.show', component: PostShow,
    meta: { tab: 'post' }
  },
 }
 ...

Thêm 1 file ~/router/index.js

import Vue from 'vue'
import Meta from 'vue-meta' //thêm meta cho các page, keyword, description...
import routes from './routes' //routes
import Router from 'vue-router'
import { sync } from 'vuex-router-sync' //sync routes với vuex

Vue.use(Meta)
Vue.use(Router)

const router = make()

sync(store, router)

function make() {
   const router = new Router({
       ....
   })
   ....
}

export default router

Và mình sẽ thêm router vào file ~/app.js, bạn xem lại file bên trên nhé.

Xây dựng các page

~/pages/posts/Index.vue

<template>
    <div>
        <p v-if="!posts.length">Chưa có nội dung, hãy <router-link :to="{ name: 'post.create' }">đăng bài đầu tiên</router-link>!</p>
        <template v-else>
            <post :post="post" v-for="post in posts" :key="post.id"/>
            <pagination/>
            ...
        </template>
    </div>
</template>
<script>
    import axios from 'axios'
    import Post from './post'
    export default {
        data() {
            return {
                posts: [],
                page: 1
            }
        },
        created() {
            // call api ở `created`
            this.fetchPosts()
        },
        methods: {
            async fetchPosts() {
                // gọi api
                let { data } = await axios.get(route('post.index'), {
                  params: {
                    page: this.page
                  }
                })
                this.posts = data.data
                ......
            }
        }
        ......
    }
</script>

Tiếp tục với các page còn lại, vậy là chúng ta đã xây dựng thành công một SPA đơn giản sử dụng Laravel + Vuejs rồi. Hẹn gặp lại bạn ở các bài tiếp theo nhé! 😙


All Rights Reserved