12/08/2018, 16:28

[Laravel] Single Page Application sử dụng Vue, JWTAuth (P2)

Ở bài trước tôi đã đi đến bước tạo unit test, và phần còn lại như root component, child component, sử dụng router, axios, sử dụng JWTAuth ... sẽ được trính bày nốt trong bài này. Vue.js là hướng component nên tôi sẽ ra những component có đuôi .vue dưới đây : Template Script Style Kết ...

Ở bài trước tôi đã đi đến bước tạo unit test, và phần còn lại như root component, child component, sử dụng router, axios, sử dụng JWTAuth ... sẽ được trính bày nốt trong bài này.

Vue.js là hướng component nên tôi sẽ ra những component có đuôi .vue dưới đây :

  • Template
  • Script
  • Style

Kết hợp với chúng sẽ thực hiện được việc di chuyển giữa các trang dưới client side.

Root Component

Đầu tiên sẽ phải tạo ra app.vue làm gốc.

resources/assets/js/app.vue
<template>
  <div id="app">
      <div class="container">
        <router-view></router-view>
      </div>
      <hr>
      <div class="container-fluid">
          <a href="https://github.com/acro5piano/laravel-vue-jwtauth-spa-todo-app" target="_blank">
              <img src="https://image.flaticon.com/icons/svg/25/25231.svg" awidth="30" height="20">
          </a>
      </div>
  </div>
</template>

Rồi cần tạo ra app.js để đọc component này. Sau khi browser nhận được reponse từ server thì nó sẽ là entry point được thực thi.

resources/assets/js/app.js
import Vue from 'vue'

require('bootstrap-sass')

const app = new Vue({
  el: '#app',
  render: h => h(require('./app.vue')),
})

Child component + Routing

Kế đến là đi tạo những component con của root bên trên. Đầu tiên sẽ tạo từ component tĩnh là trang about us.

resources/assets/js/components/About.vue
<template>
  <div>
    This page describes who we are.
  </div>
</template>

Rồi thực hiện đăng kí component này với vue-router :

resources/assets/js/router.js
import VueRouter from 'vue-router'
import Vue from 'vue'

Vue.use(VueRouter)

export default new VueRouter({
  mode: 'history',
  routes: [
    { path: '/about', component: require('./components/About.vue') },
  ],
  scrollBehavior (to, from, savedPosition) {
    if (savedPosition) {
      return savedPosition
    } else {
      return { x: 0, y: 0 }
    }
  },
})

Khi mà truy cập vào URL /about thì component About.vue sẽ được mount vào <router-view></router-view> của app.vue. Ở phần mode: 'history' tôi đang dùng push state của HTML5, còn mặc định của mode sẽ là hash. Còn phần scrollBehavior sẽ giúp bảo lưu vị trí scroll trình duyệt.

Tôi sẽ đọc vào file router.js từ app.js.

resources/assets/js/app.js
import Vue from 'vue'

// Thêm vào
import router from './router'

require('bootstrap-sass')

const app = new Vue({
  // Thêm vào
  router,
  el: '#app',
  render: h => h(require('./app.vue')),
})

Nếu bạn access vào localhost:8000/about thì sẽ được routing như bên dưới :

Tôi sẽ tạo ra trước tất cả component và những routing tương ứng cho chúng. Những routing cần thiết sẽ là :

/
/about
/login

Ở / sẽ hiện thị task list nên sẽ cần những component con sau :

  • components/Tasks.vue
  • components/About.vue
  • components/Login.vue

Ngoài những cái đó ra thì cũng cần navigation bar lúc nào cũng được hiển thị :

