zoukankan      html  css  js  c++  java
  • 聊聊vue组件开发的“边界把握”和“状态驱动”

    vue有着完整的组件化开发机制,但是官网只给了开发的方式,对于开发规范以及组件化开发的最佳实践,还需要我们来摸索。本文就平时开发中的经验来谈谈“把握边界”和“状态驱动”这两个话题。

    边界把握

    边界把握其实很好理解。在模块化编程中,我们通常要定义好一个模块的功能边界,做什么,不做什么,从外部接收什么,向外部提供什么。在vue的组件化系统之下,这些问题又更具体一些,需要我们细细把握。

    划分业务逻辑

    这个原则适用于任何模块化开发,一个组件要负责哪些业务,在开始写之初就应该非常明确,否则边界就容易模糊了。举个例子,页面上有个弹出层,里面会显示用户名。那么在弹出层组件中,需要有username这样一个数据吗?

    很显然是不需要的。弹出层的任务就是:弹出、关闭、显示内容。至于是什么内容,组件并不需要关心。所以我们顶多会定义一个通用的content字段,或者干脆用slot。

    组件简单了尚且容易把握,当业务较复杂的时候就需要好好斟酌了,这是个基本思维。

    父子通信的注意点

    这个话题想必大家不陌生,你甚至可以朗朗上口的背出来:父通过props传递数据给子,子通过emit发送消息给父。这有什么好说的呢?

    props容易忽略的问题在于,当父组件传递一个对象给子组件时,这个传递就不再是“单向”的。因为子组件拿到的是一个引用,当子组件修改了该对象上的属性值,父组件的数据也会相应变化。数据流就变成了双向的,子组件是不应该直接修改父组件的数据的。所以我们要在props中只传递简单值。对象、数组这样的引用类型要避免传递。

    为了保证props传递的数据类型,推荐在定义props的时候写明类型和默认值:

    复制代码
    props: {
        name: {
            type: string,
            default: ''
        }
    }
    复制代码

    关于子组件emit消息,我之前也谈到过一个原则,子组件需要对外通知的是“我发生了什么”,而不是“你去干什么”。这只是语义上的一个差别,往小里说只是一个命名的事。但从逻辑上来讲,缺是一个边界把握不清楚的行为。

    这也是很容易想通的,如果让子组件决定父组件的行为,那么他们在逻辑上便耦合了。举个例子:点击弹出层上的确定按钮,父组件去请求商品列表。那么子组件发出的消息应该叫"confirm"或"ok",而不是叫"request-product"。

    避免全局操作

    我们在平时的编程中,通常会用一些BOM的方法如history,或者是使用document上的方法,这类访问全局对象的行为,我也视之为“越界”行为。毕竟已经跨出了组件之外了。

    一旦一个组件有操作全局对象的行为,那它就可以被认为有潜在威胁。所以通常应该注意以下方面:

    1. 用this.$el.querySelector代替document.querySelector,不要去查询组件外的DOM

    2. 用到的BOM接口,统一封装成模块,在组件中引入使用

    3. 本地存储也进行一次包装,例如,把localStorage相关操作统一封到一个storage.js模块中

    4. 子组件尽量避免监听window的事件,可让最外层组件监听,然后传递数据

    vuex的状态管理

    如果你使用了vuex,那么store中的数据管理也是需要留意的。vue完美集成了vuex这样一个全局状态管理工具,可以在任何组件中通过this.store访问/提交状态。

    既然是全局状态,我们担心的又来了,组件内操作全局的东西,岂不是一次越界行为?而且各种commit散落在各个组件中,将来找起来岂不是很麻烦?

    我的做法是这样的,单独定义一个模块,姑且叫做storeMonitor吧,所有修改全局状态的方法全部定义在这里面,组件借助这个storeMonitor去修改store中的数据,相当于是一个门面模式。这样的好处是,组件间接地去修改全局状态,相当于建立了一个隔离层。另一方面,所有的commit操作都集中在这个文件中,一目了然。

    状态驱动

    何为状态驱动

    状态驱动也可以说是数据驱动,只不过数据是具体存在的(比如一个js对象),“状态”是抽象出来的一种描述。状态驱动就是指代码逻辑集中在数据操作, 而不是DOM操作以及样式操作。

    举个例子,一个表单提交按钮,不可点击的时候要灰色背景,可点击的时候要蓝色背景。那么我们通过一个js变量disabled来控制,大致代码如下:

    <button :class="disabled ? 'bg-gray' : 'bg-blue'">提交</button>

    这不就是mvvm双向绑定的终极奥义嘛,说了半天废话。

    其实上面的代码是有问题的。如果你隐隐觉得bg-gray、bg-blue这俩名字有点别扭,甚至那个disabled也看着不顺眼,那么你有可能要理解我想说什么了。

    问题在哪里呢?想想这段代码表达了什么语义。“按钮不可用的时候给灰色背景,可用的时候给蓝色背景”,这,明明还是DOM世界的说法嘛。只是包上了双向绑定的皮而已,根本不是状态驱动。

    而状态驱动的精髓,是要保留业务逻辑,消灭和DOM、样式有关的一切思维。而我们真正的业务逻辑可能是什么呢?“校验通过的时候让按钮可用,不通过的时候失效”。所以,正确的代码应该这么写:

    <button :class="validate ? 'enable' : 'disabled'">提交</button>

    什么?别骗我!你只是改了命名而已。

    我没骗你,“命名即思维“,这是我一贯坚持的准则,胡乱给变量命名的人必然有一颗乱成麻团的脑袋。等你明白了舍生取义的道理,自然会回来和我一起念:「命名即思维」。

    把页面上的所有功能都完整的抽象成状态,那就是状态驱动了,而这状态,不是样式的状态。那么,如何拥有正确的状态驱动思维呢?答案就是:面向对象。

    面向对象的思维

    不看表象,看抽象。前端所要有的面向对象思维差不多就是这样。

    表象是啥呢?是输入框,是弹出层,是列表,是表格,是花里胡哨的各种颜色。

    抽象是啥呢?是用户名,是密码,是登陆状态,是各种业务数据。我们把页面的内容抽象成对象的属性,把交互抽象成对象的方法。

    还是举个例子吧,看下面这个丑陋的原型图:

    那我们抽象出来的对象应该大致这样:

    复制代码
    {
        businessOptions: [],
        currentIndex: 0,
        selectedList: [],
        select: function(index){ //选中操作  }
        remove: function(index){ //删除操作   }
    }
    复制代码

    我们的代码逻辑应该是切换currentIndex,以及调用select方法来添加选项到selectedList数组。如果你想用active来表示当前激活的tab,或者是用left/right表示左边/右边两栏,那就大大的犯了表象主义错误。

    在写小游戏的时候可能用到的面向对象思维较多,组件化开发中,也应当用这个思维去做整体设计。一个组件就是很具象的实体,所以要将之“物件化”。

    css也要“状态”

    css作为样式的描述语言,其命名方式以及组织方式有多种规则。在状态驱动的开发思维下,我倾向让css也具有“描述状态”的能力。看下面的一段sass代码:

    复制代码
    .sidebar{
        position: absolute;
        bottom: 0;
         80%;
        &.show{
            display: block;
        }
        &.hidden{
            display: none;
        }
        .btn{
            display: inline-block;
             200px;
            height: 20px;
        }
        &.open{
            left: 0;
            .btn{
                background-image: url(left.png);
            }
        }
        &.close{
            left: -80%;
            .btn{
                background-image: url(right.png);
            }
        }
    }
    复制代码

    光看css,不看js代码的情况下,我们已经可以得知界面的展示逻辑了:有一个名为sidebar的侧边栏,它有四种状态,分别是:show、hidden、open、close。sidebar下有一个按钮btn,它在sidebar打开的时候是向左的背景图,在sidebar关闭的时候是向右的背景图。

    这样一套结构清晰,语义明确的css规则,能够帮助我们很快理清页面逻辑,别人在看你的代码的时候一目了然。上面只是一个简单的例子,实践的时候会有复杂的场景,可根据具体功能划分出各自的作用域(嵌套语法),稍微需要花时间去设计,换来的是清晰的代码。

    不需要动态创建组件

    用mvvm框架去写弹框组件的时候,往往会有这样一个困惑:在jquery时代,我们通过 $.msg('内容')这样的方式调用弹框,此时在页面上动态创建一个节点,关闭弹框的时候再把节点移除。习惯于此,我们很希望能用同样的方式来处理弹框。

    当然这在vue中也是可以做到的,方式就是动态创建标签,并且动态new一个组件实例去渲染它,在监听到close消息时,把这个节点手动删掉。大体代码如下:

    复制代码
    const MessageConstructor = Vue.extend(alert);
     
    const Message = (config) => {
       instance = new MessageConstructor({
           el: document.createElement('div')
       });
       document.body.appendChild(instance.$el);
     
       Vue.nextTick(()=>{
           instance.show = true;
           instance.content = config.content || '';
           instance.type = config.type || 'danger';
           instance.$on('close', function(){
               this.show = false;
               document.body.removeChild(this.$el);
           });
           instance.$on('confirm', config.onConfirm)
       });
    }
    
    export default Message;
    复制代码

    这样的方式确实可以实现,但是其思想却是和状态驱动违背的,某个应用在某时某刻弹窗,这可以理解为这个应用的状态,我们只需用一个变量来标记该状态即可,犯不着手动创建节点、删除节点这么大动干戈。事实上vue作者也推崇这样来处理弹窗,节点始终挂载在页面,需要弹的时候给显示即可。

    本篇结束,以上是笔者在实际开发者总结出的最佳实践,当然这只是一个开发模式,并无对错。大家可以参考,或引发其他思考。

  • 相关阅读:
    js中不同的height, top的对比
    正则表达式入门以及记录
    CSS小记(持续更新......)
    Git远程操作
    Git Push 避免用户名和密码方法
    Git本地操作
    CSS3动画属性
    c语言文法
    实验一、词法分析器实验
    词法分析器
  • 原文地址:https://www.cnblogs.com/libin-1/p/7030519.html
Copyright © 2011-2022 走看看