本文主要記錄筆者在使用開源Node.js web框架Koa.js過程中遇到的一個小bug,爲修復此Bug查找Koa及其middleware源碼的過程,以及最終發起Pull Request並被採納的過程。
緣起
事情的起因是這樣的,在我剛入職當前公司時,由於團隊組件不久,開發人員尚未配備期權,尤其缺乏服務端(Java
)開發人員;而恰好有一個對內視頻服務的需求比較緊急,所以本人雖然是一名(資深)前端工程師,依然主動承擔起了Server
端開發的責任。項目最終選擇FE
們最愛的Node.js
進行開發,web
框架則選擇了Koa.js
。
問題
Service
中有一個功能是爲生成的視頻提供下載功能。因爲僅是內部人員下載,加上每天都要生成,所以決定直接在存儲在服務器,然後提供鏈接供用戶下載。
於是在服務端選擇中間件koa-static
,煎蛋設置一下緩存即可。主要代碼如下:
const Koa = require('koa');
const serve = require('koa-static');
const app = new Koa();
app.use(
serve(path.join(__dirname + '/dist'), {
extensions: ['mp4'],
maxage: 1000 * 60 * 60 * 24 * 100
})
);
網頁部分提供一個下載按鈕,採用a
標籤加download
,外面套button
的形式,代碼如下(vue):
<button><a :href="video.outputPath" download>下載</a></button>
於是,功能完成,順利上線,運營小mm們效率提升,齊聲誇讚,完滿解決。
本集完。
如果生活是童話故事,那麼上面便是結局。可惜,生活不是童話。
大概在今年(2018)2月左右,忽然大家反映,下載按鈕不能用了,點擊後,都是直接在新的Tab
頁打開鏈接。
歸因
遇到bug後,第一反應是分析,能用 -> 不能用 的過程中,發生了什麼。經過大致判斷,可以得出結論是chrome
自動升級後,對download
的支持發生了變化。
接下來,我的第一反應是,是不是download
屬性沒有用好呢。於是去搜了搜標準,然後嘗試給賦值,結果發現一樣是不行。
這個時候我忽然想到,可以去看看別的網站,是否有同樣的問題,以及怎麼做的。
找了好久之後,發現了一個網站,視頻還可以下載,於是在chrome Develop Tools
的Network
面板下,苦苦尋找差異。終於發現,在Response Header
的Content-Type
中存在差異。我的請求情況如下:
而可以下載的視頻請求,內容則是:Content-Type: video/mpeg4
。於是我懷疑,是不是瀏覽器把自己能夠識別的擴展名直接打開,不能識別的則進行保存操作。那麼接下來要做的事情就簡單了:修改我們的響應頭。
初次嘗試
對於npm
安裝的package,個人建議直接去npm官網搜索,一般都會提供源碼地址,文檔地址。
於是直接進入npm官網,搜索koa-static
,進入該package主頁,發現如下內容:
- setHeaders Function to set custom headers on response.
既然官方直接提供了功能,那麼事情好辦了,直接加上吧。
修改Server端代碼如下:
app.use(
serve(path.join(__dirname + '/dist'), {
extensions: ['mp4'],
maxage: 1000 * 60 * 60 * 24 * 100,
setHeaders: function (res) {
res.setHeader('Content-Type', 'video/mpeg4');
}
})
);
歡天洗地,打開瀏覽器刷新重試,結果呢,無效!
深入源碼探索
柴犬屁股一沉,發現事情並不簡單
文檔救不了我們,只能去看源碼了。好在這些中間件一般都短小精悍並且邏輯嚴謹,讀一讀還是很有價值的。
對於node/js的項目,用到的package,直接打開項目目錄下的node_modules
找到對應目錄閱讀就可以了,十分方便。PS:大多數package入口在目錄下的 index.js
文件。
打開node_modules/koa-static/index.js
後,發現koa-static
直接把傳入的options
原封不動傳遞給了koa-send
:
function serve (root, opts) {
......
done = await send(ctx, ctx.path, opts)
於是繼續,打開node_modules/koa-send/index.js
,仔細閱讀代碼,發現對options中的setHeaders
處理如下:
// 此處爲一個Assertion,若setHeaders不是函數,直接拋出錯誤
const setHeaders = opts.setHeaders
if (setHeaders && typeof setHeaders !== 'function') {
throw new TypeError('option setHeaders must be function')
}
......
// 如果是函數,則將其加入到reponse header
if (setHeaders) setHeaders(ctx.res, path, stats)
這裏關於Assertion可以多說一句,斷言是編程中很使用的一種技巧,不管是開發、調試過程中快速發現錯誤,還是線上的防禦性編程。在《代碼大全》等經典書籍中都有介紹,推薦大家閱讀相關章節。
這麼看沒問題啊,傳入的config應該都使用了啊。於是繼續往下讀,發現玄機:
ctx.type = type(path, encodingExt)
...
/**
* File type.
*/
function type (file, ext) {
return ext !== '' ? extname(basename(file, ext)) : extname(file)
}
原來,在setHeader之後,源代碼又根據文件擴展名,修改了其content-type。爲了驗證自己的想法,我簡單修改這裏的代碼,進行嘗試:
if (!ctx.type) ctx.type = type(path, encodingExt)
重啓服務,刷新後,發現效果如下:
果然ok了。
Pull Request
既然折騰了這麼一大圈,解決了問題,於是我決定一不做二不休,直接給koa-send
開源項目Pull Request,如果被採納,還算是給開源屆做了Contribution。
過程很簡單,到項目主頁,fork項目。到自己主頁,把fork的項目checkout到本地,修改代碼,commit, push。
修改的代碼很簡單,但是注意,這些開源項目一般會有很重視測試,所以如果有UT,一定記得添加用例。我的代碼具體如下(提交內容不包含註釋):
// 刪除原來代碼:ctx.type = type(path, encodingExt)
if (!ctx.type) ctx.type = type(path, encodingExt)
// 添加Test Case
it('should set the Content-Type', function (done) {
const app = new Koa()
app.use(async (ctx) => {
await send(ctx, '/test/fixtures/user.json')
})
request(app.listen())
.get('/')
.expect('Content-Type', /application\/json/)
.end(done)
})
然後到還是到自己fork的項目中,選擇第二個Tab:Pull requests
,然後點擊New pull request
按鈕,選擇自己想提交的分支即可。
結論
發起請求後,項目維護者愉快的採納了,於是我也有了對Node.js
生態開源圈的第一次貢獻,心裏還是很高興的。
Pull Request的地址在這裏。
這件事情也給我帶來了一定的思考,整理後,結論如下:
- 寫代碼,解決問題,是充滿快樂的,能夠給我們帶來滿足感。
- 認真調研,閱讀文檔,甚至深入源碼,問題總歸是可以解決的。
- 我發現這些開源項目其實都有issue,並且有些維護者也公開說了
pull request is welcomed
,所以有時間可以多讀一些源碼,找機會多做一些貢獻。
以上就是這次修復bug、貢獻源碼的全過程以及給我帶來的思考。只做了一點小小的工作,謝謝大家。