阅读 6396

JS写的一个抽奖小Demo从普通写法到设计模式再向ES6的进阶路程

前言

写这个小Demo是想提升自己的JS代码风格和水平,所以这个Demo我写了三个版本

这款抽奖小Demo拥有类似现实中转盘抽奖的效果,会在最后慢慢停止。


献上效果

转盘图片

效果地址请点这里


工程目录

1. 整体目录

工程目录

2. HTML结构

    <p class="result none" ></p>
    
    <div class="wrap">
        <ul class="turntable">
            <li id="num2"> <div class="title">2</div> </li>
            <li id="num3"> <div class="title">3</div> </li>
            <li id="num4"> <div class="title">4</div> </li>
            <li id="num5"> <div class="title">5</div> </li>
            <li id="num6"> <div class="title">6</div> </li>
            <li id="num7"> <div class="title">7</div> </li>
            <li id="num8"> <div class="title">8</div> </li>
            <li id="num9"> <div class="title">9</div> </li>
            <li id="num10"> <div class="title">10</div> </li>
            <li id="num11"> <div class="title">11</div> </li>
            <li id="num12"> <div class="title">12</div> </li>
            <li id="num1"> <div class="title">1</div> </li>
        </ul>
        <div class="pointerDisk">
            <span class="triangleUp"></span>
            <p class="internal">开始抽奖</p>
        </div>
    </div>

复制代码
  • 这里有一点需注意,因为每个格子是通过CSS整体进行30°旋转,所以1号格子想要在0°的话需要放到最后一个。

3. CSS样式


    .wrap ul {
      width: 90%;
      height: 90%;
      position: absolute;
      left: 50%;
      margin-left: -45%;
      top: 50%;
      margin-top: -45%;
      border-radius: 50%;
      overflow: hidden;
      background: #166dab;
      box-shadow: 0px 0px 12px 2px #152c3c;
    }
    
    .wrap ul li:nth-child(1) {
      position: absolute;
      left: 50%;
      margin-left: -90px;
      transform: rotate(30deg);
      transform-origin: center 315px 10px;
    }
    /*  依次类推到第12个... */
    
    .wrap ul li:nth-child(odd):after {
      content: '';
      display: block;
      width: 0;
      height: 0;
      border-left: 89px solid transparent;
      border-right: 89px solid transparent;
      border-top: 308px solid #1b7b54;
    }

复制代码
  • 这里就先放上一个li的样式,剩下的以此类推只需改变 rotate(30deg * n),例如下一个 li 则为 rotate(60deg)
  • 然后每个 li 设置成上三角形的样式。ul进行溢出隐藏,即可实现效果。
  • 这里我用了SASS去处理这个问题,这样就不用每个样式都需要手动去设置。
    ul{
        $value:90;
        width: $value+%;
        height: $value+%;
        @include position_center($value,$value,%);
        border-radius:50%;
        overflow: hidden;
        background:#166dab;
        box-shadow: 0px 0px 12px 2px #152c3c;

        @for $i from 1 through 12{
            li:nth-child(#{$i}){
                @include position_center(180,false,px);
                @include browser(transform,rotate(30deg*$i));
                @include browser(transform-origin,center 315px 10px);
            }
        }

        li:nth-child(odd){
            &:after{
                content: '';
                display: block;
                @include triangle(bottom,178px,308px,$oddColor);
            }
        }

        li:nth-child(even){
            &:after{
                content: '';
                display: block;
                @include triangle(bottom,178px,308px,$evenColor);
            }
        }
    }


复制代码
  • 具体的样式代码在文章的最后我会放上GitHub地址,今天的重点在于JS,所以这里我们可以先粗略理解下即可。

4. js原始版


    function getClassName(tagName, classname) {
        if (document.getElementsByClassName) {
            return document.getElementsByClassName(classname);
        } else {
            var results = [];
            var elems = document.getElementsByTagName('*');
            for (var i = 0; i < elems.length; i++) {
                if (elems[i].className.indexOf(classname) != -1) {
                    results[results.length] = elems[i];
                }
            }
            return results;
        }
    }

复制代码
  • 首先是封装了一个获取类名的方法。

