
這次直播的主題來自於老闆在2018年印象清華-物聯網科技藝術展中創作的展品〈英文8-2〉,其概念為十根代表清大不同學院的光柱,使用者只要掃描光柱上的QR Code就會跳出互動式的英文單字題目,只要答對題數越多、分數就越多,累積的分數便會即時地投射到光柱上,形成高高低低、動態交錯的有趣光景。
在這次直播中會來聊些這個專案內使用Vue的相關經驗,聊聊製作互動裝置藝術實作時整合的各種辛酸血淚史,以及如何快速地解析別人資料,利用Vue框架製作成幫你找回國中逝去英文能力的遊戲。
我們將以Code Pen做為本次實作的平台,這是一個可以在創作的當下即時看到程式碼運作狀況的線上程式碼編輯器,只要簡單註冊就可以使用囉!
如果想搭配直播影片一起實作,請往這邊走 👉🏻 https://www.youtube.com/watch?v=maFbo96YT8U
前期準備
在開始之前我們根據概念來進行規劃,想像一個英文單字的互動答題App需要哪些東西:
- 整理網路上現成的單字表,把資料變成符合我們條件的JSON格式物件,單字表必須同時具備英文、中文與詞性(今天借用的是109英文銜接教材2000單字)
- 產生隨機的英文題目,並利用整理後的物件選出正確答案和其他類似的詞當作選項,並判斷答題者的正確與否
- 如果答題者正確,跟後端同步狀態累加分數
首先在Code Pen上開一個新的pen,將HTML的預處理器設定成Pug、CSS的預處理器設定成Sass、JS的CDN掛入Vue。

