Создаем SPA с помощью Vue и Laravel Часть 3

В этой части мы продолжим разрабатывать SPA на Vue и Laravel, и научимся делать Ajax запросы ДО того как Vue Router отобразит страницу.

Во второй части Создаем SPA с помощью Vue и Laravel Часть 2 мы остановились на том, что закончили UsersIndex.vue компонент, который загружает и отображает иформацию о пользователях. Мы не стали использовать реальную базу данных, и вместо нее воспользовались генератором фейковых пользователей с помощью factory() функции.

Если вы еще не читали первую и вторую части, то настоятельно рекомендую вам сделать это. Без предварительной подготовки вы не сможете продолжить чтение этой статьи.

Сегодня мы так же заменим использование фейковых данных получаемых с помощью factory(), на загрузку реальных данных из MySQL.

После завершения второй части, ваш UsersIndex.vue файл должен выглядеть так:

created() {
  this.fetchData();
},
methods: {
  fetchData() {
    this.error = this.users = null;
    this.loading = true;
    axios
      .get('/api/users')
      .then(response => {
        this.loading = false;
        this.users = response.data;
      }).catch(error => {
        this.loading = false;
        this.error = error.response.data.message || error.message;
      });
  }
}

Выше я обещал что мы научимся отправлять Ajax запрос ДО того как VueRouter начнет обработку маршрута, но сначала давайте изменим наш API, чтобы он возвращал реальные данные из базы данных.

Получаем данные из Базы Данных

Давайте создадим UsersController, который будет возвращать JSON ответ используя API resources добавленные в обновлении Laravel 5.5.

Прежде чем создавать контроллер, нам необходимо подготовить базу данных, и заполнить ее начальными данными.

Seeder для нашей новой таблицы

Создайте новый Seeder с помощью команды make:seeder:

$ php artisan make:seeder UsersTableSeeder

И измените его следующим образом:

<?php

use Illuminate\Database\Seeder;

class UsersTableSeeder extends Seeder
{
  public function run()
  {
    factory(App\User::class, 50)->create();
  }
}

И не забудьте добавить вызов UsersTableSeeder в database/seeds/DatabaseSeeder.php:

<?php

use Illuminate\Database\Seeder;

class DatabaseSeeder extends Seeder
{
  /**
   * Run the database seeds.
   *
   * @return void
   */
  public function run()
  {
    $this->call([
      UsersTableSeeder::class,
    ]);
  }
}

Чтобы наш seeder мог заполнить базу данных, нам сперва необходимо ее создать.

Настраиваем Базу Данных

Настало время создать нашу базу данных. Вы можете использовать SQLite с GUI навроде TablePlus или использовать MySQL. Выбор за вами. Если вы новичек в Laravel, то советую вам прочитать документацию по работе с БД.

Если у вас есть запущенный MySQL сервер, вы можете создать новую базу данных из командной строки с помощью одной команды:

$ mysql -u root -e"create database vue_spa;"

После этого отредактируйте .env файл, и измените строчку DB_DATABASE=vue_spa. Если же у вас возникли трудности с созданием базы данных, вы можете попросить помощи в комментариях.

Теперь, когда у нас настроено подключение к базе данных, мы можем запустить миграции:

$ composer dump-autoload
$ php artisan migrate:fresh --seed

Ключ --seed говорит Laravel о том, что после миграций нужно так же запустить команду db:seed. На этом все, теперь у вас в базе данных должна быть информация о 50 пользователях.

Контроллер UsersController

Как вы помните, во второй части мы настроили маршрут /users в файле routes/api.php:

Route::get('/users', function () {
  return factory('App\User', 10)->make();
});

Теперь давайте создадим UsersController, который кроме всего прочего, даст нам замечательную возможность использовать команду php artisan route:cache, которая недоступна если мы используем замыкания (Closures) для наших маршрутов. Выполните команды ниже:

$ php artisan make:controller Api/UsersController
$ php artisan make:resource UserResource

Первая команда создает UsersController и помещает его в папку app/Http/Controllers/Api. Вторая команда создает UserResource в папке app/Http/Resources.

Теперь измените файл routes/api.php, и добавьте в него код:

Route::namespace('Api')->group(function () {
  Route::get('/users', '[email protected]');
});

И отредактируйте файл UsersController:

<?php

namespace App\Http\Controllers\Api;

