使用 Laravel 和 Vuejs 构建一个 SPA 单页应用
52

我们将继续使用Laravel构建我们的Vue SPA 单页应用,向您展示如何在vue-router进入路由之前异步加载数据。
我们在 使用Laravel构建Vue SPA单页应用教程第二节 完成了一个UsersIndex Vue组件,该组件异步请求API加载用户数据。我们不需要构建一个真正的由后端返回数据的API,可以使用Laravel factory()方法返回模拟数据。
如果您还没有阅读过用Laravel构建 Vue SPA的第1部分第2部分,我建议你先从这些帖子开始,然后再回来。我会等你的!

在本教程中,我们将真正的从数据库中请求数据来替换模拟的用户数据。我喜欢使用MySQL,但是你可以使用任何你擅长的数据库!

我们的UsersIndex.vue组件使用了“Created()”钩子方法从API加载数据。在第2部分的结尾处,“fetchData()”方法如下所示:

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;
            });
    }
}

我保证在通过路由进入组件之前,向你展示如何从API请求数据,但在这之前,我们需要将API替换为一些真实的数据。

创建真实用户数据

我们即将创建用户控制器 UsersController 返回 JSON 数据,JSON数据将使用 Laravel 5.5 中新功能 API resources 生成资源。

在创建控制器和 API 资源之前,我们要先设置数据库并且填充一些测试数据给 SPA 单页应用

用户数据填充

我们可以使用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,
        ]);
    }
}

我们在执行数据填充前必须要创建和配置好数据库

配置数据库

是时候让我们的 Vue SPA Laravel 应用连接一个真正的数据库了. 你可以使用带GUI的SQLite例如TablePlus或者使用MySQL. 如果你是 Laravel新手, 你可以在 数据库入门中阅读大量相关文档.

如果你本机已安装好Mysql的话, 你可以使用如下命令去快速创建一个数据库 (假设你的本地开发环境没有密码):

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

# 或者你可以加个-p参数输入密码
mysql -u root -e"create database vue_spa;" -p

一旦创建好数据库, 请在 .env 文件中配置 DB_DATABASE=vue_spa. 如果你卡住了, 请根据文档来操作就很容易让你的数据库跑起来.

一旦你配置好了你的数据库连接配置, 你就可以迁移你的数据表并填充数据. Laravel 自带了用户表的迁移文件和填充数据文件:

# 为了确保数据库填充器能够自动加载
composer dump-autoload
php artisan migrate:fresh --seed

你也可以单独使用 artisan db:seed 命令如果你喜欢的话! 然后, 你数据库应该就有了50位用户,可以通过API进行查询和返回.

用户控制器

如果你回看第二节,在 routes/api.php 中的 /users 假数据接口就像这样的:

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

让我们来创建一个控制器类,这也给了我们在生产环境中使用 php artisan route:cache 的附加好处,这对闭包来说是不可能的,我们会使用以下命令来创建控制器和用户的资源 API

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

第一条命令是在 app/Http/Controllers/Api 下的 Api 文件夹下添加一个 User 控制器,第二条命令是在 app/Http/Resources  文件夹下添加一个UserResource 。

routes/api.php新代码包含我们的控制器和 Api  命名空间: 

Route::namespace('Api')->group(function () {
    Route::get('/users', 'UsersController@index');
});

控制器非常简单;我们返回一个带分页的 Eloquent API 资源:

<?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));
    }
}

当我们连接  UserResource API格式 ,返回JSON 的例子看起来是这样的:

{
   "data":[
      {
         "name":"Francis Marquardt",
         "email":"schamberger.adrian@example.net"
      },
      {
         "name":"Dr. Florine Beatty",
         "email":"fcummerata@example.org"
      },
      ...
   ],
   "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为我们提供了分页数据并自动将 users 添加到 data 键!

这是 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,
        ];
    }
}

UserResource 类将每个在集合中的 User 模型转换为数组,并且提供了 UserResource::collection() 方法将用户集合转换为 JSON 格式。

现在,你应该有了一个在 SPA 应用中可以使用的 /api/users 接口了,如果你正跟着步骤操作,你可能会注意到我们新的响应格式中断了组件。

修复 UsersIndex 组件

我们想要 UsersIndex.vue 组件重新正常工作的话,要调整一下 then() 方法去获取新的响应格式里 data 中的数据。可能看起来有点古怪。response.data 是响应对象,所以用户数据要用如下代码获取。

this.users = response.data.data;

下面是根据新 API 接口调整后的 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;
        });
}

导航前获取数据

我们的组件与我们的新API一起工作,这是一个极好的时间展示导航组件出现之前 你如何获取用户 。

通过这种方法,我们获取的数据然后导航到新的路由。我们可以通过使用 beforerouteenter 守卫在传入的成分。一个来自  the vue-router documentation 的 看起来像这样例子:

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

查看完整的示例文件之后,可以肯定地说,我们将异步获取用户数据,一旦完成之后,在这之后,我们触发 next() 和在组件中的数据集(the vm variable)。

这是一个可能看起来像异步获取用户API的 GetUsers 方法,然后触发一个回调函数的组件:

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);
        });
};

注意,该方法不会返回一个响应,而是在完成或失败时触发回调。回调传递参数、错误和API调用的响应。