接著把單字表上的文字複製下來貼到Javascript中,var一個a,並且用ES6的頓號 ` 把文字包起來。
//Javascript var a = ` A able adj. 有能力的 about prep. 有關 above adv. 在上方 `
註:考慮篇幅關係這邊只貼上部分A字首的單字,實際資料請參考單字表。
我們快速分析一下他的架構組成,參考字首A的部分得知單字表主要可以分為:字首的段落開頭、英文、詞性、中文,也就是──只要是沒有英文單字的那一行就不會有「.」,如果說我們今天要把單字整理成一個一個的物件時,可以把每一行先分割出來、把含有「.」的留下,再分別拆解成英文、詞性、中文,這就是我們所需的資料。
1. 拆解單字表
利用語法split
以空行來做分隔,再用語法filter
把含有.的行過濾保留下來,利用list.length
或list[n]
在console查看過濾後的list
和list2
數量上的差異,代表我們的資料越來越乾淨了。利用語法map
把原先陣列的一行一行轉化成一個一個,再存成另一個陣列,轉換的條件為用空格分割,console會發現list3
裡面裝著一坨拉庫的[object Array](3)
。再把list3
拆分成word
、cata
、trans
,分別對應英文、詞性、中文的物件。
//Javascript var list = a.split("\n") //分割空行 var list2 = list.filter(item=>item.indexOf(".")!=-1) //過濾沒有. var list3 = list2.map(item=>item.split(" ")) //單行轉單個 var list4 = list3.map(item=>({ english: item[0], cata: item[1], trans: item[2] })) //拆分成英文、詞性、中文
資料搬運小幫手Vue
Vue的特色在於資料雙向綁定,相較於jQuery需要選取物件、重新定義、再塞回去以直接操作 DOM 物件為主的方式,利用Vue的同步更新渲染資料可以幫助我們節省不少時間。Vue的寫法為在JS透過 new Vue建立作用範圍和待即時同步的資料,同時在HTML以{{}}
包裹被更新的變數。以下為官方網站所舉的範例,Vue會將大括號{{}}
的內容對應到message
狀態,並且將之即時渲染至畫面上,也就是所指的「宣告式渲染」。
//HTML <div id="app"> {{ message }} </div>
//Javascript var app = new Vue({ el: '#app', data: { message: 'Hello Vue!' } })
延伸閱讀: 「Vue.js 學習筆記 Day1」- 建立 Vue 應用程式 重新認識 Vue.js | Kuro Hsu
2. 實際運用Vue-單字小卡
運用Vue和剛剛整理好的單字表資料來試做一些英文小卡吧!取出list4
中倒數20個單字用v-for
迭代陣列中的物件,指定其資料種類並渲染在li
的span,再給一些CSS的參數後就可以看到一張張排列整齊的你國中的惡夢單字小卡。透過Vue的幫忙,我們不用自己產生元件跟呈現,只需要確定資料是否正確即可。
//HTML #app h2 我的名字是{{name}} ul li(v-for="word in words") span {{word.english}} {{word.cata}} {{word.trans}} //指定word中的種類
//CSS html,body background-color: #222 ul li background-color: #fff padding: 20px display: inline-block margin: 20px width: 200px
//Javascript var vm = new Vue({ el: '#app', data: { name: "Frank", catas: ["a","b","c","d"], words: list4.slice(-20) //負號代表從後面數來的20個單字 } })

3. 製作答題選項
製作完單字小卡有沒有覺得長得很像我們的答題選項呢?在Vue中我們定義methods
為操作不同 DOM 元素的方法,這邊需要綁定幾個動作:
click
DOM元素根據滑鼠點擊的動作,在console回傳所點擊的單字。getOptions
挑選同詞性、同字首的隨機四個單字。用filter
過濾word.cata == question.cata
也就是詞性需與答案相同,過濾第二次word.english[0] == question.english[0]
英文單字的字首(第0個字)需相同,過濾第三次word.english !== question.english
確認答案不會等於題目。- 使用
sort
把陣列的順序打亂:.sort((a,b)⇒a-b)
是將大的往後排,但sort((a,b)=>Math.random())
則是隨機取值,再扣掉比較函數0.5後成為真正隨機排序的陣列,加上.slice(0,4)
限縮在一次只取四個單字。 - 在
.sort
前面加入第二個.slice()
把原本的元素複製一份成新的陣列,避免影響到既有陣列的順序,雖然針對新的陣列動屬性依然會影響原先的物件,但兩個陣列的順序是分開的。 - 亂數打亂正確答案的位置,目前都是把正確答案推到最前面,取得
result
後用concat
連接question
這個陣列,再打亂一次排序,成為result2
。
//HTML #app h2 我的名字是{{name}} ul li(v-for="word in words", v-on:click="click(word.english)") //讓console顯示出滑鼠點擊到哪個英文單字 span {{word.english}} {{word.cata}} {{word.trans}}
//Javascript var vm = new Vue({ el: '#app', data: { name: "Frank", catas: ["a","b","c","d"], words: list4 }, methods:{ click(word){ console.log("click",word) }, getOptions(question){ let result = this.words.filter( word => word.cata == question.cata).filter( word => word.english[0] == question.english[0]).filter( word => word.english !== question.english ).slice().sort((a,b)=>Math.random()-0.5).slice(0,4) let result2 = result.concat([question]).slice().sort((a,b)=>Math.random()-0.5) return result2 } } })
註:concat只能做陣列與陣列的連接。
4. 建立一個出題目按鈕,產生新題目
在methods
新增pick()
,在data
中定義還沒開始之前question: null
,用this
存取本身的資料屬性,隨機選取陣列裡的其中一個字,記得因為index須為整數所以加上parseInt
。
//HTML #app button(@click="pick") 出題囉 h2(v-if="question") {{ question.english }}
//CSS html,body background-color: #222 color: #fff
//Javascript var vm = new Vue({ el: '#app', data: { ... question: null }, methods:{ click(word){ console.log("click",word) }, pick(){ this.question = this.words[parseInt(Math.random()*this.words.length)] }, getOptions(question){ ... } } })

5. 建立選項
把單字的中文抓出來印成題目,同時也把選項抓出來存取,一開始會是空的陣列所以在data
定義options: []
,再在pick()
中多加一行程式碼把產生的新題目裝回去。
//HTML #app button(@click="pick") 出題囉 h2(v-if="question") {{ question.trans }} ul li(v-for="option in options") {{option.english}}
//CSS中要先把li的樣式暫時註解掉
//Javascript var vm = new Vue({ el: '#app', data: { ... question: null, options: [] }, methods:{ click(word){ ... }, pick(){ this.question = this.words[parseInt(Math.random()*this.words.length)] this.options=this.getOptions(this.question) }, ... } })

6. 判斷答案正確與否
比較簡單的做法是在產生資料時同時附加他是否正確的資訊在其中,我們複製一份新的question
避免影響原本的,在let result2
前面加上let questionClone = JSON.parse(JSON.stringify(question))
,再把帶有正確與否屬性的物件混到原有的選項中,把questionClone
作為判斷的正確答案,而result2
中原本的question
也要記得替換成questionClone
。
在methods
新增check(option)
,點擊選項時如果正確,console印出correct、不正確則印出wrong,回答完後再重新出題this.pick()
。
//HTML ... ul li(v-for="option in options", @click="check(option)") {{option.english}}
//Javascript ... methods:{ click(word){ console.log("click",word) }, check(option){ if (option.correct){ console.log("correct") }else{ console.log("wrong") } this.pick() //點選答案無論對錯都會換下一題 }, pick(){ ... }, getOptions(question){ let result = this.words.filter( word => word.cata == question.cata).filter( word => word.english[0] == question.english[0]).filter( word => word.english !== question.english ).slice().sort((a,b)=>Math.random()-0.5).slice(0,4) let questionClone = JSON.parse(JSON.stringify(question)) questionClone.correct=true let result2 = result.concat([questionClone]).slice().sort((a,b)=>Math.random()-0.5) return result2 } } })

7. 增加答題分數計算機制以及顯示正確或錯誤
要增加答題分數grade的計算機制,首先在data
中定義grade: 0
,在check(option)
中如果答對了就加一分this.grade++
,並在HTML中顯示。
//HTML #app h3 Score:{{grade}} ...
//Javascript ... data: { ... grade: 0 }, methods:{ click(word){ ... }, check(option){ if (option.correct){ console.log("correct") this.grade++ }else{ console.log("wrong") } this.pick() }, pick(){ ... } }
目前答題正確與否只能靠分數是否有增加得知,要改成更直觀一點,點擊選項時如果正確,在題目右邊會印出correct並累加分數this.grade++
、不正確則印出wrong,我們使用一個預設是空字串的變數status
去儲存這個資訊,status
設定過1秒後消失,回答完後再重新出題this.pick()
。
通常使用者在進到介面時題目已經出好了,答題後會自動更新,所以頁面剛載入時便自動執行一次pick
,mounted
代表Vue已經準備好可以幫忙計算資料了,所以跟methods
、data
、el
在同一層級。
//HTML #app .container .row .col-sm-12 h3 Score:{{grade}} h2(v-if="question") Q: {{question.trans}} .status {{status}} ul li(v-for="option in options", @click="check(option)") {{option.english}}
//Javascript var vm = new Vue({ el: '#app', data: { name: "Frank", catas: ["a","b","c","d"], words: list4, question: null, options: [], status: "", grade: 0 }, mounted(){ this.pick() }, methods:{ click(word){ console.log("click",word) }, check(option){ if (option.correct){ this.status =("correct") this.grade++ }else{ this.status = ("wrong") } setTimeout(()=>{ this.status="" this.pick() },1000) this.pick() }, pick(){ this.question = this.words[parseInt(Math.random()*this.words.length)] this.options = this.getOptions(this.question) }, getOptions(question){ let result = this.words.filter( word => word.cata == question.cata).filter( word => word.english[0] == question.english[0]).filter(word => word.english !== question.english).slice().sort((a,b)=>Math.random()-0.5).slice(0,4) let questionClone = JSON.parse(JSON.stringify(question)) questionClone.correct = true let result2 = result.concat([questionClone]).sort((a,b)=>Math.random()-0.5) return result2 } } })

8. 設計畫面及加入動畫
完成骨幹後我們來稍微美化一下吧!在codepen設定CSS的地方引入Bootstrap和Animate.css,把DOM元素放到container
內,讓版面豐富一些可以加入hover的滑鼠互動效果和答對或答錯時相對應的變色效果。
變色效果我們可以利用Vue的特性來製作──透過判斷式給予class,也就是判斷當前的status
為correct或wrong,如果是correct則顯示綠色、wrong則顯示橘色,必須注意的是由於其他四個錯誤答案的status
同時都會是wrong,所以要多下一個判斷點currentOption
記錄只有點選的這個選項是wrong時才顯示橘色。在data
定義currentOption: {}
、methods
的check(option)
的加入條件this.currentOption = option
。

淡入的效果我們則透過Animate.css和Vue的key來製作,每次物件重新產生時都帶有新的english
值,所以我們給予的key
值也會不一樣,這樣他的animated.fadeIn
效果就會重新被載入。
//HTML #app .container .row .col-sm-12 h3 Score:{{grade}} h2.animated.fadeIn(v-if="question",:key="question.english") Q: {{question.trans}} .status {{status}} ul.animated.fadeIn(:key="question.english") li(v-for="option in options", @click="check(option)",:class="{correct: status=='correct'&& option.correct, error:status=='wrong' && currentOption.english==option.english}") {{option.english}}
//CSS ul list-style: none padding: 0 li padding: 10px margin-top: 20px border: 1px solid white cursor: pointer font-size: 30px transition: .5s &:hover background-color: rgba(white,0.1) &.correct background-color: #38d138 &.error background-color: #ff7332 .status float: right
//Javascript ... var vm = new Vue({ el: '#app', data: { name: "Frank", catas: ["a","b","c","d"], words: list4, question: null, currentOption: {}, //給予{}以防報錯 options: [], status: "", grade: 0 }, mounted(){ this.pick() }, methods:{ click(word){ console.log("click",word) }, check(option){ this.currentOption = option //加入條件 if (option.correct){ this.status =("correct") this.grade++ }else{ this.status = ("wrong") } setTimeout(()=>{ this.status="" this.pick() },1000) }, pick(){ this.question = this.words[parseInt(Math.random()*this.words.length)] this.options = this.getOptions(this.question) }, getOptions(question){ let result = this.words.filter( word => word.cata == question.cata).filter( word => word.english[0] == question.english[0]).filter(word => word.english !== question.english).slice().sort((a,b)=>Math.random()-0.5).slice(0,4) let questionClone = JSON.parse(JSON.stringify(question)) questionClone.correct = true let result2 = result.concat([questionClone]).sort((a,b)=>Math.random()-0.5) return result2 } } })
以上就是這次的英文2000字即時互動小遊戲網頁的製作介紹拉,這次講解了許多Vue的基礎概念與用法,如果是剛開始接觸Vue的朋友很適合拿來小練身手唷!我們下次再見啦~
成品請參考這邊 👉🏻 https://codepen.io/frank890417/pen/RygVde

重點回顧:
- 利用filter()、sort()、slice()整理與排序資料
- 宣告式渲染的使用方式,資料與function的對應關係
- 動態判斷class製作css animation效果
動畫互動網頁程式入門(HTML/CSS/JS)以簡單例子帶你入門網站的基礎架構及開發,用素材刻出簡單有趣又美觀的網頁和動畫,享受做出獨一無二的網頁所帶來的成就感,在職場上與設計師和工程師合作無間。
打好基本的互動網頁基礎之後,可以進階動畫互動網頁特效入門(JS/CANVAS),紮實掌握JavaScript 程式與動畫基礎以及進階互動,整合應用掌控資料與顯示的Vue.js前端框架,完成具有設計感的互動網站。長達3085分鐘,超過60個精緻範例與400張的投影片以上,以及四個加碼單元vue-cli、GSAP、D3、Three.js的投影片,成為hahow上最長的課程。
此篇直播筆記由幫手 Jeudi Kuo 協助整理
