组件化todo应用

Vue 的组件是一个可复用的 Vue 实例,组件可以有自己的数据(data),组件可以互相嵌套而形成一个树状组织结构。因为组件也是一个 Vue 的实例,因此它的创建和我们之前使用 new Vue 创建 Vue 实例非常类似。

Vue 引入组件的目的,就是为了复用。例如一个按钮,把和这个按钮相关的数据和操作逻辑封装到一个组件里,那么在需要的地方,引入这个组件就可以生成一个按钮,提高了代码的复用性。

Vue 组件,主要需要了解的是父组件如何向子组件传递数据,以及子组件如何向父组件发送消息。现在不理解没有关系,下面的教程会使用 todo 应用来向你演示这两个机制,以及我们如何在 Vue 中使用它。

为了简单起见,我们只演示将 todo 项组件化,这个例子已经足以说明 Vue 组件的使用方法了,整个应用的组件化将作为练习,由你自己利用教程中所学的的知识完成,具体来说,我们把这个看成一个组件:

<li v-for='todo in filteredTodos' :key='todo.id'>
  <span :class="{completed: todo.completed}"
        @dblclick="editTodo(todo)">{{ todo.title }}</span>
  <input type="button"
         value="标为完成"
         @click="markAsCompleted(todo)"/>
  <input v-if="todo.removed" type="button" value="还原" @click="restoreTodo(todo)"/>
  <input v-else="todo.removed" type="button" value="删除" @click="removeTodo(todo)"/>
  <input type="text"
         value="编辑 todo..."
         v-focus="true"
         v-if="editedTodo!==null && editedTodo.id===todo.id"
         v-model="todo.title"
         @keyup.enter="editDone(todo)"
         @keyup.esc="cancelEdit(todo)"/>
</li>

Vue 需要一个模板来渲染组件,上面就是要渲染的模板,我们把模板写在一个类型为 text/x-template 的 script 标签里:

<script type="text/x-template" id="todo-item">
  <li>
    <span :class="{completed: todo.completed}"
          @dblclick="editTodo(todo)">{{ todo.title }}</span>
    <input type="button"
           value="标为完成"
           @click="markAsCompleted(todo)"/>
    <input type="button" value="删除" @click="removeTodo(todo)"/>
    <input type="text"
           value="编辑 todo..."
           v-focus="true"
           v-if="editedTodo!==null && editedTodo.id===todo.id"
           v-model="todo.title"
           @keyup.enter="editDone(todo)"
           @keyup.esc="cancelEdit(todo)"/>
  </li>
</script>

要注意模板和上面一段 html 代码的不同,去掉了 li 标签上的 v-for 和绑定指令,因为我们这只是在定义组件,而不是像上面那样循环渲染组件。还要注意 script 标签设置了 id="todo-item",这将告诉 Vue 在注册组件时,如何定位渲染的模板所在位置。

然后我们在 Vue 中注册我们自定义的组件,使用 Vue.component 方法,第一个参数是你给这个组件起的组件名,第二个参数是一个对象,和我们 new Vue 示例时的选项非常类似:

Vue.component('todo-item', {
    template: '#todo-item',
    data: function () {
        return {
            editedTodo: null // 用户暂存编辑前的 todo 状态
        }
    },
    props: {
        todo: {
            type: Object,
            required: true,
        },
    },
    methods: {
        ...
    },
    directives: {
        focus: {
            inserted: function (el) {
                el.focus()
            }
        }
    }
});

data 中只有一个 editedTodo,在编辑功能中会用到,其用法和之前是一样的,只是从 Vue 实例移到了组件中。props 是一个新的对象,用来接收组件需要使用到的数据。试想,我们的 todo-item 组件需要一个 todo 模型的数据,才能渲染成一个待办事项,但这个数据从哪里来呢?答案就是通过这个 props 传递。当某个值传给 props 的时候,他就变成了组件实例的一个属性。比如这里 props 它接收一个 todo,那么当一个 todo 传给组件时,组件就多了一个 todo 属性,我们可以在组件中通过 this.todo 引用。同时,我们还设置了 todo 数据的要求,即它的类型是一个 Object,而且必须传递,不能缺省。