use App\User;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use App\Http\Resources\UserResource;

class UsersController extends Controller
{
  public function index()
  {
    return UserResource::collection(User::paginate(10));
  }
}

Пример того, что возвращает наш контроллер:

{
  "data":[
    {
      "name":"Francis Marquardt",
      "email":"[email protected]"
    },
    {
      "name":"Dr. Florine Beatty",
      "email":"[email protected]"
    },
    ...
  ],
  "links":{
    "first":"http:\/\/vue-router.test\/api\/users?page=1",
    "last":"http:\/\/vue-router.test\/api\/users?page=5",
    "prev":null,
    "next":"http:\/\/vue-router.test\/api\/users?page=2"
  },
  "meta":{
    "current_page":1,
    "from":1,
    "last_page":5,
    "path":"http:\/\/vue-router.test\/api\/users",
    "per_page":10,
    "to":10,
    "total":50
  }
}

Самое замечательное то, что теперь Laravel "изкоробки" дает нам подобный функционал, который ранее приходилось писать самим.

Содерижмое UserResource:

<?php

namespace App\Http\Resources;

use Illuminate\Http\Resources\Json\Resource;

class UserResource extends Resource
{
  /**
   * Transform the resource into an array.
   *
   * @param \Illuminate\Http\Request $request
   * @return array
   */
  public function toArray($request)
  {
    return [
      'name' => $this->name,
      'email' => $this->email,
    ];
  }
}

На данный момент у вас должен быть рабочий маршрут /api/users который вы можете использовать из Vue. К сожалению, новый формат ответа ломает наш предыдущий код, так что нам необходимо его исправить.

Исправляем компонент UsersIndex

Мы можем быстро починить наш UsersIndex компонент, исправив вызов метода then():

this.users = response.data.data;

И теперь наш метод fetchData() должен выглядеть так:

fetchData() {
  this.error = this.users = null;
  this.loading = true;
  axios
    .get('/api/users')
    .then(response => {
      this.loading = false;
      this.users = response.data.data;
    }).catch(error => {
      this.loading = false;
      this.error = error.response.data.message || error.message;
    });
}

Получаем данные

На данный момент мы получаем данные после того как пользователь перешел по ссылке. Настало время изменить это, и научиться получать данные еще до того как пользователь совершил переход по ссылке.

Чтобы получить данные до того как будет совершен фактический переход по ссылке, мы будем использовать метод beforeRouteEnter, который поможем нам в этом. Пример из документации по VueRouter выглядит так:

beforeRouteEnter (to, from, next) {
  getPost(to.params.id, (err, post) => {
   next(vm => vm.setData(err, post))
  })
},

Изучите документацию прежде чем читать дальше. Мы можем использовать пример из документации, и создать свою функцию getUsers(), которая будет получать данные по API и затем вызывать метод next():

const getUsers = (page, callback) => {
  const params = { page };

  axios
    .get('/api/users', { params })
    .then(response => {
      callback(null, response.data);
    }).catch(error => {
      callback(error, error.response.data);
    });
};

Обратите внимание, этот метод не возвращает Promise, вместо этого он вызывает callback который ему передали. Метод getUsers() так же принимает аргумент page, который берется из QueryString. Если page пустой, то он автоматически устанавливается в page=1.

Подводя итоги, наш метод beforeRouteEnter теперь выглядит так:

beforeRouteEnter (to, from, next) {
  const params = {
    page: to.query.page
  };

  getUsers(to.query.page, (err, data) => {
    next(vm => vm.setData(err, data));
  });
},

Метод beforeRouteUpdate

Метод beforeRouteUpdate вызывается тогда, когда пользователь переходит по ссылке, и компонент уже был отрендерен ранее. Например когда пользователь переходит со ссылки /users?page=2 на /users?page=3.

Метод beforeRouteUpdate очень похож на метод beforeRouteEnter, однако метод beforeRouteUpdate имеет доступ к переменной this, которая относится к текущему компоненту. Поэтому код немного отличается:

// when route changes and this component is already rendered,
// the logic will be slightly different.
beforeRouteUpdate (to, from, next) {
  this.users = this.links = this.meta = null
  getUsers(to.query.page, (err, data) => {
    this.setData(err, data);
    next();
  });
},

Так как компонент уже был отрендерен ранее, нам необходимо сбросить несколько переменных, перед тем как получать новые данные. А так как мы имеем доступ к самому компоненту из этого метода, то мы можем вызвать метод setData() (код ниже), который запишет новые данные:

setData(err, { data: users, links, meta }) {
  if (err) {
    this.error = err.toString();
  } else {
    this.users = users;
    this.links = links;
    this.meta = meta;
  }
},

Подводя итоге по UsersIndex компоненту

Выше мы разобрали несколько кусков кода, и теперь мы готовые собрать их все вместе, и даже добавить простую пагинацию в наш UsersIndex компонент. Полный код компонента выглядит так:

<template>
  <div class="users">
    <div v-if="error" class="error">
      <p>{{ error }}</p>
    </div>

    <ul v-if="users">
      <li v-for="{ id, name, email } in users">
        <strong>Name:</strong> {{ name }},
        <strong>Email:</strong> {{ email }}
      </li>
    </ul>

    <div class="pagination">
      <button :disabled="! prevPage" @click.prevent="goToPrev">Previous</button>
      {{ paginatonCount }}
      <button :disabled="! nextPage" @click.prevent="goToNext">Next</button>
    </div>
  </div>
</template>
<script>
import axios from 'axios';

const getUsers = (page, callback) => {
  const params = { page };

  axios
    .get('/api/users', { params })
    .then(response => {
      callback(null, response.data);
    }).catch(error => {
      callback(error, error.response.data);
    });
};

export default {
  data() {
    return {
      users: null,
      meta: null,
      links: {
        first: null,
        last: null,
        next: null,
        prev: null,
      },
      error: null,
    };
  },
  computed: {
    nextPage() {
      if (! this.meta || this.meta.current_page === this.meta.last_page) {
        return;
      }

      return this.meta.current_page + 1;
    },
    prevPage() {
      if (! this.meta || this.meta.current_page === 1) {
        return;
      }

      return this.meta.current_page - 1;
    },
    paginatonCount() {
      if (! this.meta) {
        return;
      }

      const { current_page, last_page } = this.meta;

      return `${current_page} of ${last_page}`;
    },
  },
  beforeRouteEnter (to, from, next) {
    getUsers(to.query.page, (err, data) => {
      next(vm => vm.setData(err, data));
    });
  },
  // when route changes and this component is already rendered,
  // the logic will be slightly different.
  beforeRouteUpdate (to, from, next) {
    this.users = this.links = this.meta = null
    getUsers(to.query.page, (err, data) => {
      this.setData(err, data);
      next();
    });
  },
  methods: {
    goToNext() {
      this.$router.push({
        query: {
          page: this.nextPage,
        },
      });
    },
    goToPrev() {
      this.$router.push({
        name: 'users.index',
        query: {
          page: this.prevPage,
        }
      });
    },
    setData(err, { data: users, links, meta }) {
      if (err) {
        this.error = err.toString();
      } else {
        this.users = users;
        this.links = links;
        this.meta = meta;
      }
    },
  }
}
</script>

Тут есть несколько моментов которые мы еще не разобрали, поэтому я остановлюсь на них подбронее.

Методы goToNext() и goToPrev() служат для демонстрации того, как можно сделать навигацию используя vue-router  с помощью this.$router.push:

this.$router.push({
  query: {
    page: `${this.nextPage}`,
  },
});

Используя метод push, мы заставляем vue-router изменить текущую страницу, что приведет к вызову метода beforeRouteUpdate, который мы использовали ранее.

Так же мы использовали три "вычисляемых" (computed) свойства: nextPage, prevPage и paginationCount.

Теперь мы готовы пересобрать наш проект, и запустить его! Не забудьте запустить npm (или yarn) чтобы пересобрать ваши JS файлы:

# Если вы используете npm
$ npm run dev (или watch)

# Если вы используете yarn
$ yarn dev (или watch)

Теперь откройте проект в браузере и у вас все должно заработать.

SPA with Laravel and Vue

Что дальше

К этому моменту у нас есть работающий API, который возвращает данные в JSON формате из базы данных. У нас есть Vue компонент, который отображает эти данные, и даже имеет простую пагинацию.

В следующих частях мы добавим: создание, обновление и удаление пользователей в наше проект.

Спасибо что читали. Оставляйте свои коментарии.

 

Перевод статьи Building a Vue SPA with Laravel Part 3.

Опубликовано:

Категории: Статьи