下面由laravel教程栏目给大家介绍基于laravel+vue组件实现文章发布、编辑和浏览功能,希望对需要的朋友有所帮助!
我们将基于 laravel 提供后端接口,基于 vue.js 作为前端 javascript 组件开发框架,基于 bootstrap 作为 css 框架。
laravel 后端接口首先,我们基于上篇教程创建的资源控制器 postcontroller 快速编写后端增删改查接口实现代码:
<?phpnamespace app\http\controllers;use app\models\post;use exception;use illuminate\contracts\foundation\application;use illuminate\contracts\view\factory;use illuminate\contracts\view\view;use illuminate\http\request;use illuminate\http\response;use illuminate\support\collection;use illuminate\support\facades\auth;class postcontroller extends controller{ public function __construct() { $this->middleware('auth')->except('index', 'all', 'show', 'data'); } /** * display a listing of the resource. * * @return application|factory|view|response|\illuminate\view\view */ public function index() { return view('posts.index', ['pagetitle' => '文章列表页']); } /** * show the form for creating a new resource. * * @return application|factory|view|response|\illuminate\view\view */ public function create() { return view('posts.create', ['pagetitle' => '发布新文章']); } /** * store a newly created resource in storage. * * @param request $request * @return array */ public function store(request $request) { $data = $request->validate([ 'title' => 'required|max:128', 'content' => 'required' ]); $post = new post($data); $post->status = 1; $post->user_id = auth::user()->id; if ($post->save()) { return ['success' => true, 'message' => '文章发布成功']; } return ['success' => false, 'message' => '保存文章数据失败']; } /** * display the specified resource. * * @param post $post * @return application|factory|view|response|\illuminate\view\view */ public function show(post $post) { return view('posts.show', ['id' => $post->id, 'pagetitle' => $post->title]); } /** * show the form for editing the specified resource. * * @param post $post * @return application|factory|view|response|\illuminate\view\view */ public function edit(post $post) { return view('posts.edit', ['pagetitle' => '编辑文章', 'id' => $post->id]); } /** * update the specified resource in storage. * * @param request $request * @param post $post * @return array */ public function update(request $request, post $post) { $data = $request->validate([ 'title' => 'required|max:128', 'content' => 'required' ]); $post->fill($data); $post->status = 1; if ($post->save()) { return ['success' => true, 'message' => '文章更新成功']; } return ['success' => false, 'message' => '更新文章数据失败!']; } /** * remove the specified resource from storage. * * @param post $post * @return array * @throws exception */ public function destroy(post $post) { if ($post->delete()) { return ['success' => true, 'message' => '文章删除成功']; } return ['success' => false, 'message' => '删除文章失败']; } /** * 获取所有文章数据 * * @return collection */ public function all() { return post::orderbydesc('created_at')->get(); } /** * 获取单个文章数据 * * @param post $post * @return post */ public function data(post $post) { $post->author_name = $post->author->name; return $post; }}
除了 laravel 资源控制器自带的方法之外,我们额外提供了 all 和 data 两个方法,分别用于在 vue 组件中通过 ajax 请求获取文章列表数据和文章详情数据。因此,需要在路由文件 routes/web.php 中注册资源路由之前添加这两个方法对应的路由:
use app\http\controllers\postcontroller;route::get('posts/all', [postcontroller::class, 'all']);route::get('posts/{post}/data', [postcontroller::class, 'data']);route::resource('posts', postcontroller::class);
注意这里我们使用了 laravel 路由提供的隐式模型绑定功能快速获取模型实例。此外,相应的视图模板路径也做了调整,我们马上会介绍这些视图模板文件。
通过填充器填充测试数据如果你在上篇教程填充的测试数据基础上新增过其他数据,可以运行 php artisan migrate:refresh 命令重建数据表快速清空已有数据并重新填充。
如果你不想查看返回实例数据格式的细节,可以在自带填充器 database/seeders/databaseseeder.php 中定义填充代码:
<?phpnamespace database\seeders;use app\models\post;use illuminate\database\seeder;class databaseseeder extends seeder{ /** * seed the application's database. * * @return void */ public function run() { // \app\models\user::factory(10)->create(); post::factory(10)->create(); }}
然后运行 php artisan migrate:refresh --seed 命令即可一步到位完成数据表重建、测试数据清空和重新填充:
通过模板继承重构视图模板由于我们使用的是 laravel 提供的 laravel/ui 扩展包提供的 bootstrap 和 vue 前端脚手架代码,该扩展包还提供了用户认证相关脚手架代码实现,并且提供了一个视图模板布局文件 resources/views/layouts/app.blade.php,我们将通过模板继承基于这个布局文件来重构文章列表、表单、详情页相关视图模板文件,让整体 ui 统一。
不同页面设置不同标题我们前面在 postcontroller 中,为所有 get 路由渲染的视图文件传递了 pagetitle 值作为不同页面的标题,要实现该功能,需要修改 resources/views/layouts/app.blade.php 布局文件中 title 标签对应的标签文本值:
<title>{{ $pagetitle config('app.name', 'laravel') }}</title>
文章列表视图接下来,将原来的文章相关视图文件都移动到 resources/views/posts 目录下,改写文章列表视图文件模板代码如下(将原来的 posts.blade.php 重命名为 index.blade.php):
@extends('layouts.app')@section('content') <p class="container"> <post-list></post-list> </p>@endsection
文章发布视图将原来的 form.blade.php 重命名为 create.blade.php,并编写文章发布表单页面视图文件模板代码如下:
@extends('layouts.app')@section('content') <p class="container"> <p class="row justify-content-center"> <post-form title="发布新文章" action="create" url="{{ route('posts.store') }}"> </post-form> </p> </p>@endsection
由于文章发布和编辑表单共用一个 vue 表单组件,所以我们这里额外传递了一些 props 属性到组件模板,包括表单标题(title)、操作类型(action)、表单提交 url(url),后面马上会介绍表单组件的调整。
文章编辑视图在 resources/views/posts 目录下新建一个 edit.blade.php 作为文件编辑页面视图文件,并编写模板代码如下:
@extends('layouts.app')@section('content') <p class="container"> <p class="row justify-content-center"> <post-form title="编辑文章" action="update" id="{{ $id }}" url="{{ route('posts.update', ['post' => $id]) }}> </post-form> </p> </p>@endsection
同样也使用 post-form 模板渲染文章编辑表单,只不过额外传递了一个 id 属性,用于在表单组件初始化待编辑的文章数据。
文章详情页视图后面单独介绍。
重构 vue 表单组件代码为了适配文章编辑表单,以及后端接口返回数据格式的调整,我们需要修改 vue 表单组件实现代码:
<template> <formsection @store="store"> <template slot="title">{{ title }}</template> <template slot="input-group"> <p class="form-group"> <label name="title" label="标题"></label> <inputtext name="title" v-model="form.title" @keyup="clear('title')"></inputtext> <errormsg :error="form.errors.get('title')"></errormsg> </p> <p class="form-group"> <label name="content" label="内容"></label> <textarea name="content" v-model="form.content" @keyup="clear('content')"></textarea> <errormsg :error="form.errors.get('content')"></errormsg> </p> </template> <template slot="action"> <button type="submit">立即发布</button> </template> <template slot="toast"> <toastmsg :success="form.success" :validated="form.validated"> {{ form.message }} </toastmsg> </template> </formsection></template><script>import formsection from './form/formsection';import inputtext from './form/inputtext';import textarea from './form/textarea';import button from './form/button';import toastmsg from './form/toastmsg';import label from ./form/label;import errormsg from ./form/errormsg;export default { components: {formsection, inputtext, textarea, label, errormsg, button, toastmsg}, props: ['title', 'url', 'action', 'id'], data() { return { form: new form({ title: '', content: '' }) } }, mounted() { let post_id = number(this.id); if (this.action === 'update' && post_id > 0) { this.load(post_id); } }, methods: { load(id) { this.form.title = '加载中...'; this.form.content = '加载中...'; let url = '/posts/' + id + '/data'; axios.get(url).then(resp => { this.form.title = resp.data.title; this.form.content = resp.data.content; }).catch(error => { alert('从服务端初始化表单数据失败'); }); }, store() { if (this.action === 'create') { this.form.post(this.url) .then(data => { // 发布成功后跳转到列表页 window.location.href = '/posts'; }) .catch(data => console.log(data)); // 自定义表单提交失败处理逻辑 } else { this.form.put(this.url) .then(data => { // 更新成功后跳转到详情页 window.location.href = '/posts/' + this.id; }) .catch(data => console.log(data)); // 自定义表单提交失败处理逻辑 } }, clear(field) { this.form.errors.clear(field); } }}</script>
文章发布和编辑页面需要通过标题予以区分,所以我们通过 title 属性从父级作用域传递该标题值。
对于文章编辑表单,首先,我们会根据父级作用域传递的 id 属性值在 mounted 钩子函数中调用新增的 load 方法从后端接口 /posts/{post}/data 加载对应文章数据填充表单。
现在后端接口可以自动获取当前认证用户的 id,所以 author 字段就没有必要填写了,直接将其移除。
文章创建和编辑对应的请求方式是不一样的,操作成功后处理逻辑也是不一样的(前者重定向到列表页,后者重定向到详情页),所以根据 action 属性值分别进行了处理。
此外,由于后端对表单数据进行验证后,保存数据阶段依然可能失败,所以前端提交表单后返回的响应状态码为 200 并不表示表单提交处理成功,还需要借助响应实体(json 格式)中的 success 字段进一步判断,进而通过 toastmsg 子组件渲染成功或失败的提示文本。
toastmsg 是从之前的 successmsg 组件升级而来,直接将 successmsg 组件重命名为 toastmsg 并改写组件代码如下:
<style scoped>.alert { margin-top: 10px;}</style><template> <p class="alert" :class="{'alert-success': success, 'alert-danger': !success}" role="alert" v-show="validated"> <slot></slot> </p></template><script>export default { props: ['validated', 'success']}</script>
可以看到,如果表单提交处理成功(依然基于父级作用域传递的 form.success 属性)则显示成功提示样式及文案,否则显示失败提示样式和文案,而是否渲染该组件取决于表单验证是否成功,该字段基于父级作用域传递的 form.validated 属性,之前是没有这个属性的,所以我们需要额外添加,在 resources/js/form.js 中,调整相关代码实现如下:
class form { constructor(data) { ... this.validated = false; } ... /** * 表单提交处理 * * @param {string} url * @param {string} method */ submit(url, method) { return new promise((resolve, reject) => { axios[method](url, this.data()) .then(response => { this.onsuccess(response.data); this.validated = true; if (this.success === true) { resolve(response.data); } else { reject(response.data); } }) .catch(error => { this.onfail(error.response.data.errors); reject(error.response.data); }); }); } /** * 处理表单提交成功 * * @param {object} data */ onsuccess(data) { this.success = data.success; this.message = data.message; this.reset(); } ...}
这样一来,文章发布和编辑共用的 vue 表单组件就重构好了。
文章详情页视图和 vue 组件实现我们接着来实现文章详情页。
postdetail 组件在 component-practice/resources/js/components 目录下新建一个 postdetail.vue 文件作为渲染文章详情的 vue 单文件组件,并编写组件代码如下:
<style scoped>.post-detail { width: 100%;}.post-title { margin-bottom: .25rem; font-size: 2.5rem;}.post-meta { margin-bottom: 1.25rem; color: #999;}.post-content { font-size: 1.1rem; font-weight: 400; line-height: 1.5; color: #212529;}</style><template> <p class="spinner-border" role="status" v-if="!loaded"> <span class="sr-only">loading...</span> </p> <p class="post-detail" v-else> <h2 class="post-title">{{ title }}</h2> <p class="post-meta"> created at {{ created_at | diff_for_human }} by <a href="#">{{ author_name }}</a>, status: {{ status | post_status_readable }}, action: <a :href="'/posts/' + id + '/edit'">编辑</a> </p> <p class="post-content"> {{ content }} </p> </p></template><script>export default { props: ['post_id'], data() { return { id: this.post_id, title: '', content: '', status: '', author_name: '', created_at: '', loaded: false } }, mounted() { if (!this.loaded) { this.load(number(this.id)); } }, methods: { load(id) { axios.get('/posts/' + this.id + '/data').then(resp => { this.title = resp.data.title; this.content = resp.data.content; this.status = resp.data.status; this.author_name = resp.data.author_name; this.created_at = resp.data.created_at; this.loaded = true; }).catch(err => { alert('加载文章数据失败'); }); } }}</script>
这个组件功能比较简单,在 mounted 钩子函数中通过父级作用域传递的 id 属性值调用 load 函数加载后端接口返回的文章数据,并通过数据绑定将其渲染到模板代码中,在加载过程中,会有一个动态的加载状态提示用户文章数据正在加载。
这里我们还使用了过滤器对数据进行格式化,日期过滤器已经是全局的了,状态过滤器之前是本地的,这里我们将其从文章列表卡片组件 carditem 中将其迁移到 app.js 中作为全局过滤器:
vue.filter('post_status_readable', status => { switch(status) { case 0: return '草稿'; case 1: return '已发布'; default: return '未知状态'; }});
然后就可以在任何 vue 组件中调用它了(carditem 中过滤器调用代码做一下相应调整)。
在 app.js 中注册这个组件:
vue.component('post-detail', require('./components/postdetail.vue').default);
文章详情页视图文件再到 component-practice/resources/views/posts 目录下新建 show.blade.php 视图文件引用 post-detail 组件即可:
@extends('layouts.app')@section('content')<p class="container"> <post-detail post_id="{{ $id }}"></post-detail></p>@endsection
优化文章列表组件最后,我们到文章列表组件中新增一个发布文章入口。
打开子组件 listsection,在视图模式切换按钮右侧新增一个插槽,用于从父级作用域传递更多额外操作按钮:
<style scoped>.card-header h5 { margin-top: 0.5em; display: inline-block;}.card-header .float-right { float: right;}</style><template><p class="card"> <p class="card-header"> <h5><slot name="title"></slot></h5> <p class="float-right"> <button class="btn btn-success view-mode" @click.prevent="switch_view_mode"> {{ view.switch_to }} </button> <slot name="more-actions"></slot> </p> </p> ...
然后在 postlist 中将发布文章按钮放到这个插槽中(样式代码也做了微调):
<style scoped>.post-list { width: 100%;}</style><template> <p class="post-list"> <listsection :view_mode="view_mode" @view-mode-changed="change_view_mode"> <template #title>文章列表</template> <template #more-actions> <a href="/posts/create" class="btn btn-primary">新文章</a> </template> <template v-if="view_mode === 'list'"> <listitem v-for="post in posts" :key="post.id" :url="'/posts/' + post.id"> {{ post.title }} </listitem> </template> ...
顺便也为文章列表所有文章设置详情页链接,listitem 链接是从 postlist 通过 props 属性传递的,carditem 需要去子组件中设置:
<a :href="'/posts/' + post.id" class="btn btn-primary"><slot name="action-label"></slot></a>
至此,我们就完成了文章列表、发布、编辑和详情页的所有前后端功能代码编写。
整体测试如果你已经在本地运行了 npm run watch 并且通过 php arstisan serve 启动 php 内置 web 服务器的话,就可以在浏览器通过 http://127.0.0.1:3002/posts (启用了 browsersync 代理)访问新的文章列表页了:
点击任意文章链接,即可进入文章详情页,加载数据成功之前,会有如下动态加载效果:
你可以点击「编辑」链接对这篇文章进行编辑:
更新成功后,会跳转到文章详情页,对应字段均已更新,并且状态也从草稿变成了已发布:
当然,文章发布和编辑功能需要用户处于已登录状态(目前未做权限验证),如果未登录的话,点击编辑和新文章按钮会先跳转到登录页面(该功能由 postcontroller 控制器构造函数中定义的中间件方法实现),我们在已登录情况下在文章列表页点击右上角的「新文章」按钮进入文章发布页面:
发布成功后,页面会跳转到文章列表页,并在列表中出现刚刚创建的文章:
增删改查还剩下一个「删」,下篇教程,就来给大家演示文章删除功能实现,为什么单独介绍呢,因为我想结合删除功能演示基于 vue 组件的模态框、对话框以及过渡效果的实现。
以上就是教你基于laravel+vue组件实现文章发布、编辑和浏览功能的详细内容。