Read the source code of connect-flash

TD
TD’s note
Published in
12 min readJun 4, 2020
Source: https://getbootstrap.com/docs/4.0/components/alerts/

其實本來只是想要找 flash 的一個問題,結果意外發現 connect-flash 這個 package 的程式碼相當的小,於是就趁機嘗試讀了一下。

哪裡找 source code?

這個問題的答案看起來超簡單:直接到 Github 上看。也的確是這樣沒錯,不過前幾週看了 Code Spelunking: teach yourself how Rails works,講者提到

如果今天在飛機上我們遇到程式問題,但卻又連不上網路,無法 Google 或是 Stack Overflow 的話,我們是否能透過手上僅有的程式碼,來教會我們自己呢?

因此這讓我想到,如果我能夠讀懂 source code,是不是就有機會教會自己更多東西呢?然後在我們使用 Node.js 來做專案的時候,其實就已經把所有套件的程式碼都下載到 node_modules 裡面,根本就是寶庫啊(前提是要看得懂)

從哪裡開始?

程式碼總有個起頭,就像是 Node.js 的 app.js或是 index.js。對一個 Node.js package 來說,通常不是在最外面的 index.js ,就是在 lib 資料夾當中,可以找到 index.js 或其他名稱的檔案。

以 connect-flash 來說,就是 /lib/index.js 。同時也會發現, lib 資料夾裡面也只有兩份檔案(真的是超級小)

輸出什麼?

接下來,可能會想要很快看一下這個 package 最後輸出了什麼東西,好讓我們在自己專案當中可以用

const flash = require('connect-flash')

來引入。於是我們在 /lib/index.js 當中看到

exports = module.exports = require('./flash');

其實整份文件也就只有這一行程式碼!就是引入隔壁的 flash.js 檔案。如果我們進入 flash.js 檔案,首先,可以看到需要引入的套件

var format = require('util').format;
var isArray = require('util').isArray;

接著,很快就可以看到 flash.js 的主體

module.exports = function flash(options) {  
options = options || {};
var safe = (options.unsafe === undefined) ? true : !options.unsafe;
return function(req, res, next) {
if (req.flash && safe) { return next(); }
req.flash = _flash;
next();
}

}

就會發現,它回傳了一個 function,其實也就是個 middleware。另一方面,也發現我們在專案當中啟用 connect-flash 的時候,其實可以加入設定值 (options),在沒有輸入設定值的情況下,safe 值永遠為 true。

所以這段程式碼的意思就是,如果已經有了 req.flash,那麼就直接轉移控制權,交棒給下一個 middleware;如果還沒有 req.flash,那麼就讓 req.flash 等於 _flash 這個 function。看起來所有重要的功能都放在 _flash 裡面了。

至於為什麼我知道 _flash 是個 function,因為整份文件其實也只有兩個 functions、總共 82 行的程式碼(包含一堆註解),一眼望過去就看到了。

最後,回想一下我們在 Node.js 專案當中是怎麼啟用 connect-flash 的

const flash = require('connect-flash')     // 引入
app.use(flash()) // 啟用

所以其實在這裡我們是 invoke 了一個 JavaScript function,並過程中建立了 req.flash 這個未來我們會使用的 function。

Parameters of _flash

接著,讓我們來看 _flash 這個 function。通常變數命名前面有個底線,代表為 private variables 或 functions,也就是只有 connect-flash 內部的程式可以取用,外部(使用者)無法直接使用它。當然它本身也沒有 expose 任何的方式讓外部使用者取用。

function _flash(type, msg) {
...
}

這個 function 會接收兩個參數,分別是 type 和 msg。如果回想我們使用 req.flash,舉例來說會是

req.flash('error', 'xxxx')

就是建立一個帶有類別 (type) 的 flash message (msg)。不過實際上我們可以輸入更多的參數進去。讓我們繼續一步一步往下看。

Require sessions

function _flash(type, msg) {
...
if (this.session === undefined) throw Error('req.flash() requires sessions');

...
}

首先,要先檢查 Node.js 專案有沒有使用到 sessions,如果沒有,就丟出錯誤訊息提醒使用者。這裡的 this 指向 req。

msgs

function _flash(type, msg) {
...
var msgs = this.session.flash = this.session.flash || {};
...
}

接者,這裡建立一個新的變數 msgs 。如果 this.session.flash 裡面已經有訊息了,那麼就直接存入 msgs 當中;若無,就先建立一個空物件作後續使用。

前置工作準備完畢之後,接下來,就是要建立 flash message 並且回傳正確值給使用者。這裡設計處理三種狀況

  1. 如果使用者有輸入 type 和 msg
  2. 如果只用者只有輸入 type
  3. 如果使用者沒有輸入任何資料

其中第一種狀況,又細分為

  • 1–1. 使用者輸入超過 2 個參數
  • 1–2. 使用者輸入陣列當作 msg

1–1. 使用者輸入 type 和 msg,以及其他的參數

function _flash(type, msg) {  
...
if (type && msg) {
// util.format is available in Node.js 0.6+

if (arguments.length > 2 && format) {
var args = Array.prototype.slice.call(arguments, 1);
msg = format.apply(undefined, args);
} else if (isArray(msg)) {
...
}
return (msgs[type] = msgs[type] || []).push(msg);
} else if {
...
} else {
...
}
}

