基于 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-cli // 已全局安装过 vue-cli 略过此步骤
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 支持; -
graphql
:JavaScript 中的 GraphQL 实现; -
apollo-client
:Apollo GraphQL 客户端,允许你轻松构建通过 GraphQL 获取数据的 UI 组件; -
apollo-link
:获取/修改 GraphQL 请求和结果控制流的接口 -
apollo-link-context
:用于设置操作上下文 -
apollo-link-http
:基于 HTTP 通过网络请求获取 GraphQL 结果 -
apollo-cache-inmemory
:Apollo 客户端缓存实现 -
graphql-tag
:用于解析 GraphQL 查询的 JavaScript 模板语义标签
设置 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;
}
}
在 UserType
的 fields
方法中新增返回字段 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 信息以实现用户认证。
本篇至此结束,下篇教程我们将围绕后台用户列表和用户详情页展开。
18 Comments
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() });