彩票项目实战学习记录(三)

这里是主要的业务逻辑,主要是针对模块化做笔记记录。

模块化

这里不关注业务逻辑代码,只关注实现和应用 es6开发
  1. 对于没有强耦合性的东西进行模块化
  2. 对于功能模块进行模块化
  3. 管理模块化的模块

所以分成了四个模块+2个特殊模块:

  • app/js/lottery/calculate.js 计算模块-负责彩票投注数和奖金数运算的
  • app/js/lottery/timer.js 倒计时模块
  • app/js/lottery/base.js 基础模块-跟彩票本身相关的基础信息的模块
  • app/js/lottery/interface.js 接口模块-负责跟彩票中心(相当于后台)交互的模块
  • app/js/index.js 入口模块-特殊模块1,负责处理整个应用的入口管理
  • app/js/lottery.js 整合模块-特殊模块2,负责整合被分散的模块

计算模块calculate.js

  • 使用 class 类的写法,更加直观的管理代码。
class Calculate {
    
}
// 使用 es6的语法导出这个 class
export default Calculate
  • 方法computeCount,代码如下:
/**
 * [computeCount 计算注数]
 * @param  {number} active    [当前选中的号码的个数]
 * @param  {string} play_name [当前的玩法标识,如R2,即任二]
 * @return {number}           [注数]
 */
computeCount(active, play_name) {
    let count = 0;
    // 使用 es6的 map 结构
    const exist = this.play_list.has(play_name); //判断玩法列表里面是否有这样的玩法,set形式
    // 使用 es6的填充数组功能 fill
    const arr = new Array(active).fill('0'); //生成长度为active的数组,并填充为0
    if (exist && play_name.at(0) === 'r') {
        // 调用静态方法combine
        count = Calculate.combine(arr, play_name.split('')[1]).length;
    }
    return count;
}
  • map 结构数据可以很方便的判断是否包含某个元素,这里一行代码就得到了结果
  • 数组填充功能主要是方便填充一个数组来进行计算,
  • 因为 es6里面支持 class,也支持 static 静态方法,所以可以实现调用静态方法的处理,由于是静态方法,所以需要使用class 来调用。
  • 静态方法combine

这是里面combine方法,这是一个静态方法,这是 es6才有的,需要 static 关键字即可。

/**
 * [combine 组合运算 C(m,n)]
 */
static combine(arr, size) { 
    // 省略
    })(arr, size, [])
    return allResult
}

倒计时模块timer.js

  1. 倒计时模块也是使用 class 的方式写的。
  2. 支持2个回调函数传入,一个是倒计时更新的回调函数,一个是倒计时结束的回调函数。
  3. 这里注意到回调函数都使用 call 的方式来使用,目的是为了保证 this 指向保持不变,所以使用 call 并且传入 self。
  4. 这里的倒计时主要逻辑在于 setTimeout 部分,通过 setTimeout 不断调用自身 countdown 函数来实现了倒计时的效果。
  5. 很标准的一个倒计时模块写法,可以参考学习。
class Timer {
    /**
     * 倒计时方法
     * @param  number end    截止时间
     * @param  function update 每次更新时间时的回调函数
     * @param  function handle 倒计时结束时的回调函数
     * @return
     */
    countdown(end, update, handle) {
        const now = new Date().getTime();
        const self = this;
        if (now - end > 0) {
            handle.call(self);
        } else {
            // 剩余时间
            let last_time = end - now;
            // 常量,用来处理毫秒转天,时,分,秒
            const px_d = 1000 * 60 * 60 * 24;
            const px_h = 1000 * 60 * 60;
            const px_m = 1000 * 60;
            const px_s = 1000;
            // 剩余时间转换为天,时,分,秒
            let d = Math.floor(last_time / px_d);
            // 需要减去天的毫秒数
            let h = Math.floor((last_time - d * px_d) / px_h);
            // 需要减去天和小时的毫秒数
            let m = Math.floor((last_time - d * px_d - h * px_h) / px_m);
            // 需要减去天和小时和分钟的毫秒数
            let s = Math.floor((last_time - d * px_d - h * px_h - m * px_m) / px_s);
            let r = [];
            if (d > 0) {
                r.push(`<em>${d}</em>天`);
            }
            // 判断数组长度主要是为了防止数据错乱,例如只有时,没有分,秒的情况
            if (r.length || (h > 0)) {
                r.push(`<em>${h}</em>时`);
            }
            if (r.length || m > 0) {
                r.push(`<em>${m}</em>分`);
            }
            if (r.length || s > 0) {
                r.push(`<em>${s}</em>秒`);
            }
            // self.last_time = r.join('');
            // 执行更新回调函数,使用的是计算之后的倒计时时间
            update.call(self, r.join(''));
            // 间隔每秒执行一次,重新执行倒计时程序
            setTimeout(function () {
                self.countdown(end, update, handle);
            }, 1000);
        }
    }
}

export default Timer

基础模块base.js

  • 导入 jquery,因为需要操作 dom 数据。
