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

概述

今天开始,学院君将通过三篇教程的篇幅来系统介绍如何基于 Laravel + Vue 实现一个简单的、带用户认证的、前后端分离的博客应用,同时趁热打铁地将 GraphQL 融入进来实现 API 构建 —— Laravel 作为 GraphQL 服务端为前端 Vue 应用提供 API 接口。

前端应用初始化

后端 Laravel 应用我们以前面地 API 系列教程中使用的 apidemo.test 为基础,不再赘述 Laravel 应用安装配置及 GraphQL 扩展包安装和使用。这里,我们花一点时间来演示前端 Vue 应用的安装和初始化。

在 Web 服务器根目录下(与 Laravel 应用目录平级)通过以下命令初始化安装 Vue 应用:

npm install -g vue  // 已全局安装过 vue 略过此步骤
vue init webpack graphql-blog-app

应用初始化过程中命令行会有一系列交互,按照默认的选项一路回车即可:

初始化完成后,进入 Vue 应用根目录并安装博客应用所需依赖:

cd graphql-blog-app
npm install --save vue-apollo@next graphql apollo-client apollo-link apollo-link-context apollo-link-http apollo-cache-inmemory graphql-tag

我们来看一下上面的每个依赖是干嘛的:

设置 Vue Apollo

通过上述安装的依赖可以看到,在 Vue 中我们是通过 Apollo 相关扩展包实现与 GraphQL 服务端的交互。下面我们将使用这些扩展包构建所需的功能,打开 Vue 应用下的 src/main.js 文件,添加如下代码:

import { ApolloClient } from 'apollo-client'
import { HttpLink } from 'apollo-link-http'
import { InMemoryCache } from 'apollo-cache-inmemory'
import VueApollo from 'vue-apollo'

const httpLink = new HttpLink({
  // GraphQL 服务器 URL,需要使用绝对路径
  uri: 'http://apidemo.test/graphql'
})

// 创建 apollo client
const apolloClient = new ApolloClient({
  link: httpLink,
  cache: new InMemoryCache()
})

// 安装 vue plugin
Vue.use(VueApollo)

我们通过 GraphQL 服务端 URL 创建了一个新的 httpLink 实例,然后使用这个 httpLink 实例创建 Apollo 客户端,并指定使用内存缓存,最后安装 Vue Apollo 插件。

接下来创建一个 apolloProvider 对象并将其传入应用根组件:

const apolloProvider = new VueApollo({
  defaultClient: apolloClient
})

/* eslint-disable no-new */
new Vue({
  el: '#app',
  router,
  apolloProvider,
  components: { App },
  template: '<App/>'
})

最终我们的 src/main.js 文件应该是这样子:

import Vue from 'vue'
import App from './App'
import router from './router'
import { ApolloClient } from 'apollo-client'
import { HttpLink } from 'apollo-link-http'
import { InMemoryCache } from 'apollo-cache-inmemory'
import VueApollo from 'vue-apollo'

Vue.config.productionTip = false

const httpLink = new HttpLink({
  // URL to graphql server, you should use an absolute URL here
  uri: 'http://apidemo.test/graphql'
})

// create the apollo client
const apolloClient = new ApolloClient({
  link: httpLink,
  cache: new InMemoryCache()
})

// install the vue plugin
Vue.use(VueApollo)

const apolloProvider = new VueApollo({
  defaultClient: apolloClient
})

/* eslint-disable no-new */
new Vue({
  el: '#app',
  router,
  apolloProvider,
  template: '<App/>',
  components: { App }
})

引入 Bulma CSS

博客应用前端视图层使用 Bulma CSS,在 index.html 中引入即可:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width,initial-scale=1.0">
    <title>GraphQL 博客</title>
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.6.1/css/bulma.min.css">
  </head>
  <body>
    <div id="app"></div>
    <!-- built files will be auto injected -->
  </body>
</html>

添加主布局

博客会在不同页面使用共用的布局,这个主布局组件就是位于 src 目录下地 App.vue,更新其代码如下:

<template>
    <div id="app">
        <nav class="navbar is-primary" role="navigation" aria-label="main navigation">
            <div class="container">
                <div class="navbar-brand">
                    <router-link class="navbar-item" to="/">GraphQL 博客</router-link>

                    <button class="button navbar-burger">
                        <span></span>
                        <span></span>
                        <span></span>
                    </button>
                </div>
            </div>
        </nav>
        <router-view/>
    </div>
</template>

<script>
export default {
  name: 'app'
}
</script>

我们在其中定义了所有页面都会共用的顶部导航条。

用户注册功能实现

