五星评分功能插件的原理及代码实现

字数 434阅读 241
stars.png

星级评分功能插件的实现原理:

原理分析
HTML{
ul.rating#rating>li.rating-item*5
}

CSS {
两张背景图片(点亮的星星/灰色的星星)
}

JS行为 {

遍历所有的星星,在循环中去点亮指定位置的星星,熄灭剩余星星
mouseover事件:{
    鼠标移入的时候mouseover(遍历所有的星星颗数){
        click事件: 点亮指定星星
    }
}
mouseout事件:{
    鼠标离开的时候mouseoout(遍历所有的星星颗数){
        熄灭剩余星星
    }
}

}

代码实现:

<script>
  var num =2, //初始化默认点亮星星颗数
      $rating = $('#rating'),
      $item = $rating.find(".rating-item");

  //点亮星星
  var lightOn = function (num) {
    $item.each(function (index) {
      if(index < num) {
        $(this).css('background-position', '-46px -10px'); //亮星星
      }else {
        $(this).css('background-position', '-10px -10px'); //灰星星
      }
    });
  };

  lightOn(num); //初始化函数

  //事件绑定
  $item.on('mouseover', function () {//鼠标移入时
    lightOn($(this).index() + 1); //点亮当前指定的星星颗数
  }).on('click', function () {//点击鼠标
    num = $(this).index() + 1; //获取当前星星的索引值
  });
  
  $rating.on('mouseout', function () {//鼠标移出时 要在父容器上绑定mouseout
    lightOn(num); //因为上一步里num值已经改变
  });
 
</script>

但是,以上这段代码有问题:

1. 暴露的全局变量太多,不利于代码拓展

解决办法1:独立命名空间。
解决办法2: 立即执行的匿名函数的写法(function(){})(), 使得我们的变量变成局部作用域。

2. 事件绑定的写法是为每一颗星星都绑定了每一次事件,这样会造成事件浪费。

3. 事件无法复用。

针对以上发现的问题,对JS代码进行改进:

<body>
<ul class="rating" id="rating">
  <li class="rating-item" title="极差"></li>
  <li class="rating-item" title="很差"></li>
  <li class="rating-item" title="一般"></li>
  <li class="rating-item" title="好"></li>
  <li class="rating-item" title="极好"></li>
</ul>

<ul class="rating" id="rating1">
  <li class="rating-item" title="极差"></li>
  <li class="rating-item" title="很差"></li>
  <li class="rating-item" title="一般"></li>
  <li class="rating-item" title="好"></li>
  <li class="rating-item" title="极好"></li>
</ul>

<ul class="rating" id="rating2">
  <li class="rating-item" title="极差"></li>
  <li class="rating-item" title="很差"></li>
  <li class="rating-item" title="一般"></li>
  <li class="rating-item" title="好"></li>
  <li class="rating-item" title="极好"></li>
</ul>
</body>

<style>
    body, ul , li {
      margin: 0;
      padding: 0;
    }
    li {
      list-style: none;
    }
    .rating {
      width: 130px;
      height: 26px;
      margin: 100px auto;
    }
    .rating-item {
      float: left;
      width: 26px;
      height: 26px;
      background: url("img/css_sprites.png") no-repeat;
      cursor: pointer;
    }
  </style>

<script>
  <!--  闭包/立即执行函数  -->
  var rating = (function () {
    //点亮星星
    var lightOn = function ($item, num) {
      $item.each(function (index) {
        if(index < num) {
          $(this).css('background-position', '-46px -10px'); //亮星星
        }else {
          $(this).css('background-position', '-10px -10px'); //灰星星
        }
      });
    };

    //让函数可复用
    var init = function (el, num) {
      var $rating = $(el), //可复用:将这里变成通过外界传递进来的参数
            $item = $rating.find(".rating-item");

      lightOn($item, num); //初始化函数

      //事件委托
      $rating.on('mouseover', '.rating-item', function () {//鼠标移入时
        lightOn($item, $(this).index() + 1); //点亮当前指定的星星颗数
      }).on('click', '.rating-item', function () {//点击鼠标
        num = $(this).index() + 1; //获取当前星星的索引值
      }).on('mouseout', function () {//鼠标移出时
        lightOn($item, num); //因为上一步里num值已经改变
      });

    };

    //生成 jQuery 插件
    $.fn.extend({
      rating: function (num) {
        return this.each(function () {
          init(this, num);
        })
      }
    });

    return {//返回一个对象
      init: init  //对象de方法
    };

  })();

  //复用
  rating.init('#rating1', 1);
  rating.init('#rating2', 3);

  //调用jQuery 插件
  $('#rating').rating(2);