這裡又有兩種狀況,第一種是使用者一路輸入不同的參數,譬如

req.flash('error', 1, 2, 3, 4, 5)

也就是 arguments 大於 2 的狀況。在 JavaScript 的 function 當中,其實有一些內建好的變數是我們可以使用的,譬如這裡的 arguments 就是代表輸入的多少的變數。

這裡用了一個比較複雜的方式,把 arguments 用 slice 方法切割。array.slice(1) 的回傳值,其實就是除了 array 的第一個元素,其他都回傳。因此這裡代表的意思是,會將第一個參數認定為 type ,其餘的參數都將會是 msg 的一部分。

[2020.06.04 補充 by Huli]

arguments 其實不是陣列,只是長得跟陣列很像的物件

會看起來拐彎抹角呼叫 slice 就是因為他不是個陣列,所以沒有 slice 可以用,之前有寫過一篇,其中一部分有提到:https://blog.huli.tw/2020/04/18/javascript-function-is-awesome/

之後,透過 format 方法,將陣列當中的元素整併成單一字串

msg = format.apply(undefined, args)

譬如剛剛輸入的 1,2,3,4,5 ,中間會變成 [1,2,3,4,5] ,最後的 msg 會變成 "1 2 3 4 5"

發現這裡比較特別的是,上面這段任務的回傳值(程式碼),是寫在本身 if/else 之外。而 else 區塊的程式碼,本身就會 return 一個值。所以這裡 retunr 了 push 的結果。push 會回傳 array 最終的長度。而這裡的 array,就是我們所指定的 type當中的訊息 array 長度。

return (msgs[type] = msgs[type] || []).push(msg);

回顧一下,這裡的 msgs 其實是指向 this.session.flash ,也就是 req.session.flash。實驗一下即可知道

console.log(req.session.flash)                   // {}
console.log(req.flash('error', 1, 2, 3, 4, 5)) // 1
console.log(req.session.flash) // { error: [ '1 2 3 4 5' ] }

1–2. 使用者輸入 type 和 msg,其中 msg 為 array

function _flash(type, msg) {  
...
if (type && msg) {
// util.format is available in Node.js 0.6+

if (arguments.length > 2 && format) {
...
} else if (isArray(msg)) {
msg.forEach(function(val){
(msgs[type] = msgs[type] || []).push(val);
});
return msgs[type].length;
}

...
} else if {
...
} else {
...
}
}

這一段就相對單純許多。首先,先用 isArray() 來確認使用者是否輸入了 array 作為 msg。接著,就把這個陣列裡面的資料陸續 push 到 msgs[type] 當中。最後,同樣回傳陣列長度當作回傳值。

實驗一下:

console.log(req.session.flash)                     // {}
console.log(req.flash('error', [1, 2, 3, 4, 5])) // 5
console.log(req.session.flash) // { error: [ 1, 2, 3, 4, 5 ] }

2. 使用者只有輸入 type

if (type && msg) {    
// util.format is available in Node.js 0.6+
if (arguments.length > 2 && format) {
...
} else if (type) {
var arr = msgs[type];
delete msgs[type];
return arr || [];
}
else {
this.session.flash = {};
return msgs;
}
}

那麼就會清空該 type 裡面先前的所有資訊。不過這裡有趣的是,在刪掉資料之前,這裡用了 arr 先接了原本該 type 裡面的資訊,並作為回傳值。

實驗一下:

console.log(req.flash('error', 'hello'))   // 1
console.log(req.session.flash) // { error: [ 'hello' ] }
console.log(req.flash('error')) // [ 'hello' ]
console.log(req.session.flash) // {}

3. 如果使用者沒有輸入任何資料

if (type && msg) {    
// util.format is available in Node.js 0.6+
if (arguments.length > 2 && format) {
...
} else if (type) {
...
} else {
this.session.flash = {};
return msgs;
}

}

如果使用者沒有輸入任何資料,那麼就會直接清空 this.session.flash ,也就是 req.session.flash

這裡的回傳值也很有趣,雖然我們清空了資料,但早些時候 msgs 其實先接過了 this.session.flash當中的所有資料,所以這裡回傳的 msgs 也就是原本我們所擁有的資料。

看到這裡,就算是看完了整個 connect-flash 的 source code 了。內容本身不難,不過讓我比較好奇的是當初設計這個 package 的思維,譬如

  • 為什麼要讓 _flash 可以接收超過兩個以上的 arguments?
  • 要怎麼設計回傳值? (譬如那些刪掉資料的動作)
  • 如何設計「設定值」來改變整個 function 的運作(雖然這裡沒有用到)

等等。過程中也同時複習/學習了一些 JavaScript 語法,像是

  • call
  • apply
  • format

之前就一直期待自己有機會能夠好好讀一下 source code,這次閱讀 connect-flash 算是個蠻好的練習與開始,也很期待下一次的機會到來。

About me

Self-taught and trained in software development knowledge and skills, I am passionate about creating changes through technology.

Find more at Github, LinkedIn, Teaching at ALPHA Camp

--

--