定义完前端应用所需的基础组件后,接下来我们来逐步实现博客应用包含的所有功能:

  • 用户注册
  • 用户登录
  • 用户列表
  • 用户详情
  • 发布文章
  • 应用首页
  • 文章详情

我们大概围绕这几个模块实现博客功能,首先从用户注册开始。

后端注册接口

在开始之前,我们假设你已经看过学院前面的 API 系列教程二(基于 jwt-auth 实现 API 认证)和教程四(GraphQL 在 Laravel 中的配置&使用)并编写好相关代码,我们将在其基础上进行编码工作。

在 Laravel 应用根目录下执行以下 Artisan 命令创建新的 Mutation:

php artisan make:graphql:mutation SignupMutation

然后编辑刚生成的 SignupMutation 类代码如下:

namespace App\GraphQL\Mutation;

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

class SignupMutation extends Mutation
{
    protected $attributes = [
        'name' => 'Signup',
        'description' => 'A mutation for user sign up'
    ];

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

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

    public function rules()
    {
        return [
            'name' => ['required', 'unique:users'],
            'email' => ['required', 'email', 'unique:users'],
            'password' => ['required', 'min:6'],
        ];
    }

    public function resolve($root, $args, $context, ResolveInfo $info)
    {
        $user = new User();
        $user->name = $args['name'];
        $user->email = $args['email'];
        $user->password = bcrypt($args['password']);
        $user->save();
        return $user;
    }
}

config/graphql.php 中注册刚刚创建的 Mutation:

'schemas' => [
    'default' => [
        'query' => [
            ... // 所有 Query
        ],
        'mutation' => [
            ... // 其它 Mutation
            'createUser' => \App\GraphQL\Mutation\SignupMutation::class,
        ]
    ]
],

接下来就可以在 GraphiQL 中测试这个接口了,我们先测试下字段验证:

验证失败会在结果中返回每个字段对应的错误信息。如果注册接口调用成功,返回信息如下:

前端注册组件

定义 GraphQL Mutaion

在正式编写前端代码之前需要先创建一个用于处理所有 GraphQL 查询和变更的全局文件,我们在 src 目录下新增一个 graphql.js 文件来处理这些逻辑,首先定义一段用于处理用户注册的语句:

import gql from 'graphql-tag'

export const SIGNUP_MUTATION = gql`
    mutation SignupMutation($name: String!, $email: String!, $password: String!) {
        createUser(
            name: $name,
            email: $email,
            password: $password
        ) {
            id
            name
            email
        }
    }
`

创建 SignUp 组件

接下来在 src/components 目录下新增 Admin 子目录,并在 Admin 目录下创建 SignUp 组件 SignUp.vue

<template>
    <section class="section">
        <div class="columns">
            <div class="column is-4 is-offset-4">
                <h2 class="title has-text-centered">用户注册</h2>

                <form method="POST" @submit.prevent="signup">
                    <div class="field">
                        <label class="label">用户名</label>

                        <p class="control">
                            <input
                                type="text"
                                class="input"
                                v-model="name">
                        </p>
                    </div>

                    <div class="field">
                        <label class="label">邮箱</label>

                        <p class="control">
                            <input
                                type="email"
                                class="input"
                                v-model="email">
                        </p>
                    </div>

                    <div class="field">
                        <label class="label">密码</label>

                        <p class="control">
                            <input
                                type="password"
                                class="input"
                                v-model="password">
                        </p>
                    </div>

                    <p class="control">
                        <button class="button is-primary is-fullwidth is-uppercase">注册</button>
                    </p>
                </form>
            </div>
        </div>
    </section>
</template>

<script>
import { SIGNUP_MUTATION } from '@/graphql'

export default {
    name: 'SignUp',
    data () {
        return {
            name: '',
            email: '',
            password: ''
        }
    },
    methods: {
        signup () {
            this.$apollo
                .mutate({
                    mutation: SIGNUP_MUTATION,
                    variables: {
                        name: this.name,
                        email: this.email,
                        password: this.password
                    }
                })
                .then(response => {
                    // 重定向到登录页面
                    this.$router.replace('/login')
                })
            }
        }
    }
</script>

该组件用于渲染一个简单的、用于用户注册的表单。用户点击注册按钮后会调用 signup 方法,在该方法中我们可以使用 this.$apollo (Vue Apollo 插件)上的 mutate 方法,通过前面创建的 SIGNUP_MUTATION 将参数传递到 GraphQL 服务端(即我们的 Laravel 应用),用户注册成功后就会跳转到 /login 页面。

添加注册路由

打开 src/router/index.js,更新代码如下新增 /signup 路由:

import Vue from 'vue'
import Router from 'vue-router'
import SignUp from '@/components/Admin/SignUp'

Vue.use(Router)

export default new Router({
  routes: [
    {
         path: '/signup',
         name: 'SignUp',
         component: SignUp
       }
  ]
})

这样我们就可以通过 /signup 路由访问注册页面了。

在浏览器访问 Vue 应用

有两种方式在浏览器中实现对 Vue 应用的访问,一种是通过在 Vue 应用根目录下通过运行如下命令:

npm run dev

这样就可以在通过 http://localhost:8080 (具体端口以命令行提示为准,有可能是 8081)以开发环境模式访问应用,这样做的好处是可以方便代码调试,建议在代码调试及本地测试阶段使用这种方式访问应用。

还有一种方式是在 Nginx 中配置站点通过域名方式进行访问,建议代码调试无误后使用这种方式访问,生产环境也是通过这种方式访问应用。在代码测试通过后,在应用根目录下运行如下命令:

npm run build

该命名会对前端代码进行编译并将编译后代码放到新生成地 dist 目录下,所以我们在 Nginx 中对前端应用配置如下:

server {

    listen 80;
    listen [::]:80;

    server_name apollo-blog.test;
    root /var/www/graphql-blog-app/dist;
    index index.html index.htm;

    location / {
        try_files $uri $uri/ @rewrites;
    }

    location @rewrites {
        rewrite ^(.+)$ /index.html last;
    }

    location ~* \.(?:ico|css|js|gif|jpe?g|png)$ {
        # Some basic cache-control for static files to be sent to the browser
        expires max;
        add_header Pragma public;
        add_header Cache-Control "public, must-revalidate, proxy-revalidate";
    }
}

配置完成后运行对 Nginx 服务进行 reload,然后在 /etc/hosts 中进行域名绑定:

127.0.0.1 apollo-blog.test 

这种方式需要每次代码变动后重新运行 npm run build,所以适合代码稳定(如生产环境
)后部署。

按照以上步骤配置完成后,通过 http://apollo-blog.test/#/signup(或者http://localhost:8080/#/signup) 即可访问注册页面:

允许跨域请求

这时候我们填写表单后点击注册按钮还不能访问后端接口,因为前后端分离之后带来的一个问题是前后端域名不一样,由于浏览器的同源策略,前端无法跨域请求后端接口,为了解决这个问题,我们在后端使用 CORS 解决方案以支持跨域请求,关于该方案原理及使用明细可以参考Laravel CORS 扩展包教程,这里不再赘述,我们只需在 Laravel 后端应用的配置文件 config/graphql.php 中做如下中间件配置即可:

'middleware_schema' => [
    'default' => [
        \Barryvdh\Cors\HandleCors::class,
    ],
],

这样,我们就可以通过注册表单注册新用户了。

用户登录功能实现

后端登录接口

在 Laravel 应用根目录下运行以下 Artisan 命令生成登录 Mutation 类:

php artisan make:graphql:mutation LoginMutation

编辑刚生成的 LoginMutation 类代码如下:

namespace App\GraphQL\Mutation;

use Folklore\GraphQL\Error\AuthorizationError;
use Folklore\GraphQL\Support\Mutation;
use GraphQL\Type\Definition\ResolveInfo;
use GraphQL\Type\Definition\Type;
use GraphQL;
use JWTAuth;
use Auth;

class LoginMutation extends Mutation
{
    protected $attributes = [
        'name' => 'Login',
        'description' => 'A mutation for user login'
    ];

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

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

    public function rules()
    {
        return [
            'email' => ['required', 'email'],
            'password' => ['required']
        ];
    }

    public function resolve($root, $args, $context, ResolveInfo $info)
    {
        $credentials = [
            'email' => $args['email'],
            'password' => $args['password']
        ];
        if (!$token = JWTAuth::attempt($credentials)) {
            throw new AuthorizationError('Invalid Credentials.');
        }
        $user = Auth::user();
        $user->token = $token;
        return $user;
    }
}

UserTypefields 方法中新增返回字段 token

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

最后在 config/graphql.php 中注册刚编写的 LoginMutation

'schemas' => [
    'default' => [
        'query' => [
            ... // 所有 Query
        ],
        'mutation' => [
            ... // 其它 Mutation
            'login' => \App\GraphQL\Mutation\LoginMutation::class,
        ]
    ]
],

接下来先在 GraphiQL 中对接口进行测试,邮箱密码验证失败返回结果如下:

邮箱密码验证成功返回登录用户信息(含token):

前端登录组件

定义 GraphQL Mutation