</script>

以为到此就算圆满的话,那就太幼稚了,试想一下,一两个月后,产品经理会告诉你:"我们需要增加新功能,比如用户可以选中半颗星...."。

好吧,那只能继续改改改......

这里就有引出一个设计模式,那就是【开放封闭原则】:即对拓展是开放的,而对修改是封闭的:

改写JS:

<!-- 模板方法模式: 点亮整颗星 -->
<script>
  var rating = (function () {
    //点亮整颗星
    var LightEntire = function (el, options) {
      this.$el = $(el);
      this.$item = this. $el.find('.rating-item');
      this.opts = options;
    };
    LightEntire.prototype.init = function () {
      this.lightOn(this.opts.num);
      if (!this.opts.readOnly){
        this.bindEvent();
      }
    };
    LightEntire.prototype.lightOn = function (num) {
      num = parseInt(num);
      this.$item.each(function (index) {
        if (index < num) {
          $(this).css('background-position', '-46px -10px'); //亮星星
        } else {
          $(this).css('background-position', '-10px -10px'); //灰星星
        }
      });
    };

    LightEntire.prototype.bindEvent = function () {
      var self = this,
          itemLength = self.$item.length;

      self.$el.on('mouseover', '.rating-item', function () {
        var num = $(this).index() + 1;
        self.lightOn(num);
        (typeof self.opts.select === 'function') &&
        self.opts.select.call(self.opts.num, itemLength); //判断/改变this指向
        //触发事件
        self.$el.trigger('select', [self.opts.num, itemLength]);

      }).on('click', '.rating-item', function () {
        self.opts.num = $(this).index() + 1;
        (typeof self.opts.chosen === 'function') &&
        self.opts.chosen.call(self.opts.num, itemLength); //判断/改变this指向
        //触发事件
        self.$el.trigger('chosen', [self.opts.num, itemLength]);

      }).on('mouseout', function () {
        self.lightOn(self.opts.num);
      });

    };

    //默认参数
    var defaults = {
      num: 0,
      readOnly: false,
      select: function () {

      },
      chosen: function () {

      }
    };

    //初始化
    var init = function (el, options) {
      options = $.extend({}, defaults, options);
      new LightEntire(el, options).init();
    };

    return {
      init: init
    };

  })();

  rating.init('#rating', {
    num: 1,
    select: function (num, total) {
      console.log(this);
      console.log(num + '/' + total); //打印当前第几颗/总共多少颗星
    }
  });

  $('#rating').on('select', function (e, num , total) {
    console.log(num + '/' + total);
  }).on('chosen', function (e, num , total) {
    console.log(num + '/' + total);
  })
</script>

代码拓展:点亮半颗星功能

原理分析:
利用mousemove
e.pageX
$().offset().left
e.pageX - $().offset().left
$().width()/2

