基于 Laravel + Vue + Vuex 实现博客应用文章发布功能
Laravel 后端接口准备
既然是博客应用,肯定要支持文章发布功能。
我们先在 Laravel 后端 PostController
控制器中编写相应的文章发布处理方法:
public function store(Request $request)
{
// 表单验证
$data = $request->validate([
'title' => 'required|string|max:100',
'category_id' => [
'required',
Rule::in(Category::query()->pluck('id')->toArray())
],
'image' => 'required|image|max:1024',
'content' => 'required|string',
'summary' => 'required|string|max:200',
]);
$post = new Post($data);
// 封面图片上传处理
if ($request->hasFile('image')) {
$image = $request->file('image');
$local_path = $image->storePublicly('images', ['disk' => 'public']);
$post->image_url = '/storage/' . $local_path;
}
try {
$post->user_id = 1; // 默认为测试用户发布文章
$post->save();
} catch (\Exception $exception) {
return response()->json(['success' => false, 'message' => '文章发布失败']);
}
return response()->json(['success' => true, 'data' => $post->id, 'message' => '文章发布成功']);
}
先验证文章发布表单上传的标题、摘要、详情、分类 ID 和封面图片字段,然后对上传图片进行保存,最后通过 Post
模型类保存文章信息到数据库完成文章发布。
为了让图片路径可以从浏览器访问,需要在 public
入口目录下为 storage/app/public
目录创建软链接:
php artisan storage:link
此外,我们还要在这个控制器中实现一个获取分类列表的方法,以便在文章发布界面渲染分类选择下拉列表:
use App\Http\Resources\Categories as CategoryCollection;
...
public function categories()
{
return new CategoryCollection(
Category::all()
);
}
这里,我们通过新建一个 Category
资源集合类来处理返回数据:
<?php
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\ResourceCollection;
class Categories extends ResourceCollection
{
public $collects = Category::class;
/**
* Transform the resource collection into an array.
*
* @param \Illuminate\Http\Request $request
* @return array
*/
public function toArray($request)
{
return parent::toArray($request);
}
}
和 Post
资源集合类实现一样,不再赘述。
最后,我们在 routes/api.php
中为这两个控制器方法注册 API 路由:
Route::get('/posts/categories', [PostController::class, 'categories']);
Route::post('/posts/store', [PostController::class, 'store']);
至此,就完成了后端接口的准备工作。
基于 Vuex 管理文章发布状态
接下来,我们在前端编写文章发布相关的页面交互代码,通过调用后端接口完成文章发布工作。
还是基于 Vuex 来管理文章发布相关的数据状态,首先在 resources/js/api.js
中编写后端接口请求方法:
// 获取分类列表 API
getPostsCategories() {
return axios.get(base + '/categories');
},
// 文章发布 API
createNewPost(formData) {
return axios.post(base + '/store', formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
});
}
需要注意的是对于文章发布接口,由于表单中包含文件上传,所以需要指定内容类型为 multipart/form-data
。
然后打开 stores.js
,为文章发布状态和分类列表数据编写相应的 state
、getters
、mutations
和 actions
实现代码,参照上篇教程 Vuex Store 的实现思路照猫画虎即可:
export default new Vuex.Store({
state: {
...
postStoredStatus: 0,
postsCategories: []
},
getters: {
...
getPostStoredStatus(state) {
return state.postStoredStatus;
},
getPostsCategories(state) {
return state.postsCategories;
}
},
mutations: {
...
setPostStoredStatus(state, status) {
state.postStoredStatus = status;
},
setPostsCategories(state, categories) {
state.postsCategories = categories;
}
},
actions: {
...
loadPostsCategories(context) {
PostAPI.getPostsCategories().then((resp) => {
context.commit('setPostsCategories', resp.data.data);
}).catch((err) => {
console.log(err);
});
},
publishNewPost(context, formData) {
PostAPI.createNewPost(formData).then((resp) => {
if (resp.data.success === true) {
context.commit('setPostStoredStatus', 1); // 存储成功
dispatch('loadPosts');
} else {
context.commit('setPostStoredStatus', 2); // 存储失败
}
}).catch((err) => {
console.log(err);
});
}
}
});
新增文章发布 Vue 组件
完成后端接口调用和相关数据状态管理的准备工作后,我们就可以在 resources/js/components
目录下新建一个 Vue 组件 NewPost.vue
,作为新文章发布的交互界面。
在这个页面组件中,主要实现一个文章发布表单功能即可:
<template>
<div class="bg-white shadow-md rounded mt-2 px-8 pt-6 pb-8 mb-6 flex flex-col">
<flash-message v-if="publishedStatus" :success="publishedStatus === 1">
<template slot="success">New Post Publish Successfully! </template>
<template slot="error">New Post Publish Failed! </template>
</flash-message>
<div class="flex flex-wrap -mx-3 mb-6">
<div class="w-full px-3">
<label class="block uppercase tracking-wide text-gray-700 text-xs font-bold mb-2">
Title
</label>
<input required v-model="title" class="shadow appearance-none border rounded w-full py-2 px-3 text-grey-darker" type="text" placeholder="在这里输入文章标题">
</div>
</div>
<div class="flex flex-wrap -mx-3 mb-6">
<div class="w-full px-3 md:w-1/2">
<label class="block uppercase tracking-wide text-gray-700 text-xs font-bold mb-2">
Category
</label>
<select required v-model="category_id" class="shadow appearance-none border rounded w-full py-2 px-3 text-grey-darker">
<option v-for="category in categories" :value="category.id">{{ category.name }}</option>
</select>
</div>
</div>
<div class="flex flex-wrap -mx-3 mb-6">
<div class="w-full px-3 md:w-1/2">
<label class="block uppercase tracking-wide text-gray-700 text-xs font-bold mb-2">
Cover Image
</label>
<input required ref="image" class="shadow appearance-none border rounded w-full py-2 px-3 text-grey-darker" type="file">
</div>
</div>
<div class="flex flex-wrap -mx-3 mb-6">
<div class="w-full px-3">
<label class="block uppercase tracking-wide text-gray-700 text-xs font-bold mb-2">
Content
</label>
<textarea required v-model="content" class="shadow appearance-none border rounded w-full py-2 px-3 text-grey-darker h-48" placeholder="在这里输入正文内容..."></textarea>
</div>
</div>
<div class="flex flex-wrap -mx-3 mb-6">
<div class="w-full px-3">
<label class="block uppercase tracking-wide text-gray-700 text-xs font-bold mb-2">
Summary
</label>
<textarea required v-model="summary" class="shadow appearance-none border rounded w-full py-2 px-3 text-grey-darker" placeholder="在这里输入文章摘要..."></textarea>
</div>
</div>
<div class="flex items-center">
<button @click="publishNewPost" type="button" class="border border-green-500 bg-green-500 text-white rounded-md px-4 py-2 m-2 transition duration-500 ease select-none hover:bg-green-600 focus:outline-none focus:shadow-outline">
立即发布
</button>
</div>
</div>
</template>
<script>
import FlashMessage from "./common/FlashMessage";
export default {
components: {FlashMessage},
data() {
return {
title: '',
category_id: '',
content: '',
summary: ''
}
},
created() {
this.$store.dispatch('loadPostsCategories');
},
computed: {
categories() {
return this.$store.getters.getPostsCategories;
},
publishedStatus() {
return this.$store.getters.getPostStoredStatus;
}
},
methods: {
publishNewPost() {
let formData = new FormData();
formData.append('title', this.title);
formData.append('category_id', this.category_id);
formData.append('image', this.$refs.image.files[0]);
formData.append('content', this.content);
formData.append('summary', this.summary);
this.$store.dispatch('publishNewPost', formData);
}
}
}
</script>
在表单顶部,是一个从外部引入的 Flash 消息子组件(resources/js/components/common/FlashMessage.vue
),它会根据文章发布状态(通过计算属性读取)进行条件渲染:
<template>
<div v-if="success" class="block text-sm text-green-600 bg-green-200 border border-green-400 h-12 flex items-center p-4 mb-4 rounded-sm relative" role="alert">
<slot name="success"></slot>
<button type="button" data-dismiss="alert" aria-label="Close" onclick="this.parentElement.remove();">
<span class="absolute top-0 bottom-0 right-0 text-2xl px-3 py-1 hover:text-green-900" aria-hidden="true" >×</span>
</button>
</div>
<div v-else class="block text-sm text-red-600 bg-red-200 border border-red-400 h-12 flex items-center p-4 mb-4 rounded-sm relative" role="alert">
<slot name="error"></slot>
<button type="button" data-dismiss="alert" aria-label="Close" onclick="this.parentElement.remove();">
<span class="absolute top-0 bottom-0 right-0 text-2xl px-3 py-1 hover:text-red-900" aria-hidden="true" >×</span>
</button>
</div>
</template>
<script>
export default {
props: ['success']
}
</script>
然后是文章发布所需的表单字段,包括文章标题、分类、封面图片、正文和摘要等,这里使用了数据绑定与 Vue 组件数据属性进行同步(封面图片除外),分类下拉选择列表是在这个 Vue 组件创建之初通过 Vuex 的 loadPostsCategories
加载的:
created() {
this.$store.dispatch('loadPostsCategories');
},
而后通过计算属性从 Vuex 读取:
categories() {
return this.$store.getters.getPostsCategories;
}
最后,在填写完表单数据后,点击「立即发布」按钮,就可以触发 publishNewPost
方法提交这个文章发布表单:
publishNewPost() {
let formData = new FormData();
formData.append('title', this.title);
formData.append('category_id', this.category_id);
formData.append('image', this.$refs.image.files[0]);
formData.append('content', this.content);
formData.append('summary', this.summary);
this.$store.dispatch('publishNewPost', formData);
}
计算属性中定义的文章发布状态 publishedStatus
会根据 Vuex Store actions
中定义的 publishNewPost
方法执行结果进行动态变更,再反馈到 Flash 消息组件的文案来告知用户文章是否发布成功。
PS:这里没有对表单验证失败做处理,作为课后练习,你可以结合前面 Vue 组件实战中的实现自行去处理。
新增文章发布导航菜单
最后,我们需要在 routes.js
中为文章发布页面注册前端路由(放到 /:name
前面,否则会被它拦截):
{
path: '/new',
component: require('./components/NewPost').default
}
并且在 resources/views/app.blade.php
中新增文章发布导航菜单:
<nav class="flex-grow md:block px-4 pb-4 md:pb-0 md:overflow-y-auto">
... // 其他导航菜单
<router-link class="block px-4 py-2 mt-2 text-sm font-semibold text-gray-900 bg-transparent rounded-lg dark-mode:bg-transparent dark-mode:hover:bg-gray-600 dark-mode:focus:bg-gray-600 dark-mode:focus:text-white dark-mode:hover:text-white dark-mode:text-gray-200 hover:text-gray-900 focus:text-gray-900 hover:bg-gray-200 focus:bg-gray-200 focus:outline-none focus:shadow-outline" to="/new">New Post</router-link>
</nav>
这样一来,重新编译前端资源,就可以在博客首页看到文章发布导航菜单了:
测试文章发布功能
点击这个导航菜单,就可以进入文章发布界面了:
填写这个文章发布表单,点击「立即发布」按钮,就可以发布这篇文章了,如果发布成功,会在顶部通过 Flash 消息显示发布成功文案。
点击「Home」导航菜单,就可以在首页看到这篇新发布的文章了:
好了,关于文章发布功能实现学院君就简单介绍到这里,不过,文章发布功能通常不是开放的,而是需要用户认证之后才能使用,为了简化流程,我们这里限定只有博客应用管理员才能发布新文章,下篇教程,我们就来给大家演示如何在单页面应用中实现用户认证功能。
本项目教程所有代码可以从 Github 代码仓库获取:https://github.com/nonfu/demo-spa。
No Comments