基于 Laravel + Vue + GraphQL 实现前后端分离的博客应用(二) —— 用户列表及详情页


创建菜单组件

书接上文,用户登录成功之后会进入管理后台,为此我们需要为后台界面编写导航菜单组件。还是在 components/Admin 目录下创建一个 Menu.vue

<template>
    <aside class="menu">
        <p class="menu-label">文章</p>
        <ul class="menu-list">
            <li>
                <router-link to="/admin/posts/new">新文章</router-link>
            </li>
            <li>
                <router-link to="/admin/posts">文章列表</router-link>
            </li>
        </ul>
        <p class="menu-label">用户</p>
        <ul class="menu-list">
            <li>
                <router-link to="/admin/users">用户列表</router-link>
            </li>
        </ul>
    </aside>
</template>

显示用户列表

后端接口实现

获取用户列表的接口我们已经在 API 系列教程四中编写过了,为了适配这里的查询需要,我们将其略作调整。

先到配置文件 config/graphql.php 中修改下查询名称:

'schemas' => [
    'default' => [
        'query' => [
            'allUsers' => \App\GraphQL\Query\UserQuery::class,
            ... // other queries
        ],
        ... // mutations
    ]
],

然后到 UserType 类的 fields 方法返回字段中新增一个返回字段 name

'name' => [
    'type' => Type::nonNull(Type::string()),
    'description' => 'The name of the user'
],

这样就可以在 GraphiQL 中测试了:

前端用户列表

定义 GraphQL Query

后端接口准备好了之后,接下来在前端管理后台我们希望能够看到所有已注册用户。这需要创建一个 Users 组件,在此之前还要在 src/graphql.js 中编写 GraphQL 查询语句获取所有注册用户:

export const ALL_USERS_QUERY = gql`
    query AllUsersQuery {
        allUsers {
            id
            name
            email
        }
    }
`

创建 Users 组件

接下来在 components/Admin 目录下创建 Users.vue 文件来定义 Users 组件:

<template>
    <section class="section">
        <div class="container">
            <div class="columns">
                <div class="column is-3">
                    <Menu/>
                </div>
                <div class="column is-9">
                    <h2 class="title">用户列表</h2>

                    <table class="table is-striped is-narrow is-hoverable is-fullwidth">
                        <thead>
                        <tr>
                            <th>用户名</th>
                            <th>邮箱</th>
                            <th></th>
                        </tr>
                        </thead>
                        <tbody>
                        <tr v-for="user in allUsers" :key="user.id">
                            <td>{{ user.name }}</td>
                            <td>{{ user.email }}</td>
                            <td>
                                <router-link :to="`/admin/users/${user.id}`">查看</router-link>
                            </td>
                        </tr>
                        </tbody>
                    </table>
                </div>
            </div>
        </div>
    </section>
</template>

<script>
import Menu from '@/components/Admin/Menu'
import { ALL_USERS_QUERY } from '@/graphql'

export default {
  name: 'Users',
  components: {
    Menu
  },
  data () {
    return {
      allUsers: []
    }
  },
  apollo: {
    // fetch all users
    allUsers: {
      query: ALL_USERS_QUERY
    }
  }
}
</script>

这段代码中用到了前面创建的 Menu 组件,在 apollo 对象中,我们添加了一段获取所有用户的 GraphQL 查询,其中用到了 ALL_USERS_QUERY 操作(这里用到的名字 allUsers 需要和 GraphQL 服务端中定义的查询名字一样),一旦从服务端获取到用户数据后就会将其渲染到前端视图中,此外我们还为每个用户提供了链接跳转到用户详情页。

添加用户列表路由

src/router/index.js 的合适位置注入以下代码:

import Users from '@/components/Admin/Users'

// 将下面的代码插入 `routes` 数组最后
{
  path: '/admin/users',
  name: 'Users',
  component: Users
}

