【BUI實戰篇】BUI數據驅動做的拼圖遊戲 Webapp移動適配版,基於vuejs拼圖遊戲改造

原文鏈接:https://segmentfault.com/a/1190000019865515

前言

如果你還不瞭解bui是什麼?建議先看看這兩篇文章。

bui.store 是bui的核心一部分,基於DOM的數據驅動。爲了讓熟悉vuejs更容易上手,接口參照 vuejs 的api設計。剛好看到一個有意思的基於vuejs設計的拼圖遊戲,於是動手轉換,把整個開發過程記錄下來,並做了移動端適配,優化,不費吹灰之力,拼圖源碼的創意歸原作者所有。

玩一玩

拼圖遊戲

在線拼圖玩一玩

代碼分析

vuejs版核心

該拼圖遊戲項目來源於:https://github.com/luozhihao/...

App.vue 核心代碼:具體的實現可以查看源碼,這裏只保留基本的結構。

<template>
    <div class="box">
        <ul class="puzzle-wrap">
            <li 
                :class="{'puzzle': true, 'puzzle-empty': !puzzle}" 
                v-for="puzzle in puzzles" 
                v-text="puzzle"
                @click="moveFn($index)"
            ></li>
        </ul>
        <button class="btn btn-warning btn-block btn-reset" @click="render">重置遊戲</button>
    </div>
</template>

<script>
export default {
    data () {
        return {
            puzzles: []
        }
    },
    methods: {
        // 重置渲染
        render () {
            //部分省略
            this.puzzles = puzzleArr
            this.puzzles.push('')
        },
        // 點擊方塊
        moveFn (index) {
            //省略
        },
        // 校驗是否過關
        passFn () {
            //省略
        }
    },
    mounted () {
        this.render()
    }
}
</script>
樣式部分代碼暫時去掉

從零開始,基於bui.store改造過程

1. 新建BUI工程,工程名:bui-puzzle

# 創建工程
buijs create bui-puzzle

clipboard.png

如果沒有安裝 buijs構建工具, 可以直接點擊下載單頁工程包

2. 運行預覽效果

# 進入文件夾
cd bui-puzzle

# 安裝依賴
npm install

# 起服務預覽效果
npm run dev 
這些是工程化的處理,這個遊戲只有一個頁面,也沒有數據請求那些,所以直接打開 index.html 就可以了。

clipboard.png

3. 修改模板 main.html

瞭解bui的都知道,單頁工程裏面,打開index.html 默認會加載 pages/main/main.html ,pages/main/main.js 模板,所以我們接下來要更改這2個文件,一個是模板,一個是邏輯。

main.html

我們把它改成這樣,去掉頂部圖標,修改標題,去掉footer標籤,把vuejs版的內容複製過來,放在main標籤裏面,做以下修改。

<div class="bui-page">
    <header class="bui-bar">
        <div class="bui-bar-left"> </div>
        <div class="bui-bar-main">BUI拼圖遊戲-數據驅動</div>
        <div class="bui-bar-right"> </div>
    </header>
    <main>
        <div class="box">

            <ul class="puzzle-wrap" b-template="page.tplGame(page.puzzles)"></ul>

            <button class="bui-btn" b-click="page.render">重置遊戲</button>
        </div>
    </main>
</div>
說明:bui-page是標準的頁面結構,main標籤是滾動的內容容器;b-開頭的是 bui.store的行爲屬性,b-template爲模板,b-click爲事件。值爲 page.xxx page可以理解爲自定義的作用域,初始化的時候的 scope參數。

4. 修改模塊 main.js

初始化bui.store

main.js 輸入bui-store,生成初始化,需要安裝 bui-fast(vscode直接搜索插件bui-fast安裝),把剛剛導出的模塊裏面的方法,複製到 methodstemplates 爲 main.html 指向的模板名tplGame