import $ from 'jquery';
  • 方法initNumber

这里的初始化号码因为需要补0,所以要使用 es7才提供的 padStart,需要借助babel-polyfill来实现,因为他的方便易用性,所以会被大量地被大家使用。

  /**
   * [initNumber 初始化号码]
   * @return {[type]} [description]
   */
  initNumber(){
    for(let i=1;i<12;i++){
      this.number.add((''+i).padStart(2,'0'))
    }
  }
  • 方法setOmit
omit 的数据是在整合模块里面被定义为一个 map 结构的数据的,下面有说。

这里主要关注 map 结构的应用:

  • 清空数据 clear
  • 添加数据 set
  • 获取数据 get
  • 遍历数据可以使用 for...of 的方式,使用omit 的 entries()获取到所有值,然后遍历
 /**
   * [setOmit 设置遗漏数据]
   * @param {[type]} omit [description]
   */
  setOmit(omit){
    let self=this;
    // map 结构处理数据
    self.omit.clear();
    for(let [index,item] of omit.entries()){ //omit是个map结构
      self.omit.set(index,item)
    }
    $(self.omit_el).each(function(index,item){
      $(item).text(self.omit.get(index))
    });
  }
  • 方法addCodeItem

这里需要注意2个地方:

  1. es6的字符串模板,代替了以往的+的方式,非常直观并且简单。
  2. self.getTotal()直接调用当前实例的方法,这个其实很大程度上,将项目 class 化,然后通过实例这个 class,然后很方便的使用这个实例的所有方法。
  /**
   * [addCodeItem 添加单次号码]
   * @param {[type]} code     [description]
   * @param {[type]} type     [description]
   * @param {[type]} typeName [description]
   * @param {[type]} count    [description]
   */
  addCodeItem(code,type,typeName,count){
    let self=this;
    // es6的字符串模板使用
    const tpl=`
    <li codes="${type}|${code}" bonus="${count*2}" count="${count}">
         <div class="code">
             <b>${typeName}${count>1?'复式':'单式'}</b>
             <b class="em">${code}</b>
             [${count}注,<em class="code-list-money">${count*2}</em>元]
         </div>
     </li>
    `;
    $(self.cart_el).append(tpl);
    self.getTotal(); //获取总金额
  }

接口模块interface.js

  • 导入了 jquery 模块来使用,导入的目的是因为这个接口模块里面使用了其他模块的函数,例如self.setOmit(res.data);,因为这个函数里面涉及了 jquery 的相关使用,所以如果在这里需要使用的话,就要引入 jquery

    • 需要注意 this 的指向,这里因为是在闭包里面,this 指向会被改变,所以需要提前保存 this 指向。
  • 使用了 es6的 promise进行异步操作,promise可以代替 es5的无限回调问题。

    • 这里 resolve 使用 call 的意思也是保持 this 指向不被改变。
import $ from 'jquery';

class Interface{
 /**
   * 先获取遗漏数据,然后进行前端显示,需要promise
   * @param  string issue json数据
   * @return promise
   */
  getOmit(issue){
    // 保存 this 指向
    let self=this;
    // es6的 promise
    return new Promise((resolve,reject)=>{
      $.ajax({
        url:'/get/omit',
        data:{
          issue:issue
        },
        dataType:'json',
        success:function(res){
          self.setOmit(res.data);
          resolve.call(self,res)
        },
        error:function(err){
          reject.call(err);
        }
      })
    });
  }
  // 省略

整合模块lottery.js

  • 导入模块

    • 导入所有需要的模块,因为这个是整合模块,所以需要导入所有之前写的模块,主要路径,路径是从当前模块文件的路径开始计算。
    • 这里是有顺序要求的,因为有些语法需要使用babel-polyfill来处理,所以需要先导入它
import 'babel-polyfill';
import Base from './lottery/base.js';
import Timer from './lottery/timer.js';
import Calculate from './lottery/calculate.js';
import Interface from './lottery/interface.js';
import $ from 'jquery';
  • 深度拷贝

    • 这里使用深度拷贝的原因是因为复制的是对象,并且使用 es6的方式进行深度拷贝。
    • es6里面Reflect.ownKeys可以拿到原对象的所有属性。参考Reflect
    • 构造函数,原型,name 这3个属性不需要拷贝,所以要排除。
const copyProperties = function (target, source) {
    for (let key of Reflect.ownKeys(source)) {  //拿到源对象上的所有属性
        if (key !== 'constructor' && key !== 'prototype' && key !== 'name') {  //过滤
            let desc = Object.getOwnPropertyDescriptor(source, key); // 获取指定对象的自身属性描述符
            Object.defineProperty(target, key, desc);
        }
    }
}

备注:

关于Object.definePropertyObject.getOwnPropertyDescriptor

  • 前者就是为了给对象定义或修改属性的,如果配合后者来使用的话,那么就会变成直接给目标对象定义一个真实可用的属性(因为后者可以获取源对象的真实属性)
  • 通过遍历使用,就相当于能够复制了一个新对象了。

关于深拷贝(深复制)和浅拷贝(浅复制)