components/Navbar.vue
resources/assets/js/components/Tasks.vue
<template>
  <div>
    please <router-link to="/login">Login.</router-link>

    <div>
      <strong>Hello, HuongNV!</strong>
      <p>Your tasks here.</p>

      <ul>
        <li>
          Learn Vue.js
        </li>
        <button class="btn btn-sm btn-success">Done</button>

        <button class="btn btn-sm btn-danger">Remove</button>
      </ul>

      <div class="form-group">
        <div class="alert alert-danger" role="alert">
           Task name should not be blank.
        </div>
        <input type="text" class="form-control" placeholder="new task...">
        <button class="btn btn-primary">
          Add task
        </button>
      </div>
    </div>
  </div>
</template>
resources/assets/js/components/About.vue
<template>
  <div>
    This page describes who we are.
  </div>
</template>
resources/assets/js/components/Login.vue
<template>
  <div>
    <div class="container">
      <div class="row">
        <div class="col-md-8 col-md-offset-2">
          <div class="panel panel-default">
            <div class="panel-heading">Login</div>
            <div class="panel-body">
              <div class="alert alert-danger" role="alert">
                Wrong email or password.
              </div>

              <div class="form-group">
                <label for="email" class="col-md-4 control-label">E-Mail Address</label>
                <div class="col-md-6">
                  <input id="email" type="email" class="form-control" required autofocus>
                </div>
              </div>

              <div class="form-group">
                <label for="password" class="col-md-4 control-label">Password</label>
                <div class="col-md-6">
                  <input id="password" type="password" class="form-control" required autofocus>
                </div>
              </div>

              <div class="form-group">
                <div class="col-md-8 col-md-offset-4">
                  <button type="submit" class="btn btn-primary">
                    Login
                  </button>
                </div>
              </div>
              </form>
            </div>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>
resources/assets/js/components/Navbar.vue
<template>
  <nav class="navbar navbar-default">
    <div class="container-fluid">
      <!-- Brand and toggle get grouped for better mobile display -->
      <div class="navbar-header">
        <button type="button" class="navbar-toggle collapsed"
                 data-toggle="collapse" data-target="#bs-example-navbar-collapse-1"
                 aria-expanded="false">
          <span class="sr-only">Toggle navigation</span>
          <span class="icon-bar"></span>
          <span class="icon-bar"></span>
          <span class="icon-bar"></span>
        </button>
        <router-link to="/" class="navbar-brand">Vue TODO</router-link>
      </div>

      <!-- Collect the nav links, forms, and other content for toggling -->
      <div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
        <ul class="nav navbar-nav navbar-right">
          <li><router-link to="/about">About</router-link></li>

          <li>
            <router-link to="/login">Log in</router-link>
          </li>
        </ul>
      </div><!-- /.navbar-collapse -->
    </div><!-- /.container-fluid -->
  </nav>
</template>

Việc tiếp theo sẽ cần phải đăng kí những component đã tạo vào Router :

resources/assets/js/router.js
import VueRouter from 'vue-router'
import Vue from 'vue'

Vue.use(VueRouter)

export default new VueRouter({
  mode: 'history',
  routes: [
    { path: '/',      component: require('./components/Tasks.vue') },  // Thêm
    { path: '/about', component: require('./components/About.vue') },
    { path: '/login', component: require('./components/Login.vue') },  // Thêm
  ],
  scrollBehavior (to, from, savedPosition) {
    if (savedPosition) {
      return savedPosition
    } else {
      return { x: 0, y: 0 }
    }
  },
})

Tiến hành đặt Navbar. Do ko phải biến đổi trên Router nên tôi sẽ viết ngay vào app.vue :

resources/assets/js/app.vue
<template>
  <div id="app">
      <!-- Thêm -->
      <navbar></navbar>

      <div class="container">
        <router-view></router-view>
      </div>
      <hr>
      <div class="container-fluid">
          <a href="https://github.com/acro5piano/laravel-vue-jwtauth-spa-todo-app" target="_blank">
              <img src="https://image.flaticon.com/icons/svg/25/25231.svg" awidth="30" height="20">
          </a>
      </div>
  </div>
</template>

<script>
  // Thêm
  export default {
    components: {
      navbar: require('./components/Navbar.vue'),
    },
  }
</script>

Đến đây thì việc di chuyển màn hình trong SPA đã chắc chắn hoàn thiện. Bạn có thể access vào localhost:8000/login để thử. Note : sẽ có alert nhưng bạn cứ bỏ qua nó.

Tôi sẽ dùng axios để gửi request và viết xử lý lấy tasks về từ API server. Việc dùng axios đẩy request sẽ có một vài cách nhưng mà để không cho một vài component nào đó gọi được thì tôi sẽ tạo ra services/http.js thực hiện việc đó.

resources/assets/js/services/http.js
import axios from 'axios'

/**
 *  Responsible cho tất cả HTTP requests.
 */
export default {
  request (method, url, data, successCb = null, errorCb = null) {
    axios.request({
      url,
      data,
      method: method.toLowerCase()
    }).then(successCb).catch(errorCb)
  },

  get (url, successCb = null, errorCb = null) {
    return this.request('get', url, {}, successCb, errorCb)
  },

  post (url, data, successCb = null, errorCb = null) {
    return this.request('post', url, data, successCb, errorCb)
  },

  put (url, data, successCb = null, errorCb = null) {
    return this.request('put', url, data, successCb, errorCb)
  },

  delete (url, data = {}, successCb = null, errorCb = null) {
    return this.request('delete', url, data, successCb, errorCb)
  },

  /**
   * Khởi tạo service.
   */
  init () {
    axios.defaults.baseURL = '/api'

    // Intercept the request to make sure the token is injected into the header.
    axios.interceptors.request.use(config => {
      config.headers['X-CSRF-TOKEN']     = window.Laravel.csrfToken
      config.headers['X-Requested-With'] = 'XMLHttpRequest'
      return config
    })
  }
}
resources/assets/js/app.js
import Vue from 'vue'
import router from './router'
import http from './services/http.js' // Thêm

require('bootstrap-sass')

const app = new Vue({
  router,
  el: '#app',

  // Thêm
  created () {
    http.init()
  },
  render: h => h(require('./app.vue')),
}).$mount('#app')

Và ở Task component tôi sẽ sử dụng http service này để đẩy request.

resources/assets/js/components/Tasks.vue
<template>
  <div>
    please <router-link to="/login">Login.</router-link>

    <div>
      <strong>Hello, HuongNV!</strong>
      <p>Your tasks here.</p>

      <ul v-for="task in tasks">
        <li v-if="task.is_done">
          <strike> {{ task.name }} </strike>
        </li>
        <li v-else>
          {{ task.name }}
        </li>
        <button @click="completeTask(task)" class="btn btn-sm btn-success" v-if="task.is_done">Undo</button>
        <button @click="completeTask(task)" class="btn btn-sm btn-success" v-else>Done</button>

        <button @click="removeTask(task)" class="btn btn-sm btn-danger">Remove</button>
      </ul>

      <div class="form-group">
        <div class="alert alert-danger" role="alert" v-if="showAlert">
          {{ alertMessage }}
        </div>
        <input type="text" class="form-control"
            v-model="name" @keyup.enter="addTask" placeholder="new task...">
        <button class="btn btn-primary" disabled="disabled" v-if="name === '">
          Add task
        </button>
        <button class="btn btn-primary" @click='addTask' v-else>
          Add task
        </button>
      </div>
    </div>
  </div>
</template>
<script>
  import http from '../services/http'

  export default {
    mounted() {
      this.fetchTasks()
    },
    data() {
      return {
        tasks: [],
        name: ',
        showAlert: false,
        alertMessage: ',
      }
    },
    methods: {
      fetchTasks () {
        // TODO: not to send request when the user is not authenticated
        http.get('tasks', res => {
          this.tasks = res.data
        })
      },
      addTask () {
        if (this.name === ') {
          this.showAlert = true
          this.alertMessage = 'Task name should not be blank.'
          return false
        }
        http.post('tasks', {name: this.name}, res => {
          this.tasks
                                          
0