0%

新手JavaScript地下城第三關 - 計算機

系列文的第三篇,這次回過頭來完成前一關的線上計算機。

[design concept]

  • 首先,快速瀏覽一下設計稿,可以發現大致上分為三個區塊:
    • 上方的算式和當下結果顯示
    • 中間的數字按鍵和四則運算符號
    • 下方的功能按鍵,含清除(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">
<!-- 數字按鍵 1 -->
<div class="box" @click="enterOne">1</div>
<!-- 運算按鍵 加號 -->
<div class="box math-symbol" @click="clickPlus">&#43;</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(){
//... 對應使用者按下 "1" 數字鍵的函數
},
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;
},
// 數值型別做法:因計算機是十進位,先按 1 再按 2,預期是 12
// 也就是"現有值"乘以 10,再加上該按鍵的數字
enterOneString: function(){
this.currentValue += "1";
},
// 字串型別做法:字串的加法,就是在現有值後方直接加上按鍵的字串
// 不論現有值是 "1" 或 1,在加了 "2" 之後,都會變成 "12"

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);
}
// 對進入小數狀態的現有值,不轉回數字,才能順利輸入 1.01, 1.001
// 但非小數狀態的現有值,則與其他數字輸入鍵相同,轉回數值型別
},
enterOne: function(){
this.currentValue += "1";
this.currentValue = Number(this.currentValue);
// 非 0, 00 輸入鍵,現有值一律轉回數值型別儲存
},

clickPoint: function(){
this.isDecimal = true;
// 按下小數點,使得 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;
}
// 搭配 mathOperator 的判斷式,讓使用者按下等於時可以計算結果
},
}
})
  • 至於在算式的記憶顯示上,因程式語言使用的加減乘除符號與日常使用的寫法不同,我在 HTML 和 JS 都用了另外的方法處理:
