接上篇。上篇中給出了代碼框架,沒有具體實現,這一篇會對上篇定義的幾個組件進行分別介紹和完善:
1、TodoContainer組件
TodoContainer組件,用來組織其它組件,這是react中推薦的方式,也是redux中高階組件一般就是用來包裝成容器組件用的,比如redux中的connect函數,返回的包裝組件就是一個容器組件,它用來處理這樣一種場景:加入有A、B兩個組件,A組件中需要通過Ajax請求和后端進行交互;B組件也需要通過Ajax請求和后端交互,針對這種場景有如下代碼:
//組件A定義 var CompA={ template:'<div>A 渲染 list</div>', data:function(){ return { list:[] } }, methods:{ getListA:function(){ $.ajax({ url:'xxx', dataType:'json', success:r=>this.list=r.data }) } } }; //組件B定義 var CompB={ template:'<div>B 渲染list</div>', data:function(){ return { list:[] } }, methods:{ getListB:function(){ $.ajax({ url:'xxx', dataType:'json', success:r=>this.list=r.data }) } } }
可以看出,A組件和B組件中都會存在Ajax訪問重復代碼,有重復代碼就要提取出來;第一種方式,提取公共方法,使用mixin混入到兩個組件中,所謂混入就是動態把方法注入到兩個對象中;
第二種方法使用外部傳入,這是react中推薦的方式,使用props傳入;其實我們仔細分析我們的兩個組件,都是為了渲染列表數據,至於是在組件外請求還是在組件內請求,它是不關注的,這樣我們可以進一步考慮,把AB組件重構成只用來渲染數據的pure組件,數據由外部傳入,而vue正好提供了這種props父傳子的機制,把Ajax操作定義到父組件中(就是我們這里提到的容器組件),也起到了重復代碼提取的作用,基於此請看我們的第二版代碼:
//A組件 var CompA={ template:'<div>A</div>', props:['list'] }; //B組件 var CompB={ template:'<div>B</div>', props:['list'] } //容器組件 var Container={ template:` <comp-a :list="listA" ></comp-a> <comp-b :list="listB" ></comp-b> `, components:{ 'comp-a':CompA, 'comp-b':CompB }, methods:{ getListA:function(){ //TODO:A 邏輯 }, getListB:function(){ //TODO:B 邏輯 } } }
這樣A、B組件變成了傻白甜組件,只是負責數據渲染,所有業務邏輯由容器組件處理;容器組件把A、B組件需要的數據通過props的方式傳遞給它們。
已經明白了容器組件的作用,那么我們來實現一下前幾篇中todolist的容器組件吧,上篇已有基本結果,這里先出代碼后解釋:
/** * 容器組件 * 說明:容器組件包括三個字組件 */ var TodoContainer = { template: ` <div class="container"> <search-bar @onsearch="search($event)"></search-bar> <div class="row"> <todo-list :items="items" @onremove="remove($event)" @onedit="edit($event)"></todo-list> <todo-form :init-item="initItem" @onsave="save($event)" ></todo-form> </div> </div> `, data: function () { return { /** * Todolist數據列表 * 說明:通過props傳入到Todolist組件中,讓組件進行渲染 */ items: [], /** * TodoForm初始化數據 * 說明:由於TodoForm包括兩種操作:新增和編輯;新增操作無需處理,編輯操作需要進行數據綁定,這里通過傳入initItem屬性進行編輯時數據的初始化 * 如果傳入的值為空,說明為新增操作,由initItem參數的Id是否為空,來確認是更新保存還是新增保存 */ initItem: { title: '', desc: '', id: '' } } }, components: { 'search-bar': SearchBar,/**SearchBar組件注冊 */ 'todo-form': TodoForm,/**TodoForm組件注冊 */ 'todo-list': todoList/**TodoList組件注冊 */ }, methods: { /** * 模擬保存數據方法 * 輔助方法 */ _mock_save: function (lst) { list = lst; }, /** * 根據id查詢對象 * 輔助方法 */ findById: function (id) { return this.items.filter(v => v.id === id)[0] || {}; }, /** * 查詢方法 * 由SearchBar組件觸發 */ search: function ($e) { this.items = list.filter(v => v.title.indexOf($e) !== -1); }, /** * 保存方法 * 響應新增和更新操作,由TodoForm組件觸發 */ save: function ($e) { //id存在則為編輯保存 if (this.initItem.id) { var o = this.findById($e.id); o.title = $e.title; o.desc = $e.desc; } else { this.items.push(new Todo($e.title, $e.desc)); } this.initItem = { id: '', title: '', desc: '' }; this._mock_save(this.items); }, /** * 刪除方法 * 響應刪除按鈕操作 * 由TodoItem組件觸發 */ remove: function ($e) { this.items = this.items.filter(v => v.id !== $e); this._mock_save(this.items); }, /** * 編輯按鈕點擊時,進行表單數據綁定 */ edit: function ($e) { this.initItem = this.findById($e); } } }
我們把所有業務邏輯也放在容器組件中處理,其它組件都是傻白甜,這樣的好處是,其它組件容易重用,因為只是數據渲染,並不涉及業務操作,這種組件沒有業務相關性,比如一個list組件我可以用它顯示用戶信息,當然也可以用它顯示角色信息,根據傳入的數據而變化。
對上述代碼,需要簡單解釋一下的是,Vue中父子event傳遞是通過$emit和$on來實現的,但是寫法和angular中有一些差異;在angular中我們一般這樣寫:
//事件發射 $scope.$emit("onxxx",data); //事件監聽 $scope.$on("onxxx",function(e,data){ //TODO: })
但是在vue中$on是直接使用v-on:onxxx或@onxxx來寫的,所以一般存在的是這樣的代碼:
<todo-list :items="items" @onremove="remove($event)" @onedit="edit($event)"></todo-list> <todo-form :init-item="initItem" @onsave="save($event)" ></todo-form>
其它代碼中加入了很多注釋,也比較簡單,就不做過多解釋了,有疑問可提交。
2、SearchBar組件
SearchBar組件比較簡單,只是簡單觸發查詢按鈕,發射(觸發)onsearch事件,然后TodoContainer組件中使用 @onsearch="search($event)" 進行監聽。
/** * 搜索組件 */ var SearchBar = { template: ` <div class="row toolbar"> <div class="col-md-8"> keyword: <input type="text" v-model="keyword" /> <input type="button" @click="search()" value="search" class="btn btn-primary" /> </div> </div> `, data: function () { return { keyword: '' } }, methods: { search: function () { this.$emit('onsearch', this.keyword); } } }
3、TodoForm組件
TodoForm組件,是我們Todolist中的表單組件,編輯和新增公用,我們需要考慮的是,我們的初始化數據由外部傳入,首先看第一版代碼,考慮有什么坑?
/** * 表單組件 */ var TodoForm = { template: ` <div class="col-md-6"> <div class="form-inline"> <label for="title" class="control-label col-md-4">title:</label> <input type="hidden" v-bind:value="todo.id" /> <input type="text" v-model="todo.title" class="form-control col-md-8"> </div> <div class="form-inline"> <label for="desc" class="control-label col-md-4">desc</label> <input type="text" v-model="todo.desc" class="form-control col-md-8"> </div> <div class="form-inline"> <input type="button" value="OK" v-on:click="ok()" class="btn btn-primary offset-md-10" /> </div> </div> `, props: ['initItem'],
data: function { return {todo: this.initItem} }, methods: { ok: function () { this.$emit('onsave', this.todo); } } }
這段代碼有什么問題呢?我們把傳入的初始化參數給了我們的todo對象,這樣導致的直接問題是:新增的時候沒問題,但是編輯的時候無法綁定數據,原因是,編輯操作實際上就是修改外部傳入的initItem對象,但是todo只在組件初始化的時候被賦值,其它時候是不響應initItem變化的,如何才能響應initItem變化,很明顯是我們的computed屬性,computed屬性會響應其封裝對象的變化;代碼第二版修改如下:
/** * 表單組件 */ var TodoForm = { template: ` <div class="col-md-6"> <div class="form-inline"> <label for="title" class="control-label col-md-4">title:</label> <input type="hidden" v-bind:value="todo.id" /> <input type="text" v-model="todo.title" class="form-control col-md-8"> </div> <div class="form-inline"> <label for="desc" class="control-label col-md-4">desc</label> <input type="text" v-model="todo.desc" class="form-control col-md-8"> </div> <div class="form-inline"> <input type="button" value="OK" v-on:click="ok()" class="btn btn-primary offset-md-10" /> </div> </div> `, props: ['initItem'], computed: { todo: function () {
//這里不直接返回this.initItem 是因為會導致雙向綁定,因為傳遞的是引用 return { id: this.initItem.id, title: this.initItem.title, desc: this.initItem.desc }; } }, methods: { ok: function () { this.$emit('onsave', this.todo); } } }
4、TodoList && TodoItem組件
TodoList組件是數據列表組件,它的每一個列表項我們進行了一次封裝,每一個list中的列表項,就是一個TodoItem組件,所以在TodoItem組件中,只需要引入todoitem數據即可,唯一需要關注的就是todoItem組件中會觸發onremove和onedit事件。
/** * 列表項組件 */ var TodoItem = { template: ` <tr> <th>{{todo.id}}</th> <td>{{todo.title}}</td> <td>{{todo.desc}}</td> <td> <input type="button" value="remove" @click="remove()" class="btn btn-danger" /> <input type="button" value="edit" @click="edit()" class="btn btn-info" /> </td> </tr> `, props: ['todo'], methods: { edit: function () { console.log(this.todo); this.$emit('onedit', this.todo.id); }, remove: function () { this.$emit('onremove', this.todo.id); } } } /** * 列表組件 */ var TodoList = { template: ` <div class="col-md-6"> <table class="table table-bordered"> <tr> <th></th> <th>title</th> <th>desc</th> <th></th> </tr> <todo-item v-for="item in items" :todo="item" :key="item.id" @onedit="edit($event)" @onremove="remove($event)"></todo-item> </table> </div> `, props: ['items'], components: { 'todo-item': TodoItem }, methods: { edit: function ($e) { this.$emit('onedit', $e); }, remove: function ($e) { this.$emit('onremove', $e); } } }
這兩個數據渲染組件就沒什么好說名的了;但是大家發現一個很不爽的問題:由於我們在容器中統一管理了業務邏輯(更逼格高一些,叫狀態),所以在todoitem組件中觸發的事件沒辦法直接到TodoContainer組件中,只能通過一級一級的往上傳遞,所以在todolist中也有和todoitem中類似的觸發事件的代碼this.$emit('onremove', $e);這里組件層級才2級,如果多了狀態管理就是災難了,幸好vuex的出現,就是專門處理這種問題的,后期用到vuex的時候會詳細介紹。
5、小結
todolist這個demo,就暫時告一段落了,下一片會以一個稍微復雜的demo(信息管理)來介紹vue-router,當然在一步一步學習的過程中,我還是沒能做到把所有基本概念過一遍,我個人覺得還是用到再解釋吧,否則還不如直接看文檔來的方便。。。
完整代碼:
index.html

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>demo1</title> <script src="https://cdn.bootcss.com/vue/2.4.1/vue.js"></script> <link href="https://cdn.bootcss.com/bootstrap/4.0.0-alpha.6/css/bootstrap.css" rel="stylesheet"> </head> <body class="container"> <div id="app"> <todo-container></todo-container> </div> <script src="./todolist.js"></script> </body> </html>
todolist.js

; (function () { var list = []; var Todo = (function () { var id = 1; return function (title, desc) { this.title = title; this.desc = desc; this.id = id++; } })(); /** * 搜索組件 */ var SearchBar = { template: ` <div class="row toolbar"> <div class="col-md-8"> keyword: <input type="text" v-model="keyword" /> <input type="button" @click="search()" value="search" class="btn btn-primary" /> </div> </div> `, data: function () { return { keyword: '' } }, methods: { search: function () { this.$emit('onsearch', this.keyword); } } } /** * 表單組件 */ var TodoForm = { template: ` <div class="col-md-6"> <div class="form-inline"> <label for="title" class="control-label col-md-4">title:</label> <input type="hidden" v-bind:value="todo.id" /> <input type="text" v-model="todo.title" class="form-control col-md-8"> </div> <div class="form-inline"> <label for="desc" class="control-label col-md-4">desc</label> <input type="text" v-model="todo.desc" class="form-control col-md-8"> </div> <div class="form-inline"> <input type="button" value="OK" v-on:click="ok()" class="btn btn-primary offset-md-10" /> </div> </div> `, props: ['initItem'], computed: { todo: function () { return { id: this.initItem.id, title: this.initItem.title, desc: this.initItem.desc }; } }, methods: { ok: function () { this.$emit('onsave', this.todo); } } } /** * 列表項組件 */ var TodoItem = { template: ` <tr> <th>{{todo.id}}</th> <td>{{todo.title}}</td> <td>{{todo.desc}}</td> <td> <input type="button" value="remove" @click="remove()" class="btn btn-danger" /> <input type="button" value="edit" @click="edit()" class="btn btn-info" /> </td> </tr> `, props: ['todo'], methods: { edit: function () { console.log(this.todo); this.$emit('onedit', this.todo.id); }, remove: function () { this.$emit('onremove', this.todo.id); } } } /** * 列表組件 */ var TodoList = { template: ` <div class="col-md-6"> <table class="table table-bordered"> <tr> <th></th> <th>title</th> <th>desc</th> <th></th> </tr> <todo-item v-for="item in items" :todo="item" :key="item.id" @onedit="edit($event)" @onremove="remove($event)"></todo-item> </table> </div> `, props: ['items'], components: { 'todo-item': TodoItem }, methods: { edit: function ($e) { this.$emit('onedit', $e); }, remove: function ($e) { this.$emit('onremove', $e); } } } /** * 容器組件 * 說明:容器組件包括三個字組件 */ var TodoContainer = { template: ` <div class="container"> <search-bar @onsearch="search($event)"></search-bar> <div class="row"> <todo-list :items="items" @onremove="remove($event)" @onedit="edit($event)"></todo-list> <todo-form :init-item="initItem" @onsave="save($event)" ></todo-form> </div> </div> `, data: function () { return { /** * Todolist數據列表 * 說明:通過props傳入到Todolist組件中,讓組件進行渲染 */ items: [], /** * TodoForm初始化數據 * 說明:由於TodoForm包括兩種操作:新增和編輯;新增操作無需處理,編輯操作需要進行數據綁定,這里通過傳入initItem屬性進行編輯時數據的初始化 * 如果傳入的值為空,說明為新增操作,由initItem參數的Id是否為空,來確認是更新保存還是新增保存 */ initItem: { title: '', desc: '', id: '' } } }, components: { 'search-bar': SearchBar,/**SearchBar組件注冊 */ 'todo-form': TodoForm,/**TodoForm組件注冊 */ 'todo-list': TodoList/**TodoList組件注冊 */ }, methods: { /** * 模擬保存數據方法 * 輔助方法 */ _mock_save: function (lst) { list = lst; }, /** * 根據id查詢對象 * 輔助方法 */ findById: function (id) { return this.items.filter(v => v.id === id)[0] || {}; }, /** * 查詢方法 * 由SearchBar組件觸發 */ search: function ($e) { this.items = list.filter(v => v.title.indexOf($e) !== -1); }, /** * 保存方法 * 響應新增和更新操作,由TodoForm組件觸發 */ save: function ($e) { //id存在則為編輯保存 if (this.initItem.id) { var o = this.findById($e.id); o.title = $e.title; o.desc = $e.desc; } else { this.items.push(new Todo($e.title, $e.desc)); } this.initItem = { id: '', title: '', desc: '' }; this._mock_save(this.items); }, /** * 刪除方法 * 響應刪除按鈕操作 * 由TodoItem組件觸發 */ remove: function ($e) { this.items = this.items.filter(v => v.id !== $e); this._mock_save(this.items); }, /** * 編輯按鈕點擊時,進行表單數據綁定 */ edit: function ($e) { this.initItem = this.findById($e); } } } var app = new Vue({ el: '#app', components: { 'todo-container': TodoContainer } }); })(); /** * * * <div id="app"> * <todo-container></todo-container> * </app> */