封装类名参考了脚本之家的 这篇教程


    var turntable = getClassName('ul','turntable')[0];//获取转盘的dom节点
    var result = getClassName('p','result')[0];//获取结果的dom节点
    var internal = getClassName('p','internal')[0];//获取点击按钮的dom节点

    var flag = true;//一个判断开关
    var turns = Math.ceil(Math.random()*3+1);//旋转圈数
    var speed = Math.floor(Math.random()*6)+3;//速度
    var num = Math.ceil(Math.random()*12)-1;//随机抽取的位置
    var times = 20;//进行时间
    
    var arr = [];//每个格子相对应的角度参数
    var MathNum = 14;//重新编排编号数字与转盘对应,14是因为i=1时已经减去了一个
    var turnNum = 12;//格子数量
    var deg = 360/turnNum;//转盘所对应的度数
    var turnBuffer = deg/2-5;//每个格子对应的度数缓冲区

    for(let i=1; i<=turnNum; i++){

        let num = MathNum-i;// 编号
        if(i==1){num = i}
        let turnDeg = deg*i-deg;//每个编号相对应的角度计算 
        arr.push([num,turnDeg+turnBuffer,turnDeg-turnBuffer]) ;//最后把所有数据放进一个数组里  [ 编号,最大角度,最大角度  ]

    }

    var initialDegMini = turns*360+arr[num][2];//初始最小值度数
    var initialDegMax = turns*360+arr[num][1];//初始最大值度数
    var initital = 0;//转盘初始角度

复制代码
  • 原理介绍

    1. 随机好转盘要旋转的圈数以及速度。

    2. 因为角度从0°到360°是逆时针。所以对应格子的角度则需要从逆时针算起,这里用一个for循环去计算即可。

    3. for 循环对每个格子设置相印的参数。按照转盘的顺序则为:1,12,11,10...4,3,2。
      例如 1号格子的参数为 [ 1 , 10 , -10 ] ,
      然后到12号格子的参数为 [ 12 , 40 , 20 ],
      依次类推...
      最后到 2号格子的参数为[ 2 , 340 , 320 ]

    4. 当上面三个步骤完成了之后那么我们就需要计算这个转盘旋转后的度数。
      变量 initialDegMini = 随机旋转圈数 * 360° + 随机抽的格子的最小度数。
      变量 initialDegMax = 随机旋转圈数 * 360° + 随机抽的格子的最大度数。
      例如:假设随机抽到的格子数是 1号格子
      那么相应的变量为 initialDegMini = 随机旋转圈数 * 360° + (-10) , initialDegMax = 随机旋转圈数 * 360° + 10
      依次类推...

    5. 设置初始角度 0 一直叠加到 initialDegMini 和 initialDegMax 的区间即可。

    
        function star (){
    
            turntable.style.transform ="rotate("+initital+"deg)";//对转盘设置旋转角度
            initital += speed; 
    
            if(initital >= initialDegMini-800){ //判断当前旋转的角度是否达到定义的值,若达到则进行减速
                if(speed>1.2){
                    speed = speed-0.05;
                }
            }
           
            if(initital>initialDegMini &&  initital<initialDegMax ){ //判断当前旋转角度是否已经进入最大角度和最小的角度区间
    
                result.innerHTML ='结果为:'+ arr[num][0]
               
                initital = arr[num][2];
                turntable.style.transform ="rotate("+initital+"deg)";
    
                //重置
                num =  Math.ceil(Math.random()*12)-1;
                turns = Math.ceil(Math.random()*5+1);
                speed = Math.floor(Math.random()*3)+3;
                times = 20;
                initialDegMini = turns*360+arr[num][2];
                initialDegMax = turns*360+arr[num][1];
    
                flag = true;
            }else{
                setTimeout(star,times);
            }
    
        }
    
    复制代码
  • 递归函数 star 用了 setTimeout 来延时循环执行,确保转盘不会一下子就转到最后的结果上,当 initital 值达到一定的角度区间,才会停止执行。

    
    document.onclick = function(e){
       var target = e.target || e.srcElement;
       if(target.className == 'internal' && flag== true){
           flag = false;
           result.classList.remove('none');
           result.innerHTML = '抽奖中...';
           setTimeout(star,times);
       }
   }    
    
复制代码
  • 最后设定监听事件。当按下抽奖按钮时就开始执行递归函数 star 。
  • 到此,我们的程序逻辑就写完了。接下来就开始升级成设计模式。

设计模式我用了单例模式 , 写法参考了 这篇博客


