教程:Laravel + Vuejs + Tailwind CSS 构建一个 Todo App 第三部分
让我们继续完善 Todo 应用程序,通过我们 上个教程 完成的控制器操作。要继续学习本教程,可以在GitHub上下载此项目的副本。
回到控制器
回到项目,找到 app/Http/Controllers/TodoController.php
控制器。如果你还记得 上一篇 文章,我们使用 Laravel 创建了控制器,并且在控制器中创建了 index()
和 store()
方法。
接下来,我们依然需要编写 create()
, show()
, edit()
, update()
, 以及 destroy()
方法。 但是,由于我们现在使用 Vue.js 来请求 API 接口,可以忽略掉 create()
和 edit()
方法。因为现在已经用不上了他们了(不需要传统的blade 渲染页面路由)。所以我们把他们从控制器中移除。
Show 方法
我们可以开始对 show()
方法进行编码了。Laravel 为我们提供了以下内容:
public function show(Todo $todo)
{
}
可以看到默认的方法正在鼓励我们使用 路由模型绑定。那就不必要使用查询语句去查询对应的 todo 了。Laravel 意思到我们正在查询一个 todo,会自动将这个 todo 实例注入到该路由。
因此,我们可以这样写我们的代码:
public function show(Todo $todo)
{
return response($todo, 200);
}
这里我们只需要返回 Laravel 为我们注入的 $todo
。它不香吗?现在,把焦点放到 update
方法。
Update() 方法
Laravel 脚手架再次为我们提供了实用的方法。
public function update(Request $request, Todo $todo)
{
}
这一次,我们同时接收了一个 $request
和 一个 $todo
实例。因此,就像在 store()
方法中一样,我们验证接收的数据,并且更新这个路由绑定的模型。
public function update(Request $request, Todo $todo)
{
$data = $request->validate([
'text' => 'required',
'finished' => 'required|boolean',
]);
$todo->update($data);
return response($todo, 200);
}
就像上面这样,我们完成了 update()
方法!
Delete() 方法
最后,继续完成我们的 delete()
方法。
public function destroy(Todo $todo)
{
}
我们依然能从路由模型绑定中获取到 $todo
的实例。我们要做的仅仅是删除它然后响应成功的信息。
public function destroy(Todo $todo)
{
$todo->delete();
return response('Deleted Succesfully', 200);
}
这样,我们的 Laravel 后端就完成了。完成了 show()
, update()
, 以及 delete()
方法之后,我们可以开始编写 Vue 的代码了。
如果你想知道路由是如何建立的,可以先看 上一篇文章 我们在 web.php
中使用 Route::apiResource('todos', 'TodoController')
,再一次膜拜 Laravel 的强大吧。
编写 Vue.js
到目前为止,我们只能使用一个 Vue 组件。在 resources/assets/js/components
内部, 我们可以看到 todo.vue
组件, 如下所示:
<template>
<div class="bg-white rounded shadow p-6 m-4 w-full lg:w-3/4 lg:max-w-lg">
<div class="mb-4">
<h1 class="text-grey-darkest">Todo List</h1>
<div class="flex mt-4">
<input class="shadow appearance-none border rounded w-full py-2 px-3 mr-4 text-grey-darker" v-model="newTodo" @keyup.enter="add" placeholder="Add Todo">
<button class="flex-no-shrink p-2 border-2 rounded text-teal border-teal hover:text-white hover:bg-teal" @click="add" :disabled="newTodo.length === 0">Add</button>
</div>
</div>
<div class="max-h-screen-1/2 overflow-y-scroll">
<div class="flex mb-4 items-center" v-for="(todo, index) in todos" :key="todo.id">
<input type="checkbox" class="mr-2" @click="updateStatus(todo)">
<p class="w-full" :class="todo.finished ? 'line-through text-green' : 'text-grey-darkest'">{{todo.text}}</p>
<button class="flex-no-shrink p-2 ml-2 border-2 rounded text-red border-red hover:text-white hover:bg-red" @click="remove(index)">Remove</button>
</div>
<div v-show="todos.length === 0">
<p class="w-full text-center text-grey-dark">There are no todos</p>
</div>
</div>
</div>
</template>
<script>
export default{
data(){
return{
todos: [],
newTodo: '',
}
},
created() {
this.getTodos();
},
methods: {
getTodos() {
const t = this;
axios.get('/todos')
.then(({data}) => {
t.todos = data;
});
},
createTodo(text) {
const t = this;
axios.post('/todos', {text: text, finished: false})
.then(({data}) => {
t.todos.unshift(data);
});
},
add() {
const t = this;
if(t.newTodo.length > 0) {
t.createTodo(t.newTodo);
t.newTodo = '';
t.baseId++;
}
},
updateStatus(todo) {
todo.finished = !todo.finished;
},
remove(index) {
const t = this;
t.todos.splice(index, 1);
}
}
}
</script>
我们可以选择是使用一个组件还是分成两个组件。我认为应该分成两个组件。一个用于 todo 列表。另一个用于 todo 本身。这样我们就可以隔离特定于列表或项目的行为。
我们重命名 todo.vue
为 todo-list.vue
。 我们需要在 app.js
and welcome.blade.php
文件中同步更新。现在,在同一个目录中创建新的组件 todo-item.vue
。
在编写代码之前,我们需要知道这个组件用来干嘛。他们需要像现在这样完成并删除自己。另外,他们也需要更新自己。
我们让 todo 看起来像现在这样,然后我们可以添加一个用户编辑的输入框 toggle。 最终,我们的 todo 应用看起来就像这样:
为了做到这一点,我们需要对 todo 进行一些改造。需要一些状态管理来进行编辑的切换。需要处理 updates 和 deletes 的方法,以及 Tailwind CSS 的更新,总之,让他们看起来不错。
Todo 模板
把所有内容都放在一起, 我们的 todo-item.vue
模板看起来就像这样:
<template>
<div class="mb-4">
<div class="flex items-center w-full" v-show="state.edit === false">
<input type="checkbox" class="mr-2" v-model="data.finished" @click="updateTodo">
<p class="w-auto" :class="data.finished ? 'line-through text-green' : 'text-grey-darkest cursor-pointer hover:text-black hover:font-bold'" @click="startEdit">{{todo.text}}</p>
<button class="flex-no-shrink p-2 ml-auto border-2 rounded text-red border-red hover:text-white hover:bg-red" @click="remove(index)">Remove</button>
</div>
<div class="flex items-center w-full" v-show="state.edit === true">
<input class="appearance-none border rounded w-full py-2 px-3 mr-2 text-black" v-model="data.text" @keyup.enter="updateTodo" placeholder="Update Todo">
<button class="flex-no-shrink p-2 ml-2 border-2 rounded text-teal border-teal hover:text-white hover:bg-teal" @click="updateTodo" :disabled="data.text.length === 0">Update</button>
<button class="flex-no-shrink p-2 ml-2 border-2 rounded text-red border-red hover:text-white hover:bg-red" @click="cancelEdit">Cancel</button>
</div>
</div>
</template>
首先,我们需要一个包装器 div
。每一个 Vue 组件都需要它。另外还有两个 div
。一个用来读取状态,另一个用来编辑。我们根据组件 data
中设置的state.edit
状态来切换数据。现在,让我们看下这些状态。
我们可以在第一个 div
内读取状态。除了一些简单的调整,其他和之前一样。
前几个带有复选框。我们可以看到现在有一个 v-model
,我们可以在用户与其交互中跟踪它。我们还将一个方法绑定到它的 click 事件用来更新 todo。
继续 <p>
标签。可以看到我们已经更新了 Tailwind classes。我们把它从 .w-full
更改成 .w-auto
。所以现在我们单击目标仅仅是内容大小的改变而不是整体空间大小。
我们还在动态类中添加一些交互式悬停类。当用户悬停在文本时,可以提供良好的反馈。最后,我们将 startEdit()
方法绑定到这个悬停类中,以便我们可以切换编辑的状态。
让我们进入编辑状态的模板。
本节中包含了三个元素,一个 <input>
和两个 <button>
。用户可以在 <input>
更改 todo。
我们向其中添加了一些 Tailwind 样式,以移除浏览器默认的样式,为其提供圆角边框,并将它定位在 flex 的上下文中。
我们再次拥有了 v-model
,用来跟踪用户的输入。最后,我们把 updateTodo
方法绑定到 enter 事件中,这样做的好处是用户无需再点击按钮就可以保存他们的更新。
我们的 <button>
几乎相同。他们有相同的 Tailwind 样式,除了第一个是蓝绿色,第二个是红色。我们可以把某些组件的样式提取出来。
这些 <button>
确实不同。第一个在点击时触发 updateTodo
,第二个通过cancelEdit
修改状态。另外,当 data.text
文件是空的时候,第一个<button>
是禁止的,以防止用户提交空的 todo。
我们已经完成了模板。现在,看看在 <script>
所做的事情。
Todo 脚本
<script>
export default{
props: ['todo', 'index'],
data(){
return{
state: {
edit: false,
},
data: {
text: '',
finished: false,
}
}
},
mounted() {
const t = this;
t.data.text = t.todo.text;
t.data.finished = t.todo.finished;
},
methods: {
updateTodo() {
const t = this;
t.$nextTick(() => {
bus.$emit('update-todo', {data: t.data, index: t.index, id: t.todo.id});
})
t.state.edit = false;
},
remove() {
const t = this;
bus.$emit('remove-todo', {index: t.index, id: t.todo.id});
},
startEdit() {
const t = this;
if(t.data.finished === false) {
t.state.edit = true;
}
},
cancelEdit() {
const t = this;
t.state.edit = false;
t.data.text = t.todo.text;
}
}
}
</script>
从顶部开始,我们传递 todo
和 index
到我们的组件。这样我们就可以访问 todo-list
组件正在管理的数据。
在 data()
中,有两个对象: state
和 data
。我们可以像下面这样设置它。
data(){
return{
edit: false,
text: '',
finished: false,
}
},
state.edit
负责管理何时显示模板中的 edit 和 read 部分。尽管 data
绑定在 <input>
之外,但是我们依然可以跟踪用户的操作。
我们可以看到在 mounted()
的生命周期回调中,使用 todo props
细节初始化data()
数据。
我们有了 methods()
-也就是这个组件的基础。我们的第一个方法是 updateTodo()
。在这里,我们等待 Vue 的下一个 DOM 更新周期将事件发送回 todo-list
事件,其中包含我们重置之前的所有信息以及编辑的状态。
最重要的是我们已经将 $nextTick()
方法绑定到了复选框。如果没有这样,我们在更新值的同时触发事件,这样更新就不会发生。
此外,这个方法使用了事件总线
来发出此信息。因此,我们需要在 app.js
文件中对其初始化。在 window.Vue = require('vue')
的正下方,我们增加了 window.bus = new Vue()
。
remove()
方法本质上和 updateTodo
一样,将事件发送回 todo-list
中,其中包含了删除 todo 的详细信息。
同时,startEdit()
和 cancelEdit
帮助我们切换编辑的状态。startEdit()
确保了在切换状态之前不会编辑已完成的 todo。而 cancelEdit()
会将 data.text
恢复为原始值。
包装完毕。现在,让我们看看 todo-list
模板。
Todo 列表模板
对于模板,我们没有做太多的修改:
<template>
<div class="bg-white rounded shadow p-6 m-4 w-full lg:w-3/4 lg:max-w-lg">
<div class="mb-6">
<h1 class="text-grey-darkest">Todo List</h1>
<div class="flex mt-4">
<input class="shadow appearance-none border rounded w-full py-2 px-3 mr-4 text-grey-darker" v-model="newTodo" @keyup.enter="addTodo" placeholder="Add Todo">
<button class="flex-no-shrink p-2 border-2 rounded text-teal border-teal hover:text-white hover:bg-teal" @click="addTodo" :disabled="newTodo.length === 0">Add</button>
</div>
</div>
<div class="max-h-screen-1/2 overflow-y-scroll">
<todo-item v-for="(todo, index) in todos" :key="todo.id" :todo="todo" :index="index"></todo-item>
<div v-show="todos.length === 0">
<p class="w-full text-center text-grey-dark">There are no todos</p>
</div>
</div>
</div>
</template>
最大的更新在于 v-for
不再输出任何的代码块。相反,他将输出新的 todo-item
模板。另外,我们可以看到我们将 todo
和 index
作为 props
传递。
<todo-item v-for="(todo, index) in todos" :key="todo.id" :todo="todo" :index="index"></todo-item>
除此之外,我们的模板没有改变。继续深入研究脚本。
Todo 列表脚本
作为父组件,我们的 todo-list
负责管理 todo 的所有状态。它的大部分功能就是这样的。
<script>
import todoItem from './todo-item'
export default{
data(){
return{
todos: [],
newTodo: '',
}
},
created() {
this.getTodos();
this.initListeners();
},
methods: {
initListeners() {
const t = this;
bus.$on('update-todo', function (details) {
t.update(details);
})
bus.$on('remove-todo', function (details) {
t.remove(details);
})
},
getTodos() {
const t = this;
axios.get('/todos')
.then(({data}) => {
t.todos = data;
});
},
createTodo(text) {
const t = this;
axios.post('/todos', {text: text, finished: false})
.then(({data}) => {
t.todos.unshift(data);
});
},
updateTodo(details) {
const t = this;
axios.patch('/todos/'+ details.id, details.data)
.then(({data}) => {
t.todos.splice(details.index, 1, data)
})
},
removeTodo(details) {
const t = this;
axios.delete('/todos/'+ details.id)
.then(() => {
t.todos.splice(details.index, 1)
})
},
addTodo() {
const t = this;
if(t.newTodo.length > 0) {
t.createTodo(t.newTodo);
t.newTodo = '';
}
},
},
components: {
todoItem
}
}
</script>
可以看到我们正在导入我们的 todo-item
,因此可以在模板中使用它。我们的 data()
没变,但是 created()
肯定有变。
我们正在调用 initListeners()
来初始化在 todo-item
中定义的监听器。继续看 methods
,可以看到首先定义了 initListeners()
方法。
在这里,我们用事件总线来注册监听器。当他们被触发时,分别将 details
传递到 updateTodo()
和 removeTodo()
方法,用来执行对应的操作。
我们的 getTodos()
, createTodo()
, 以及 addTodo()
方法都没有改变。 在他们的正下方,是 updateTodo()
和 removeTodo()
方法。
在 updateTodo()
我们创建了一个 axios
请求来调用新的控制器方法。注意到我们的路由使用了 todo 的 id。这就是路由模型绑定能够找到我们需要的 todo 的方式。我们还在请求中传递了 details.data
请求数据,因此,我们进行了定义的修改。
一旦 axios
请求成功,我们将返回数据中的 splice()
放入到已编辑的 todo 索引处,以此保存状态。现在,仅使用 Vue 即可维护状态。我发现这种方式可以最大程度的减少后端数据和前端状态的不一致。
最后,removeTodo()
和 updateTodo()
几乎一样。我们正在对新的控制器进行 axios
调用,一旦成功,我们就从 todos
中移除 todo 以保持状态。
现在,我们拥有了一个完善的 todo 应用。
总结
到这里,你已经用Laravel,Vue.js,Tailwind CSS成功构建了一个todo app。回顾整个过程,你应该会感到无比自豪。抛开所有的细节不谈,我们从零开始创建了Larave项目,并在项目基础上引入Vue.js,Tailwind CSS;创建了Vue.js 的todo 组件以并用Tailwind美化样式;建立了MySQL数据库,编写Laravel后台逻辑代码对接Vue.js组件。
即使我们完成了上述目标,但是整个项目依然还有很多值得改进的地方。我们可以创建单元测试,并保证我们的所有代码通过单元测试,包括Vue.js的测试,Larave的测试。还可以添加账号,这样每个用户可以创建自己独立的todo列表,并且将自己的todo任务分享给其他用户。我们还可以花更多的时间改善我们todo app的用户体验,增加应用的可用性。
如果你想探索项目中其他未提及的细节,你可以克隆项目 ,并自己尝试去解决项目中遇到的问题。到这里,我们的教程就结束了。
本文中的所有译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。
推荐文章: