在vue.js之类的mvvm的框架大行其道的当下,开发中最常见的场景就是通过改变数据来展示页面或模块的不同状态,当我们把mvvm玩的不亦乐乎的时候,有时也会停下了想想:在某些项目中不能用vuejs之类的框架时,我们怎么通过改变数据来修改页面或者模块的状态呢。
嗯。说到状态,就想到了状态模式

状态模式:

在很多情况下,一个对象的行为取决于一个或多个动态变化的状态属性,这样的对象叫做有状态的(stateful)对象,对象的状态是从事先定义好的一系列状态值中取出的。当状态对象与外部事件产生互动时,其内部状态就会改变,从而使得系统的行为也随之发生变化。

状态模式主要解决的问题:

当控制对象状态的条件表达式过于复杂时的情况,把状态的判断逻辑转移到表示不同的状态的一系列类或者方法当中,可以把复杂的逻辑判断简单化

下面我们看看状态模式在web前端开发中得一些常见应用场景

tab标签的切换

tab标签切换是web开发中最常见的交互了,交互顺序大致是这样子:

1.一般会有2-3个标签,对应了2-3个tab内容块。其中一个默认tab会是active状态

2.点击其他tab标签,则:去掉active的tab标签样式,隐藏对应的区块,给点击的tab添加active样式,显示这个tab对应的区块。

哎。。想想需要挨个tab找对应的元素然后去remove掉class再add一个class。。。好繁琐。。能不能少写点js,简简单单的实现tab切换呢。。。

应用状态模式解决tab切换示例如下:

html 代码

<div class="tab-box-wrap" data-tab-index="tab01" id="tab_box_wrap">
    <ul>
        <li data-tab="tab01">我是tab01</li>
        <li data-tab="tab02">我是tab02</li>
        <li data-tab="tab03">我是tab03</li>
    </ul>
    <div tab-box="tab01">
        我是tab01 的内容
    </div>
    <div tab-box="tab01">
        我是tab02的内容
    </div>
   <div tab-box="tab01">
       我是tab03的内容
    </div>

</div>

css代码

.tab-box-wrap {
   height: 500px;
   width: 900px;
   margin: 0 auto;
}

.tab-list {
   list-style: none;;
   height: 32px;
   border-bottom: 1px solid #6856f9;
   position: relative;
   padding: 0 50px;
   margin: 0;
}

.tab-list li {
   float: left;
   height: 20px;
   padding: 5px;
   border: 1px solid #6856f9;
   line-height: 20px;
   font-size: 14px;
   margin-right: -1px;
   border-bottom: 1px solid transparent;
   background: #fff;
   cursor: pointer;
}

[data-tab-index="tab01"] [tab-index="tab01"] {
   border-bottom: 2px solid #fff;
}

[data-tab-index="tab02"] [tab-index="tab02"] {
   border-bottom: 2px solid #fff;
}

[data-tab-index="tab03"] [tab-index="tab03"] {
   border-bottom: 2px solid #fff;
}

.tab-box {
   padding: 20px;
   height: 500px;
   border: 1px solid #6856f9;
   border-top: 0 none;
   display: none;
}

[data-tab-index="tab01"] [tab-box="tab01"] {
   display: block;
}

[data-tab-index="tab02"] [tab-box="tab02"] {
   display: block;
}

[data-tab-index="tab03"] [tab-box="tab03"] {
   display: block;
}

js代码如下

var  tab_box_wrap = document.getElementById("tab_box_wrap");
   tab_box_wrap.addEventListener("click",function(event){
       var ele = event.target;
       var tab = ele.getAttribute("tab-index");
       if(tab){
           var t = tab_box_wrap.getAttribute("data-tab-index");
           if(t!==tab){
               tab_box_wrap.setAttribute("data-tab-index",tab);
           }
       }

   },false)

点击查看demo

代码很少,主要的代码其实在css里面, 我们用属性选择器[data-tab-index="xxx"]来控制页面的tab切换,js里只需要获取对应的tab点击然后更改属性的值就好了。

缺点:css有点多,需要枚举每个tab的状态,css属性选择器ie8+才支持,

优点:js代码很少,逻辑控制全在css里,有什么改动 (比如添加tab) 我们只需要改css就好了。一听只改css,是不是很开森^_^

下面我们看一个复杂的例子

global物流查询

主要是物流详情状态的动画,这个动画会根据对应的运单号的状态来展示对应的动画节点。那么我们一共有10个运单状态,如下:

{statusDesc: "揽收", status: "PICKEDUP",}
{statusDesc: "运输中", status: "SHIPPING"},
{statusDesc: "离开发件国", status: "DEPART_FROM_ORIGINAL_COUNTRY"},
{statusDesc: "到达目的国", status: "ARRIVED_AT_DEST_COUNTRY"},
{statusDesc: "妥投", status: "SIGNIN"}
{statusDesc: "待揽收", status: "WAIT4PICKUP"},
{statusDesc: "到达待取", status: "WAIT4SIGNIN"}
{statusDesc: "妥投失败", status: "SIGNIN_EXC"},
{statusDesc: "交航失败", status: "DEPART_FROM_ORIGINAL_COUNTRY_EXC"},
{statusDesc: "清关失败", status: "ARRIVED_AT_DEST_COUNTRY_EXC"}

无状态的时候动画状态如下:
animate

妥投状态动画截图如下:
animate

每一个正常节点中间都会有一个异常节点,【到达目的国】和【妥投】之间会有一个【到达待取】状态,
【揽收】和【离开发件国】之间会有一个【运输中】状态

下面我们来思考我们的代码应该怎么写。。。

  • 无状态灰色div里放一个背景图片,上面的文字是html节点。

  • 灰色模块上面附一层紫色block,文字也是html节点,不同状态设置不同的div的width就好了。

  • 状态文字单独是节点一共10个状态文字节点

我期望改变一个div属性整个动画都会相应的变化

html代码如下

    <div img-animate="init" id="waybill_img_box" class="waybill-img-box" >
        <div class="waybill-img01"></div>
       <div id="waybill_img_color" class="waybill-img02 dot-scale"></div>
        <b class="waybill-no-color-01">揽收</b>
        <b class="waybill-no-color-02">离开发件国</b>
        <b class="waybill-no-color-03">到达目的国</b>
        <b class="waybill-no-color-04">妥投</b>
        <b class="waybill-color-01">揽收</b>
        <b class="waybill-color-02">离开发件国</b>
        <b class="waybill-color-03">到达目的国</b>
        <b class="waybill-color-04">妥投</b>
        <b class="waybill-color-05">到达待取</b>
        <b class="waybill-color-06">运输中</b>
    </div>

我们给#waybill_img_box 这个div添加一个img-animate的属性,用来控制他的子节点的展示。

  • waybill-img01这个div用来放灰色的图片

  • waybill_img_color 这个div放的是彩色图片,我们更改她的width来实现动画,它是绝对定位的

  • 剩下的一堆b标签都是文字内容的定位。当然他们也是绝对定位的

css代码如下:为了简短,我只列举了两种状态

/*初始化的时候所有的紫色模块的字全都应该隐藏*/
 [img-animate="init"] .waybill-color-01,
 [img-animate="init"] .waybill-color-02, 
 [img-animate="init"] .waybill-color-03, 
 [img-animate="init"] .waybill-color-04,
 [img-animate="init"] .waybill-color-05, 
 [img-animate="init"] .waybill-color-06 {
    z-index: -1;
}
[img-animate="init"] .waybill-img02 {
    width: 0;
}
/*紫色元素的字的默认状态*/
.waybill-color-01,
.waybill-color-02, 
.waybill-color-03,
.waybill-color-04,
.waybill-color-05,
.waybill-color-06 {
    text-align: center;
    font-weight: 500;
    position: absolute;
    top: 78px;
    left: 12px;
    z-index: 20;
    color: #fff;
    background: #4b4ebe;
    padding: 4px 5px;
    height: 12px;
    line-height: 12px;
    border-radius: 3px;
    display: none\9;
    -moz-transform: scale(0);
    -webkit-transform: scale(0);
    -o-transform: scale(0);
    -ms-transform: scale(0);
    transform: scale(0);
}

.waybill-color-02 {
    left: 208px;
    top: 28px;
    min-width: 35px;
}

.waybill-color-03 {
    left: 419px;
    top: 30px;
    min-width: 70px;
}

.waybill-color-04 {
    left: 636px;
    top: 75px;
    min-width: 50px;
}

.waybill-color-05 {
    left: 562px;
    top: 50px;
    min-width: 50px;
}

.waybill-color-06 {
    left: 105px;
    top: 42px;
    min-width: 50px;
}

/*
*下面写揽收状态的样式 
*PICKEDUP 
*/
[img-animate="PICKEDUP"] .waybill-no-color-01,
[img-animate="DEPART_FROM_ORIGINAL_COUNTRY"] .waybill-no-color-01 {
    -moz-transition: all 0.3s ease-in 0s;
    -webkit-transition: all 0.3s ease-in 0s;
    -o-transition: all 0.3s ease-in 0s;
    transition: all 0.3s ease-in 0s;
    -moz-transform: translate(8px, 95px) scale(0);
    -webkit-transform: translate(8px, 95px) scale(0);
    -o-transform: translate(8px, 95px) scale(0);
    -ms-transform: translate(8px, 95px) scale(0);
    transform: translate(8px, 95px) scale(0);
    }

[img-animate="PICKEDUP"] .waybill-color-01,
[img-animate="DEPART_FROM_ORIGINAL_COUNTRY"] .waybill-no-color-02{
    -moz-transition: all 0.3s ease-in 0s;
    -webkit-transition: all 0.3s ease-in 0s;
    -o-transition: all 0.3s ease-in 0s;
    transition: all 0.3s ease-in 0s;
    -moz-transform: scale(1);
    -webkit-transform: scale(1);
    -o-transform: scale(1);
    -ms-transform: scale(1);
    transform: scale(1);
    z-index: 20;
    }
[img-animate="PICKEDUP"] .waybill-color-02, 
[img-animate="PICKEDUP"] .waybill-color-03, 
[img-animate="PICKEDUP"] .waybill-color-04, 
[img-animate="PICKEDUP"] .waybill-color-06, 
[img-animate="PICKEDUP"] .waybill-color-05 {
    z-index: -1;
}
/*写一个离开发件国的状态
* DEPART_FROM_ORIGINAL_COUNTRY
*/
[img-animate="DEPART_FROM_ORIGINAL_COUNTRY"] .waybill-color-03,
[img-animate="DEPART_FROM_ORIGINAL_COUNTRY"] .waybill-color-04{
    z-index: -1;
 }
 
[img-animate="DEPART_FROM_ORIGINAL_COUNTRY"] .waybill-no-color-01{
    -moz-transform: translate(8px, 85px) scale(0);
    -webkit-transform: translate(8px, 85px) scale(0);
    -o-transform: translate(8px, 85px) scale(0);
    -ms-transform: translate(8px, 85px) scale(0);
    transform: translate(8px, 85px) scale(0);
}

[img-animate="DEPART_FROM_ORIGINAL_COUNTRY"] .waybill-color-01{
    -moz-transition: all 0.3s ease-in 0s;
    -webkit-transition: all 0.3s ease-in 0s;
    -o-transition: all 0.3s ease-in 0s;
    transition: all 0.3s ease-in 0s;
    -moz-transform: scale(1);
    -webkit-transform: scale(1);
    -o-transform: scale(1);
    -ms-transform: scale(1);
    transform: scale(1);
    z-index: 20;
}