单例模式

   
   function getClassName(tagName, classname) {
       if (document.getElementsByClassName) {
           return document.getElementsByClassName(classname);
       } else {
           var results = [];
           var elems = document.getElementsByTagName('*');
           for (var i = 0; i < elems.length; i++) {
               if (elems[i].className.indexOf(classname) != -1) {
                   results[results.length] = elems[i];
               }
           }
           return results;
       }
   }
   
       var turntable = getClassName('ul','turntable')[0];
       var result = getClassName('p','result')[0];
   
复制代码
  • 这段代码不变
    
    function CreateParameter (turntableDom,resultDom){
        //参数
        this.turntable = turntableDom;//转盘dom
        this.result = resultDom;//结果dom
        this.flag = true;//开关设置
        this.times = 20;//执行时间
        this.turns = Math.ceil(Math.random()*3+1);//旋转圈数
        this.speed = Math.floor(Math.random()*5)+3;//速度
        this.turnNum = 12;//格子总数
        this.deg = 360/this.turnNum;//转盘所对应的度数
        this.initital = 0;//转盘旋转角度
        this.turnBuffer = this.deg/2-5;//每个格子对应的度数缓冲区
        this.num = Math.ceil(Math.random() * this.turnNum)-1;//随机抽取的位置
        this.MathNum = 14;//重新编排编号数字与转盘对应,14是因为i=1时已经减去了一个
        this.arr =  this.NewArr(this.MathNum,this.deg,this.turnBuffer);//转盘角度参数
        this.initialDegMini = this.turns*360+this.arr[this.num][2];//初始最小值度数
        this.initialDegMax = this.turns*360+this.arr[this.num][1];//初始最大值度数
        this.MathAngle = Math.ceil(Math.random()*(this.initialDegMax-this.initialDegMini) )+this.initialDegMini;//转盘停止的角度
        this.text ='结果为:'+ this.arr[this.num][0];
        
        console.log(this.arr[this.num])
        console.log(this.arr)
    }

复制代码
  • 把这些参数用 CreateParameter 方法封装了起来, 并全部用 this 来调用。

    CreateParameter.prototype.NewArr = function (MathNum,deg,turnBuffer){
        //计算转盘的各个角度参数
        var arr = [];
        for(let i = 1;i<=this.turnNum;i++){
            let num = MathNum-i;//做倒叙,跳过1
            if(i==1){num = i}
            let turnDeg = deg*i-deg; 
            arr.push([num,turnDeg+turnBuffer,turnDeg-turnBuffer]) ;
        }
        return arr;
    }
    
    CreateParameter.prototype.OperatingDom = function(dom){
        //dom节点操作
        if(dom == 'rotate'){
            this.turntable.style.transform ="rotate("+this.initital+"deg)";
        }

        if(dom == 'innerHTML'){
            this.result.innerHTML = this.text;
        }
    
    }
    
    CreateParameter.prototype.judgment = function(){
        //判断
        if(this.initital >= this.initialDegMini-800){
            if(this.speed>1.2){
                this.speed = this.speed-0.05;
            }
        }

        if(this.initital >= this.MathAngle ){
            this.OperatingDom('innerHTML')
            this.reset();
        }else{
            //setTimeout内部指针会混乱所以需要外部定义
            var _this = this;
            setTimeout(function(){
                _this.star()
            },this.times)
        }
    }
    
        CreateParameter.prototype.reset = function (){
            //重置
            this.initital = this.MathAngle-(parseInt(this.MathAngle/360)*360);
            this.OperatingDom('rotate')
            this.num =  Math.ceil(Math.random()*12)-1;
            this.turns = Math.ceil(Math.random()*5+1);
            this.speed = Math.floor(Math.random()*3)+3;
            this.initialDegMini = this.turns*360+this.arr[this.num][2];
            this.initialDegMax = this.turns*360+this.arr[this.num][1];
            this.MathAngle = Math.ceil(Math.random()*(this.initialDegMax-this.initialDegMini) )+this.initialDegMini;
            this.flag = true;
            this.text ='结果为:'+ this.arr[this.num][0];
    
        }
    
        CreateParameter.prototype.star = function(){
            this.OperatingDom('rotate');//让转盘旋转
            this.initital+=this.speed;//增加角度
            this.judgment();//运行判断
        }