那么这个组件有哪些方法呢?以下方法是和这个组件相关的,我们从 app 这个 Vue 实例里复制过来:

methods: {
    markAsCompleted: function (todo) {
        todo.completed = true
    },
    removeTodo: function (todo) {
        this.todos.splice(this.todos.indexOf(todo), 1)
    },
    editTodo: function (todo) {
        this.editedTodo = {id: todo.id, title: todo.title}
    },
    editDone: function (todo) {
        this.editedTodo = null
    },
    cancelEdit: function (todo) {
        todo.title = this.editedTodo.title;
        this.editedTodo = null
    },
},

和单个待办事项操作相关的方法我们都复制了过来,仔细观察可以发现,除了 removeTodo 这个方法外,其它方法均只涉及从 props 传递过来的 todo,而 removeTodo 这个操作则涉及到从 app 这个 Vue 实例的 data 中 todos 数组中删除数据。这里就出现了问题,组件是封闭,它无法直接访问父组件的数据,而且组件的数据流向是单向的,只能从父组件通过 props 传到子组件,那么如何使组件中的操作反应到父组件上呢?

我们可以通过事件向父级组件发送消息,我们可以使用如下的方法发送一个事件消息:

removeTodo: function (todo) {
    this.$emit('remove-todo', todo)
},

第一个是你给这个事件取得名字,同时时间还可以携带数据,这里我们把组件实例绑定的 todo 也发送了出去。

定义了这个组件,我们就可以在应用中使用了,将原来渲染待办事项的 html 代码替换为自定义的组件:

<!-- todo list -->
<ul>
  <todo-item v-for="todo in filteredTodos"
             :todo="todo"
             :key="todo.id"
             @remove-todo="removeTodo"/>
</ul>
<!-- end todo list -->

和之前的 li 元素是类似的,只是这里换成了我们自定义的组件,Vue 最终还是把 todo-item 渲染成 li 元素,因为这个组件使用的模板就是那个 li 元素。

和 @click 一样,我们这里绑定监听了一个 remove-todo 事件,这个事件组件中点击删除按钮触发的,一旦监听到这个事件被触发,就会调用 app 这个 Vue 实例中的 removeTodo 方法(注意不是组件 todo-item 中的 removeTodo 方法),因为只有在 app 这个 Vue 实例中才能访问到 todos 数组:

var app = new Vue({
    el: '#todo-app',
    data: function () {
        return {
            todos: todoStorage.fetch(),
            newTodoTitle: '',
            intention: 'all', // 默认为 all
        }
    },
    ...
    methods: {
        ...
        removeTodo: function (todo) {
            this.todos.splice(this.todos.indexOf(todo), 1)
        },
    },
})

要注意 app 这个实例中 data 对象有一个 editedTodo 属性,这个属性移到了组件中,所以被删除了,同时所有移到组件中的方法也删除了。

同样的,在组件中也可以定义计算属性,举个例子,为了决定编辑框是否显示,我们使用了如下的判断表达式:

v-if="editedTodo!==null && editedTodo.id===todo.id"

我们可以把这个表达式定义成组件中的一个计算属性:

computed: {
    editing: function () {
        return this.editedTodo !== null && this.editedTodo.id === this.todo.id
    }
},

让后在组件模板中引用它:

<input type="text"
       value="编辑 todo..."
       v-focus="true"
       v-if="editing"
       v-model="todo.title"
       @keyup.enter="editDone(todo)"
       @keyup.esc="cancelEdit(todo)"/>

至此,我们应用的组件化示例就完成了。

尽管这是一个刻意设计的例子,组件的划分也不一定合理,但通过这个例子很好地演示了父组件到子组件的数据传递方式以及子组件通过事件向父组件传递消息的方式。把这两点牢记在心,然后把整个应用都组件化吧!这个任务就交给你了,把之前学到的东西都用起来,另外 Vue 的官方文档更是你遇到问题时最为权威的参考资料,不要忘了它!

教程至此全部结束,请期待后续 Vue HackerNews 项目的教程,以及 webpack 前端工程化的实践教程,感谢阅读!

-- EOF --

最后更新:2019年3月17日 01:25