想要探索 Vue 前端框架嗎?本次的圈圈叉叉範例可以帶你一同進入 Vue 世界中,認識前端框架的強大與方便之處。圈圈叉叉是我們每個人小時候都有玩過的遊戲,規則簡單易上手,在本次範例中會實作出以下功能:
- 透過 Vue 產生九宮格的框框,當玩家點擊時會顯示出圈圈叉叉
- 遊戲過程中系統會不斷比對資料,找尋是否有贏家產生,當有贏家產生時會顯示出哪一方勝利
- 可將遊戲畫面清除,重新開始進行一局新的遊戲
- 顯示當下該輪到哪一位玩家進行出手
在開始製作之前你該知道的 Js 操作與 Css 屬性
Js操作:
Css排列版面的利器flex
- display: flex:預設會將子層水平排列不斷行
- flex-wrap: nowrap | wrap | wrap-reverse 預設為 nowrap,超過寬度的子元素是否換行
- flex-direction: row | row-reverse | column | column-reverse 主軸線的元素排列方向,預設為橫向順排 row
- justify-content: 元素在主軸上排列的方式
- align-items: 元素在副軸上排列的方式
事前準備
在Code Pen上開一個新的pen,將HTML的預處理器設定成Pug、CSS的預處理器設定成Sass、JS的CDN掛入Vue。
1. 用CSS建立外框線以及圈圈圖示
首先建立名稱為 size
的 mixin,可以讓後續在使用上比較方便,像是繪製框線。
//HTML .block
//CSS @mixin size($w, $h:$w) width: $w height: $h .block +size(150px) border: solid 1px
接著要來建立框線以及繪製圈圈的圖示,在繪製圈圈圖示上,會使用擬元素 before
與 after
,這裡可以嘗試在 content 中加上符號來顯示看看效果。不過如果沒有要顯示字元的話,像是本次範例所要的是畫幾何圖形,一樣需要加上 content,雙引號中間空白無文字,不可直接省略。
//HTML .block .block.circle
//CSS @mixin size($w, $h:$w) width: $w height: $h .block +size(150px) border: solid 1px &.circle &:after, &:before content: "" display: block
不管是圈圈與叉叉,都是由 :after
與 :before
這兩個偽元素左組成,在圈圈上,是兩個圓形疊在一起所產生視覺效果,透過將中間的圓圈所設定的顏與背景顏色相同,這樣看上去可產生甜甜圈形狀也就是圓形的效果。
這邊要注意的是,由於在圖層的排列順序上,:after
是在 :before
之上,所以比較大的圓是要寫在 :before
上,而中間的小圓則是寫在 :after
上。
在圓形的置中設定上,先將 :after
與 :before
設定成絕對定位,而由於絕對定位的位置規則是會透過尋找上層非 static 的區塊,當作定位的起始點。為了使 .circle
當成定位起的點,所以在母元素 .circle
加上相對定位 position: relative
。接著以左上角為參考點,向右以及向下各移動 50% ,再向上以及向左移動本身一半的長度 (transform: translate(-50%, -50%)
),這樣一來就完成置中了。
//CSS $color_blue: #46f $color_red: #f35 $color_bg: #222 @mixin size($w, $h:$w) width: $w height: $h html, body background-color: $color_bg margin: 0 +size(100%) .block +size(150px) border: solid 1px &.circle position: relative &:after, &:before content: "" display: block border-radius: 50% position: absolute left: 50% top: 50% transform: translate(-50%, -50%) &:before +size(90%) background-color: $color_red &:after +size(60%) background-color: $color_bg
2. 繼續使用CSS完成叉叉圖示
完成圈圈後,接著來做叉叉。
為了測試方便,所以就先以滑鼠移入區域,也就是 :hover
的方式來寫叉叉的效果。叉叉是由兩條長方形所組成的,所以將 size
的比例改成長方形,也將圓角設定變為 0px。
將長方形個別旋轉正負 45 度,形成交錯的叉叉,這邊可以發現,除了旋轉 (rotate
) 之外,也保留了在圓形中所設定的移動 (translate
)。原因在於,transform
會複寫掉前面的屬性,為了達到與圈圈一樣的效果,所以也必須指定移動的效果。
此時滑鼠移入有叉叉的效果了,但是切換時很生硬,要達成柔順切換的效果,只要在 &:after, &:before
上加上transition: 0.5s
就可以了。
//CSS &:after, &:before content: "" display: block position: absolute left: 50% top: 50% transform: translate(-50%, -50%) transition: 0.5s // 圈叉變化時間 0.5秒 &:hover // 與 .circle 同一層級 &:after, &:before +size(90%, 15%) background-color: $color_blue border-radius: 0px &:before transform: translate(-50%, -50%) rotate(45deg) &:after transform: translate(-50%, -50%) rotate(-45deg)
3. 調整圈叉的共用CSS屬性
在叉叉測試測試完畢後,由於剛剛 hover 只是用來測試,使用 hover 模擬沒問題後,就將其改為 .cross
,並在 html 新增 .cross
的元素。
//HTML .block .block.circle .block.cross
//CSS .block +size(150px) border: solid 1px position: relative &:after, &:before //change the after and before position content: "" display: block position: absolute left: 50% top: 50% transform: translate(-50%, -50%) transition: 0.5s &.circle &:after, &:before border-radius: 50% &:before +size(90%) background-color: $color_red &:after +size(60%) background-color: $color_bg &.cross &:after, &:before +size(90%, 15%) background-color: $color_blue border-radius: 0px &:before transform: translate(-50%, -50%) rotate(45deg) &:after transform: translate(-50%, -50%) rotate(-45deg)
4. 進入 Vue.js 世界,綁定建立的樣式
在正式進入 Vue.js 的世界前,我們先模擬使用 jQuery的情況 ,在Console中嘗試新增以及拿掉元素上的 class。
//Console // 抓取 .block.circle,有 circle 這個 class 就將其拿掉 $(".block.circle").toggleClass("circle") // 抓取 .block,無 circle 這個 class 就加上去 $(".block").toggleClass("circle")
假如使用 jQuery 來寫的話,首先會先建立一個長度為 9 的陣列 (var blocks = [1,0,-1]
),裡面放置的數字分別代表每一個格的狀態,比如說 1 代表圈圈、-1 代表叉叉、0 代表空,有了這些一串數字後,再根據數字去新增與刪減 Class。
這樣可以達到效果沒錯,但是如果可以當資料更新的時候,外觀也就自動更新,不需要自己再去新增與刪除 Class 的話,會是更好的方法,而我們接下來要使用的 Vue 便可以完成這部分。
在使用 Vue 的起手式上,首先需要一個溝通橋梁,類似搬運工的角色,名為 vm
,接著要定義作用的範圍,設定為 el: "#app"
,而在 HTML 最外面一層上加上 #app
。這樣就連接完畢,接下來在 Vue 中所做的資料更新都會做用到整個 HTML 上。
接下來要做的功能是,根據在 Js 中的資料,來判定在 HTML 上的元素要不要加上相對應的 Class。先處理 Vue 的部分,由於所設定的是資料相關,所以要擺放在 data{}
之中,裡面新增一個物件, key 為 blocks
,value 為 { type: -1 }
。
接著來到 HTML,在切換 Class 上,需要 v-bind
這個標籤,它是用來綁定與參數相關的,像是Class、href 就會使用它。後面雙引號部分則是來決定要加上的 Class 以及可否能加的條件 ("{ Class : 成立條件 }"
),這裡的 Class 不僅可設定一個,也可以是多個,只需要以逗號相隔開來即可。所以下方 HTML 中所代表的是,如果 type 是 1 的話,那就加上圈圈,而如果 type 是 -1 的話,那就加上叉叉。大家可以利用Console嘗試更新 blocks: { type: -1 }
中 type 數字(輸入 vm.blocks.type=1或0或-1
),來觀察外觀上的變化。
//JS var vm = new Vue({ el: "#app", data: { blocks: { type: -1 }, } });
//HTML #app .block(v-bind:class="{ circle: blocks.type == 1, cross: blocks.type == -1 }") //刪除先前的.block.circle及.block.cross
5. 用 Vue.js 的 Array 產生九個框框
在前一個步驟,完成了最左上角的單一方塊在外觀與資料的連結,但是圈圈叉叉是九宮格,所以老闆使用 Array.from
來產生九個 { type: 1 }
並存放到 Blocks 中,讓 Blocks 是長度為九的陣列。
接著我們來到了 HTML,回想起當需要存取陣列的每一個元素時,就使用迴圈,而迴圈中所設置的變數可依據需求自行定義,而在 Vue 中也是相同的概念,透過 v-for
來存取陣列中的每一個元素,使用方式為 v-for="自訂義名稱 in 陣列"
,由於 blocks 長度是九,所以 Vue 就會在畫面上產生九個.block
元素,在這裡老闆使用了 block 來當作識別的變數,所以在判斷 Class 上名稱就是使用 block.type == 1
,來判斷每一個 Block 所設置的 type 是不是等於 1。
由於現在設定上都是圈圈 (type: 1
),老闆後來改以隨機變數的方式產生 -1、0、1 ( type: 1 - parseInt(Math.random() * 3)
),這樣一來畫面上不同方格就會各自呈現不同的形狀。
//JS var vm = new Vue({ el: "#app", data: { blocks: Array.from({ length: 9 }, function () { return { type: 1 } }) } });
//HTML #app .block(v-for="block in blocks", v-bind:class="{circle: block.type == 1, cross:block.type == -1}")
v-for與v-bind的差別:v-for用來產生陣列裡的多個複製物,並存取或綁定陣列中的各個元素,v-bind:屬性可將符合條件的單個元素綁定屬性
6. 調整HTML及CSS排列成為九宮格
在步驟五,透過 v-for
自動產生九個方塊了,在方塊中也隨機擺放著不同的符號。不過在圈圈叉叉的設置上是水平三格與垂直三格所組成的九宮格,因此要在這組 .block
上再新增一個母元素取名為 .block_area
,用來規範整個九宮格的大小與位置。我們首先在每個框框中加入編號:
//HTML #app .block_area .block(v-for="(block,bid) in blocks", v-bind:class="{circle: block.type == 1, cross:block.type == -1}") .small_number {{ bid+1 }}
在 css 的設定上,由於裡面的框框預設上的寬高都是 150px,所以在外框的寬度上就乘上 3 倍。在排列方式上使用 flex,在預設上是當母元素也就是 .block_area
加上 flex
後,下層的子元素 .block
就會依序由左向右排列且不換行。為了達成換行的效果,會需要加上 flex-wrap: wrap
,這樣當寬度超過時,子元素就會自己自動往下排列了。
//CSS .block_area +size(150px*3) display: flex flex-wrap: wrap
結果此時會發現還是不太對,確實有往下排列了沒錯,但是卻以兩格兩格的方式排列。原因在於,在預設上每一個元素的寬度 = Set width + border + padding,以現在的框框來說就會是 150px +1px*2 = 152px,所以當排列到第三個框框時就會超出範圍,自然地就會向下排來排列。為了要讓指定的寬度 150px 涵蓋 border 的寬度,那就要再加上 box-sizing: border-box
就可以囉。
//CSS .block +size(150px) border: solid 1px rgba(white, 0.2) position: relative box-sizing: border-box
7. Vue.js中設定重新開始
要如何設定所有物件重置呢?回想一下在第五步驟中,提到框框中的圖形是根據 type 的數字來做變化的,所以要將畫面清空,僅需要將數值統一設定為零即可。然而將清空畫面屬於動作,是需要視情況不斷執行的,所以要將重置的設定寫在 methods 中。
設定好後,會發現畫面上的方格都消失了,原因在於一開始 Vue 的元件建立後,並不會自動執行寫在 methods 中的函式,為了讓元件建立後就初始化將每個方格方格設定為空,必須在 mounted()
也就是 Vue 初始化剛完成時,執行 restart()
函式。
//JS var vm = new Vue({ el: "#app", data: { blocks: [], }, mounted() { this.restart() }, methods: { restart() { this.blocks = Array.from({ length: 9 }, function () { return { type: 0 } }) } } });
8. 點擊後下棋
設定好九宮格初始化的格式後,就要來使用滑鼠點擊。前面有提到,在 Vue.js 中與樣式有關的會使用 v-bind
,而點擊是動作,這與事件有關則是使用 v-on。所以要達成當點擊框框內後會呈現圈圈的語法如下 v-on:click="block.type=1"
,表示當偵測到點擊時,就把 type 設定為 1 ,也就是圈圈的形式。此時會發現不管點擊畫面上哪一位置都會變成圈圈。
//HTML #app .block_area .block(v-for="(block,bid) in blocks", v-bind:class="{circle: block.type == 1, cross:block.type == -1}" v-on:click="block.type=1") .small_number {{ bid+1 }}
確定點擊效果沒問題後,接著來製作輪流下棋。輪流就是將 type 一開始設定為 1,再來就是 -1,並接續輪流交替,由於數值的切換是動作,所以透過將 v-on:click
後面包成一個動作 player_go(block)
,讓設定的細節到 Vue 中函式做處理。每當有方塊偵測到點擊時,就會傳送當下被點擊方塊設定 type,並且會交替的更換 turn 的數值,讓 turn 在 1 與 -1 兩者之間交替。可以注意到,在 data 中新增了資料 turn: 1
,代表遊戲開始是由圈圈這方開始,若遊戲要從叉叉開始,那只要改為 turn: -1
就可以囉。
//HTML #app .block_area .block(v-for="(block,bid) in blocks", v-bind:class="{circle: block.type == 1, cross:block.type == -1}" v-on:click="player_go(block)") .small_number {{ bid+1 }}
//JS var vm = new Vue({ el: "#app", data: { blocks: [], turn: 1 }, mounted() {... }, methods: { restart() {... }, player_go(block) { block.type = this.turn this.turn = -this.turn } } });
9. 顯示下棋者
在與九宮格同一的層級之下,新增一個 .block.small
的元素,另外會在綁定 vue 中的資料 turn,這樣一來加上相對應的 Class。另外,由於這只是作為提示框而非遊戲操作部分,所以將格子縮小會比較好看一些。
//HTML #app .block_area ... .block.small(v-bind:class="{circle: turn == 1, cross: turn == -1}")
//CSS .block +size(150px) border: solid 1px rgba(white, 0.2) position: relative box-sizing: border-box &.small //縮小格子 +size(60px)
10. 理出邏輯,再用Vue.js判斷贏家
本小節是此範例中最核心也是最有挑戰的的地方:要如何判斷有贏家產生,而贏家又是哪一方呢?以下先用圖示講解觀念,再著手進入程式。
在畫面上,每一格都有相對應的數值,圈圈是 1,叉叉是 -1,而都沒有劃記則是 0,那可以得知只要有某一條線的所有數值相加起來為 3 或是 -3 ,這就代表有連成一線。接著要考慮可連成一線的方式有哪些情況,下面以格子的數字做為表示,一共有八種情況,分別是 123 / 456 / 789 / 147 / 258 / 369 / 159 / 357,每當有玩家點選方格時,便會依序檢查上述八種情況的任一是否有連線成功。
接著在進入撰寫連線的程式前,先來認識在 Vue 中新的小夥伴 – computed,它會自動監看在 Vue 中的數值是否有更動了。當有內部有數值更新時,所設定的屬性也會隨之更新。下面以 user() 作為範例,當數值 turn 改變時, user() 所計算過後回傳的值也會改變,而這個值可放在 HTML 中來使用。
//JS var vm = new Vue({ ... methods: { restart(){... }, player_go(block) {... }, computed:{ user(){ return this.turn == 1? "O'turn" : "X'turn" } } } })
//HTML #app h1 {{ user }} ...
接下來就要開來寫連線判定的部分囉,先將前面所提到的連線情況存成一組字串,接著透過字串分割的方式形成陣列,並回傳數值顯示在畫面上:
//JS computed:{ pattern_data(){ var verify_list = "123,456,789,147,258,369,159,357" var result = verify_list.split(",") return result } }
//HTML #app h1 {{ pattern_data }} ...
為了可以顯示出每一個格子的序號與相對狀態,所以在原本顯示狀態的 type 之上加上屬性 id,由於程式是由 0 開始,格子的序號是由 1 開始,所以 id = i +1。
//JS restart(){ this.blocks = Array.from({ length: 9 }, function (d,i) { return { id: i+1, // 新增序號 type: 0 } }) },
這樣子九宮格所呈現的資料會是如下,總共九個物件,每一個物件都會有相對應 id
以及 type
。
[ { "id": 1, "type": 0 }, { "id": 2, "type": 0 }, { "id": 3, "type": 0 }, { "id": 4, "type": 0 }, { "id": 5, "type": 0 }, { "id": 6, "type": 0 }, { "id": 7, "type": 0 }, { "id": 8, "type": 0 }, { "id": 9, "type": 0 } ]
回到 pattern_data()
,依據剛剛所列出的驗證組合,取出相對應的物件,比如說:
[ { "id": 1, "type": 0 }, { "id": 2, "type": 0 }, { "id": 3, "type": 0 }]
在陣列的轉換上使用 map
,以 vtext
來代表每一項驗證的組合。由於是要將長度九的陣列依照條件轉換成各個長度為三的陣列,透過使用 filter
來進行來過濾,過濾的條件則是察看序號是否相同,這裡使用到 indexOf
,若有相同數值會回傳 1,否則則會回傳 -1 。
取出陣列組合後,可以點擊看看九宮格,確認 type
的是數值是會有變化的,而這個數值就是用來相加並進行判斷的,再次使用 map 來取出陣列中 type
數值的部分,並且透過 reduce
來計算三個數值相加總合。
//JS var verify_list = "123,456,789,147,258,369,159,357" var result = verify_list.split(",") .map((vtext)=>{ var add = this.blocks .filter((d, i) => vtext.indexOf(i + 1) != -1) .map((d) => d.type) .reduce((a, b) => (a + b), 0); return add; }) return result
下方圖片標示出在做完每一項操作後,所產生出的結果。
目前有個每一個驗證規則的數值加總,但是只需要回傳贏家的判斷即可,也就是 3 (三個圈成一線)與 -3 (三個叉成一線),所以使用到 .filter()
,判斷條件為只要絕對值等於 3 就代表有贏家產生了。除了知道有贏家外,也希望可以知道是哪一條線成立的,所以在回傳值的上面也加上了 rule: vtext
。
//JS var verify_list = "123,456,789,147,258,369,159,357" var result = verify_list.split(",") .map((vtext)=>{ var add = this.blocks .filter((d, i) => vtext.indexOf(i + 1) != -1) .map((d) => d.type) .reduce((a, b) => (a + b), 0); return { rule: vtext, value: add } }) result = result.filter((obj) => Math.abs(obj.value) == 3) return result
11. 在畫面上顯示贏家
有了贏家之後,需要顯示在畫面中,我們在 computed:{}
新增另一個 win_text()
的函式。在預設上 winner = -1,
代表沒有任何贏家產生,而當 this.pattern_data.length > 0
,則表示有贏家產生數值時,將 winner 的值替換,再依據是正 3 還是負 3 顯示贏家。
//HTML #app h1 {{ win_text }} // win text .block_area .block(v-for="(block,bid) in blocks", v-bind:class="{circle: block.type == 1, cross:block.type == -1}" v-on:click="player_go(block)") .small_number {{ bid+1 }} .block.small(v-bind:class="{circle: turn == 1, cross: turn == -1}")
//JS win_text() { var winner = -1 if (this.pattern_data.length > 0) { winner = this.pattern_data[0].value } if (winner == 3) { return 'O wins' } else if (winner == -3) { return 'X wins' } return (this.turn == 1 ? 'X' : 'O') + "' turn" }
12. Vue.js中加上「防止每格內的重複點擊」的機制
在贏家產生後,照理來說遊戲就該停止了,但是現在遊戲還是可以不斷進行,原因在於還沒有防止重複點選機制。解決方式很簡單,只需要在點選前加上判斷格子是否為空的 block.type == 0
就可以了。
//JS player_go(block) { if (block.type == 0) { block.type = this.turn; this.turn = -this.turn; } else { alert("Not allow") } }
13. 增加按鈕重新開始遊戲
在遊戲結束後,需要清空畫面才能將再次進行遊戲,所以在最下方加上重新開始遊戲的按鈕。前面有提到 Click 動作點擊要搭配的前綴字為 v-on:click
,這可以縮寫成 @click
,而清空的功能在步驟七的地方已經寫好了,所以這裡只需要呼叫就可以囉。
//HTML #app h1 {{ win_text }} .block_area .block(v-for="(block,bid) in blocks", v-bind:class="{circle: block.type == 1, cross:block.type == -1}" v-on:click="player_go(block)") .small_number {{ bid+1 }} .block.small(v-bind:class="{circle: turn == 1, cross: turn == -1}") h2(@click="restart") Restart // Restart 重新遊戲
14. 最後來CSS裡調整畫面
目前元素都靠向左側,我們希望調整到整個畫面的中心位置對齊,會比較好看。除了 html
與 body
要將子元素#app
排列置中, #app
裡的所有元素也必須排列整齊垂直置中,所以在元素設定上也一同加上了 #app
。
在排列上使用 flex,前面有提到,flex 預設上是橫列的由左向右排列,但是我們要的是由上而下的垂直排列,所以方向設定上改成 flex-direction: column
,在主軸 justify-content
以及 交叉軸 align-items
都設定為置中 center。
//CSS html, body, #app background-color: $color_bg margin: 0 color: white +size(100%) display: flex justify-content: center align-items: center flex-direction: column
//HTML h2 Player // 在顯示輪到誰的圖示前面加上文字 Player .block.small(v-bind:class="{circle: turn == 1, cross: turn == -1}")
結語
我們總結一下這次的圈圈叉叉遊戲的範例,製作過程可以分為五個部分:
- 以CSS擬元素(:before, :after)的方式繪製圈圈叉叉。
- 透過 Vue 的 Array 產生九個框框,並且透過綁定資料,讓畫面可以根據資料顯示對應的樣式。
- Restart – 重新遊戲,可在遊戲一開始或是遊戲中點擊 Restart 後清空畫面。
- 利用
v-on:click
選寫下棋功能。 - 以
computed:{}
計算出贏家成立的條件以及在畫面上顯示贏家。
這就是我們用 CSS及Vue.js 寫出來的圈圈叉叉遊戲啦!老闆的成品這邊去,也非常歡迎大家到社團裡跟我們分享你們完成的作品。
動畫互動網頁程式入門(HTML/CSS/JS)以簡單例子帶你入門網站的基礎架構及開發,用素材刻出簡單有趣又美觀的網頁和動畫,享受做出獨一無二的網頁所帶來的成就感,在職場上與設計師和工程師合作無間。
打好基本的互動網頁基礎之後,可以進階動畫互動網頁特效入門(JS/CANVAS),紮實掌握JavaScript 程式與動畫基礎以及進階互動,整合應用掌控資料與顯示的Vue.js前端框架,完成具有設計感的互動網站。長達3085分鐘,超過60個精緻範例與400張的投影片以上,以及四個加碼單元vue-cli、GSAP、D3、Three.js的投影片,成為hahow上最長的課程。
此篇直播筆記由幫手 阮柏燁 協助整理