2017年12月8日

5 分鐘快速了解 FontAwesome 5

keywords: icon, logo, fa
Imgur
FontAwesome 正式釋出第五版,在 FontAwesome 5 中,除了主色系從綠色變成藍色之外,究竟第 5 版中還多了哪些新功能呢?讓我們用 5 分鐘快速了解 FontAwesome 5 帶來什麼新功能吧!

2017年12月7日

[JS] 談談 JavaScript 中的錯誤處理 Error Handling

圖片來源:Proper Error Handling in JavaScript @ Scotch
keywords: exception handling, javascript, 錯誤處理, 例外處理
本文主要內容翻譯自 Exceptional Exception Handling in JavaScript @ SitePoint
在撰寫程式的過程中發生錯誤(error)或出現例外情況(exception)是經常出現的情況,一般我們會把「錯誤」稱作「例外」,兩者可以交替著使用。
當 JavaScript 程式執行的過程中發生錯誤時,它會丟出例外狀況(throw an exception),JS 並不會繼續往下走,而是尋找有沒有任何程式碼能夠處理這些錯誤(exception handling code),如果沒有找到任何可以處理錯誤狀況的程式碼,則它會從丟出例外狀況的函式中跳出(return),就這樣重複尋找錯誤處理的程式碼、跳出,直到觸及到最外層的函式(top level function)後終止。

錯誤類型

當例外產生時,會產生一個用來代表錯誤的物件。在 JS 中內建了 7 種錯誤類型的物件:

1. Error

