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

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>

点击查看预览效果

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,108评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,699评论 1 296
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,812评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,236评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,583评论 3 288
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,739评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,957评论 2 315
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,704评论 0 204
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,447评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,643评论 2 249
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,133评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,486评论 3 256
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,151评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,108评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,889评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,782评论 2 277
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,681评论 2 272

推荐阅读更多精彩内容