zoukankan      html  css  js  c++  java
  • 一步一步学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
  • 相关阅读:
    docker 容器启动初始化,centos镜像启动并执行
    odoo 分布式session 失效解决方案
    文件分布式存储 minio docker
    odoo reports 报表打印pdf初探
    odoo 分布式快速更新
    linux Warning: Stopping docker.service, but it can still be activated by:
    linux 查看80端口的连接数
    css flex 涨姿势了
    odoo 后台打印日志修改
    iOS 导航栏消失
  • 原文地址:https://www.cnblogs.com/Johnzhang/p/7223064.html
Copyright © 2011-2022 走看看