一步一步學Vue(四)


接上篇。上篇中給出了代碼框架,沒有具體實現,這一篇會對上篇定義的幾個組件進行分別介紹和完善:

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>
View Code

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>
 */
View Code

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM