原文鏈接: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
如果沒有安裝 buijs構建工具
, 可以直接點擊下載單頁工程包
2. 運行預覽效果
# 進入文件夾
cd bui-puzzle
# 安裝依賴
npm install
# 起服務預覽效果
npm run dev
這些是工程化的處理,這個遊戲只有一個頁面,也沒有數據請求那些,所以直接打開 index.html 就可以了。
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
main.js 輸入bui-store
,生成初始化,需要安裝 bui-fast
(vscode直接搜索插件bui-fast
安裝),把剛剛導出的模塊裏面的方法,複製到 methods
, templates
爲 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 對比
模板對比
模塊腳本對比
通過比對 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效果還不錯,但到了手機端就成了這樣,手機無法正常操作。
優化:去除引入 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 |
---|---|---|
最後一步
執行編譯壓縮,把工程生成 dist
目錄,這個就是用來發布的文件夾,代碼會執行轉換編譯成es5以及把js壓縮。
npm run build
源碼下載
該源碼放在bui-puzzle。喜歡就給顆星星吧。^_^
結語
謝謝閱讀!歡迎關注BUI Webapp專欄。BUI還有很多姿勢等待你的開啓。