首先还是在 Vue 应用的 src/graphql.js 中为用户登录定义 GraphQL 操作语句:

export const LOGIN_MUTATION = gql`
    mutation LoginMutation($email: String!, $password: String!) {
        login(
            email: $email,
            password: $password
        ) {
            id
            name
            email
            token
        }
    }
`

创建登录组件

接下来在 components/Admin 目录下创建一个登录组件 LogIn.vue,并编写组件代码如下:

<template>
    <section class="section">
        <div class="columns">
            <div class="column is-4 is-offset-4">
                <h2 class="title has-text-centered">用户登录</h2>

                <form method="POST" @submit.prevent="login">
                    <div class="field">
                        <label class="label">邮箱</label>

                        <p class="control">
                            <input
                                    type="email"
                                    class="input"
                                    v-model="email">
                        </p>
                    </div>

                    <div class="field">
                        <label class="label">密码</label>

                        <p class="control">
                            <input
                                    type="password"
                                    class="input"
                                    v-model="password">
                        </p>
                    </div>

                    <p class="control">
                        <button class="button is-primary is-fullwidth is-uppercase">登录</button>
                    </p>
                </form>
            </div>
        </div>
    </section>
</template>

<script>
import { LOGIN_MUTATION } from '@/graphql'

export default {
  name: 'LogIn',
  data () {
    return {
      email: '',
      password: ''
    }
  },
  methods: {
    login () {
      this.$apollo
        .mutate({
          mutation: LOGIN_MUTATION,
          variables: {
            email: this.email,
            password: this.password
          }
        })
        .then(response => {
          // 保存用户 token 到 local storage
          localStorage.setItem('blog-app-token', response.data.login.token)

          // 重定向用户到文章列表页
          this.$router.replace('/admin/posts')
        })
    }
  }
}
</script>

该组件用于渲染一个简单的用户登录表单,表单提交后就会调用 login 方法,在 login 方法中我们可以使用 mutate 方法来完成 LOGIN_MUTATION 操作;登录成功之后,将从 GraphQL 服务器获取到的 token 保存到 localstorage 并将用户重定向到后台文章列表页面。

添加登录路由

打开 src/router/index.js 文件,将下面的代码插入到合适的位置:

import LogIn from '@/components/Admin/LogIn'

// 将这段代码放到 `routes` 数组内
{
  path: '/login',
  name: 'LogIn',
  component: LogIn
},

测试登录功能

通过 http://localhost:8080/#/login 确认代码无误后,运行 npm run build 然后在浏览器访问 http://apollo-blog.test/#/login 页面:

我们使用前面注册的用户信息登录,登录成功后可以通过在浏览器 F12 查看保存在 Local Storage 的 token 信息:

后续我们将通过在每次请求时在请求头中传递该 token 信息以实现用户认证。

本篇至此结束,下篇教程我们将围绕后台用户列表和用户详情页展开。

学院君 has written 848 articles

终身学习者,Laravel学院院长

9 thoughts on “基于 Laravel + Vue + GraphQL 实现前后端分离的博客应用(一) —— 用户注册登录

  1. 范闲 says:

    后面的小伙伴注意了,如有用到vue-router或vuex的,需要 在根组件上指定 apolloProvider , 一个apollo client客户端实例,否则会报错。
    将Vue根实例中的配置项 apolloProvider, 配置为如下
    provide: apolloProvider.provide(),

  2. bigrocs says:

    这个更标准

    import { ApolloLink } from “apollo-link”;
    import { createHttpLink } from “apollo-link-http”;

    const httpLink = createHttpLink({ uri: “/graphql” });
    const middlewareLink = new ApolloLink((operation, forward) => {
    operation.setContext({
    headers: {
    authorization: localStorage.getItem(“token”) || null
    }
    });
    return forward(operation);
    });

    // use with apollo-client
    const link = middlewareLink.concat(httpLink);

    const apolloClient = new ApolloClient({
    link: link
    cache: new InMemoryCache()
    });

  3. bigrocs says:

    Apollo 怎么携带 token 不知道对不对

    import { ApolloLink, concat, split } from 'apollo-link';

    const token = localStorage.getItem(GC_AUTH_TOKEN) || null
    const authMiddleware = new ApolloLink((operation, forward) => {
    // add the authorization to the headers
    operation.setContext({
    headers: {
    authorization: `Bearer ${token}`
    }
    })
    return forward(operation)
    })

    const apolloClient = new ApolloClient({
    link: concat(authMiddleware, httpLink),
    cache: new InMemoryCache()
    });

发表评论

标记为*的字段是必填项(邮箱地址不会被公开)

你可以使用这些HTML 标签和属性: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>