再次运行 npm run build 就可以在浏览器中通过 http://apollo-blog.test/#/admin/users 访问用户列表了:

访问用户详情页

用户详情后端接口

我们设想的用户详情页不仅展示用户属性信息,还要展示用户发表的文章数,所以我们需要在之前 API 系列教程一 中创建的 articles 表中新增 user_id 字段以便和用户建立关联。

文章表新增 user_id 字段

先在 Laravel 应用根目录运行 Artisan 命令创建迁移文件:

php artisan make:migration alter_articles_add_user_id --table=articles

然后编辑刚生成的 AlterArticlesAddUserId 类:

use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class AlterArticlesAddUserId extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::table('articles', function (Blueprint $table) {
            $table->integer('user_id')->after('body')->unsigned()->default(0);
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::table('articles', function (Blueprint $table) {
            $table->dropColumn('user_id');
        });
    }
}

运行迁移将变更更新到数据表:

php artisan migrate

此外我们还要编辑之前的填充器 ArticlesTableSeeder 来填充 user_id 字段值:

class ArticlesTableSeeder extends Seeder
{
    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {
        // Let's truncate our existing records to start from scratch.
        Article::truncate();

        $faker = \Faker\Factory::create();

        // And now, let's create a few articles in our database:
        for ($i = 0; $i < 50; $i++) {
            Article::create([
                'title' => $faker->sentence,
                'body' => $faker->paragraph,
                'user_id' => random_int(1, 14)
            ]);
        }
    }
}

运行填充器填充数据:

php artisan db:seed --class=ArticlesTableSeeder

定义 GraphQL 关联查询

关于关联查询的定义我们在 API 系列教程五中已经演示过,这里依葫芦画瓢。先创建一个新的 GraphQL 类型 Article

php artisan make:graphql:type ArticleType

编写刚生成的 ArticleType 类型类:

namespace App\GraphQL\Type;

use GraphQL\Type\Definition\Type;
use Folklore\GraphQL\Support\Type as BaseType;
use GraphQL;

class ArticleType extends BaseType
{
    protected $attributes = [
        'name' => 'Article',
        'description' => 'An Article'
    ];

    public function fields()
    {
        return [
            'id' => [
                'type' => Type::nonNull(Type::string()),
                'description' => 'The id of the article'
            ],
            'title' => [
                'type' => Type::nonNull(Type::string()),
                'description' => 'The title of the article'
            ],
            'content' => [
                'type' => Type::nonNull(Type::string()),
                'description' => 'The body of the article'
            ]
        ];
    }

    protected function resolveContentField($root, $args)
    {
        return $root->body;
    }
}

config/graphql.phptypes 配置中新增 Article 类型:

'types' => [
    ... // other types
    'Article' => \App\GraphQL\Type\ArticleType::class,
],

接下来在 User 模型类中定义关联关系:

public function posts()
{
    return $this->hasMany(Article::class, 'user_id', 'id');
}

然后在 UserType 类的 fields 方法返回字段中新增 posts 字段:

public function fields()
{
    return [
        ... // other fields
        'posts' => [
            'type' => Type::listOf(GraphQL::type('Article')),
            'description' => 'The articles by the user'
        ]
    ];
}  

同时在 UserType 类中新增 posts 字段对应获取方法:

protected function resolvePostsField($root, $args)
{
    if (isset($args['id'])) {
        return $root->posts->where('id', $args['id']);
    }

    return $root->posts;
}

最后为用户详情页定义单独的查询接口:

php artisan make:graphql:query UserQueryById

编写 UserQueryById 类代码如下:

namespace App\GraphQL\Query;

use App\User;
use Folklore\GraphQL\Support\Query;
use GraphQL\Type\Definition\ResolveInfo;
use GraphQL\Type\Definition\Type;
use GraphQL;

class UserQueryById extends Query
{
    protected $attributes = [
        'name' => 'QueryUserById',
        'description' => 'A query'
    ];