  • JS的数据类型可以分为两种:基本数据类型(null,undefined,string,number和boolean)和引用数据类型(Object,Array,function)。
  • 因为对象和数组在赋值的时候都是引用传递。赋值的时候只是传递一个指针。如果一个引用类型赋值给一个变量,那么这个变量装的是这个对象的地址!那么就会出现修改他的“引用”的值的时候,源值也被改变了。
  • “浅拷贝”就是复制一份引用,所有引用对象都指向一份数据,并且都可以修改这份数据。
  • “深拷贝”就是能够实现真正意义上的数组和对象的拷贝。深复制不是简单的复制引用,而是在堆中重新分配内存,并且把源对象实例的所有属性都进行新建复制,以保证深复制的对象的引用图不包含任何原有对象或对象图上的任何对象,复制后的对象与原来的对象是完全隔离的

多重继承 Mixin

多重继承可以根据各个不同的功能模块分不同程序员独立开发,最后合并起来,而且功能模块耦合度比较小,出现BUG也能很快定义到功能模块,修改其中某一个对其他没有影响。

js 设计的时候是定位为简单的脚本语言,所以没有考虑 class 和继承的问题,直至现在也依然木有考虑继承的问题,都是 js 的开发者自己开发出来使用的,所有就有了这种类似多重继承的多重继承,本质上是将一个对象的属性拷贝到另一个对象上面去,其实就是对象的融合。

参考:Mixin、多重继承与装饰者模式

  • 使用 es6的 rest 语法...mixins,利用 rest 语法获取函数的多余参数简化了写法。
  • 使用之前提到的深度拷贝函数进行多重继承

    • 需要注意的是 js 对象的原型prototype也需要单独进行拷贝,因为原型链没了,对象就是object,所属的类没了,如果有需要这个所属的类的相关属性或者方法的话,就没办法调用了。
const mix = function (...mixins) {
    class Mix {
    }  //声明一个空的类
    for (let mixin of mixins) {
        copyProperties(Mix, mixin);  //深度拷贝
        copyProperties(Mix.prototype, mixin.prototype);  //也拷贝原型
    }
    return Mix
}

多重继承class

将Lottery进行多重继承,继承来自Base, Calculate, Interface, Timer的 class。

  • 使用 extends继承,继承后需要重写父类属性使用super(),但需要注意顺序,必须先使用super()
  • 构造函数进行Lottery的一些属性的初始化。
  • 可以看到很多使用了 Map和 Set 结构的数据
  • 多重继承之后,可以使用其他来自于被继承的 class 的方法和属性
class Lottery extends mix(Base, Calculate, Interface, Timer) {
    constructor(name = 'syy', cname = '11选5', issue = '**', state = '**') {
        super();
        this.name = name;
        this.cname = cname;
        this.issue = issue;
        this.state = state;
        this.el = '';
        this.omit = new Map();
        this.open_code = new Set();  //开奖号码
        this.open_code_list = new Set(); //开奖记录
        this.play_list = new Map();
        this.number = new Set();  //奖号
        this.issue_el = '#curr_issue';
        this.countdown_el = '#countdown'; //倒计时的选择器
        this.state_el = '.state_el'; //状态的选择器
        this.cart_el = '.codelist'; //购物车的选择器
        this.omit_el = ''; //遗漏
        this.cur_play = 'r5'; //当前的默认玩法
        // 这个方法是在其他类中已经被实现了,这里只需要直接调用即可
        this.initPlayList();
        this.initNumber();
        this.updateState(); //更新状态
        this.initEvent();
    }

方法updateState

这里通过异步获取到后台的数据,然后根据得到的数据结果进行调用倒计时函数等操作。这是一个比较完整的异步调用函数处理写法方式。

/**
* [updateState 状态更新]
* @return {[type]} [description]
*/
updateState() {
   let self = this;
   this.getState().then(function (res) {  // getState()是接口里的方法
       self.issue = res.issue; //拿到期号
       self.end_time = res.end_time; //拿到截止时间
       self.state = res.state; //拿到状态
       $(self.issue_el).text(res.issue); //更新期号
       self.countdown(res.end_time, function (time) { //倒计时
           $(self.countdown_el).html(time)
       }, function () { //重新获取
           setTimeout(function () {
               self.updateState();
               self.getOmit(self.issue).then(function (res) {

               });
               self.getOpenCode(self.issue).then(function (res) {

               })
           }, 500);
       })
   })
}

入口模块index.js

这个入口就将逻辑分离出来了,如果需要处理多个入口,可以很方便的在这里根据不同饿实例导入不同的模块,并且统一管理。

import Lottery from './lottery';  //引入彩票的入口文件

// 实例化Lottery实例
const syy=new Lottery();

线上猛如虎
2.2k 声望178 粉丝

你们都有梦想的,是吧.怀抱着梦想并且正朝着梦想努力的人,寻找着梦想的人,我想为这些人加油呐喊!