上一篇文章中,我们使用Laravel框架构建了一个JSON Rest API。在本教程中,我们将使用Vuejs来构建一个前端,它可以消费我们已经到位的API。我们可以使用Vue组件和AJAX来简单地从API中获取我们需要显示的数据,而不是渲染刀片文件。我们甚至会学习一点关于使用SCSS和Laravel Mix来定制Laravel的CSS。
安装依赖性
我们需要在Laravel实例的package.json文件中安装指定的依赖项来开始工作.要做到这一点,只需运行 **npm install**像这样在项目根目录下运行.

这将会下载并安装所有前端开发所需的软件.
Laravel Mix
Laravel Mix是现在安装的依赖项之一。Webpack真的很难。Laravel Mix让它更容易。通过Mix和SCSS,我们可以快速改变网站的外观。在运行Mix之前, 我们需要了解它要为我们做什么。因此,默认情况下,Mix遵循的逻辑是在 webpack.mix.js这是在Laravel项目的根目录下。
const mix = require('laravel-mix');
/*
|--------------------------------------------------------------------------
| Mix Asset Management
|--------------------------------------------------------------------------
|
| Mix provides a clean, fluent API for defining some Webpack build steps
| for your Laravel application. By default, we are compiling the Sass
| file for the application as well as bundling up all the JS files.
|
*/
mix.js('resources/js/app.js', 'public/js')
.sass('resources/sass/app.scss', 'public/css');
然而这意味着什么呢?那么, 当你运行 npm run dev例如, Mix会查看资源/js/app.js和资源/sass/app.scss的内容.然后,它将把这些原始资产编译成可用的代码,放在public/js和public/css中。
我如何定制我的样式?
比方说,你想在Laravel项目中尝试很酷的Minty Bootswatch主题。我们怎么能用Mix来实现呢?非常简单!从Bootswatch网站下载**_variables.scss**文件,然后替换在 /resources/sass/_variables.scss.现在我们可以看到原文件和新文件。
原始的_variables.scss
// Body
$body-bg: #f8fafc;
// Typography
$font-family-sans-serif: 'Nunito', sans-serif;
$font-size-base: 0.9rem;
$line-height-base: 1.6;
// Colors
$blue: #3490dc;
$indigo: #6574cd;
$purple: #9561e2;
$pink: #f66d9b;
$red: #e3342f;
$orange: #f6993f;
$yellow: #ffed4a;
$green: #38c172;
$teal: #4dc0b5;
$cyan: #6cb2eb;
Minty版本 _variables.scss
// Minty 4.3.1
// Bootswatch
//
// Color system
//
$white: #fff !default;
$gray-100: #f8f9fa !default;
$gray-200: #f7f7f9 !default;
$gray-300: #eceeef !default;
$gray-400: #ced4da !default;
$gray-500: #aaa !default;
$gray-600: #888 !default;
$gray-700: #5a5a5a !default;
$gray-800: #343a40 !default;
$gray-900: #212529 !default;
$black: #000 !default;
$blue: #007bff !default;
$indigo: #6610f2 !default;
$purple: #6f42c1 !default;
$pink: #e83e8c !default;
$red: #FF7851 !default;
$orange: #fd7e14 !default;
$yellow: #FFCE67 !default;
$green: #56CC9D !default;
$teal: #20c997 !default;
$cyan: #6CC3D5 !default;
$primary: #78C2AD !default;
$secondary: #F3969A !default;
$success: $green !default;
$info: $cyan !default;
$warning: $yellow !default;
$danger: $red !default;
$light: $gray-100 !default;
$dark: $gray-800 !default;
$yiq-contrasted-threshold: 250 !default;
// Body
$body-color: $gray-600 !default;
// Components
$border-radius: .4rem !default;
$border-radius-lg: .6rem !default;
$border-radius-sm: .3rem !default;
// Fonts
$headings-font-family: "Montserrat", -apple-system, system-ui, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif !default;
$headings-color: $gray-700 !default;
// Tables
$table-border-color: rgba(0,0,0,0.05) !default;
// Dropdowns
$dropdown-link-hover-color: $white !default;
$dropdown-link-hover-bg: $secondary !default;
// Navbar
$navbar-dark-color: rgba($white,.6) !default;
$navbar-dark-hover-color: $white !default;
$navbar-light-color: rgba($black,.3) !default;
$navbar-light-hover-color: $gray-700 !default;
$navbar-light-active-color: $gray-700 !default;
$navbar-light-disabled-color: rgba($black,.1) !default;
// Pagination
$pagination-color: $white !default;
$pagination-bg: $primary !default;
$pagination-border-color: $primary !default;
$pagination-hover-color: $white !default;
$pagination-hover-bg: $secondary !default;
$pagination-hover-border-color: $pagination-hover-bg !default;
$pagination-active-bg: $secondary !default;
$pagination-active-border-color: $pagination-active-bg !default;
$pagination-disabled-color: $white !default;
$pagination-disabled-bg: #CCE8E0 !default;
$pagination-disabled-border-color: $pagination-disabled-bg !default;
// Breadcrumbs
$breadcrumb-bg: $primary !default;
$breadcrumb-divider-color: $white !default;
$breadcrumb-active-color: $breadcrumb-divider-color !default;
这个文件设置了所有这些变量的值,给Bootstrap一个全新的外观。现在我们可以像这样修改welcome.blade.php文件,只是为了测试一下。我们想测试一下不同的Bootstrap类,看看是否发生了新的效果。
<!doctype html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Laravel</title>
<!-- Styles -->
<link rel="stylesheet" href="{{ asset('/css/app.css') }}">
</head>
<body class="container">
<div class="jumbotron mt-3">
<h1 class="display-3">Hello, world!</h1>
<p class="lead">This is a simple hero unit!</p>
<hr class="my-4">
<p>It uses utility classes for typography and spacing to space content out within the larger container.</p>
<button type="button" class="btn btn-primary">Primary</button>
<button type="button" class="btn btn-secondary">Secondary</button>
<button type="button" class="btn btn-success">Success</button>
<button type="button" class="btn btn-info">Info</button>
<button type="button" class="btn btn-warning">Warning</button>
<button type="button" class="btn btn-danger">Danger</button>
<button type="button" class="btn btn-link">Link</button>
</div>
<script src="{{ asset('js/app.js') }}"></script>
</body>
</html>
运行混合
我们现在可以用 npm run dev运行mix,应该会有一个类似的结果。

