其實本來只是想要找 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 並且回傳正確值給使用者。這裡設計處理三種狀況
- 如果使用者有輸入 type 和 msg
- 如果只用者只有輸入 type
- 如果使用者沒有輸入任何資料
其中第一種狀況,又細分為
- 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)) // 1console.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])) // 5console.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