JS寫的一個抽獎小Demo從普通寫法到設計模式再向ES6的進階路程

Scherger發表於2019-03-02

前言

寫這個小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的寫法,那麼這塊蛋糕不是畫線的狀態而是切成一塊一快的樣子。
最後大家對我的程式碼有什麼建議、或者我在哪個地方寫錯了,寫的不夠好的地方。就指出來。我做好被吐槽的準備了,畢竟現階段寫的程式碼我已經達到了瓶頸,需要被指出來。最後最後,覺得不錯的話就留下個腳印吧,感謝大家觀看。

相關文章