    public function type()
    {
        return GraphQL::type('User');
    }

    public function args()
    {
        return [
            'id' => ['name' => 'id', 'type' => Type::string()]
        ];
    }

    public function resolve($root, $args, $context, ResolveInfo $info)
    {
        if (empty($args['id'])) {
            throw new \InvalidArgumentException('请传入用户ID!');
        }
        $user = User::find($args['id']);
        return $user;
    }
}

config/graphql.php 中注册新的查询类:

'schemas' => [
    'default' => [
        'query' => [
            ... // other queries
            'user' => \App\GraphQL\Query\UserQueryById::class,
        ],
        // mutations
    ]
],

这样就可以在 GraphiQL 中进行测试了:

前端用户详情页实现

定义 GraphQL Query

首先还在 src/graphql.js 中定义 GraphQL 查询语句:

export const USER_QUERY = gql`
    query UserQueryById($id: String) {
        user(id: $id) {
            id
            name
            email
            posts {
                id
            }
        }
    }
`

创建 UserDetail 组件

components/Admin 目录下新增用户详情组件 UserDetail.vue

<template>
    <section class="section">
        <div class="container">
            <div class="columns">
                <div class="column is-3">
                    <Menu/>
                </div>
                <div class="column is-9">
                    <h2 class="title">用户详情</h2>

                    <div class="field is-horizontal">
                        <div class="field-label is-normal">
                            <label class="label">用户名</label>
                        </div>
                        <div class="field-body">
                            <div class="field">
                                <p class="control">
                                    <input class="input is-static" :value="user.name" readonly>
                                </p>
                            </div>
                        </div>
                    </div>

                    <div class="field is-horizontal">
                        <div class="field-label is-normal">
                            <label class="label">邮箱</label>
                        </div>
                        <div class="field-body">
                            <div class="field">
                                <p class="control">
                                    <input class="input is-static" :value="user.email" readonly>
                                </p>
                            </div>
                        </div>
                    </div>

                    <div class="field is-horizontal">
                        <div class="field-label is-normal">
                            <label class="label">文章数</label>
                        </div>
                        <div class="field-body">
                            <div class="field">
                                <p class="control">
                                    <input class="input is-static" :value="user.posts.length" readonly>
                                </p>
                            </div>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </section>
</template>

<script>
import Menu from '@/components/Admin/Menu'
import { USER_QUERY } from '@/graphql'

export default {
  name: 'UserDetail',
  components: {
    Menu
  },
  data () {
    return {
      user: '',
      id: this.$route.params.id
    }
  },
  apollo: {
    // fetch user by ID
    user: {
      query: USER_QUERY,
      variables () {
        return {
          id: this.id
        }
      }
    }
  }
}
</script>

我们在用户详情页展示了用户名、邮箱和该用户发布文章数。USER_QUERY 查询需要传递用户ID参数到服务端,我们可以从路由参数中获取用户ID,两者之间的连接的桥梁是 variables 函数,该函数返回包含从路由获取到的用户ID对象并将其传递给 GraphQL 查询。

为用户详情页注册路由

src/router/index.js 文件中的合适位置插入以下代码:

import UserDetail from '@/components/Admin/UserDetail'

// 将下面的代码添加到 `routes` 数组最后
{
  path: '/admin/users/:id',
  name: 'UserDetail',
  component: UserDetail,
  props: true
},

这样我们就可以在运行 npm run build 之后在浏览器中通过类似 http://apollo-blog.test/#/admin/users/13 这样的 URL 访问指定用户详情了:

下一篇我们将演示文章发布(需认证)、文章列表和文章详情页开发。


点赞 取消点赞 收藏 取消收藏

<< 上一篇: 基于 Laravel + Vue + GraphQL 实现前后端分离的博客应用(一) —— 用户注册登录

>> 下一篇: 基于 Laravel + Vue + GraphQL 实现前后端分离的博客应用(三) —— 文章发布及浏览