注意:如果你在运行mix时遇到错误,请看这个帖子,应该会有帮助。
现在,我们看到的不是标准的闪屏,而是应用了新的样式。帅呆了!

构建一个Vue前端
有了这一点点的设置和配置,我们现在可以开始为我们的API构建Vue前端了。只要有几个组件就可以了。我们将有一个简单的导航条组件和一个PostList组件。开始的时候,我们只需要把东西摆出来,然后边走边添加动态数据。
welcome.blade.php
Laravel仍然会加载这个视图文件作为主页。在这个主页上,有一个ID为 "app "的div。我们的Vue应用程序将附加或安装到这个div上。下面是该文件的开头部分。
<!doctype html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="csrf-token" content="{{ csrf_token() }}">
<script>window.Laravel = {csrfToken: '{{ csrf_token() }}'}</script>
<title>Vue Front End For A Laravel API</title>
<link rel="stylesheet" href="{{ asset('/css/app.css') }}">
</head>
<body class="container-fluid">
<div id="app">
<navbar></navbar>
</div>
<script src="{{ asset('js/app.js') }}"></script>
</body>
</html>
注意包含了csrf_token字段。如果没有这些字段,你会在控制台得到 "未找到CSRF令牌:laravel.com/docs/csrf#c… "的错误,所以一定要包括这些代码行。
添加一个导航条组件
你会注意到,在上面的第13行,有一个对组件的引用。我们需要建立和注册这个组件,以便它能够显示。我们可以导航到 resources/js/components目录并创建一个Navbar.vue文件。这个文件有一个特殊的 **.vue**扩展名,这意味着一些事情。首先,它是一个Vue的单文件组件。这些类型的文件允许你把模板、脚本、甚至自定义css放在同一个文件中。然后你通过Webpack(在这个例子中是Laravel Mix)构建文件,你就可以得到一个工作结果。非常酷!
resources/js/components/Navbar.vue
<template>
<nav class="navbar navbar-expand-lg navbar-dark bg-primary">
<a class="navbar-brand" href="#">Minty Fresh</a>
</nav>
</template>
现在, 我们把这个组件注册在 resources/js/app.js.
资源/js/app.js
require('./bootstrap');
window.Vue = require('vue');
Vue.component('navbar', require('./components/Navbar.vue').default);
const app = new Vue({
el: '#app'
});
只要你已经设置了Laravel Mix来观察你的文件,现在通过运行 npm run watch或 npm run watch-poll,那么你就可以访问主页,看到一个Minty导航条。