复制代码
  • 将原先的 star 方法拆分成四个CreateParameter的原型方法,并将原先的 arr 数组也封装成CreateParameter的原型方法
  • 五个原型方法分别为:
    1. NewArr 方法,计算转盘的各个角度参数。
    2. OperatingDom 方法,dom节点操作。
    3. judgment 方法,判断 this.initital 是否达到预设的界限值。
    4. reset 方法,重置相关参数
    5. star 方法, 运行 OperatingDom 和 judgment 方法。

    var ProxySingleParameter = (function(){
    
        var  instance =  new CreateParameter(turntable,result);//存储参数
        var flag = instance.flag;//开关判断是否正在运行中

        return function (turntable,result){
            if(!flag){
                instance = new CreateParameter(turntable,result);//更新参数
               console.log(instance)
            }
            return instance;
        }

    })()

复制代码
  • 单例控制方法,程序的开始会先存储参数,并根据 flag 判断是否正在抽奖。这样做是为了防止用户多次点击抽奖而进行不停的重置参数。

    document.onclick = function(e){
        var target = e.target || e.srcElement;
        if(target.className == 'internal'){
            let Parameter = new ProxySingleParameter(turntable,result);
            if(Parameter.flag){
                Parameter.result.classList.remove('none');
                Parameter.star()
                Parameter.flag = false
            }else{
                console.log(Parameter.arr[Parameter.num])
            }
        }
    }

复制代码
  • 最后监听事件也做出相应的调整。
  • 至此,单例模式的写法已经完成。

ES6写法

    
// main3-configuration.js

let GetClassName = (tagName, classname) => {
    if (document.getElementsByClassName) {
        return document.getElementsByClassName(classname);
    } else {
        let results = [];
        let elems = document.getElementsByTagName('*');
        for (let i = 0; i < elems.length; i++) {
            if (elems[i].className.indexOf(classname) != -1) {
                results[results.length] = elems[i];
            }
        }
        return results;
    }
}
export default GetClassName;
    
复制代码
  • 首先把获取类名的方法拆分成了一个文件,命名为 main3-configuration.js

//main3-class.js

  class Turntable {
    
    constructor(turntableDom,resultDom){
        //参数
        this.turntable = turntableDom;//转盘dom
        this.result = resultDom;//结果dom
        this.flag = true;//开关设置
        this.times = 20;//执行时间
        this.turns = Math.ceil(Math.random()*3+1);//旋转圈数
        this.speed = Math.floor(Math.random()*3)+3;//速度
        this.turnNum = 12;//格子总数
        this.deg = 360/this.turnNum;//转盘所对应的度数
        this.initital = 0;//转盘旋转角度
        this.turnBuffer = this.deg/2-5;//每个格子对应的度数缓冲区
        this.num = Math.ceil(Math.random() * this.turnNum)-1;//随机抽取的位置
        this.MathNum = 14;//重新编排编号数字与转盘对应,14是因为i=1时已经减去了一个
        this.arr =  this.NewArr(this.MathNum,this.deg,this.turnBuffer);//转盘角度参数
        this.initialDegMini = this.turns*360+this.arr[this.num][2];//初始最小值度数
        this.initialDegMax = this.turns*360+this.arr[this.num][1];//初始最大值度数
        this.MathAngle = Math.ceil(Math.random()*(this.initialDegMax-this.initialDegMini) )+this.initialDegMini;//转盘停止的角度
        this.text = `结果为:${this.arr[this.num][0]} `;
        
        console.log(this.MathAngle)
        console.log(this.arr[this.num])
        console.log(this.speed);

    }

    NewArr(MathNum,deg,turnBuffer){
        //计算转盘的各个角度参数
        let arr = [];
        for(let i = 1;i<=this.turnNum;i++){
            let num = MathNum-i;//做倒叙,跳过1
            if(i==1){num = i}
            let turnDeg = deg*i-deg; 
            arr.push([num,turnDeg+turnBuffer,turnDeg-turnBuffer]) ;
        }
        return arr;
    }

    async Timeout(time){
        //封装settimeout
        await new Promise( (resolve)=> { setTimeout(resolve,time)})
    }

    // asyncTimeout(time){
    //     //封装settimeout
    //     return new Promise( (resolve)=> { setTimeout(resolve,time)})
    // }

    OperatingDom(dom){
        //dom节点操作
        if(dom == 'rotate'){
            this.turntable.style.transform ="rotate("+this.initital+"deg)";
        }

        if(dom == 'innerHTML'){
            this.result.innerHTML = this.text;
        }
    }

    judgment(){
        //判断
        if(this.initital >= this.initialDegMini-800){
            if(this.speed>1.2){
                this.speed = this.speed-0.05;
            }
        }

        if(this.initital >= this.MathAngle ){
            this.OperatingDom('innerHTML')
            this.reset();
        }else{
                    
            // this对象的指向是可变的,但是在箭头函数中,它是固定的。方法一
            this.Timeout(this.times).then(()=>{
                this.star()
            })

            //方法二
            // this.asyncTimeout(this.star(),this.times);

            //方法三
            // setTimeout(()=>{
            //     console.log(111)
            //     this.star()
            // },this.times)
        
        }
    }

    reset(){
        //重置
        this.initital = this.MathAngle-( Math.trunc(this.MathAngle/360)*360);
        this.OperatingDom('rotate')
        this.num =  Math.ceil(Math.random()*12)-1;
        this.turns = Math.ceil(Math.random()*5+1);
        this.speed = Math.floor(Math.random()*3)+3;
        this.initialDegMini = this.turns*360+this.arr[this.num][2];
        this.initialDegMax = this.turns*360+this.arr[this.num][1];
        this.MathAngle = Math.ceil(Math.random()*(this.initialDegMax-this.initialDegMini) )+this.initialDegMini;
        this.flag = true;
        this.text = `结果为:${this.arr[this.num][0]} `;

    }

    star(){
        this.OperatingDom('rotate');//让转盘旋转
        this.initital+=this.speed;//增加角度
        this.judgment();//运行判断
    }

}

export default Turntable;

复制代码
  • 将CreateParameter的方法以及原型方法全部封装在 class 类里面并新建一个JS文件起名为 main3-class.js
  • 上文代码中对 setTiomeout 方法进行了封装,我发现有三种方法可以去实现。最后觉得 async 和 await方法挺有语义的,所以采用了此方法。其他的代码和原先两个版本无变化。

    //main3-constructor.js

    import Turntable from './main3-class.js';
    import GetClassName from './main3-configuration.js';
    
    const turntable = GetClassName('ul','turntable')[0];
    const result = GetClassName('p','result')[0];
    
    let ProxySingleParameter = (()=>{
    
        let  instance =  new Turntable(turntable,result);//存储参数
        let flag = instance.flag;//开关判断是否正在运行中
    
        return function (turntable,result){
            if(!flag){
                instance = new Turntable(turntable,result);//更新参数
                console.log(instance)
            }
            return instance;
        }
    
    })();
    
    export default  ProxySingleParameter ;

复制代码
  • 将单例控制方法也拆分成一个文件并命名成 main3-constructor.js 文件储存起来。
  • 也将函数方法缩写成了箭头函数。其他地方无变化。

    //main3.js

    import ProxySingleParameter  from './main3-constructor.js';

    window.onload = ()=>{
        document.onclick = (e) =>{
            let target = e.target || e.srcElement;
            if(target.className == 'internal'){
                let Par = new ProxySingleParameter;
                if(Par.flag){
                    Par.result.classList.remove('none');
                    Par.star()
                    Par.flag = false
                }else{
                    console.log(Par.arr[Par.num])
                }
            }
        }
    }

复制代码
  • 最后通过 main3.js 里的监听事件操作单例控制方法。即可完成整个程序的操作逻辑
  • 到这里 ES6 的写法改版已经完成

学习 ES6 我看的是阮一峰老师的这本电子版书籍 ECMAScript 6 入门


地址相关

GitHub源码地址

文章地址 (转载请附上这个地址噢)


小结

当写完这三种模式后,我对模块开发的了解又深入了些。以前普通写法是从上到下的写下去,想到什么功能就在后面加上。就这样,代码不停的累计下去。接着到了设计模式的写法,就好像一块蛋糕在上面画了线去区分开来。最后到ES6的写法,那么这块蛋糕不是画线的状态而是切成一块一快的样子。
最后大家对我的代码有什么建议、或者我在哪个地方写错了,写的不够好的地方。就指出来。我做好被吐槽的准备了,毕竟现阶段写的代码我已经达到了瓶颈,需要被指出来。最后最后,觉得不错的话就留下个脚印吧,感谢大家观看。