[img-animate="DEPART_FROM_ORIGINAL_COUNTRY"] .waybill-no-color-02{
    -moz-transition: all 0.3s ease-in 0s;
    -webkit-transition: all 0.3s ease-in 0s;
    -o-transition: all 0.3s ease-in 0s;
    transition: all 0.3s ease-in 0s;
    -moz-transform: translate(8px, 20px) scale(0);
    -webkit-transform: translate(8px, 20px) scale(0);
    -o-transform: translate(8px, 20px) scale(0);
    -ms-transform: translate(8px, 20px) scale(0);
    transform: translate(8px, 20px) scale(0);
}

[img-animate="DEPART_FROM_ORIGINAL_COUNTRY"] .waybill-color-02{
    -moz-transition: all 0.3s ease-in 0s;
    -webkit-transition: all 0.3s ease-in 0s;
    -o-transition: all 0.3s ease-in 0s;
    transition: all 0.3s ease-in 0s;
    -moz-transform: scale(1);
    -webkit-transform: scale(1);
    -o-transform: scale(1);
    -ms-transform: scale(1);
    transform: scale(1);
    z-index: 20;
   }

大致思路和tab的实现是一样的,我们在css里面枚举了每个状态下每个模块应该对应的状态。 我们把c通用的css写在一个样式里,其他的每个状态只需要重置特定的样式就可以了

接下来看状态模式在js里的应用

js里面我们把状态对应到每一个函数里面就好了。动画使用了jquery的动画。js代码如下

var statusAnimateMap={
    PICKEDUP: function(callback){
        var self = waybillDetail;
        //更具对应的状态设置动画
            //var w = ONEWAYBILL?32:32;
            self.waybill_img_colorBox.animate(
                {
                    width:32
                },500,function(){
                    self.waybill_img_box.attr("img-animate",'PICKEDUP');
                    if(callback&&typeof callback==="function"){
                        callback();
                    }

                });

    },
    DEPART_FROM_ORIGINAL_COUNTRY: function(callback){
        var self = waybillDetail;
        //更具对应的状态设置动画
            var w = ONEWAYBILL?345:243;
            this.PICKEDUP(function(){
                self.waybill_img_colorBox.animate(
                    {
                        width:w
                    },800,function(){
                        self.waybill_img_box.attr("img-animate",'DEPART_FROM_ORIGINAL_COUNTRY');
                        if(callback&&typeof callback==="function"){
                            callback();
                        }
                    });
            });
    },
}
//使用的时候很简单
function statusAnimate(status){
    if(status&& statusAnimateMap[status]){
        statusAnimateMap[status]();
    }
}
statusAnimate("PICKEDUP");

这里用到了jquery的animate动画。并且业务要求:

每个状态结束才能执行下一个状态的动画,比如DEPART_FROM_ORIGINAL_COUNTRY这个状态就需要

1.先执行PICKEDUP的动画

2.再执行DEPART_FROM_ORIGINAL_COUNTRY的动画,

听起来是不是很耳熟,嗯有点promise的感觉。。额不过这么一个简单的场景当然不需要劳烦promise的大驾了。。。我们给animate绑定一个回调方法就好了。

嗯,具体的实现见这个demo

这个页面还有个单条查询的详情状态,页面结构会不一样,左侧列表会隐藏。。。。查看示例,图片会拉长,也就是说每个状态都需要单独写一个单条的查询的样式。。。额还好我们的状态都写在css里我们只需要给页面加一个属性[oneMailNo]然后重置一下每个状态下各个节点的位置就好了,js只需要修改一下waybill_img_color的宽度就好了。嗯改css的成本很低的。。bingo

以上就是状态模式在实际开发中得应用,我们结合了css html js 综合应用状态模式。可以大大减少项目里面的逻辑代码。提高开发效率,剩下的时间可以去和设计师美眉聊聊生活。。谈谈人生理想。。。。

详见我的博客https://www.56way.com


小无路
221 声望6 粉丝

小小小前端


下一篇 »
SUI踩坑记录