列出帖子
为了列出一些帖子,我们可以在下面添加一个名为PostList.vue的新组件 resources/js/components.所以首先,继续添加该文件,就像我们在这里看到的那样。

新的组件需要在app.js文件中注册。
资源/js/app.js
require('./bootstrap');
window.Vue = require('vue');
Vue.component('navbar', require('./components/Navbar.vue').default);
Vue.component('post-list', require('./components/PostList.vue').default);
const app = new Vue({
el: '#app'
});
现在在PostList.vue文件中,我们需要填充模板和脚本区域。我们利用创建的Lifecycle Hook。这个函数在组件的实例被创建后被自动调用。这是在组件被装入页面之前,所以这是一个完美的时机,我们需要使用JavaScript Fetch api从API中获取数据。
resources/js/components/PostList.vue
<template>
<div>
<div class="card mb-2" v-for="post in posts" v-bind:key="post.id">
<div class="card-body ">
<h4 class="card-title">{{ post.title }}</h4>
<p class="card-text">{{ post.body }}</p>
</div>
</div>
</div>
</template>
<script>
export default {
data() {
return {
posts: []
};
},
created() {
this.getPosts();
},
methods: {
getPosts(api_url) {
api_url = api_url || '/api/posts';
fetch(api_url)
.then(response => response.json())
.then(response => {
this.posts = response.data;
})
.catch(err => console.log(err));
}
}
};
</script>
在上面的代码中,第16行的 posts数组在第16行,它将保存包含所有帖子的api响应。这将在页面加载和 created()钩子运行时填充。当这种情况发生时 getPosts()函数被调用,该函数从api获取数据并将其分配给 **posts**变量。随着数据的到位,模板部分使用标准的Vue v-for来创建一个帖子列表。