loader.define(function(require, exports, module) {

    // 初始化數據行爲存儲, this 指向當前模塊, bs 爲 Behavior Store 簡稱。
    let bs = bui.store({
        //el: ".bui-page",
        scope: "page",
        data: {
            puzzles: []
        },
        methods: {
            // 重置渲染
            render() {
                //部分省略
                puzzleArr.push('');
                // 這裏不能直接採用賦值,需要使用數組提供的方法才能觸發視圖更新
                this.puzzles.$replace(puzzleArr)
            },

            // 點擊方塊
            moveFn(index) {
                //省略
            },

            // 校驗是否過關
            passFn() {
                //省略
            }
        },
        watch: {},
        computed: {},
        templates: {
            tplGame(data) {
                // b-template指向當前模板
                var html = "";
                
                // 返回結構
                return html;
            }
        },
        mounted: function() {
            // 數據解析後執行
            this.render();
        }
    })
})

參數說明

  • el:".bui-page" 默認指向 .bui-page ,使用標準結構則不用管這個參數;
  • scope:"page" 默認可以都寫page, 這個可以理解爲作用域,寫模板的時候,需要以這樣 page.xxx 指向對應的方法或者數據;
  • templates:{} 爲 b-template 指向的模板方法名,需要返回對應的結構;

其它基本跟vuejs保持一致。初始化必須寫在 loader.define 裏面。

最終代碼

main.html

<style>
@import url('css/bootstrap.min.css');

body {
    font-family: Arial, "Microsoft YaHei"; 
}

.box {
    width: 400px;
    margin: 50px auto 0;
}

.puzzle-wrap {
    width: 400px;
    height: 400px;
    margin-bottom: 40px;
    padding: 0;
    background: #ccc;
    list-style: none;
}

.puzzle {
    float: left;
    width: 100px;
    height: 100px;
    font-size: 20px;
    background: #f90;
    text-align: center;
    line-height: 100px;
    border: 1px solid #ccc;
    box-shadow: 1px 1px 4px;
    text-shadow: 1px 1px 1px #B9B4B4;
    cursor: pointer;
}

.puzzle-empty {
    background: #ccc;
    box-shadow: inset 2px 2px 18px;
}

.btn-reset {
    box-shadow: inset 2px 2px 18px;
}
</style>
<div class="bui-page">
    <header class="bui-bar">
        <div class="bui-bar-left"> </div>
        <div class="bui-bar-main">BUI拼圖遊戲-數據驅動</div>
        <div class="bui-bar-right"> </div>
    </header>
    <main>
        <div class="box">

            <ul class="puzzle-wrap bui-fluid-4" b-template="page.tplGame(page.puzzles)"></ul>

            <button class="bui-btn warning round" b-click="page.render">重置遊戲</button>
        </div>
    </main>
</div>

main.js

/**
 * 拼圖遊戲
 * 默認模塊名: main
 */
loader.define(function(require, exports, module) {

    // 初始化數據行爲存儲
    var bs = bui.store({
        scope: "page",
        data: {
            puzzles: []
        },
        methods: {
            // 重置渲染
            render:function() {
                let puzzleArr = [],
                    i = 1

                // 生成包含1 ~ 15數字的數組
                for (i; i < 16; i++) {
                    puzzleArr.push(i)
                }

                // 隨機打亂數組
                puzzleArr = puzzleArr.sort(() => {
                    return Math.random() - 0.5
                });

                // 頁面顯示,前面聲明是數組以後,不能使用賦值監聽到變更,先處理再替換,這樣只觸發一次視圖變更
                puzzleArr.push('');
                this.puzzles.$replace(puzzleArr)
            },

            // 點擊方塊
            moveFn:function (index) {

                // 獲取點擊位置及其上下左右的值
                let curNum = this.puzzles[index],
                    leftNum = this.puzzles[index - 1],
                    rightNum = this.puzzles[index + 1],
                    topNum = this.puzzles[index - 4],
                    bottomNum = this.puzzles[index + 4]

                // 和爲空的位置交換數值
                if (leftNum === '' && index % 4) {
                    this.puzzles.$set(index - 1, curNum)
                    this.puzzles.$set(index, '')
                } else if (rightNum === '' && 3 !== index % 4) {
                    this.puzzles.$set(index + 1, curNum)
                    this.puzzles.$set(index, '')
                } else if (topNum === '') {
                    this.puzzles.$set(index - 4, curNum)
                    this.puzzles.$set(index, '')
                } else if (bottomNum === '') {
                    this.puzzles.$set(index + 4, curNum)
                    this.puzzles.$set(index, '')
                }

                this.passFn()
            },

            // 校驗是否過關
            passFn:function () {
                if (this.$data.puzzles[15] === '') {
                    const newPuzzles = this.puzzles.slice(0, 15)

                    const isPass = newPuzzles.every((e, i) => e === i + 1)

                    if (isPass) {
                        alert ('恭喜,闖關成功!')
                    }
                }
            }
        },
        watch: {},
        computed: {},
        templates: {
            tplGame: function (data) {
                var html = "";
                data.forEach(function (puzzle,index) {
                    var hasEmpty = !puzzle ? "puzzle-empty" : "";
                     html +=`<li class="puzzle ${hasEmpty}" b-click="page.moveFn($index)">${puzzle}</li>`
                })
                return html;
            }
        },
        mounted: function(){
            // 數據解析後執行
            this.render();
        }
    })
})
這裏我們的方法定義改爲了 es5的寫法,移動端有些對es6並不友好。