1
2
3
4
5
6
<div class="box math-symbol" @click="clickDivide">&#247;</div> <!-- 除號 -->
<div class="box math-symbol" @click="clickMultiply">&#215;</div> <!-- 乘號 -->
<div class="box math-symbol" @click="clickPlus">&#43;</div> <!-- 加號 -->
<div class="box math-symbol" @click="clickMinus">&#8722;</div> <!-- 減號 -->
<div class="box clean" @click="clickBack">&#9003;</div> <!-- 倒退 -->
<span class="equal">&#61;</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){
// "運算態" (還在輸入兩個數字跟運算符號) 時,動態顯示在 .formula
return (this.previousValue + " " + this.showOperator + " " + this.currentValue);
} else {
// 等號事件(clickEqual)最後會組成的字串,於"結果態"時顯示
// 因最後會清空 previousValue, mathOperator,並讓 currentValue = resultValue
// 所以先組成字串丟進去顯示
return this.equationText;
}
},
mainDisplay: function(){
// 主顯示區,"運算態"時顯示 currentValue,"結果態"時顯示 resultValue
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(){
// 將 currentValue 字串化
let currentString = String(this.currentValue);
// 依照字元組成陣列元素
let currentStringArray = currentString.split("");
// 清除最後一個陣列元素
currentStringArray.pop();
// 組回字串再轉回數字
this.currentValue = Number(currentStringArray.join(""));
},
clickClean: function(){
// 讓 data 的內容回歸初始設定
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:{
// 使用 Vue 的 $refs 功能來抓取 DOM 元素
mainDisplay: function(){
//...
let stringArray = String(this.currentValue).split("");
// 超過 10 的數字就會破版
if (stringArray.length > 10){
this.$refs.result.style.fontSize = "1.5rem";
} else {
// 初始化的時候就去抓 this.$refs.result 會是 undefined,加上判斷式來避免這種情形
if (this.$refs.result){
this.$refs.result.style.fontSize = "3.5rem"
}
}
//...
},
// 使用原生的 JS 語法選取的做法如下
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
// 會失敗的寫法 (元素選取在 Vue 實體外)
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,但因為這個關卡是計算機,無法事先得知使用者要輸入幾位小數,目前仍然沒有想到比較好的方法來處理這個議題。

參考資料與專案連結

Node.js 基礎學習筆記 - Part 3

路徑與 path 模組

在 app.js 內,使用下列兩個語法,可以抓出路徑資料。

1
2
3
4
5
6
7
// 到達檔案的目錄路徑
console.log(__dirname);
// e.g. C:\Users\(User Name)\Desktop\Node.js Project

// 含有檔案的完整路徑
console.log(__filename);
// e.g. C:\Users\(User Name)\Desktop\Node.js Project\app.js

Node.js 的內建路徑模組,提供更多抓取方法,詳細的範例碼如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 載入內建的 path 模組
var path = require("path");

// 抓取到檔案的目錄路徑
console.log(path.dirname("/firstFolder/secondFolder/thirdFolder/target.js"));
// firstFolder/secondFolder/thirdFolder

// 合併前後的兩個路徑,通常後者是更下一層的路徑
console.log(path.join(__dirname, "/xx/yy"));
// C:\Users\(User Name)\Desktop\Node.js Project\xx\yy

// 抓取檔案名稱
console.log(path.basename("/xx/yy/zz.js"));
// zz.js

// 抓副檔名
console.log(path.extname("/xx/yy/zz.js"));
// .js

// 路徑分析 (集大成!)
console.log(path.parse("/xx/yy/zz.js"));
// {root:"/", dir:"/xx/yy", base:"zz.js", ext:".js", name:"zz"}

NPM

NPM 就是 Node.js 用來管理各種套件的工具,全稱為 Node Package Manager。透過 NPM,我們可以很容易取得其他開發者設計好的模組,只要有裝 Node.js,就已經會自動裝好 NPM,若不確定是否安裝完成,可以在終端機環境使用 npm -v 來查詢是否會出現版號。許多好用的套件如: gulp, express, grunt, cordova,都可透過 NPM 進行安裝和管理。

一般來說,只要是使用 Node.js 的應用程式 (專案),一定會有一個描述檔 package.json 用來記錄專案的各種狀態,而要產生這個檔案,首先要把終端機程式的路徑移動到專案資料夾下,並下指令 npm init,接著填入一系列問題的答案:

  • name (套件名稱)
  • version (版本號)
  • description (套件描述)
  • keywords (有關此套件的關鍵字)
  • author (作者)
  • license (授權種類)
  • Documentation of NPM

安裝套件,此處以 Express 為例,在命令提示介面使用以下指令:

  • npm install express --save

安裝完畢後,package.json 中會新增 "dependencies": {"express": "^4.16.2"},如此便可讓其他使用者馬上了解這個專案有幾個模組,版本號為何,而在 node_modules 資料夾中,就是載入的各項套件所需的相依程式檔案。接著,我們已經可以開始使用 Express 套件,在主檔的 app.js 中寫入 var express = require("express") 來載入。

npm install 上的差異

在大型的專案時,可能引用非常多的套件,造成 node_modules 非常肥大,不可進入版控 (git),如果這時有人要協作專案,只需要輸入 npm install 指令,即可讓 NPM 從描述檔的 dependencies 撈出對應的套件安裝。

  • npm install myModule –save
    • 適用於應用程式釋出後,仍會用到的套件
    • 最為推薦的做法,讓協作者能夠順利進入狀況
  • npm install myModule –save -dev
    • 適用於只用來除錯、測試用的套件,如:jsHint, mocha
    • 描述檔的紀錄為: "devDependencies": {"myModule": "^1.12.0"}
  • npm install myModule -g
    • 全域安裝,直接裝在本機端的底層
      • 路徑位置: C:\Users\(User Name)\AppData\Roaming\npm\node_modules
    • 優點是只要安裝一次,多個專案都可直接使用

Node.js 基礎學習筆記 - Part 2

Global 全域物件

對應於瀏覽器的 window,在 Node.js 中的全域物件為 Global。雖然概念上雷同,但當你在 Node.js 上想在全域物件下新增一個變數時,請使用以下的寫法。

1
2
3
global.addThis = "Hello!"
// 如果像瀏覽器一樣寫 var addThis = "Hello!"
// 則此變數會宣告在 local 下,而非全域共通

沒辦法像瀏覽器一樣簡單地宣告全域變數,原因在於 Node.js 在設計時,是把每個 JS 檔當成不同的模組 (modules) 來看待,所以不希望各模組的內容可以隨意汙染到全域去。

Require, Module, Exports 模組基礎

在做一個實際的專案時,通常會把數支的 JS 模組載入主程式中,假設目前專案目錄下有一支主檔 app.js,要載入其他模組,會使用語法 require

1
2
3
4
5
6
// 載入另一支位於同層的 JS 檔
var moduleTest = require('./moduleTest');
// 寫 moduleTest 即是代表去抓 moduleTest.js
// 若要抓上一層,使用 '../'

console.log(moduleTest); // {} 空物件

儘管你的 moduleTest.js 已經寫入內容,只用 require 語法還是會反應為空物件,是因為你還必須在被載入的檔案中加入輸出的語法,有兩種方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 在 moduleTest.js 中,使用 module.exports,寫在物件中的內容就會被傳到 app.js 的 moduleTest 裡
var myVar = "Hello world."
module.exports = {
strData: "example",
numData: 12345,
booleanData: true,
varData: myVar,
myGreet: function(name){
console.log(name + ", how's it going?");
},
}
// 各項 value 可以使用經前面程式處理過的變數
// module.exports = myExports;
// 傳出物除了上例使用的物件,也可是一支函數,一個純值等

// 另一種載入方式
exports.myExports = "hello";

// 此種寫法等同
module.exports = {
myExports: "hello"
}
1
2
3
// 在 app.js 取用引入的各項資料
moduleTest.varData // "Hello world."
moduleTest.myGreet("Eugene") // "Eugene, how's it going?"

※ 注意,這兩種方法不可混用,有可能產生蓋掉數值的狀況:

1
2
3
4
5
exports.myData = 666;
module.exports = {
myData: 999
}
// myData 為 999,會蓋掉 666

核心模組 createServer

在 Node.js 的官網文件上,可以看到其內建的各項模組,其中 HTTP 的模組即是用來建立 web server 的,首先必須引入該模組:

1
2
// 引入 HTTP 模組
var http = require("http");

引入後,我們開始使用此模組提供的方法。首先,建立一個網頁伺服器,使用 http.createServer() 方法,此方法會產生一個 HTTP Server 物件,而這個伺服器物件可以監聽電腦的特定連接埠,並且在每次客戶端發起請求時,執行函式 (requestListener),語法如下:

1
2
3
4
// 建立一個網頁伺服器
http.createServer(function(req, res){
// ...
});
  • requestListener:
    • 當每次請求發出時,伺服器端會執行的函式。會處理客戶端送來的請求,也負責伺服器傳給客戶的回應。
    • 此函式是 http.createServer() 方法的參數。
    • 語法:function(request, response){}
1
2
3
4
5
6
7
8
9
10
11
// 寫一個回應給客戶端
http.createServer(function(req, res){
// 顯示請求的 URL
console.log(req.url);
// 回應的檔頭 1. 狀態碼 (200, OK) 2. 回傳的格式為普通文字
res.writeHead(200, {"Content-Type": "text/plain"});
// 回應的主要內容,此處為字串 "Hello!"
res.write("Hello!");
// 結束回應
res.end();
}).listen(8080); // 使用 8080 的連接埠

回應的檔頭也可以設定成 HTML 格式 ,如下:

1
2
3
4
5
http.createServer(function(req, res){
res.writeHead(200, {"Content-Type": "text/html"});
res.write("<h2>Hello world.</h2><p>This is the main content.</p>");
res.end();
}).listen(8080);

要查看是否成功,使用瀏覽器存取以下 URL:

  • localhost:8080
  • 127.0.0.1:8080,此方法會經網卡傳輸,會受到防火牆和網卡的限制
  • ※ 為何設定在 8080 連接埠? 因其為平常較少用的埠號,不能兩個應用程式使用同一個,如果有第二個 Node.js 程式試圖使用 8080 則會被拒絕。其他常用埠號的意義:
    • :21 FTP
    • :80 HTTP
    • :443 HTTPS (加密)

如果使用 Chrome,可以從 DevTools 的 Network 查看:

[Check details of connection]

在 requestListener 函式內寫入的 console.log(req.url),在用瀏覽器開啟時會回應 //favicon.ico ,代表客戶端進入的是首頁,同時還有取網站圖示 (favicon) 的行為。

V8 引擎

V8 是由一個由 Google 開發的開源 JavaScript 引擎,用於 Chromium, Chrome 中,使用 C++ 撰寫,基於 JS 的執行環境 (Web 應用框架 / web application framework) Node.js 也是採用 V8 做為其核心。

命令列與操作 CMD

Windows 命令提示字元 (cmd.exe) ,是 Windows 系統的命令列執行程式 (CLI, command-line interpreter) ,常用指令整理如下:

  • Ref. by PJChen - Command Line 操作:CMD, CLI, Bash

  • dirls ,列出路徑下的所有檔案和目錄,在 CMD 中僅接受 dir 指令,ls 為 unix 和類 unix 系統使用

    • directory, list
  • cd "path to directory" 移動到指定的目錄上 (資料夾) ,移動到同路徑下的目錄可以只輸入目錄名稱

    • cd .. 回到上一層的目錄
    • change directory
  • mkdir "name of directory" 或是 md "name of directory" 創建目錄 (資料夾)

    • make directory
  • touch "file name" 建立檔案

  • rmdir "name of directory" 或是 rd "name of directory" 移除目錄 (資料夾)

安裝與執行 Node.js

進入 Node.js 官網,推薦選擇穩定版本安裝,因為套件相容度會較高,且論壇上比較容易查到相關資源。完畢後確認使否安裝完成,使用在 CMD 指令查詢 Node.js 版號:

node --version 或是 node -v

  • 有回應,如出現: v8.11.4 ,代表成功

直接在命令列環境使用 Node.js (即開啟編譯核心):

輸入:node

  • 前方輸入出現箭頭 > ,代表成功可開始寫入 JS

輸入:> var a = 1;

  • 回應:undefined

輸入:> a

  • 回應:1

跳出 JS 執行狀態,回到 Node.js ,按 ctrl + c 兩次

※ 一般狀況下執行 Node.js ,通常是讓命令提示介面移動到指定的路徑下,再使用 node app.js 指令來執行 JS 檔案。

Express

Express 是一個輕量化且具彈性的 Node.js 網站 (web applications) 開發框架,對無論是網頁或行動應用程式,其都有充足的工具可以支援。

使用 npm 安裝 Express 到專案中:

$ npm install express --save

接著,在專案中開啟一個 app.js ,並創建一個 Express 應用程式,此為使用 Express 的起手式。

1
2
var express = require('express');
var app = express();

※ 遷移到新版,Express 3 與 Express 4 之間的差異?

以下介紹 Express 常用的 API 功能,和如何使用的範例碼。

express()

  • express.json([options])
    • 為內建的中介函式,負責解析客戶傳進來 JSON 格式的資料,此功能由 body-parser 而來。經過解析後,req.body 就會被此函式解析後的結果 (key-value pairs的形式) 取代。
1
2
// 不設路徑參數,代表對全網域都作用
app.use(express.json());
  • express.Router([options])
    • 建立一個新的 router 物件。
    • 可搭配中介層和 HTTP 方法 (get, post. put, …) ,將客戶請求導向到路由中。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 在主要的 app.js 中,把同一父路徑下的子路徑拆分到新檔案管理 (分層)
// app.js 與 routes 在同一資料夾下
var news = require("./routes/news");
// 遇到要進 "/news" 路徑下的請求,跑伺服器上 "./routes/news.js" 的內容
app.use("/news", news);

// 以下寫在 news.js 內
// 建立環境
var express = require("express");
var router = express.Router();

// 設定各項子路徑的回應
router.get("/breaking", function(req, res){
res.send("<h2>Latest news you should know.</h2>");
});
router.get("/international", function(req, res){
res.send("<h2>What's new around the world?</h2>");
});

// 最後,匯出這兩個回應設定
module.exports = router;
1
2
3
// 圖片、給 ejs 載入的 <script scr="/js/all.js"> 等靜態檔案
// public 下可再開設 img, js 等資料夾
app.use(express.static("public"));
  • express.urlencoded([options])
    • 為內建的中介函式,負責解析客戶傳進來 urlencoded 格式的資料,此功能由 body-parser 而來。經過解析後,req.body 就會被此函式解析後的結果 (key-value pairs的形式) 取代。
    • extended 屬性:
      • 為布林值,當 false 時,值可為字串或陣列,當 true 時,值可為任何種類。
1
2
// 不設路徑參數,代表對全網域作用
app.use(express.urlencoded({extended: false}));

Application

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 註冊 ejs 引擎
var engine = require("ejs-locals");
app.engine("ejs", engine);
app.set("views", "./views");
app.set("view engine", "ejs");

// 以 views 下的 product.ejs 檔案回應客戶端對 domain/product 的請求
app.get("/product", function(req, res){
res.render("product")
})

// 帶入物件給 ejs 檔使用
app.get("/index", function(req, res){
res.render("index", {
name: "Tina",
age: 31,
gender: "female",
family: "<em>Chen</em>"
isQualified: true,
interest:["hands", "jogging", "shopping"]
})
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!-- EJS 使用 express 傳入的物件 -->
<ul>
<li><%= name %></li>
<li><%= age %></li>
<li><%- family %></li>
<li><% if (isQualified) { %>
<em>Displayed!</em>
<% } %></li>
</ul>
<ul>
<% for (const index in interests) { %>
<li><%- interests[index] %></li>
<% } %>
</ul>
  • app.get(‘path’, callback)
    • 執行 callback 函式,以應對客戶端對某個特定路徑提出的 HTTP GET 請求。
    • ‘/‘ 根路徑,通常是網域首頁,為路徑預設值。
    • callback 為中介函式。
1
2
3
app.get('/', function(req, res){
res.send('GET request to homepage success!');
})
  • app.listen(port)
    • 綁定並監聽指定連接埠的連線狀況,與 Node 的 http.server.listen() 方法類似。
    • ※ 在一些主機服務上,如:Heroku, Nodejitsu, AWS,供應商可能會另行設置其他連接埠給你的網站使用,如果寫死埠號可能會造成 500 錯誤,以下有參考的解決方法。
1
2
3
4
5
6
7
var express = require('express');
var app = express();
// app.listen(3000); 在自己可控制的主機上可以寫死

// 若無預設的 port 則使用 3000
var port = process.env.PORT || 3000;
app.listen(port);
  • app.post(‘path’, callback)
    • 執行 callback 函式,以應對客戶端對某個特定路徑提出的 HTTP POST 請求。
    • ‘/‘ 根路徑,通常是網域首頁,為路徑預設值。
    • callback 為中介函式。
1
2
3
app.post('/', function (req, res) {
res.send('POST request to homepage')
})
  • app.set(‘name’, ‘value’)
    • 將 ‘name’ 設定成 ‘value’ ,但某些 ‘name’ 可以用來控制伺服器的行為。
      • 控制用的名字,請參考:Application Settings
        • “views”:範本檔所在的目錄
        • “view engine”:設定要用的範本引擎
1
2
3
4
5
6
7
// 一般應用
app.set('title', 'My Site');
app.get('title') // 'My Site'

// 設定 ejs 模板引擎,兩個皆為 Express 保留的控制用字
app.set("views", "./views");
app.set("view engine", "ejs");
  • app.use(‘path’, callback)
    • 對特定路徑(‘path’)置放和執行指定的中介函式,類似守門員的功用。
    • ‘/‘,預設值為根路徑。
1
2
3
4
5
6
// 不設定路徑,即對每個進入該網域的客戶請求都執行下列 callback
app.use(function(req, res, next){
console.log("Time: %d", Date.now());
// 進入後續程式碼
next();
})

Request

  • req.body
    • 包含了請求所提交的 key-value 資料對,預設上是 undefined,但再使用中介層 (如:express.json(), express.urlencoded) 解析後會變成物件資料。
1
2
3
4
5
6
7
8
9
10
// 解析 application/json 資料 
app.use(express.json())
// 解析 application/x-www-form-urlencoded 資料
app.use(express.urlencoded({ extended: true }))

// 接收對 "/profile" 提出的 POST 請求,查看客戶端送出的資料,並以 JSON 格式回應
app.post("/profile", function (req, res, next) {
console.log(req.body)
res.json(req.body)
})

Response

  • res.redirect(status, ‘path’)
    • 將客戶端網址重新導向指定的路徑,可同時指定前面的參數做為 HTTP 的狀態碼。未表明時,預設為 “302 Found” (Moved temporarily,資源存在但位置被臨時移動)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 可支援轉向到其他網域
res.redirect("https://google.com")

// 支援從根路徑出發的相對路徑寫法
res.redirect("/admin") // 導向到 http://example.com/admin

// 支援從現有網址出發的相對路徑寫法
// 現在網址:http://example.com/blog/admin/ (注意此處結尾有斜線)
res.redirect("post/new") // 導向到 http://example.com/blog/admin/post/new
// 若結尾無斜線:http://example.com/blog/admin
// 則會導到 http://example.com/blog/post/new

// 回到上一層
// 現在網址:http://example.com/admin/post/new
res.redirect("..") // http://example.com/admin/post

// 回到上一頁 (back to the referer)
res.redirect('back')
  • res.render(‘view’ [, locals] [, callback])
    • 渲染 view 檔案 (如放在 views 資料夾下的 product.ejs 就是填入 “product”),並發送渲染過的 HTML 字串給客戶端。 view 參數是位於 views 資料夾下的,要被渲染檔案的路徑 (可為絕對路徑或是相對於 views 的路徑)。
    • locals 為物件,可將本地端的變數帶給 view 檔案使用。
1
2
3
4
5
6
7
// 傳送已渲染的 view 給客戶端
res.render('index')

// 傳送本地變數給 view 的檔案使用
res.render('user', { name: 'Tobi' }, function (err, html) {
// ...
})
  • res.send([body])
    • 發送 HTTP 回應,body 參數可為 buffer 物件、字串、物件、陣列。
1
2
3
4
5
6
7
8
res.send(Buffer.from('whoop'));
// 當參數為物件或是陣列時,回應會使用 JSON 呈現
res.send({ some: 'json' });
// 當參數為字串時,"Content-Type" 會被設成 "text/html"
res.send('<p>some html</p>');
// 與狀態碼進行鏈式寫法
res.status(404).send('Sorry, we cannot find that!');
res.status(500).send({ error: 'something blew up' });
  • res.status(code)
    • 設定針對 HTTP 狀態碼的回應,通常採用鏈式寫法,後面函式為針對此狀態的回應。
1
2
3
4
5
6
7
8
9
10
11
12
// 針對全網域的 404 錯誤的回應
app.use(function(req, res){
res.status(404).send("<h2>抱歉,找不到您要的頁面</h2>")
})
// 針對全網域的 500 (伺服器程式錯誤,Internal Server Error) 的回應
app.use(function(req, res){
res.status(500).send("<h2>伺服器程式出現問題,請稍後再試</h2>")
})
// 官方範例
res.status(403).end()
res.status(400).send('Bad Request')
res.status(404).sendFile('/absolute/path/to/404.png')

Middleware

  • 中介層函式是一些有權存取請求物件 (req)、回應物件 (res)、下一個中介層 (next) 的函式。中介層函式能執行以下任務:
    • 執行任何程式碼。
    • 變更請求和回應物件。
    • 結束請求 / 回應循環。
    • 呼叫堆疊中的下一個中介層。
  • 如果當前的中介層不會結束請求 / 回應循環,則必須呼叫 next() ,才能將控制權傳遞到下一個中介層,否則請求會停擺。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 宣告一個中介層函式 myLogger
// 在通過此中介層後,console 會印出 "Logged!"
var myLogger = function(req, res, next){
console.log("Logged!");
next();
};

// 在 req 物件下新增一個 reqTime 屬性,記錄 timestamp
var reqTime = function(req, res, next){
req.reqTime = Date.now();
next();
};
app.use(reqTime);
app.get('/', function(req, res){
var resText = "Hello, guest!";
resText += "Requested at: " + req.reqTime + "";
res.send(resText);
});
app.listen(3000);

新手JavaScript地下城第四關 - 時區

這是系列文的第二篇,但因為我 柿子挑軟的吃 最近想精進一些有關用 JS 操作時間的技巧,所以決定先選擇這個題目來處理。

  • 首先,從老師提供的設計稿來大致看一下複雜的程度。

[design concept]

  • 從圖中可以得知,版面排列和樣式簡潔明瞭,應該在 CSS 和 HTML 結構設計上不會有太多挑戰,可以很快進入 JS 取得時間、調整時區和更新時間的部分。

重要處理步驟和技巧

HTML, CSS
  • 這邊先要說明一下,其實這個時鐘的關卡我前後做了兩個不同但類似的成品,第一個是由六角提供的設計稿,那時的想法很單純,所有的 HTML 標籤就直接先通通刻好,再分別使用 JS 來選取、操控元素,後來才發現如果要擴充其他城市會有點麻煩。第二份是採用 Bootstrap 4 來調整版面和樣式的版本,HTML 元素則使用 JS 內的資料陣列,以迴圈和 innerHTML() 來塞入,整體擴充性較佳
  • 六角的那份,採用的方法是給每個城市外層定義一個區塊 <div class="timebox">,且 CSS 定義 .timebox{display: flex},讓左右側的子元素 (區塊元素) 可以併排呈現。同時,預先填入不會變動的各地地名 (慕尼黑是朋友要求加上的),會即時變動的左右側時間元素,都一一設定好 id 供 JS 選擇。
    • 其他 CSS 較特殊的部分,我從設計稿中推測依照時間不同,各地區的文字和背景色有黑底白字 (夜間)、白底黑字 (日間) 兩種樣式,所以另外定義了 .timebox.night{background-color: #000000; color: #ffffff} 讓 JS 可以順利切換到黑夜。
    • 下圖為結果的黑白效果,定義白天時間為 06:00 到 17:59。

[day vs. night]

  • 第二份是幫朋友寫的 Bootstrap 版本,因 BS 本身就已經定義了很多兼具彈性和美觀的樣式類別,所以在 CSS 的設計上就相當簡單,在實作時有兩個經過思考的點:
  1. 因為是使用陣列資料和迴圈塞入,如果要使用 BS 定義好的樣式,在每個城市的物件資料中也要加入相關的訊息,比如說想依次對 cityA, cityB, cityC 加入 bg-primary, bg-secondary, bg-info 的樣式,則必須讓 JS 的陣列記錄資料用類似下面的方法,並在迴圈組字串時搭配 <div class="${city.background}"> 才能讓套用 BS 的樣式:
1
2
3
4
5
let dataArray = [
{name: "cityA", background: "bg-primary"},
{name: "cityB", background: "bg-seconday"},
{name: "cityC", background: "bg-info"},
]
  1. 由於 BS 的網格系統對響應式設計支援算完整,所以記得使用 <div class="col-md"> 或是 <div class="col-lg"> 之類的設定來調整版面,另外使用網格系統要記得用 <div class="row"> 包覆網格內容,我採取的是在大裝置時,三個地區成為一列,所以使用 JS 組字串時,要使用判斷式如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
let htmlStr = "";
for (let i = 0; i < newCities.length; i++) {
if (i % 3 == 0) {
// 除 3 的餘數為零時,加 .row 的起始標籤
htmlStr += `<div class="row mt-md-3">`;
}
htmlStr += `
<div class="col-md">
<div class="timebox">
</div>
</div>`;
// 除 3 的餘數為 2 時,或是最後一個地區時,加上 .row 的關閉標籤
if (i % 3 == 2 || i == newCities.length - 1) {
htmlStr += `</div>`;
}
}
mainNode.innerHTML = htmlStr;

[the layout using BS grid system]

JavaScript
  • 不論是六角版或是 BS 版,最重要的都是使用 JS 內建物件 Date,使用const date = new Date()語法,即可取得瀏覽器的本地時間,一般常見使用下列語法:
1
2
3
4
5
6
7
8
9
10
const date = new Date(); // 建立新的 Date 物件
date.getDate(); // 取得為該月幾號
date.getDay(); // 取得星期幾,回傳 0-6,0 為星期日
date.getMinutes(); // 取得幾分
date.getHours(); // 取得幾點鐘
date.getMonths(); // 取得月份,回傳 0-11,0 為一月
date.getFullYear(); // 取得公元紀年
date.getTime();
// ※ 取得自 1970/01/01 00:00:00 (UTC) 到當下的毫秒數
// 非常實用,因為是獨一無二的,常做為存取資料時的 unique ID 使用
  • 一開始我想法是抓取本地時間後,用 date.getTimeZoneOffset 的方法來回算取得 UTC 時間,接著再把各地與 UTC 的標準時差放入,算得各地的時間,是比較硬幹的做法,但在完成測試時,發現歐美普遍施行的夏令時間會造成錯誤,只好重新來過。
  • 經過研究後發現,可以使用 date.toLocaleString(locales, options) 的方法直接取得各地時間,詳細的 locales 和 options 設定,可參考 Ref. by W3Schools ,各時區的參考則是 Ref. by timezonedb
  • 我的設定是:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
let newCities = cities.map(function (cityObj) {
let option = {
day: "numeric", // "10"
month: "short", // "01"
year: "numeric", // "2020"
hour: "2-digit", // "24"
minute: "2-digit", // "59"
hour12: false, // 24 時制
timeZone: cityObj.timezone,
}
cityObj.date = new Date().toLocaleDateString("zh-TW", option);
return cityObj;
});
// 對既有的 cities 陣列,使用 map() 方法,讓原陣列成員 (各城市物件) 取得當地的時間後,變成物件的新屬性 "timezone",並建立一個新的陣列 newCities
  • 雜記:
    • 對取得的各地時間字串,可以用 string.split() 方法處理成陣列,再使用陣列方法如: array.splice() 來取得想要的值。
    • 判斷黑夜白天可以用一個小函式配判斷式,再加上 setAttribute() 方法處理。
1
2
3
4
5
6
7
8
9
10
11
12
13
function isNight(hour) {
if (hour >= 6 && hour < 18) {
return false;
} else {
return true;
}
}
// ... 各城市的幾點鐘做為參數帶入判斷式
if (isNight(cityHour)) {
rightDiv.parentNode.setAttribute("class", "timebox night");
} else {
rightDiv.parentNode.setAttribute("class", "timebox");
}

新手 JavaScript 地下城第一關 - 9X9乘法表

新手 JS 地下城 是由六角學院 (我有註冊的線上程式教學課程) 提供給課程學員的一些挑戰關卡,希望藉由這些關卡讓學員可以累積經驗、熟練網頁前端技術。大致瀏覽之後,我發現每關各有其側重的領域,有些關卡比較強調 HTML 結構 + CSS 排版,而另外一些則比較要求 JS 要做出指定的功能 (撈資料、互動等)。

因為最近 Vue 學一學有點煩了,想轉換心情。 因為最近覺得自己在 Vue 的學習上,偶爾還是會卡個一下,腦子轉不太過來,需要重想幾次、查個資料才會通,所以就想撥出一些時間來寫這些關卡,看寫完之後自己會不會更快地吸收 Vue 的課程。結論就是: 參! 戰! 決! 定!

  • 由老師提供的第一關設計稿
  • 直接講心得:就本關來說,我遇到的問題主要是在 CSS 排版上的問題,最後分別使用 flex, float 兩種排版方法來處理。

一些重要步驟 & 想法:

  • 先針對主區塊 <main></main> 和頁角區 <footer></footer> 定義背景色跟字體等設定,接著用 .container{...} 設定好整體的內容邊界。
  • 接著,因為大標和 2~9 的數字區塊,整體呈現並排,且為大小接近的區塊,因此馬上想到使用 flexbox 來做外層排版。
大標區塊的 CSS
  • 由中間區塊 (中英大標) 和兩個格線區塊形成,因此使用 flex-direction: column 加上 justify-content: space-between 來製作

[solution: flex]

數字區塊的 CSS
  • 此區比較難處理的是容納大數字跟各項小算式的元素要先想好要用 display: block 或是 display: inline-block 哪種結構。另外,因為左側是大數字加三排小算式的組合,右側則為六個算式的組合,要先設想怎麼處理兩邊的排列才能順利對齊。
  • 雖然用 flex 應該也可排好此處的區塊,但我又想跟 float 排列混熟一點,因此最後決定讓左邊區塊以 float: left ,右側區塊使用 float: right 的方式處理,使用完最後不要忘記加上 <div class="clearfix" style="clear: both;"></div> 的相關設定來清除元素浮動的特性喔。
  • 大數字的陰影效果使用的是 text-shadow: 4px 3px 0 #f0f0f0 設定。
  • 數字區塊的外框使用的是 border-radius: 100px 0 30px 0box-shadow: 0 3px 10px #d8d8d8 來完成。
撰寫 JavaScript
  • 儘管粗略地學過,但卻沒有太多 jQuery 的實戰經驗,所以希望藉這個機會熟悉一下這個老牌的套件 (雖然最後用到的語法只有一個)
  • ※ 這個挑戰有一個限制是各項算式要用 JS 寫入,不能直接一個一個刻到 html 上。
    • 解法:.innerHTML() 方法,或是 jQ 對應的 .html() 方法。
    • 因為上述兩種方法是對選擇器下的內容全部取代,所以依照前面的 flex 排版設計,要先把第一個大標區塊 (跟後面的 “9X9乘法算式” 沒有重複的內容) 先存成一個字串組,最後再跟兩個迴圈產生的算式內容一起塞入,組成網頁內容。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const prefixStr = `
<div class="titlebox d-flex direction-vertical space-between align-center"> // 預先定義的 css 樣式
<div class="d-flex">
<p>X&nbsp;&nbsp;</p> // 形成兩個空格
<hr /> // 分割線
<p>&nbsp;&nbsp;X</p>
</div>
<div class="main-title">
<h3>九九乘法表</h3>
<h4>MULTIPLICATION TABLE</h4>
</div>
<div class="d-flex">
<p>X&nbsp;&nbsp;</p>
<hr />
<p>&nbsp;&nbsp;X</p>
</div>
</div>
`;
  • 接著,因為左側的算式只到乘數為 3,右側的乘數是 4 到 9,所以先定義一個臨界值,並在兩個迴圈中加入判斷式。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
const divisionNum = 4;

for (let i = 2; i < 10; i++) {
var leftStr = ``;
var rightStr = ``;
for (let j = 1; j < 10; j++) {
if (j < divisionNum) {
leftStr += `<p>${i} x ${j} = ${i * j}</p>`;
} else {
rightStr += `<p>${i} x ${j} = ${i * j}</p>`;
}
}
//...每做完一個乘數(j)後,要另外加上起始的大數字(新的被乘數)
}
  • 接著把上面迴圈形成的字串組,加到 html 結構中:
1
2
3
4
5
6
7
8
9
10
11
12
allBoxStr += `
<div class="numbox">
<div class="left-sec">
<h2>${i}</h2> // 起始的大數字
${leftStr} // 乘數小於 4 的區塊
</div>
<div class="right-sec">
${rightStr} // 乘數大於等於 4 ,小於 10 的區塊
</div>
<div class="clearfix"></div> // 清除浮動的設定
</div>
`;
  • 最後把大標區塊和 9X9 乘法表區塊合併塞入指定的 html 標籤下,完成!
1
$("#content").html(prefixStr + allBoxStr)

CSS grid system part III

Items (Child Elements / 子元素)

預設上,每列、每行都有一個子元素 (item),但你可以使用一些語法讓子元素跨列或是跨行

  • grid-column 屬性決定了元素從哪列開始,哪列結束
    • ※ 其實是 grid-column-start, grid-column-end 的簡寫
    • 使用網格線,或是 span 來定義子元素要跨多少列
1
2
3
4
5
6
7
8
.item1 {
grid-column: 1 / 5;
/* 從 line1 跨到 line5,共四列 */
}
.item2 {
grid-column: 2 / span 3;
/* 從 line2 開始,跨三列 */
}
  • grid-row 屬性是一樣的概念,只是轉換成定義子元素從哪行開始與結束
    • ※ 為 grid-row-start, grid-row-end 的簡寫
    • 使用網格線,或是 span 來定義子元素要跨多少行
1
2
3
4
5
6
7
8
.item3 {
grid-row: 1 / 4;
/* 從 line1 開始,line4 結束,跨三行 */
}
.item4 {
grid-row: 1 / span 2;
/* 從 line1 開始,跨兩行 */
}
  • grid-area 屬性是上面這幾種的合併簡寫
1
2
3
4
5
6
7
8
.item5 {
grid-area: 1 / 2 / 5 / 6;
/* 從行的 line1,列的 line2 開始,行的 line5,列的 line6 結束 */
}
.item6 {
grid-area: 2 / 1 / span 2 / span 3;
/* 從行的 line2,列的 line1 開始,跨兩行,跨三列 */
}
  • grid-area 的其他用法
    • 給子元素指定名稱:grid-area 可以指定每個網格的名稱,讓 grid-template-areas 使用
    • 可使用 “.” 來代表沒有命名的元素
1
2
3
4
5
6
7
8
9
10
11
12
13
14
.item1 {
grid-area: myArea;
}
.grid-container {
display: grid;
grid-template-areas: 'myArea myArea myArea myArea myArea';
grid-gap: 10px;
/* 單引號內的區域會構成一行,使用空格隔開每個子元素
上例顯示為 item1 單獨成一行,其寬度為五列,而下方的 myArea 則各占一列
若是 'myArea myArea myArea . .',則 item1 橫跨三列,剩下兩列會由 item2, item3 往上遞補
若要讓 item1 跨兩行,則
grid-template-areas: 'myArea myArea . . .' 'myArea myArea . . .',
使用兩個單引號區域來定義兩行 */
}
1
2
3
4
5
6
7
8
<div class="grid-container">
<div class="item1">1</div>
<div class="item2">2</div>
<div class="item3">3</div>
<div class="item4">4</div>
<div class="item5">5</div>
<div class="item6">6</div>
</div>

+++ Mind Blown Moment +++

  • grid-area 直接一開始就定好版型
1
2
3
4
5
6
7
8
9
10
11
12
.item1 { grid-area: header;}
.item2 { grid-area: menu;}
.item3 { grid-area: main;}
.item4 { grid-area: right;}
.item5 { grid-area: footer;}

.grid-container {
grid-template-areas:
'header header header header header header'
'menu main main main right right'
'menu footer footer footer footer footer';
}
  • 改變子元素順序
    • 網格排版法允許我們將元素牌到任何位置
    • ※ 即:HTML 標籤的第一個元素,不一定會在網格中先出現
    • 搭配 media query 語法,可以大幅改變版面在裝置上的顯示 (響應式設計)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
.item1 { grid-area: 1 / 3 / 2 / 4; }
.item2 { grid-area: 2 / 3 / 3 / 4; }
.item3 { grid-area: 1 / 1 / 2 / 2; }
.item4 { grid-area: 1 / 2 / 2 / 3; }
.item5 { grid-area: 2 / 1 / 3 / 2; }
.item6 { grid-area: 2 / 2 / 3 / 3; }
/* 出現順序是
item3 > item4 > item1
item5 > item6 > item2
*/

@media only screen and (max-width: 500px) {
.item1 { grid-area: 1 / span 3 / 2 / 4; }
.item2 { grid-area: 3 / 3 / 4 / 4; }
.item3 { grid-area: 2 / 1 / 3 / 2; }
.item4 { grid-area: 2 / 2 / span 2 / 3; }
.item5 { grid-area: 3 / 1 / 4 / 2; }
.item6 { grid-area: 2 / 3 / 3 / 4; }
}

CSS grid system part II

Container 容器 / 父元素

  • grid-template-columns 屬性可以定義網格排版要有幾列 (columns),以及各列的寬度
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
.grid-container {
display: grid;
grid-template-columns: auto auto auto auto;
/* 一共四列,每列寬度相同 (父元素的寬度除以四) */
/* 如果超過四項子元素,多的會自動放到下一行 */
}

.grid-container {
display: grid;
grid-template-columns: 80px 200px auto 40px;
/* 1, 2, 4項固定寬度,第三項為可變寬度,吃掉父元素剩下的寬度*/
}

.wrap {
display: grid;
grid-template-columns: repeat(2, 1fr 2fr) 50px;
gap: 5px;
/* grid-template-columns: repeat({次數}, {格線} | {格線}) | {格線} | ...
fr 是 fraction,可以視為一個格單位,repeat 則是代表重複幾次()內的設定
上面的寫法是父元素寬扣除掉 50px 後,剩餘的空間先除 2,然後每個單位內部再分隔成 1/3, 2/3
{1/6} {2/6} {1/6} {2/6} 50px
*/
}
  • grid-template-rows 屬性可以定義每行的高度,輸入的數值使用空格分開
1
2
3
4
5
.grid-container {
display: grid;
grid-template-rows: 80px 150px;
}
/* 第一行高度 80px,第二行高度 150px */
  • justify-content 屬性用來對齊容器內的各元素
    • ※ 各網格的加總寬度不能超過容器寬度,否則這個屬性不會作用
1
2
3
4
5
6
7
8
9
10
11
.grid-container {
display: grid;
justify-content: space-evenly;
/* 或是
justify-content: space-around;
justify-content: space-between;
justify-content: center;
justify-content: start;
justify-content: end;
*/
}
  • align content 屬性用來垂直對齊容器內的各元素
    • ※ 各網格的加總高度不能超過容器高度,否則這個屬性不會作用
1
2
3
4
5
6
7
8
9
10
11
12
.grid-container {
display: grid;
height: 400px;
align-content: center;
/* 或是
align-content: space-evenly;
align-content: space-around;
align-content: space-between;
align-content: start;
align-content: end;
*/
}
  • 什麼是 minmax 語法?

    • Ref. by MDN
    • 為使用在 CSS grid system 中的函數,其提供了一個上下界,讓該列、行的大小只會在範圍內變動
    • 可使用的屬性有:
      • grid-template-columns
      • grid-template-rows
      • grid-auto-columns
      • grid-auto-rows
    • 標準語法如下:
    • minmax ( [ | | | min-content | max-content | auto ] , [ | | | min-content | max-content | auto ] )

CSS:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#container {
display: grid;
grid-template-columns: minmax(min-content, 300px) minmax(200px, 1fr) 150px;
grid-gap: 5px;
box-sizing: border-box;
height: 200px;
width: 100%;
background-color: #8cffa0;
padding: 10px;
}

#container > div {
background-color: #8ca0ff;
padding: 5px;
}

HTML:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<div id="container">
<div>
Item as wide as the content, but at most 300 pixels.
與內容同寬,但最多到 300px
</div>
<div>
Item with flexible width but a minimum of 200 pixels.
寬度會彈性變動,最少寬 200px
</div>
<div>
Inflexible item of 150 pixels width.
就是 150px,不會變動
</div>
</div>

CSS grid system part I

Intro 整體介紹

使用 CSS 內建的 grid,需要有父子結構,也就是外層須由一層 container (容器) 包住要排版的子元素

  • 首先,容器的 display 屬性必須設置為 gridinline-grid
1
2
3
4
5
6
7
8
  .grid-container {
display: grid;
}
/* 或 */

.grid-container {
display: inline-grid;
}
  • 第一層的子元素會直接成為 grid items (網格排版元素)
  • 垂直方向的排版稱為 grid columns (網格列)
  • 水平方向的排版稱為 grid rows (網格行)
  • 在排版元素間的空格稱為 grid gaps (網格間隔)
    • 有 column gaps (列的間隔),也有 row gaps (行的間隔)
    • 可以使用的屬性有:grid-column-gap, grid-row-gap, grid-gap
1
2
3
4
5
  .grid-container {
display: grid;
grid-gap: .5rem 1.5rem;
}
/* 先設定上下間隔,再設定左右間隔 */
  • 網格線 (grid lines),垂直的是 column lines (順序由左到右),水平的是 row lines (順序由上到下)
    • 對 3 X 3 的網格來說
      • 左邊界是列的 line1,右邊界是列的 line4
      • 上邊界是行的 line1,下邊界是行的 line4
1
2
3
4
5
6
7
8
9
10
11
  .item1{
grid-column-start: 1;
grid-column-end: 3;
}
/* 從垂直線1開始到線3結束的 item1 */

.item2{
grid-row-start: 1;
grid-row-end: 3;
}
/* 從水平線1開始到線3結束的 item2 */