<!-- 模板方法模式 支持 点亮半颗星 -->
<script>
  var rating = (function () {
    //点亮整颗星
    var LightEntire = function (el, options) {
      this.$el = $(el);
      this.$item = this. $el.find('.rating-item');
      this.opts = options;
    };

    LightEntire.prototype.init = function () {
      this.lightOn(this.opts.num);
      if (!this.opts.readOnly){
        this.bindEvent();
      }
    };
    LightEntire.prototype.lightOn = function (num) {
      num = parseInt(num);
      this.$item.each(function (index) {
        if (index < num) {
          $(this).css('background-position', '-46px -10px'); //亮星星
        } else {
          $(this).css('background-position', '-10px -10px'); //灰星星
        }
      });
    };
    LightEntire.prototype.bindEvent = function () {
      var self = this,
        itemLength = self.$item.length;

      self.$el.on('mouseover', '.rating-item', function () {
        var num = $(this).index() + 1;
        self.lightOn(num);
        (typeof self.opts.select === 'function') &&
        self.opts.select.call(self.opts.num, itemLength); //判断/改变this指向
        //触发事件
        self.$el.trigger('select', [self.opts.num, itemLength]);

      }).on('click', '.rating-item', function () {
        self.opts.num = $(this).index() + 1;
        (typeof self.opts.chosen === 'function') &&
        self.opts.chosen.call(self.opts.num, itemLength); //判断/改变this指向
        //触发事件
        self.$el.trigger('chosen', [self.opts.num, itemLength]);

      }).on('mouseout', function () {
        self.lightOn(self.opts.num);
      });

    };

    //点亮半颗星星
    var LightHalf = function (el, options) {

      this.$el = $(el);
      this.$item = this. $el.find('.rating-item');
      this.opts = options;
      this.add = 1;

    };

    LightHalf.prototype.init = function () {
      this.lightOn(this.opts.num);
      if (!this.opts.readOnly){
        this.bindEvent();
      }
    };
    LightHalf.prototype.lightOn = function (num) {

      var count = parseInt(num),
        isHalf = count !== num; //判断是不是小数

      this.$item.each(function (index) {
        if (index < count) {
          $(this).css('background-position', '-46px -10px'); //亮星星
        } else {
          $(this).css('background-position', '-10px -10px'); //灰星星
        }
      });

      if(isHalf) {
        this.$item.eq(count).css('background-position', '-10px -46px');
      }

    };
    LightHalf.prototype.bindEvent = function () {
      var self = this,
        itemLength = self.$item.length;

      self.$el.on('mousemove', '.rating-item', function (e) {

        var $this = $(this),
            num = 0;

        if (e.pageX - $this.offset().left < $this.width() / 2) {//判断是否半颗星
          self.add = 0.5;
        } else {//整颗星
          self.add = 1;
        }

        num = $this.index() + self.add;
        self.lightOn(num);

        (typeof self.opts.select === 'function') &&
        self.opts.select.call(self.opts.num, itemLength); //判断/改变this指向
        //触发事件
        self.$el.trigger('select', [self.opts.num, itemLength]);

      }).on('click', '.rating-item', function () {

        self.opts.num = $(this).index() + self.add;
        (typeof self.opts.chosen === 'function') &&
        self.opts.chosen.call(self.opts.num, itemLength); //判断/改变this指向
        //触发事件
        self.$el.trigger('chosen', [self.opts.num, itemLength]);

      }).on('mouseout', function () {
        self.lightOn(self.opts.num);
      });

    };

    //默认参数
    var defaults = {
      mode: 'LightEntire',
      num: 0,
      readOnly: false,
      select: function () {},
      chosen: function () {}
    };

    var mode = {
      'LightEntire': LightEntire,
      'LightHalf': LightHalf
    };

    //初始化
    var init = function (el, options) {
      options = $.extend({}, defaults, options);

      if (!mode[options.mode]){
        options.mode = 'LightEntire';
      };

      // new LightEntire(el, options).init();
      // new LightHalf(el, options).init();
      new mode[options.mode](el, options).init();
    };

    return {
      init: init
    };

  })();

  rating.init('#rating', {
    mode: 'LightHalf',
    num: 2.5,
    select: function (num, total) {
      console.log(this);
      console.log(num + '/' + total); //打印当前第几颗/总共多少颗星
    }
  });
</script>

最后,抽象出父类:

<script>
  var rating = (function () {
    //继承
    var extend = function (subClass, superClass) {
      var F = function () {};
      F.prototype = superClass.prototype;
      subClass.prototype = new F();
      subClass.prototype.constructor = subClass;
    };

    //抽象父类
    var Light = function (el, options) {
      this.$el = $(el);
      this.$item = this. $el.find('.rating-item');
      this.opts = options;
      this.add = 1;
      this.selectEvent = 'mouseover';
    };

    Light.prototype.init = function () {
      this.lightOn(this.opts.num);
      if (!this.opts.readOnly){
        this.bindEvent();
      }
    };
    Light.prototype.lightOn = function (num) {
      num = parseInt(num);
      this.$item.each(function (index) {
        if (index < num) {
          $(this).css('background-position', '-46px -10px'); //亮星星
        } else {
          $(this).css('background-position', '-10px -10px'); //灰星星
        }
      });
    };
    Light.prototype.bindEvent = function () {
      var self = this,
        itemLength = self.$item.length;

      self.$el.on(self.selectEvent, '.rating-item', function (e) {

        var $this = $(this),
            num = 0;

        self.select(e, $this);
        num = $(this).index() + self.add;
        self.lightOn(num);

        (typeof self.opts.select === 'function') &&
        self.opts.select.call(self.opts.num, itemLength); //判断/改变this指向
        //触发事件
        self.$el.trigger('select', [self.opts.num, itemLength]);

      }).on('click', '.rating-item', function () {
        self.opts.num = $(this).index() + self.add;
        (typeof self.opts.chosen === 'function') &&
        self.opts.chosen.call(self.opts.num, itemLength); //判断/改变this指向
        //触发事件
        self.$el.trigger('chosen', [self.opts.num, itemLength]);

      }).on('mouseout', function () {
        self.lightOn(self.opts.num);
      });

    };
    Light.prototype.select = function () {
      throw new Error('子类必须重写方法');
    };
    
    //点亮整颗星
    var LightEntire = function (el, options) {
      Light.call(this, el, options);
      this.selectEvent = 'mouseover';
    };
    extend(LightEntire, Light);
    LightEntire.prototype.lightOn = function (num) {
      Light.prototype.lightOn.call(this, num);
    };
    LightEntire.prototype.select = function () {
      self.add = 1;
    };

    //点亮半颗星星
    var LightHalf = function (el, options) {
      Light.call(this, el, options);
      this.selectEvent = 'mousemove';
    };
    extend(LightHalf, Light);
    LightHalf.prototype.lightOn = function (num) {
      var count = parseInt(num),
          isHalf = count !== num; //判断是不是小数

      Light.prototype.lightOn.call(this, count);

      if(isHalf) {
        this.$item.eq(count).css('background-position', '-10px -46px');
      }

    };
    LightHalf.prototype.select = function (e, $this) {
      if (e.pageX - $this.offset().left < $this.width() / 2) {//判断是否半颗星-->
        this.add = 0.5;
      } else {//整颗星
        this.add = 1;
      }
    }

    //默认参数
    var defaults = {
      mode: 'LightEntire',
      num: 0,
      readOnly: false,
      select: function () {},
      chosen: function () {}
    };

    var mode = {
      'LightEntire': LightEntire,
      'LightHalf': LightHalf
    };

    //初始化
    var init = function (el, options) {
      options = $.extend({}, defaults, options);

      if (!mode[options.mode]){
        options.mode = 'LightEntire';
      };

      // new LightEntire(el, options).init();
      // new LightHalf(el, options).init();
      new mode[options.mode](el, options).init();
    };

    return {
      init: init
    };

  })();

  rating.init('#rating', {
    mode: 'LightHalf',
    num: 2.5,
    select: function (num, total) {
      console.log(this);
      console.log(num + '/' + total); //打印当前第几颗/总共多少颗星
    }
  });
</script>

然鹅,问题又来了,正常情况下,用户点选好星星后,选中结果就会发送出去,用户就不能再继续点选其他剩余几颗星星的,

所以,以上代码依然不够满足业务场景,继续修改:

<html>
<ul class="rating" id="rating">
  <li class="rating-item" title="极差"></li>
  <li class="rating-item" title="很差"></li>
  <li class="rating-item" title="一般"></li>
  <li class="rating-item" title="好"></li>
  <li class="rating-item" title="极好"></li>
</ul>
<ul class="rating" id="rating1">
  <li class="rating-item" title="极差"></li>
  <li class="rating-item" title="很差"></li>
  <li class="rating-item" title="一般"></li>
  <li class="rating-item" title="好"></li>
  <li class="rating-item" title="极好"></li>
</ul>
</html>


