教程:Laravel + Vuejs + Tailwind CSS 构建一个 Todo App 第三部分

Laravel

让我们继续完善 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 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。

原文地址:https://nick-basile.com/blog/post/buildi...

译文地址:https://learnku.com/laravel/t/37062

本文为协同翻译文章,如您发现瑕疵请点击「改进」按钮提交优化建议
《L01 基础入门》
我们将带你从零开发一个项目并部署到线上,本课程教授 Web 开发中专业、实用的技能,如 Git 工作流、Laravel Mix 前端工作流等。
《G01 Go 实战入门》
从零开始带你一步步开发一个 Go 博客项目,让你在最短的时间内学会使用 Go 进行编码。项目结构很大程度上参考了 Laravel。
讨论数量: 0
(= ̄ω ̄=)··· 暂无内容!

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!