我们的 getusers() 方法接受 一个page 变量作为最终查询字符串参数的要求。如果它是空的(没有任何页面通过此路由),然后这个接口会自动申明 page= 1

最后我要指出的是 常量参数的值,它将像这样生效:

{
    params: {
        page: 1
    }
}

这是我们的 beforerouteenter 门面使用 GetUsers 函数获取异步数据然后放在组件调用 next()

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

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

这块是 回调参数在 getuses()调用之后从API返回数据:

(err, data) => {
    next(vm => vm.setData(err, data));
}

在这里就需要像getusers() 这里一样API成功的响应:

callback(null, response.data);

beforeRouteUpdate钩子

当组件已经是渲染状态,路由改变时,将调用 beforeRouteUpdate  钩子,并且在新路由中重用这个组件。例如,当我们用户导航从 /users?page=2 到 /users?page=3。 

beforeRouteUpdate 的调用类似于 beforeRouteEnter。 然而,前者在组件中可以访问 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();
    });
},

因为组件是渲染状态,从 API 获取下一组数据之前,我们需要重置几个数据属性。我们访问组件。因此,首先我们可以调用 this.setData() (我还没有告诉你吗) ,然后调用没有回调的 next()

最后,下面是 UsersIndex 组件中 setData 方法:

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

setData() 方法对新 API 响应获取的 data 对象解构出userslinks 和 meta。为了清楚,我们给 data:users 的数据分配新的变量名 users

UsersIndex绑定在一起。

我已经向您展示了UsersIndex组件的部分,并且我们已经准备好将它们全部绑定在一起,并在一些非常基本的分页上进行点缀。本教程没有向您展示如何构建分页,因此您可以发明(或创建)自己设想的分页!

分页 一种 极好的方式来向你展现如何在 Vue-router 编写一个SPA导航。

下面是用我们的新方法来用路由钩子获取异步数据的完整的组成实例:

<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>

如果想更容易消化,这里是 UsersIndex.vue as a GitHub Gist.

这里有很多新东西,所以我将指出一些更重要的观点。 goToNext() 和 goToPrev() 方法说明你如何通过使用 this.$router.push导航到 vue-router`:

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

我们正在推动新页面的查询字符串触发 beforerouteupdate。我还想指出,为了上一步的和下一步的行为我给你一个 <button> 元素,使用 vue-router主要是为了展示 导航编程  ,并且你可能会在页面路由中使用 <router-link /> 之间来自动分页。

前面已经介绍了三个自动计算属性(nextPageprevPagepaginatonCount)能够自动计算出下一页和前一页的数字,而 paginatonCount 可以显示当前页码和总页数。

“下一页”和“前一页”按钮根据自动计算属性判断是否应该显示为禁用状态。而 "goTo" 方法使用自动计算属性将 page 字符串参数放在下一页或前一页中。如果当前页是首页或尾页,下一页或前一页的值为空时,按钮被禁用。

在代码中可能有一些冗余, 但是这个组件演示了在进入路由之前使用  vue-router 来获取数据!

不要忘记运行 Laravel Mix 来确保构建到最新版本的JavaScript:

# NPM
npm run dev

# Watch to update automatically while developing
npm run watch

# Yarn
yarn dev

# Watch to update automatically while developing
yarn watch

最后, 在我们更新完整的 UsersIndex.vue 组件之后,我们的 SPA 看起来像这样的:

Screenshot of the /users page

下一步

我们已经有了一个来自真实数据库可用的 API 和一个简单分页器,分页器后端使用了 Laravel API 模型资源,可以返回简单分页链接并将返回内容包裹在 data 键里。

下一步,我们要创建、编辑、删除用户。在生产环境中将锁定 /users 资源。目前,我们创建了一个增删改查的功能,学习了如何创建 vue-router 并且异步获取数据。
我们还可以把axios客户端请求部分的代码再抽象出来,现在很简单。所以我们将在第四部分来讲解。当我们添加额外的API功能时,我们将创建一个专用的HTTP客户端模块。


Practice makes perfect.

原文地址:

译文地址:https://laravel-china.org/topics/8134/bu...

本帖已被设为精华帖!
《L01 基础入门》
我们将带你从零开发一个项目并部署到线上,本课程教授 Web 开发中专业、实用的技能,如 Git 工作流、Laravel Mix 前端工作流等。
《L02 从零构建论坛系统》
以构建论坛项目 LaraBBS 为线索,展开对 Laravel 框架的全面学习。应用程序架构思路贴近 Laravel 框架的设计哲学。
讨论数量: 0
(= ̄ω ̄=)··· 暂无内容!

  • 请注意单词拼写,以及中英文排版,参考此页
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`, 更多语法请见这里 Markdown 语法
  • 支持表情,使用方法请见 Emoji 自动补全来咯,可用的 Emoji 请见 :metal: :point_right: Emoji 列表 :star: :sparkles:
  • 上传图片, 支持拖拽和剪切板黏贴上传, 格式限制 - jpg, png, gif
  • 发布框支持本地存储功能,会在内容变更时保存,「提交」按钮点击时清空
  请勿发布不友善或者负能量的内容。与人为善,比聪明更重要!