Error 這個類型用來代表一般的錯誤情況,最常用在客製化例外情況。透過下面的指令可以產生一個錯誤物件的實例:
var error = new Error("error message");
console.log(error)          // Error: error message
這個 Error 物件包含兩個屬性-namemessage
  • name: 用來說明錯誤的類型(這裡就是 Error
  • message: 提供更多關於此例外情況的描述。

2. RangeError

RangeError 的例外情況會發生在當數值落在特定的區間外時,例如,透過 toFixed() 方法時,它可以接受介於 0 ~ 20 的參數來說明要顯示到小數後第幾位,當這個參數超過這個區間時,就會拋出 RangeError
var pi = 3.14159;
pi.toFixed(100000);  // RangeError: toFixed() digits argument must be between 0 and 100

3. ReferenceError(找不到變數:拼錯字)

當試圖存取一個不存在的變數時,會拋出 ReferenceError,這個錯誤經常發生在拼錯字的情況。
console.log(bar);    // ReferenceError: bar is not defined

4. SyntaxError(語法錯誤)

當有程式碼違反 JavaScript 的語法規則時,會拋出 SyntaxError 的錯誤。熟悉 C 或 Java 的通常是在編譯的過程中(compulation process)遇到語法錯誤;但 JavaScript 是直譯式語言(interpreted language),因此當程式碼被執行到時,才會辨認到語法錯誤。這種錯誤類型是所有例外狀況中唯一不能被修復的
if (false) {    // SyntaxError: Unexpected token (3:0)
  // 缺少結束的大括號

5. TypeError(找不到函式)

如果某一變項的型別和所期待的操作不同時,會拋出 TypeError這經常發生在去呼叫執行一個不存在的函式時
/**
 * 由於 foo 當中並不包含 bar 這個函式,因此會拋出 TypeError 錯誤
 **/
var foo = {};
foo.bar();     // TypeError: foo.bar is not a function

6. URIError

當使用 encodeURI()decodeURI() 的方法,但確有給了不合法的 URI 時會拋出這個錯誤:
/**
 * "%" 表示的是 URI 中的跳脫片段,但在下面的例子中 "%" 後沒有接任和字串,因此是不合法的跳脫片段
 **/
decodeURIComponent("%");   // URIError: URI malformed

7. EvalError

eval() 這個函式不恰當的使用時,會拋出 EvalError 這個錯誤。這個錯誤不再被當前的 ECMAScript 規範所採用。

處理錯誤(Handling Exceptions)

在 JavaScript 中如果碰到錯誤或例外情況時,如果沒有找到錯誤處理的程式,它會直接在那裡炸掉。那麼要如何避免錯誤產生時讓我們的程式直接炸掉呢?在 JavaScript 中,可以使用 try...catch...finally 語句。
try {
  // attempt to execute this code
} catch (exception) {
  // this code handles exceptions
} finally {
  // this code always gets executed
}

try

我們預期在 try 區塊內的程式碼會成功執行,但當有 try 區塊中有任何錯誤發生時,會立即進入 catch 的區塊;如果沒有錯誤發生,則會跳過 catch 區塊。finally 則是會在 try...catch... 之後先被執行。

catch

catch 區塊中可以指定一個參數,這個參數通常稱作 exceptioncatchID。在 catch 區塊中可以辨認這個參數,但離開這個區塊後就無法取得這個變數。透過 catch 可以阻止例外情況繼續向外冒泡,讓整個程式可以繼續執行。
try {
  foo++;              // ReferenceError
} catch (exception) {
  //  ReferenceError: foo is not defined
  console.log(`${exception.name}: ${exception.message}`)
}

finally

finally 區塊中的程式碼會在 try, catch 後被執行,不論有沒有例外情況產生,因此 finally 區塊通常用來包含清除的程式碼(例如,closing files)。
但是如果 finally 區塊中有 return 值的話,這個值會是最後整個 try-catch-finally 回傳的結果,即時在 trycatch 中有 return 或其他的 throw 都會被忽略。
function f() {
  try {
    console.log(0);
    throw 'bogus';    // 進入 catch
  } catch(e) {
    console.log(1);
    return true;      // 這個回傳值會被終止,直到整個 finally block 完成之後
    console.log(2);   // 執行不到
  } finally {
    console.log(3);
    return false;     // finally 中的 return 會覆蓋掉 try/catch 中的 return 或 throw
    console.log(4);   // 執行不到
  }
  // 執行 finally 中的 "return false"
  console.log(5);     // 執行不到
}
f();          //  0, 1, 3; returns false

客製化錯誤(Throwing Exceptions)

JavaScript 允許開發者透過 throw 來拋出客製化的例外情形:
throw expression;
透過 throw 可以丟出幾乎任何型別的錯誤,但一般還是建議使用內建的例外類型(瀏覽器會提供比較多資訊)。

客製化錯誤訊息(message)

例如,當我們透過除法卻不小心除了分母為 0 的數值時,可以使用 throw 來拋出例外情況:
let denominator = 0

// RangeError: Attempted division by zero!
try {
  if (denominator === 0) {
    throw new RangeError("Attempted division by zero!");
  }
} catch (e) {
  console.log(e.name + ': ' + e.message)
}

客製化錯誤名稱(name)

透過上面的方式我們可以根據原生的那 7 種錯誤類型(e.name)給予想要的錯誤訊息(e.message),但如果我們想要客製化錯誤名稱的話可以這麼做:
/**
 * 客製化錯誤類型
 **/
function DivisionByZeroError(message) {
  this.name = 'DivisionByZeroError';
  this.message = message;
}

// 繼承 Error 物件
DivisionByZeroError.prototype = new Error();
DivisionByZeroError.prototype.constructor = DivisionByZeroError;

// 建立 showError 的方法
DivisionByZeroError.prototype.showError = function() {
  return this.name + ': "' + this.message + '"';
}
使用客製化錯誤類型:
let denominator = 0

try {
  if (denominator === 0) {
    throw new DivisionByZeroError("Attempted division by zero!");
  }
}
catch (e) {
  console.log(e.showError())  // DivisionByZeroError: "Attempted division by zero!"
}

其他範例

透過 instanceof 處理多種不同的錯誤類型

利用 instanceof 可以根據不同的錯誤類型執行不同的 catch
try {
    // assume an exception occurs
} catch (exception) {
  if (exception instanceof TypeError) {
      // Handle TypeError exceptions
  } else if (exception instanceof ReferenceError) {
      // Handle ReferenceError exceptions
  } else {
      // Handle all other types of exceptions
  }
}

參考

2017年11月20日

[生產力] 目標設定後卻總是沒動機去做?透過這個 APP 提升自己的動力吧!

圖片來源:Projecturf
keywords: 目標設定, 任務, 時間管理, 獎勵, 酬賞, 行為改變, 教養, 快樂生活
記得小時候只要做了一些大人所認為或所定義「好的行為」之後,就可以得到一張好寶寶貼紙或印章,集滿了一定的張數之後就可以換取自己想要的禮物。
這樣的做法看起來好像有點幼稚,但實際上這招在行為改變、、習慣養成、甚至是成癮戒治上非常有效。看看各家便利商店的集點制度、銀行信用卡提出的紅利回饋等等,其實都是利用的類似的概念-「完成一個預期的行為、做完之後就給你獎賞」,只是長大之後我們想要的東西不同罷了。
過去曾經分享過一篇萬事起頭難,也許你需要的是習慣培養小利器,事隔多年之後,現在已經是手機發達的時代了,最近一直想要找一套可以用來設定工作任務,然後完成該任務之後可以透過類似集點的方式換取禮物的 APP,市面上雖然乍看有許多類似的 App,但是大部分要嘛比較偏向 Todo List、待辦清單這種(例如,TodoistTickTickWunderlistAny.do);要嘛則比較偏向習慣培養,每天記錄自己是不是有完成某件事,連續持續好幾天達標(例如,Loop 習以為常Habit TrackerHabitHubThirty;要嘛則缺少了我想要的獎勵酬賞制度。
主要的需求是希望能夠「設定任務」、「設定獎賞」、「任務完成後可以換取代幣」、「代幣可以兌換設定的獎賞」,另外希望功能不用太過複雜,簡單方便操作就好。
後來搜尋了一下找到幾款看起來比較合適的 APP ,先說結論:實際操作之後我比較喜歡使用 Incentive 、和 Agoal - 私人任務助理 這兩套,因為它們的操作簡單、設定不複雜;其他可以完成同樣需求的像是 LifeRPGHabiticaDo It Now 也都是不錯的選擇,但是設定上稍微比較複雜一些(或許比較有趣?),因為還帶入了遊戲化像是升級或經驗值的概念,喜歡這類的朋友們也可以參考看看。
下面則列出這些 App 簡單的說明和介紹。

Incentive

先來分享這套我個人最喜歡的 App ,它的設定非常的簡單,進入之後透過右下角的「+」就可以新增目標(Goal)、任務(Tasks)和獎勵(Rewards)。
Imgur

目標(Goals)

基本上目標比較像是一個大方向,裡面可以設定許多任務來完成這個目標,例如我希望可以成為網頁工程師,目標完成時可以設定會獲得多少的代幣(Points)。
Imgur

任務(Tasks)

接下來就可以設定各種想要完成的任務來達到自己的目標,其中可以選擇相對應的目標、喜歡的 ICON、截止日期和完成後可以獲得多少代幣。
Imgur
另外,非常好用的地方是在 Task Type 的地方它可以設定任務類型是屬於 每天/週/月 的或 重複性的(Repeatable) 任務,所以像是如果每天要運動、睡眠等等就可以設成 Daily,如果是可以重複很多次的像是喝水這種,就可以設 Repeatable,另外一樣可以設定完成時可以獲得多少代幣:
Imgur

酬賞(Rewards)

最後是設定獎勵的部分,獎勵的部分很棒的是也分成單次的獎勵和可重複的獎勵,所以像是買個喜歡的禮物給自己、出國遊玩這種就可以算是單次的;如果是吃大餐或小點心這種就可以算是重複的,接著就是設定需要設定多少代幣才可以兌換:
Imgur

實際操作

設定好之後畫面的右上角會有你目前有的代幣數,當完成一個任務後,就可以點選右邊的「勾勾」,右上角的代幣數目就會增加了。如果是單次的任務完成後就會進入封存(Archived)、如果是重複性的任務則會可以重複點擊(一直點錢就會一直增加XDD)。
Imgur
接著存到一定的代幣後,就可以到獎勵區(Rewards)去兌換自己想要的獎品,如果錢夠的話點了「勾勾」右上角的金額就會扣除;如果錢不夠的話當然就沒辦法兌換拉。同樣的,對於單次的獎勵換完了就會進到封存,重複性的獎勵則可以重複兌換:
Imgur
另外也提供簡單的統計資訊,也可以在封存中(Archives)看到已經完成的任務:
Imgur

使用經驗

在設定的時候有一些經驗可以簡單分享一下,就是不要用什麼代幣點數了,直接用現金吧XD,簡單來說,如果我的獎品是 iPhoneX,就先設定對應的金額要是 $35,900 ,然後直接設定完成某項任務可以獲得多少錢,這樣做起來實在很有動力啊!例如我每天只要完成 「12 點前就寢」,就可以馬上獲得 $10,每喝一次水就可以馬上獲得 $1 ,做起來真是非常有感覺啊!
Incentive @ Google Play

Agoal - 私人任務助理

Imgur
這是一款非常簡單易用的 App,任務部分可以設定完成時限、完成可以獲得多少獎勵、沒有完成的處罰;獎勵部分可以設定獎勵類型、需要多少點可以購買該獎勵、購買之後不一定要馬上兌換。唯一美中不足的是如果你有一項任務是每天都要做的習慣(例如,運動或早睡早起)那麼你就必須每天重複設定
Imgur

其他相關 App

其他類似的 App 像是 LifeRPGHabiticaDo It Now 則融入了更多遊戲化(Gamification)的特性,多了像是技能、經驗值、任務向度等等的設定,但對於我的需求來說就顯得不夠簡潔,提供給其他朋友做個參考:

Do it Now

畫面和設計風格蠻好看的:
Imgur
Do It Now @ Google Play

Life RPG

點數是寶石,可以設定任務的困難度、急迫度、和恐懼度:
Imgur
Life RPG @ Google Play

Habitica

畫面設計的很像 RPG 風格的圖案,內建了一些和遊戲有關的獎品:
Imgur
Habitica @ Google Play

2017年11月19日

[MI] 小米手機 Line 推播沒通知,設定方式與使用心得

tags: pushbullet, whoscall, notification, miui, xiaomi
Imgur
前陣子 Mix2 推出,剛好前支 LG G2 也用 3 年多有了,就在朋友的推坑之下買了小米 Mix2 想說來玩玩看。剛拿到手機的前幾天其實還有點猶豫的,畢竟價格 $14,999,相較於同期當時的 HTC U11 4G/64G 是 $16,800、U11+ 是 $19,900、Samsung S8 是 $18,500、LG G6 是 $13,990,CP 值並不算非常高;另外又因為沒有使用過中國的手機,所以猶豫了許久。最後在和 HTC U11, U11+、三星 S8、Nokia 8 等等比較後,還是決定留下來繼續使用小米 Mix2,原因就在下面的文章慢慢說明。

吸引人的部分

螢幕尺寸大顯色佳

有幾點是當初購買時還蠻猶豫的,看一下它和 HTC U11 和 Samsung S8 的比較:
Xiaomi Mix2 HTC U11 Samsung S8
材質 IPS IPS Super AMOLED
解析度 2160x1080 2560x1440 2960x1440
尺寸 5.99 inch 5.5 inch 5.8 inch
細膩度 403 ppi 534 ppi 570 ppi
光看規格其實是蠻令我擔心的,除了 5.99 吋和 IPS 螢幕(不用擔心螢幕烙印)令人滿意的之外,解析度只有 1080p、細膩度也只有 403 ppi,相較之下都顯得沒那麼優,但是實機比較的時候,不知道為什麼 HTC 的手機卻給我一種偏暗的感覺,反倒是 MIX2 的色彩比較明亮鮮豔,這部分見仁見智,畢竟有人喜歡亮有人喜歡暗,而我個人是比較喜歡亮一點的。
就如同官網廣告所說的,手機拿起來的大小和 iPhone Plus 系列差不多,但是螢幕是 5.99 吋:
Imgur

設定軟體達到真正全屏檢視

在 MIUI 中有一個我非常喜歡的功能,一般即使沒有實體按鍵的手機,下面都還是會多一條虛擬按鍵的下巴,小米也不例外,而且它的下巴常常又不是透明的,看起來不太好看,像是這樣:
Imgur
但是它在設定的地方可以選擇「隱藏虛擬鍵」:
設定 > 更多設定 > 手勢及按鍵快啟方式 > 隱藏虛擬鍵
隱藏之後螢幕的可視範圍就變得更大了:
Imgur
這時候不論是瀏覽網頁還是操作 APP 都有比較大的可視範圍,真的是非常愉快阿!
但這時候變成需要下拉選單的時候都要滑一下把下巴叫出來,非常麻煩。
這時可以開啟另一個「懸浮球」的功能:
設定 —> 更多設定 —> 懸浮球
過去的懸浮球大部分都是點擊後展開,但在這裡可以設成「滑動」,一開始可能會覺得用滑動怪怪的,可是真正使用之後,滑動的效果非常方便,在隱藏了原本的主選單之後,透過滑動的懸浮球可以更快速達到原本的操作:
Imgur

相機內建 QRCode 掃描

現在很多人都會打開 Line 透過 Line 來掃描 QRCode,而在 Mix2 的相機中就內建了 QRCode 掃描的功能,這點雖然沒什麼,但我覺得非常方便:
Imgur

喇叭音量、電量

其他像是喇叭音量還蠻大聲的、電量在使用上也還蠻足夠的(之前的 G4 真的太噴電了,我覺得和 G2 差不多省電)。

不足或困擾的部分

上面是使用三週後覺得非常方便的部分,而下面則是比較令我困擾的部分。由於過去曾經使用過 window phone ,訊息推播的問題、App 使用設定的問題真的是非常困擾,而在 Mix2 或其他的小米手機中常常也碰到類似的問題。

Line 訊息內容無法直接顯示

在 MIUI 9 中訊息無法直接顯示的問題仍然沒有解決,收到 Line 訊息之後,會顯示通知但是沒辦法直接看到訊息內容,必須要點進去 Line 之後才看的到;例外可能會有無法發出通知聲的情況。
2017/12/01 更新至 MIUI 9.1.2 已改善此問題。

Whoscall, Pushbullet 須額外設定

另外 Whoscall 不像其他的安卓手機安裝好勾一勾之後就可以使用,在 MIUI 中也需要進行設定後才能正常顯示。
設定方法可參考 小米手機 Whoscall 設定 @ Whoscall
Pushbullet 則是我另一個很常使用的 App,它可以把手機的推播訊息直接同步到電腦上,也就是當手機響的時候從電腦上就知道發生了什麼事。設定的部分和 Whoscall 很像,基本上我覺得關於推播有關的問題都可以參考類似的設定操作一次看看。
如果上面的設定你都做完了卻還是無法正常都到推播通知的話,可以參考文章後半部的推播設定。

Android Pay 和 HAMI Wallet 設定

Andorid Pay 不免俗的也是要設定一下。
  1. 設定 --> 其他連接方式 --> 安全元件位置 --> 「使用 HCE 錢包」
  2. 設定 --> 其他連接方式 --> 感應付款 --> Android Pay
  3. 通知管理全開
  4. 自啟動管理開啟
詳細設定方式可參考:小米 MIX 2 成功使用 android pay 和 HAMI Wallet @ Mobile01

拍照相機

拍照和相機是蠻多人詬病的地方,但我個人覺得色調什麼倒還好,差別在於放大之後格子狀或模糊的情形比較明顯,但如果只是要平常拍照紀錄生活、拍照分享給好友、上傳到 IG 的話,基本上我是覺得足夠了。

看不到台灣

這支手機裡面是看不到台灣和台灣國旗的,只看到的「中國台灣」。

推播設定

這一點是我在使用 MIUI 時感到最困擾的部分,如果你安裝的 APP 沒有辦法正常推播的話,可以試著嘗試一下方法:

關閉神隱模式

設定 --> 電量和性能 --> 省電最佳化 --> 應用智慧省電(神隱模式) --> 選擇你希望收到推播的 apps --> 選擇「無限制」
Imgur

自啟動管理

設定 --> 授權管理 --> 自啟動管理 --> 選擇你希望收到推播的 apps --> 勾選「允許應用自啟動」
Imgur

通知管理

設定 —> 更多應用 —> 在「已下載」中找到你希望收到推播的 apps —> 自定義通知 —> 勾選允許通知優先
相同路徑:設定 --> 通知和狀態欄 --> 通知管理 --> 選擇你希望收到推播的 apps --> 勾選允許通知優先
Imgur

應用權限管理

設定 —> 更多應用 —> 在「已下載」中找到你希望收到推播的 apps —> 應用權限管理 —> 打開權限
img

通知存取權

設定 --> 更多設定 --> 系統安全 --> 通知存取權 --> 選擇你希望收到通知的 apps
Imgur

其他權限管理

設定 —> 更多應用 —> 在「已下載」中找到你希望收到推播的 apps —> 其他權限管理 Imgur

關閉清理記憶體

設定 --> 電量和性能 --> 省電最佳化 --> 右上角齒輪(設定)--> 鎖屏清理記憶體 --> 「從不」
Imgur

關閉系統記憶體優化

  1. 設定 --> 關於手機 --> 連續點擊 「MIUI 版本」 進入開發者權限。
  2. 退回到設定 --> 更多設定 --> 開發人員選項 --> 關閉「系統記憶體優化級別」
img

將 APP 釘選在背景

開啟希望收到推播的 app --> 點選 hamburger menu ,進入多工切換視窗 --> 將該 app 往下拉後即釘選在背景。
Imgur

重新安裝

有時候重新安裝就好了...

2017.11.18 補充

最近發現小米內建的安全防護、電池效能或記憶體優化的內建掃描程式,會把一些設定過的自啟動 Apps 改回沒有自啟動的功能。如果發生這種情況,除了重新把該 app 設定為可以自啟動外,whoscall 似乎重新打開 app 就可以讓它停留在背景,但是 pushbullet 即使重新打開 app 也無法進入背景,需要搭配重新開機才有效。
要確認該 app 有無停在背景,可以到「設定 —>更多應用 —> 運行中」看看該 app 有無在運行。

參考

2017年10月19日

[BS] Bootstrap 4 自訂容器和欄間距寬度(Custom Container and Gutters Width)

圖片來源:The Hack Today
tags: bootstrap4, container, layout, gutter
這篇文章主要是說明 Bootstrap 4 中 .container 的作用,以及 .col 間的間距(gutters)是如何產生的。最後則說明如何自訂自己想要寬度的 container 容器和欄間距。因此不會介紹太多關於 Bootstrap layout 的內容,關於 Bootstrap Layout 的介紹可以參考中文官方文件
註1:閱讀前希望你先知道 FlexBootstrap Container 的基本用法。
註2:以下說明均是以 max-width: 1140px 的情況說明(.col-xl)。
註3:可搭配 程式範例 @ CodePen
註4:如果是要套用到全域的話,請參考文末。

See the Pen Dive into Bootstrap4 Container by PJCHEN (@PJCHENder) on CodePen.

基本的 Bootstrap Container 結構

簡單來說,Bootstrap Container 基本的結構長這樣:
<!-- HTML -->
<div class="container">
    <div class="row">
        <div class="col">
            <div class="h4">
                Lorem ipsum dolor sit amet consectetur.
            </div>
        </div>
    </div>
</div>
畫面則會長這樣子:
Imgur
接下來讓我們看一下這些樣式主要的作用是什麼。

.container

從 Bootstrap 4 原始當中可以看到,.container 的樣式大概是這樣子:
.container {
  margin-right: auto;
  margin-left: auto;
  padding-right: 15px;
  padding-left: 15px;
  width: 100%;
  max-width: 1140px;      // 隨螢幕尺寸而變,當螢幕尺寸 ≥ 1200px 時是 1140px。
}
其中 .containermargin-rightmargin-left 是 auto,這是讓它可以水平置中的緣故。
接著當螢幕尺寸為 xl(≥1200px )時, 因為 max-width 是 1140px,而且有 padding-rightpadding-left 各 15px ,所以 :
  • 容器寬為 1140px(如果是使用 .container-fluid 則沒有設定 max-width
  • 內容寬為 1110px
  • 左右多於的空間自動分配給 margin
我們用 Chrome Dev Tool 看一下大概會長這樣子,有幾點可以注意的是:
  • 左右 margin 100 會隨著螢幕的寬度自動伸縮
  • 容器寬雖然是 1140px,但是由於有 padding 15px 的緣故,所以實際上內容的寬度是 1110px
Imgur
容器寬雖然是 1140px,但是由於有 padding 15px 的緣故,所以實際上內容的寬度是 1110px

.row

透過 .row 會讓裡面的內容以 flex 方式排版,原本在 .container 的作用下,內容寬會是 1110px ,但是因為有 margin-rightmargin-left-15px 的緣故,所以:
  • 容器寬為 1140px
  • 內容寬為 1140px
.row {
  display: flex;
  flex-wrap: wrap;
  margin-right: -15px;
  margin-left: -15px;
}
Imgur
原本在 .container 的作用下內容寬會是 1110px ,但是因為有 margin-rightmargin-left -15px 的緣故,所以容器和內容寬度均變回 1140px。

.col

.col.row 的 flex-item。在剛剛 .row 的作用下,.col 的容器寬再次變成了 1140px ,但由於 .row 裡面有 padding-rightpadding-left 15px,所以:
  • 容器寬為 1140px
  • 內容寬為 1110px
.col {
  position: relative;
  flex-basis: 0;
  flex-grow: 1;
  max-width: 100%;
  width: 100%;
  min-height: 1px;
  padding-right: 15px;
  padding-left: 15px;
}
Imgur

.col 裡面的 div

.col 中我們建立一個 <h4> 標籤,由於 .col 帶有 padding-leftpadding-right 15px 的緣故,所以 <h4>
  • 容器寬:1140px
  • 內容寬:1110px
Imgur

綜合上述,所以呢?

綜觀上面對於 .container, .row.col 的瞭解,我們可以知道,當.containermax-width: 1140px 時,整個容器的寬會是 1140px,但實際上內容的寬只會是 1110px,這兩者間差的 30px 就是來自於 padding-left: 15pxpadding-right: 15px

客制化 .container 容器寬度(Custom Container)

從上面的說明可以瞭解到,一般切版的時候,設計師給的稿上通常標示的會是內容寬度,而非容器寬度,因此假設設計師標示的內容寬度希望是 980px 時,我們就可以回推這時候我們 .containermax-width 應該要是 980px + 30px = 1010px。
速記:容器寬度 = 內容寬度 + 30px
註:這種算法只適用在 padding-left 和 padding-right 是 15px 的情況,也就是沒有修改 gutter 間距的情況。gutter 的修改會在後面的篇幅提到。
因此,我們可以在 HTML 中的 .container 這個 class 後面新增一個客制化的 class-.container-custom-width
<!-- HTML -->
<div class="container custom-container-width">
  <div class="row">
    <div class="col">
      <h4>div Lorem ipsum dolor sit amet consectetur adipisicing elit. Veniam, minima.</h4>
    </div>
  </div>
</div>
CSS 的部分就去修改原本 .containermax-width
/**
 * 期望的內容寬 + 30px ,才會是最後的容器寬(不改變間距寬度的情況下)
**/

.container.custom-container-width {
    max-width: 1010px;       // 最後內容寬會是 980px
}
顯示的結果如下,上面原本的 container,容器寬是 1140px,內容寬是 1100px;下面是修改後的 .custom-container-width,可以看到新的樣式,容器寬是 1010px,內容寬就會是設計給的 980px,而左右兩邊的 margin 則會自動根據螢幕裝置寬度調整:
Imgur

那麼欄間距(gutters)呢?

在看完基本的 container 結構後,來看一下欄之間的間距吧。
前面有看到不論在 .container, .row, .col 中常常出現 15px 這個數字,基本上 Bootstrap 就是利用 padding 和 margin 中的 15px 做到間距的效果。
我們只要在 .row 中使用多個 .col 時,各個欄位間會自動出現間距,HTML 結構像是這樣:
<!-- HTML -->
<div class="container">
  <div class="row">
    <div class="col">
      <h4>div Lorem ipsum dolor sit amet.</h4>
    </div>
    <div class="col">
      <h4>div Lorem ipsum dolor sit amet.</h4>
    </div>
    <div class="col">
      <h4>div Lorem ipsum dolor sit amet.</h4>
    </div>
  </div>
</div>
畫面會像這樣:
Imgur
可以看到每個 .col 都有 padding-left 和 padding-right 15px,這也就是 gutter 的來源,由於左右都有 padding 的緣故,所以欄位之間的間距會是 30px。
註:這種透過左右添加 padding 和 margin 達到 gutter 的效果,稱作 "gutter on outside",可以在 skectch 中找到這個選項,若想瞭解更多可以參考這篇 What is the purpose of having outside gutters on a responsive grid? @ StackExchange(謝謝卡斯伯大大 提供)。

.no-gutters

在 Bootstrap4 中提供了 .no-gutters 這個 class,可以讓我們把間距拿掉。從 Bootstrap 4 原始檔中可以看到 .no-gutters 的 CSS 如下:
.no-gutters {
  margin-right: 0;
  margin-left: 0;
}

.no-gutters > .col,
.no-gutters > [class*="col-"] {
  padding-right: 0;
  padding-left: 0;
}
因此如果在 .row 上搭配 .no-gutters 則會把原本 .rowmargin-leftmargin-right-15px 拿掉,並且把 padding-leftpadding-right 都修為 0 ,因此原本欄和欄之間的間距會變為 0,畫面會像這樣:
Imgur
  • 容器寬(.container)為 1140px
  • 內容全寬為 1110px
  • .col間不在有間距存在

客制化欄間距(Custom Gutters)

知道欄間距的作法是透過 padding 和 margin 之後就可以客制化我們自己想要的間距大小,舉例來說,現在設計師希望欄和欄之間的間距不要是 30px 而是 20px 這時候我們就可以客制化調整我們的 gutter。
因為要同時修改 .container, .row, .col 所使用到的 margin 或 padding 的 px 值,所以我們把客制化 gutter 用的 class .custom-gutters 加在和 .container 同一層,HTML 結構長這樣:
<!-- HTML -->
<div class="container custom-gutters">
  <div class="row">
    <div class="col">
      <h4>div Lorem ipsum dolor sit amet.</h4>
    </div>
    <div class="col">
      <h4>div Lorem ipsum dolor sit amet.</h4>
    </div>
    <div class="col">
      <h4>div Lorem ipsum dolor sit amet.</h4>
    </div>
  </div>
</div>
CSS 的部分,則是去修改和間距寬度相關的像素值,並且利用 SCSS 建立一個變數名為 $custom-gutter-width
/* SCSS */
$custom-gutter-width: 20px;     // 將欄距改成 20px
.custom-gutters {
    &.container {
        padding-left: $custom-gutter-width / 2;
        padding-right: $custom-gutter-width / 2;
    }
    .row {
        margin-left: -($custom-gutter-width / 2);
        margin-right: -($custom-gutter-width / 2);
    }
    .col {
        padding-left: $custom-gutter-width / 2;
        padding-right: $custom-gutter-width / 2;
    }
}
從畫面中可以看到,.col 的 padding 變成 10px,因此欄和欄之間的間距變成 10 + 10 = 20px,而且因為沒有去改變 .container 的 max-width,所以整個容器寬還是一樣是預設的 1140px:
Imgur
要留意的是,當我們把間距改成 20px 時,表示 .col 左右兩邊各會有 10px 的 padding,因此我們的內容寬實際上會是 .col 寬度在少 20px:
Imgur

客制化 Bootstrap 變數

上面的作法主要是用在:
  1. 直接使用 Bootstrap 4 下載的 bootstrap.css 檔案,沒有辦法透過編譯器重新編譯 SCSS 檔案時。
  2. 只要套用在某些頁面,不是整個網頁都要改變容器寬度或欄間距時。
也就是說,如果你可以重新透過 SCSS 編譯出新的 CSS 檔,而且要套用在整個網頁的情況下時,就可以不用使用上述的方法,而是可以透過客制化 Bootstrap 變數的方法來達成。
Bootstrap 的變數存放在 Bootstrap 資料夾中 ./scss/_variables.scss 這支檔案,裡面列了許多變數,像這裡提到的容器寬度和欄間距都可以在這裡找到。
/* ./scss/_variables.scss  */
// Grid containers
// Define the maximum width of `.container` for different screen sizes.

$container-max-widths: (
  sm: 540px,
  md: 720px,
  lg: 960px,
  xl: 1140px
) !default;

// Grid columns
// Set the number of columns and specify the width of the gutters.

$grid-columns: 12 !default;
$grid-gutter-width: 30px !default;
另外每個變數後面都有寫 !default,代表你只要在這支 SCSS 檔之前先宣告 $container-max-widths$grid-gutter-width 的值時,它就會覆蓋掉 Bootstrap 預設的變數值。
關於客制化 Bootstrap 變數的方法,可以參考官方網站-Customization Options 的說明。

參考

Grid @ Bootstrap4

2017年9月24日

[演算法] Max Stock Profit

此系列筆記主要依照 [Udemy] Learning Algorithms in JavaScript from Scratch by Eric Traub 的課程脈絡加以整理,但部分程式碼是消化後以自己較易理解的方式重新撰寫,因此和原課程內容有些出入。

問題描述

以陣列的方式儲存許多數值,這些數值是各個不同時間點股票的價格,例如,[32, 46, 26, 38, 40, 48, 42],我們要透過演算法:
  1. 找出在哪個時間點買進、哪個時間點賣出可以獲得最高的收益。以剛剛的陣列為例,應該在價格為 26 元時買入、48 元時賣出,這時會獲得 22 元的最高收益。
  2. 如果該天沒有獲利的可能則回傳 -1

演算法實做

在計算最高獲利時有一個重點,就是它是有時間性的,也就是說一定是先買進才能賣出,不可能先賣出,之後才買進。
因此,在我們的演算法中,會先將陣列中的第一個元素當作買進價(buyPrice),然後去和後面的價格做比較:
  • 當買進價低於賣出價時,就可以計算看看此時會有多少獲利(currentProfit
  • 當買進價高於賣出價時,把這個賣出價改成新的買進價(changeBuyPrice
我們先定義在這個含式中會使用到的變數:
function maxStockProfit (pricesArr) {
  // declaring variables
  let maxProfit = -1
  let currentProfit = 0
  let buyPrice = 0
  let sellPrice = 0 
  let changeBuyPrice = true

  // ...
}
接著要開始去找出獲益最高的組合:
function maxStockProfit (pricesArr) {
  // ...

  /**
   * 找出獲益最高的組合
  **/
  for (let i = 0; i < pricesArr.length; i++) {
    if (changeBuyPrice) {
      buyPrice = pricesArr[i];
    }
    sellPrice = pricesArr[i + 1];

    // 如果賣出價格 >= 買進價格,表示這是可以買入的價格
    // 因為獲利至少 >= 0
    if (sellPrice >= buyPrice) {
      currentProfit = sellPrice - buyPrice;
      if (currentProfit > maxProfit) {
        maxProfit = currentProfit;
      }
      changeBuyPrice = false;
    } else {
      // 如果賣出價格 < 買進價格,表示賣出的話一定會賠錢
      // 所以不能在此時買進
      changeBuyPrice = true;
    }
  }                             
  // ...
}

完整程式碼

function maxStockProfit(pricesArr) {
  // declaring variables
  let maxProfit = -1;
  let buyPrice = 0;
  let sellPrice = 0;
  let changeBuyPrice = true;

  /**
   * 找出獲益最高的組合
  **/
  for (let i = 0; i < pricesArr.length; i++) {
    if (changeBuyPrice) {
      buyPrice = pricesArr[i];
    }
    sellPrice = pricesArr[i + 1];

    // 如果賣出價格 >= 買進價格,表示獲利至少 >= 0
    // 可以賣出計算獲利
    let currentProfit 
    if (sellPrice >= buyPrice) {
      changeBuyPrice = false;
      currentProfit = sellPrice - buyPrice;
      if (currentProfit > maxProfit) {
        maxProfit = currentProfit;
      }
    } else {
      // 如果賣出價格 < 買進價格,表示賣出的話一定會賠錢
      // 所以不能在此時買進
      changeBuyPrice = true;
    }
  }

  return maxProfit;
}


console.log(maxStockProfit([32, 46, 26, 38, 40, 48, 42]))     // 22
console.log(maxStockProfit([10, 18, 4, 5, 9, 6, 16, 12]))     // 12
console.log(maxStockProfit([65, 54, 43, 32, 26, 15]))         // -1

資料來源

[演算法] 合併排序法(Merge Sort)

此系列筆記主要依照 [Udemy] Learning Algorithms in JavaScript from Scratch by Eric Traub 的課程脈絡加以整理,但部分程式碼是消化後以自己較易理解的方式重新撰寫,因此和原課程內容有些出入。

問題描述

透過函式將陣列中的數值加以排序。

前置知識:Merge Sort

Merge Sort 和 Bubble Sort 一樣,都是一種用來排序的演算法。
Merge Sort 的演算法主要包含兩個部分:

1. 將陣列對半拆分

Merge Sort 的第一步是要將陣列兩兩對半拆分,直到拆到每個陣列只剩一個元素:
// 原本的陣列
[3, 20, 8, 5, 1, 12, 17, 2]
// 進行對半拆分
[3, 20, 8, 5], [1, 12, 17, 2]
// 再拆分
[3, 20], [8, 5], [1, 12], [17, 2]
// 再拆分
[3], [20], [8], [5], [1], [12], [17], [2]

2.將陣列排序後加以合併

我們把上面拆分好的陣列,兩兩一組開始進行排序,每次排序時都是取該陣列的第一個元素進行比較:
// 拆分好的陣列
[3], [20], [8], [5], [1], [12], [17], [2]

// [3] vs [20]; [8] vs [5]; [1] vs [12]; [17] vs [2]
// 兩兩比較排序後合併,每次比較都是取當時陣列中的第一個元素進行比較
[3, 20], [5, 8], [1, 12], [2, 17]

// 再一次兩兩比較後排序後合併,每次比較都是取當時陣列中的第一個元素進行比較
[3, 5, 8, 20], [1, 2, 12, 17]

// 再一次兩兩比較後排序後合併,每次比較都是取當時陣列中的第一個元素進行比較
[ 1, 2, 3, 5, 8, 12, 17, 20 ]
兩兩比較後排序的方式可以參考下圖,每次比較都是取各陣列的第一個元素來進行比較:
整個合併排序法如下圖所示:
圖片來源:合併排序 @ wikipedia
因此我們一共需要兩個函式,並且一樣會透過遞回函式的方式來處理:
function mergeSort (arr) {
  // 接受一組尚未排序的陣列當作參數,將它們對半切分  
}

function sortBeforeMerge (arr1, arr2) {
  /**
   * 代入兩個已經各自排序過的陣列
   * 每次都取這兩個陣列中當時的第一個元素進行比較
   * 把數值小的放前面,最後合併成一個陣列
   **/
}
Merge Sort 這個演算法我覺得稍微有一點點小小複雜,非常需要圖像化來幫助思考,但我們可以先看 code ,看完之後會更瞭解它實際上的運作方式。

演算法實做

1. 將陣列進行對半切分

首先要將傳進來的陣列進行對半切分,這裡可以透過 Array.prototype.slice 來進行:
function mergeSort (arr) {
  // 接受一組尚未排序的陣列當作參數,將它們對半切分
  let middleIndex = Math.floor(arr.length / 2)
  let firstHalf = arr.slice(0, middleIndex)
  let secondHalf = arr.slice(middleIndex)  
}

2. 將兩組各自已經排序過的陣列以 merge sort 的方式合併

首先透過 while 迴圈,當傳入的兩組 已經各自排序過的 的陣列 arr1arr2 都沒有空陣列的情況下,選出兩陣列中當時的第一個元速進行比較,把比較小的先放入 sortedArr 中:
function sortBeforeMerge (arr1, arr2) {
  let sortedArr = []
  
  // 當 arr1 或 arr2 都不是空陣列時
  while (arr1.length && arr2.length) {
    // 以兩陣列中第一個元素進行比較,較小的推入 sortedArr 中
    let minElement = (arr1[0] < arr2[0]) ? arr1.shift() : arr2.shift()
    sortedArr.push(minElement)
  }
}
Array.prototype.shift() 可以把移除陣列中的第一個元素,並把它存出來。
經過 while 迴圈後,我們會得到一組已經經過 merge sort 方式排序好的陣列;另外,while 迴圈終止的條件是只要 arr1 或 arr2 其中一組為空陣列時就會停止,因此這時候 arr1 或 arr2 其中有一組還不是空陣列,我們要把不為空陣列的陣列透過 Array.prototype.concat 連接到 sortedArr 後面。
由於我們傳進 sortBeforeMerge 這個函式的陣列是已經經過排序的,因此,可以確定透過 concat 連結好的陣列,其數值也會是由小到大的:
function sortBeforeMerge (arr1, arr2) {
  let sortedArr = []
  
  // 當 arr1 或 arr2 都不是空陣列時
  while (arr1.length && arr2.length) {
    // ...
  }
  
  /**
   * 會跳出上面 while 的迴圈,表示 arr1 或 arr2 其中至少有一個為空陣列
   * 因此,如果 arr1 不是空陣列,則把它 concat 到 sortedArr 內;
   * 如果是 arr2 中不是空陣列,則把它 concat 到 sortedArr 內。
  **/
  sortedArr = arr1.length ? sortedArr.concat(arr1) : sortedArr.concat(arr2)
  return sortedArr
}

3. 結合遞回函式

最後我們要把 mergeSort 這個函式結合遞回的概念,在使用遞回函式時,一定要記得設定終止條件:
function mergeSort (arr) {
  
  // 遞回函式終止條件:當陣列被拆到只剩一個元素時
  if (arr.length <= 1) {
    return arr
  }
  
  // 接受一組尚未排序的陣列當作參數,將它們對半切分
  // ...
  
  // 遞回函式
  return sortBeforeMerge(mergeSort(firstHalf), mergeSort(secondHalf))
}

完整程式碼

function mergeSort (arr) {
  
  // 遞回函式終止條件:當陣列被拆到只剩一個元素時
  if (arr.length <= 1) {
    return arr
  }
  
  // 接受一組尚未排序的陣列當作參數,將它們對半切分
  let middleIndex = Math.floor(arr.length / 2)
  let firstHalf = arr.slice(0, middleIndex)
  let secondHalf = arr.slice(middleIndex)
  
  // 遞回
  return sortBeforeMerge(mergeSort(firstHalf), mergeSort(secondHalf))
}

function sortBeforeMerge (arr1, arr2) {
  /**
   * 代入兩個已經"各自排序過"的陣列,
   * 將這兩個陣列利用 merge sort 的方式排序後,合併回傳成一個陣列
   **/
  let sortedArr = []
  
  // 當 arr1 或 arr2 都不是空陣列時
  while (arr1.length && arr2.length) {
    // 以兩陣列中第一個元素進行比較,較小的推入 sortedArr 中
    let minElement = (arr1[0] < arr2[0]) ? arr1.shift() : arr2.shift()
    sortedArr.push(minElement)
  }
  
  /**
   * 會跳出上面 while 的迴圈,表示 arr1 或 arr2 其中至少有一個為空陣列
   * 因此,如果 arr1 不是空陣列,則把它 concat 到 sortedArr 內;
   * 如果是 arr2 中不是空陣列,則把它 concat 到 sortedArr 內。
  **/
  sortedArr = arr1.length ? sortedArr.concat(arr1) : sortedArr.concat(arr2)
  return sortedArr
}

console.log(mergeSort([3, 20, 8, 5, 1, 12, 17, 2]))

圖像化思考

上面這個情況可能會有點難想像實際上的遞回是怎麼執行的,假設我們現在執行函式:
mergeSort([4, 3, 2, 1])
它實際上會先將陣列拆成 [4, 3][2, 1],接著
return sortBeforeMerge(mergeSort[4, 3], mergeSort[2, 1])
這時候遞回函式就需要先去計算出 mergeSort[4, 3]mergeSort[2, 1] 的值。
mergeSort[4, 3] 為例,它則是會先把陣列拆成 [4], [3],接著 return sortBeforeMerge([4], [3]) ,在經過 sortBeforeMerge 的函式時,它會取這兩個陣列當時的第一個元素比較大小,最後會回傳 [3, 4]
同理, mergeSort[2, 1] 在經過 sortBeforeMerge([2], [1]) 之後,會回傳 [1, 2]
如下圖所示:
因此一開始的:
return sortBeforeMerge(mergeSort[4, 3], mergeSort[2, 1])
便會變成:
return sortBeforeMerge([3, 4], [1, 2])
最後便會得到 [1, 2, 3, 4]
如下圖所示:

資料來源