新手JavaScript地下城第三關 - 計算機
系列文的第三篇,這次回過頭來完成前一關的線上計算機。
- 首先,快速瀏覽一下設計稿,可以發現大致上分為三個區塊:
- 上方的算式和當下結果顯示
- 中間的數字按鍵和四則運算符號
- 下方的功能按鍵,含清除(AC)、倒退(backspace)、等於符號
重要的處理步驟和技巧
HTML, CSS
- HTML 結構設計和 CSS 排版上,並沒有太困難的地方,上方的算式和結果顯示採用兩個區塊元素,並讓文字內容靠右對齊
text-align: right
,接著調整文字和背景顏色、字體大小、邊框圓弧後即可完成此區設計。
- 中下方的數字區、功能區,從圖面上可以看出是將寬度四等分切成小元素區塊的作法,所以我一齊採用 flex 排版來完成。實作上,在外層元素下
{display: flex; justify-content: space-around; flex-wrap: wrap}
這幾個設定,並對內層的按鍵元素用 % 設定寬度,將游標設定成一般顯示 cursor: pointer
,再稍微調整一下內外距、圓弧化即可完成,最後記得加上一些滑鼠移入元素的效果 (hover)。
JavaScript
- 這是我第一次試圖使用 Vue.js 這類資料驅動 (data-driven) 的框架來完成的地下城挑戰,中間不斷的試誤過程,讓自己對 JS 以及 Vue.js 有了更深入的理解。
- 在計算機挑戰使用 Vue.js 的好處:
- 因計算機的操作包含了較複雜的流程,有很多的數據需要記憶、運算、判斷、顯示,使用 Vue.js 和其專屬的開發工具 (Vue.js devtools) ,可以讓開發者很快速地掌握情況,加快開發速度。
- 首先,對每個 HTML 的按鍵元素上加上對應的觸發事件,並在 Vue.js 實體內開出對應的方法,如下:
1 2 3 4 5 6 7 8 9
| <div class="container" id="app"> <header></header> <div class="box-wrap d-flex space-around flex-wrap"> <div class="box" @click="enterOne">1</div> <div class="box math-symbol" @click="clickPlus">+</div> </div> </div>
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| var app = new Vue({ el:"#app", data:{ }, methods:{ enterOne: function(){ }, clickPlus: function(){ } } })
|
- 接著,開始構思如何顯示輸入的數值到結果區。在實作中,我前後採用了兩種方法,一個是使用數值型別的做法,另一個則是使用字串型別的做法,後來發現在小數點相關的計算時,使用數值型別輸入會相當棘手,所以後來改為採用字串型別的方法,範例如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| var app = new Vue({ el:"#app", data:{ currentValue: 0 }, methods:{ enterOneNum: function(){ this.currentValue = this.currentValue * 10 + 1; }, enterOneString: function(){ this.currentValue += "1"; }, clickPoint: function(){ this.currentValue += "."; }, } })
|
- 但在採用了字串來儲存現有值後,會發現當使用者不照一般情境操作,會出現不合理的現有值,如: “001”, “0.00.1” 但是計算機仍能繼續操作的現象。因此,我在 data 中加入了一組布林值來記錄小數點狀況,並調整了數字輸入鍵的邏輯如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| var app = new Vue({ el:"#app", data:{ currentValue: 0, isDecimal: false, }, methods:{ enterZero: function(){ this.currentValue += "0"; if (this.isDecimal === false){ this.currentValue = Number(this.currentValue); } }, enterOne: function(){ this.currentValue += "1"; this.currentValue = Number(this.currentValue); }, clickPoint: function(){ this.isDecimal = true; this.currentValue += "."; }, } })
|
- 完成以上步驟後,接著處理四則運算和等號的部分,我採用的方法是將四則運算代號化,在 data 中開出一項來儲存,並在算式顯示區和等號的函數中,使用多層判斷式的方法來處理。另外,在四則運算時,需要將現有值丟入另一個儲存空間,以便騰出空間來輸入新的現有值,範例如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| var app = new Vue({ el:"#app", data:{ currentValue: 0, isDecimal: false, mathOperator: "" previousValue: "" resultValue: "" }, methods:{ clickPlus: function(){ this.previousValue = this.currentValue; this.currentValue = 0; this.mathOperator = "plus"; this.isDecimal = false; }, clickEqual: function(){ if(this.mathOperator === "plus"){ this.resultValue = this.previousValue + this.currentValue; } else if (this.mathOperator === "minus"){ this.resultValue = this.previousValue - this.currentValue; } else if (this.mathOperator === "multiply"){ this.resultValue = this.previousValue * this.currentValue; } else if (this.mathOperator === "divide"){ this.resultValue = this.previousValue / this.currenValue; } }, } })
|
- 至於在算式的記憶顯示上,因程式語言使用的加減乘除符號與日常使用的寫法不同,我在 HTML 和 JS 都用了另外的方法處理:
1 2 3 4 5 6
| <div class="box math-symbol" @click="clickDivide">÷</div> <div class="box math-symbol" @click="clickMultiply">×</div> <div class="box math-symbol" @click="clickPlus">+</div> <div class="box math-symbol" @click="clickMinus">−</div> <div class="box clean" @click="clickBack">⌫</div> <span class="equal">=</span>
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| var app = new Vue({ el:"#app", computed:{ showOperator: function(){ if (this.mathOperator === "plus") { return "\u002B"; } else if (this.mathOperator === "minus") { return "\u2212"; } else if (this.mathOperator === "multiply") { return "\u00D7"; } else if (this.mathOperator === "divide") { return "\u00F7"; } else { return ""; } }, }, })
|
- 在最上排的算式顯示部分,經過多次嘗試後,最後採用的方法是區分「結果態」和「運算態」的做法,並使用 computed 在 HTML 中進行顯示,詳細的程式碼如下方範例:
1 2 3 4 5 6 7 8
| <header> <div class="formula"> {{equationDisplay}} </div> <div class="result"> {{mainDisplay}} </div> </header>
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| var app = new Vue({ el:"#app", data:{ notResult: true, previousValue: "", currentValue: 0, mathOperator: "", equationText: "", resultValue: "" }, computed:{ equationDisplay: function(){ if (this.notResult === true){ return (this.previousValue + " " + this.showOperator + " " + this.currentValue); } else { return this.equationText; } }, mainDisplay: function(){ if (this.notResult === true){ return this.currentValue; } else { return this.resultValue; } }, showOperator: function(){ }, }, })
|
- 其他按鍵的部分,也就是倒退 (Backspace) 和清空 (AC) 的功能,則相對容易處理很多。倒退鍵使用的是將當前值轉換成字串,再轉成陣列,並用陣列方法去除最末端元素的做法;清空功能則是將所有資料回歸初始設定狀態,範例如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| var app = new Vue({ el: "#app", data:{ }, methods:{ clickBack: function(){ let currentString = String(this.currentValue); let currentStringArray = currentString.split(""); currentStringArray.pop(); this.currentValue = Number(currentStringArray.join("")); }, clickClean: function(){ this.currentValue = 0; this.isDecimal = false; this.previousValue = ""; this.mathOperator = ""; this.resultValue = ""; this.notResult = true; this.equationText = ""; }, } })
|
- 最後是處理多位數字時的破版問題,也是本關卡明定要解決的議題,我參考的是最快破關者的做法,也就是在要破版時操控顯示區的 CSS 設定,讓字體從
font-size: 3.5rem
轉變成 font-size: 1.5rem
,如此一來便可以讓顯示的數字縮小,在 currentValue 達到 Infinity 前都可以順利顯示。因此,我調整了位在 computed 下,主顯示區的函數 mainDisaply,讓其去抓 currentValue 的長度,並判斷要不要調整 CSS 的字體設定,範例如下:
1 2 3
| <div class="result" ref="result"> {{mainDisplay}} </div>
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| var app = new Vue({ el: "#app", data: { }, computed:{ mainDisplay: function(){ let stringArray = String(this.currentValue).split(""); if (stringArray.length > 10){ this.$refs.result.style.fontSize = "1.5rem"; } else { if (this.$refs.result){ this.$refs.result.style.fontSize = "3.5rem" } } }, mainDisplay: function(){ const result = document.querySelector(".result"); let stringArray = String(this.currentValue).split(""); if (stringArray.length > 10){ result.setAttribute("style", "font-size: 1.5rem"); } else { result.setAttribute("style", "font-size: 3.5rem"); } }, }, })
|
- ※ 注意,如果採用原生 JS 語法,DOM 元素選取的過程要在 Vue 的作用下,不可先選了才創造 Vue 的實體,這樣會無法成功操作。
1 2 3 4 5 6 7 8 9 10
| const result = document.querySelector(".result"); var app = new Vue({ el: "#app", computed:{ mainDisplay: function(){ result.setAttribute("style","font-size: 1.5rem") } }, })
|
未解的問題
- JavaScript number 的實作是二進位的浮點數標準,所以在計算 0.1 + 0.2 時,結果會變成 0.30000000000000004,一般比較常用的調整方法是先將小數轉成整數,算完後再除回小數
this.resultValue = (this.previousValue * 10 + this.currentValue * 10) / 10
,但因為這個關卡是計算機,無法事先得知使用者要輸入幾位小數,目前仍然沒有想到比較好的方法來處理這個議題。
參考資料與專案連結