vuejs 跟 bui.store 對比

模板對比

html

模塊腳本對比

js

通過比對 vuejs 版跟 bui.store版本,你會發現,裏面的業務方法幾乎不用改變,重點在vuejs 模板引擎轉換成 bui的模板的思路轉換,bui模板裏面使用 es6模板引擎,每次數據改變都會把模板重新渲染一遍,無需通過虛擬Dom去比對改變的位置。

vuejs

  • 雙向綁定
  • 虛擬Dom
  • 單文件組件
  • 模板引擎

bui.store

  • 雙向綁定;
  • 真實Dom,可以跟Dom的UI更好配合;
  • 組件分離,模板及邏輯,這樣模板及腳本可以得到緩存,按需加載,也支持以單文件組件加載;
  • 無模板引擎,模板的渲染在腳本,無需過多關注模板,當然你可以使用第三方模板引擎配合;
  • 支持私有數據及公有數據;
  • 模塊的訪問是並行的,各自管自己,之間的訪問無需像組件樹一樣,從上到下傳參;

優化

bui.store是基於真實Dom的數據驅動。當數據改變以後,會找到對應的選擇器做對應的變更。上面的例子如果你在 tplGame模板方法裏面輸出,你會發現,每次點擊一個方塊,會有2次觸發,意味着有2次Dom渲染,這個應該避免。

優化後的 main.js

loader.define(function(require, exports, module) {

    // 初始化數據行爲存儲
    let bs = bui.store({
        scope: "page",
        data: {
            puzzles: []
        },
        methods: {
            // 重置渲染
            render: function() {

                let puzzleArr = [],
                    i = 1

                // 生成包含1 ~ 15數字的數組
                for (i; i < 16; i++) {
                    puzzleArr.push(i)
                }

                // 隨機打亂數組
                puzzleArr = puzzleArr.sort(() => {
                    return Math.random() - 0.5
                });

                // 頁面顯示,前面聲明是數組以後,不能使用賦值監聽到變更,先處理再替換,這樣只觸發一次視圖變更
                puzzleArr.push('');
                // 這裏如果直接賦值不會觸發視圖更新
                // this.puzzles = puzzleArr;
                this.puzzles.$replace(puzzleArr);

            },

            // 點擊方塊
            moveFn: function(index) {
                // 獲取點擊位置及其上下左右的值
                let curNum = this.$data.puzzles[index],
                    leftNum = this.$data.puzzles[index - 1],
                    rightNum = this.$data.puzzles[index + 1],
                    topNum = this.$data.puzzles[index - 4],
                    bottomNum = this.$data.puzzles[index + 4]

                // 和爲空的位置交換數值
                if (leftNum === '' && index % 4) {
                    // 只修改數據,不觸發視圖
                    this.$data.puzzles[index - 1] = curNum;
                    this.$data.puzzles[index] = '';
                    // 觸發一次視圖
                    this.puzzles.$replace(this.$data.puzzles);
                } else if (rightNum === '' && 3 !== index % 4) {
                    // 只修改數據,不觸發視圖
                    this.$data.puzzles[index + 1] = curNum;
                    this.$data.puzzles[index] = '';

                    // 觸發一次視圖
                    this.puzzles.$replace(this.$data.puzzles);

                } else if (topNum === '') {
                    // 只修改數據,不觸發視圖
                    this.$data.puzzles[index - 4] = curNum;
                    this.$data.puzzles[index] = '';
                    // 觸發一次視圖
                    this.puzzles.$replace(this.$data.puzzles);
                } else if (bottomNum === '') {
                    // 只修改數據,不觸發視圖
                    this.$data.puzzles[index + 4] = curNum;
                    this.$data.puzzles[index] = '';
                    // 觸發一次視圖
                    this.puzzles.$replace(this.$data.puzzles);
                }
                this.passFn()
            },

            // 校驗是否過關
            passFn: function() {
                if (this.$data.puzzles[15] === '') {
                    const newPuzzles = this.$data.puzzles.slice(0, 15)
                    const isPass = newPuzzles.every((e, i) => e === i + 1)

                    if (isPass) {
                        bui.alert('恭喜,闖關成功!')
                    }
                }
            }
        },
        watch: {},
        computed: {},
        templates: {
            tplGame: function(data) {
                var html = "";
                data.forEach(function(puzzle, index) {
                    var hasEmpty = !puzzle ? "puzzle-empty" : "";
                    html += `<li class="puzzle ${hasEmpty}" b-click="page.moveFn($index)">${puzzle}</li>`
                })
                return html;
            }
        },
        mounted: function() {
            // 數據解析後執行
            this.render();
        }
    })
})