添加一个分页器
我们在上面成功地显示了5个帖子,而且看起来非常酷!这就是我们的API。回顾一下,我们的API确实提供了关于上一个、下一个和当前链接的信息。这意味着你可以使用这些数据来创建一个分页器。下面突出显示的几行显示了显示分页器的补充内容。
<template>
<div>
<nav>
<ul class="pagination justify-content-center">
<li v-bind:class="[{disabled: !pagination.prev_page_url}]" class="page-item">
<a class="page-link" href="#" @click="getPosts(pagination.prev_page_url)">Previous</a>
</li>
<li class="page-item disabled">
<a class="page-link" href="#">{{ pagination.current_page }} of {{ pagination.last_page }}</a>
</li>
<li v-bind:class="[{disabled: !pagination.next_page_url}]" class="page-item">
<a class="page-link" href="#" @click="getPosts(pagination.next_page_url)">Next</a>
</li>
</ul>
</nav>
<div class="card mb-2" v-for="post in posts" v-bind:key="post.id">
<div class="card-body ">
<h4 class="card-title">{{ post.title }}</h4>
<p class="card-text">{{ post.body }}</p>
</div>
</div>
</div>
</template>
<script>
export default {
data() {
return {
posts: [],
pagination: {}
};
},
created() {
this.getPosts();
},
methods: {
getPosts(api_url) {
let vm = this;
api_url = api_url || '/api/posts';
fetch(api_url)
.then(response => response.json())
.then(response => {
this.posts = response.data;
vm.paginator(response.meta, response.links);
})
.catch(err => console.log(err));
},
paginator(meta, links) {
this.pagination = {
current_page: meta.current_page,
last_page: meta.last_page,
next_page_url: links.next,
prev_page_url: links.prev
};
},
}
};
</script>
这导致了分页器的显示,它告诉我们在哪一页,并根据我们所处的位置动态地启用或禁用上一页和下一页按钮。
添加一个帖子
让我们在页面上添加一个表单,这样就可以通过向API发送POST请求来添加一个帖子。在后端,我们首先将分页改为每页3个,以便给我们一些空间。
<template>
<div>
<form @submit.prevent="addPost">
<div class="form-group">
<input type="text" class="form-control" placeholder="Title" v-model="post.title">
</div>
<div class="form-group">
<textarea class="form-control" placeholder="Body" v-model="post.body"></textarea>
</div>
<button type="submit" class="btn btn-success">Save</button>
</form>
<nav>
<ul class="pagination justify-content-center">
<li v-bind:class="[{disabled: !pagination.prev_page_url}]" class="page-item">
<a class="page-link" href="#" @click="getPosts(pagination.prev_page_url)">Previous</a>
</li>
<li class="page-item disabled">
<a class="page-link" href="#">{{ pagination.current_page }} of {{ pagination.last_page }}</a>
</li>
<li v-bind:class="[{disabled: !pagination.next_page_url}]" class="page-item">
<a class="page-link" href="#" @click="getPosts(pagination.next_page_url)">Next</a>
</li>
</ul>
</nav>
<div class="card mb-2" v-for="post in posts" v-bind:key="post.id">
<div class="card-body ">
<h4 class="card-title">{{ post.title }}</h4>
<p class="card-text">{{ post.body }}</p>
</div>
</div>
</div>
</template>
<script>
export default {
data() {
return {
posts: [],
pagination: {},
post: {
id: '',
title: '',
body: ''
}
};
},
created() {
this.getPosts();
},
methods: {
getPosts(api_url) {
let vm = this;
api_url = api_url || '/api/posts';
fetch(api_url)
.then(response => response.json())
.then(response => {
this.posts = response.data;
vm.paginator(response.meta, response.links);
})
.catch(err => console.log(err));
},
paginator(meta, links) {
this.pagination = {
current_page: meta.current_page,
last_page: meta.last_page,
next_page_url: links.next,
prev_page_url: links.prev
};
},
addPost() {
fetch('api/post', {
method: 'post',
body: JSON.stringify(this.post),
headers: {
'content-type': 'application/json'
}
})
.then(response => response.json())
.then(data => {
this.getPosts();
})
.catch(err => console.log(err));
}
}
};
</script>
现在我们有能力添加一个新的帖子,这很好 🙂
删除一个帖子
要删除一个帖子,我们可以在方法对象中添加一个新函数,名为deletePost()。它接受要删除的帖子的ID,然后向api发出一个ajax删除请求。下面的亮点显示了允许删除帖子的新代码。
<template>
<div>
<form @submit.prevent="addPost">
<div class="form-group">
<input type="text" class="form-control" placeholder="Title" v-model="post.title">
</div>
<div class="form-group">
<textarea class="form-control" placeholder="Body" v-model="post.body"></textarea>
</div>
<button type="submit" class="btn btn-success">Save</button>
</form>
<nav>
<ul class="pagination justify-content-center">
<li v-bind:class="[{disabled: !pagination.prev_page_url}]" class="page-item">
<a class="page-link" href="#" @click="getPosts(pagination.prev_page_url)">Previous</a>
</li>
<li class="page-item disabled">
<a class="page-link" href="#">{{ pagination.current_page }} of {{ pagination.last_page }}</a>
</li>
<li v-bind:class="[{disabled: !pagination.next_page_url}]" class="page-item">
<a class="page-link" href="#" @click="getPosts(pagination.next_page_url)">Next</a>
</li>
</ul>
</nav>
<div class="card mb-2" v-for="post in posts" v-bind:key="post.id">
<div class="card-body ">
<h4 class="card-title">{{ post.title }}</h4>
<p class="card-text">{{ post.body }}</p>
<button type="button" @click="deletePost(post.id)" class="btn btn-secondary">Delete</button>
</div>
</div>
</div>
</template>
<script>
export default {
data() {
return {
posts: [],
pagination: {},
post: {
id: '',
title: '',
body: ''
}
};
},
created() {
this.getPosts();
},
methods: {
getPosts(api_url) {
let vm = this;
api_url = api_url || '/api/posts';
fetch(api_url)
.then(response => response.json())
.then(response => {
this.posts = response.data;
vm.paginator(response.meta, response.links);
})
.catch(err => console.log(err));
},
paginator(meta, links) {
this.pagination = {
current_page: meta.current_page,
last_page: meta.last_page,
next_page_url: links.next,
prev_page_url: links.prev
};
},
addPost() {
fetch('api/post', {
method: 'post',
body: JSON.stringify(this.post),
headers: {
'content-type': 'application/json'
}
})
.then(response => response.json())
.then(data => {
this.getPosts();
})
.catch(err => console.log(err));
},
deletePost(id) {
fetch('api/post/' + id, {
method: 'delete'
})
.then(response => response.json())
.then(data => {
this.getPosts();
})
.catch(err => console.log(err));
},
}
};
</script>
当一个帖子被删除时,页面会自动刷新。
更新一个帖子
我们可以为每个帖子添加一个新的按钮,它将提供更新该帖子的选项。这将是一个两步的过程。第一步是点击更新按钮,将帖子的数据加载到表单中。然后我们可以对表单中的数据进行修改,最后点击保存。这意味着**addPost()**函数将需要新的逻辑来说明这一点。但首先,让我们看看如何添加按钮,以允许通过加载正确的数据到表单中进行更新。
<template>
<div>
<form @submit.prevent="addPost">
<div class="form-group">
<input type="text" class="form-control" placeholder="Title" v-model="post.title">
</div>
<div class="form-group">
<textarea class="form-control" placeholder="Body" v-model="post.body"></textarea>
</div>
<button type="submit" class="btn btn-success">Save</button>
</form>
<nav>
<ul class="pagination justify-content-center">
<li v-bind:class="[{disabled: !pagination.prev_page_url}]" class="page-item">
<a class="page-link" href="#" @click="getPosts(pagination.prev_page_url)">Previous</a>
</li>
<li class="page-item disabled">
<a class="page-link" href="#">{{ pagination.current_page }} of {{ pagination.last_page }}</a>
</li>
<li v-bind:class="[{disabled: !pagination.next_page_url}]" class="page-item">
<a class="page-link" href="#" @click="getPosts(pagination.next_page_url)">Next</a>
</li>
</ul>
</nav>
<div class="card mb-2" v-for="post in posts" v-bind:key="post.id">
<div class="card-body ">
<h4 class="card-title">{{ post.title }}</h4>
<p class="card-text">{{ post.body }}</p>
<button type="button" @click="deletePost(post.id)" class="btn btn-secondary">Delete</button>
<button type="button" @click="updatePost(post)" class="btn btn-success">Update</button>
</div>
</div>
</div>
</template>
<script>
export default {
data() {
return {
posts: [],
pagination: {},
post: {
id: '',
title: '',
body: ''
},
update: false,
post_id: ''
};
},
created() {
this.getPosts();
},
methods: {
getPosts(api_url) {
let vm = this;
api_url = api_url || '/api/posts';
fetch(api_url)
.then(response => response.json())
.then(response => {
this.posts = response.data;
vm.paginator(response.meta, response.links);
})
.catch(err => console.log(err));
},
paginator(meta, links) {
this.pagination = {
current_page: meta.current_page,
last_page: meta.last_page,
next_page_url: links.next,
prev_page_url: links.prev
};
},
addPost() {
fetch('api/post', {
method: 'post',
body: JSON.stringify(this.post),
headers: {
'content-type': 'application/json'
}
})
.then(response => response.json())
.then(data => {
this.getPosts();
})
.catch(err => console.log(err));
},
deletePost(id) {
fetch('api/post/' + id, {
method: 'delete'
})
.then(response => response.json())
.then(data => {
this.getPosts();
})
.catch(err => console.log(err));
},
updatePost(post) {
this.update = true;
this.post.id = post.id;
this.post.post_id = post.id;
this.post.title = post.title;
this.post.body = post.body;
}
}
};
</script>
根据你点击更新按钮的帖子,该数据会被加载到表单中,这样我们就可以对它采取行动。因此,如果你想更新帖子2,你可以点击那个特定的更新按钮,改变数据,然后保存。
修改addPost()函数
在上面的部分中,点击某个帖子的更新按钮可以将该帖子的数据加载到表单中。然而,点击表单的 "保存",目前会创建一个新的帖子,而不是更新刚刚加载到表单中的帖子。我们可以在addPost()函数中使用一些条件逻辑来解决这个问题。我们还可以为两个目的添加一个clearForm()函数。首先是允许用户在决定不做更新时清除表单,其次是一旦有新的帖子加入,就自动清除表单。
<template>
<div>
<form @submit.prevent="addPost">
<div class="form-group">
<input type="text" class="form-control" placeholder="Title" v-model="post.title">
</div>
<div class="form-group">
<textarea class="form-control" placeholder="Body" v-model="post.body"></textarea>
</div>
<button type="submit" class="btn btn-success">Save</button>
<button @click.prevent="clearForm()" class="btn btn-warning">Clear Form</button>
</form>
<nav>
<ul class="pagination justify-content-center">
<li v-bind:class="[{disabled: !pagination.prev_page_url}]" class="page-item">
<a class="page-link" href="#" @click="getPosts(pagination.prev_page_url)">Previous</a>
</li>
<li class="page-item disabled">
<a class="page-link" href="#">{{ pagination.current_page }} of {{ pagination.last_page }}</a>
</li>
<li v-bind:class="[{disabled: !pagination.next_page_url}]" class="page-item">
<a class="page-link" href="#" @click="getPosts(pagination.next_page_url)">Next</a>
</li>
</ul>
</nav>
<div class="card mb-2" v-for="post in posts" v-bind:key="post.id">
<div class="card-body ">
<h4 class="card-title">{{ post.title }}</h4>
<p class="card-text">{{ post.body }}</p>
<button type="button" @click="deletePost(post.id)" class="btn btn-secondary">Delete</button>
<button type="button" @click="updatePost(post)" class="btn btn-success">Update</button>
</div>
</div>
</div>
</template>
<script>
export default {
data() {
return {
posts: [],
pagination: {},
post: {
id: '',
title: '',
body: ''
},
update: false,
post_id: ''
};
},
created() {
this.getPosts();
},
methods: {
getPosts(api_url) {
let vm = this;
api_url = api_url || '/api/posts';
fetch(api_url)
.then(response => response.json())
.then(response => {
this.posts = response.data;
vm.paginator(response.meta, response.links);
})
.catch(err => console.log(err));
},
paginator(meta, links) {
this.pagination = {
current_page: meta.current_page,
last_page: meta.last_page,
next_page_url: links.next,
prev_page_url: links.prev
};
},
addPost() {
if (this.update === false) {
fetch('api/post', {
method: 'post',
body: JSON.stringify(this.post),
headers: {
'content-type': 'application/json'
}
})
.then(response => response.json())
.then(data => {
this.clearForm();
this.getPosts();
})
.catch(err => console.log(err));
} else {
fetch('api/post', {
method: 'put',
body: JSON.stringify(this.post),
headers: {
'content-type': 'application/json'
}
})
.then(response => response.json())
.then(data => {
this.clearForm();
this.getPosts();
})
.catch(err => console.log(err));
}
},
deletePost(id) {
fetch('api/post/' + id, {
method: 'delete'
})
.then(response => response.json())
.then(data => {
this.getPosts();
})
.catch(err => console.log(err));
},
updatePost(post) {
this.update = true;
this.post.id = post.id;
this.post.post_id = post.id;
this.post.title = post.title;
this.post.body = post.body;
},
clearForm() {
this.update = false;
this.post.id = null;
this.post.post_id = null;
this.post.title = '';
this.post.body = '';
}
}
};
</script>
因此,对于最终的结果,我们可以在前端使用Vue,在后端使用Laravel来完成Post的完整创建,读取,更新和删除。
为Laravel的API构建Vue前端 摘要
就这样, 我们现在有了一个Vue前端,用于Laravel的API资源,我们在之前的教程中已经建立了。我们在Laravel内部使用Mix工具建立了一个构建过程,将.vue文件编译为可生产的JavaScript。