<!-- 功能扩展:选中星星后就不能再继续选择 -->
<script>
  var rating = (function () {
    //继承
    var extend = function (subClass, superClass) {
      var F = function () {};
      F.prototype = superClass.prototype;
      subClass.prototype = new F();
      subClass.prototype.constructor = subClass;
    };

    //抽象父类
    var Light = function (el, options) {
      this.$el = $(el);
      this.$item = this. $el.find('.rating-item');
      this.opts = options;
      this.add = 1;
      this.selectEvent = 'mouseover';
    };

    Light.prototype.init = function () {
      this.lightOn(this.opts.num);
      if (!this.opts.readOnly){
        this.bindEvent();
      }
    };
    Light.prototype.lightOn = function (num) {
      num = parseInt(num);
      this.$item.each(function (index) {
        if (index < num) {
          $(this).css('background-position', '-46px -10px'); //亮星星
        } else {
          $(this).css('background-position', '-10px -10px'); //灰星星
        }
      });
    };
    Light.prototype.bindEvent = function () {
      var self = this,
        itemLength = self.$item.length;

      self.$el.on(self.selectEvent, '.rating-item', function (e) {

        var $this = $(this),
          num = 0;

        self.select(e, $this);
        num = $(this).index() + self.add;
        self.lightOn(num);

        (typeof self.opts.select === 'function') &&
        self.opts.select.call(self.opts.num, itemLength); //判断/改变this指向
        //触发事件
        self.$el.trigger('select', [self.opts.num, itemLength]);

      }).on('click', '.rating-item', function () {
        self.opts.num = $(this).index() + self.add;
        (typeof self.opts.chosen === 'function') &&
        self.opts.chosen.call(self.opts.num, itemLength); //判断/改变this指向
        //触发事件
        self.$el.trigger('chosen', [self.opts.num, itemLength]);

      }).on('mouseout', function () {
        self.lightOn(self.opts.num);
      });

    };
    Light.prototype.select = function () {
      throw new Error('子类必须重写方法');
    };
    Light.prototype.unbindEvent = function () {
      this.$el.off();
    };

    //点亮整颗星
    var LightEntire = function (el, options) {
      Light.call(this, el, options);
      this.selectEvent = 'mouseover';
    };
    extend(LightEntire, Light);
    LightEntire.prototype.lightOn = function (num) {
      Light.prototype.lightOn.call(this, num);
    };
    LightEntire.prototype.select = function () {
      self.add = 1;
    };


    //点亮半颗星星
    var LightHalf = function (el, options) {
      Light.call(this, el, options);
      this.selectEvent = 'mousemove';
    };
    extend(LightHalf, Light);
    LightHalf.prototype.lightOn = function (num) {
      var count = parseInt(num),
        isHalf = count !== num; //判断是不是小数

      Light.prototype.lightOn.call(this, count);

      if(isHalf) {
        this.$item.eq(count).css('background-position', '-10px -46px');
      }

    };
    LightHalf.prototype.select = function (e, $this) {
      if (e.pageX - $this.offset().left < $this.width() / 2) {//判断是否半颗星-->
        this.add = 0.5;
      } else {//整颗星
        this.add = 1;
      }
    }

    //默认参数
    var defaults = {
      mode: 'LightEntire',
      num: 0,
      readOnly: false,
      select: function () {},
      chosen: function () {}
    };

    var mode = {
      'LightEntire': LightEntire,
      'LightHalf': LightHalf
    };

    //初始化
    var init = function (el, option) {//option 可支持字符串

      var $el = $(el),
          rating = $el.data('rating'),
          options = $.extend({}, defaults, typeof option === 'object' && option);

      if (!mode[options.mode]){
        options.mode = 'LightEntire';
      };

      // new LightEntire(el, options).init();
      // new LightHalf(el, options).init();

      if(!rating) {
        $el.data('rating', (rating = new mode[options.mode](el, options)));
        rating.init();
      }
      if(typeof option === 'string')rating[option]();

    };

    //jQuery 插件
    $.fn.extend({
      rating: function (option) {
        return this.each(function () {
          init(this, option);
        });
      }
    });

    return {
      init: init
    };

  })();

//用jQ的方式调用
  $('#rating').rating({
    mode: 'LightEntire',
    num: 2
  });
  $('#rating1').rating({
    mode: 'LightHalf',
    num: 3.5
  }).on('chosen', function () {
    $('#rating1').rating('unbindEvent');
  });

  // rating.init('#rating', {
  //   mode: 'LightHalf',
  //   num: 2.5,
  //   // select: function (num, total) {
  //   //   console.log(this);
  //   //   console.log(num + '/' + total); //打印当前第几颗/总共多少颗星
  //   // },
  //   chosen: function () {
  //     rating.init('#rating', 'unbindEvent');
  //   }
  // });
</script>

点击查看预览效果

推荐阅读更多精彩内容