優化說明

  • this.puzzles 改成了 this.$data.puzzles,這裏數據的訪問只有一層,所以沒有出現問題;
  • 用來訪問的值應該使用 this.$data.puzzles
  • this.abc = 123 的時候, this.$data.abc === 123 ;
  • this.$data.abc = 123; this.abc 還是等於原來的值;
  • this.abc 會觸發視圖更新,this.$data.abc 則不會。

例子:

這裏修改一次數據,改成就會觸發一次視圖更新,造成2次頁面渲染。

this.puzzles.$set(index + 1, curNum); this.puzzles.$set(index, '');

優化:

// 只修改數據,不觸發視圖
this.$data.puzzles[index + 4] = curNum;
this.$data.puzzles[index] = '';
// 最後觸發一次視圖
this.puzzles.$replace(this.$data.puzzles);

適配

這個效果在PC效果還不錯,但到了手機端就成了這樣,手機無法正常操作。

clipboard.png

優化:去除引入 bootstrap樣式, 按照BUI的750設計規範,把效果圖轉成 rem 單位。1rem=100px,寬度只要不大於750px就不會有橫向滾動條。把原本的float佈局也改爲 bui-fluid-4列布局,裏面只需要定義好高度就可以了。

<style type="text/css">
    .box {
        width: 6.8rem;
        margin: .4rem auto 0;
    }
    
    .puzzle-wrap {
        width: 100%;
        height: 6.8rem;
        margin-bottom: .4rem;
        padding: 0;
        background: #ccc;
    }
    
    .puzzle {
        width: 1.7rem;
        height: 1.7rem;
        font-size: .4rem;
        background: #f90;
        text-align: center;
        line-height: 1.7rem;
        border: 1px solid #ccc;
        box-shadow: 1px 1px 4px;
        text-shadow: 1px 1px 1px #B9B4B4;
        cursor: pointer;
    }
    
    .puzzle-empty {
        background: #ccc;
        box-shadow: inset 2px 2px 18px;
    }
</style>
<div class="bui-page">
    <header class="bui-bar">
        <div class="bui-bar-left"> </div>
        <div class="bui-bar-main">BUI拼圖遊戲-數據驅動</div>
        <div class="bui-bar-right"> </div>
    </header>
    <main>
        <div class="box">
            <ul class="puzzle-wrap bui-fluid-4" b-template="page.tplGame(page.puzzles)"></ul>

            <button class="bui-btn warning round" b-click="page.render">重置遊戲</button>
        </div>
    </main>
</div>

優化後的適配效果圖:

iPhone5 iPhone678 iPhone678 Plus
iphone5適配 iphone678 iphone678p

最後一步

執行編譯壓縮,把工程生成 dist 目錄,這個就是用來發布的文件夾,代碼會執行轉換編譯成es5以及把js壓縮。

npm run build

源碼下載

該源碼放在bui-puzzle。喜歡就給顆星星吧。^_^

結語

謝謝閱讀!歡迎關注BUI Webapp專欄。BUI還有很多姿勢等待你的開啓。

專欄往期熱門文章:

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章