記一次對Koa.js middleware的源碼貢獻

本文主要記錄筆者在使用開源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 ToolsNetwork面板下,苦苦尋找差異。終於發現,在Response HeaderContent-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的地址在這裏

這件事情也給我帶來了一定的思考,整理後,結論如下:

  1. 寫代碼,解決問題,是充滿快樂的,能夠給我們帶來滿足感。
  2. 認真調研,閱讀文檔,甚至深入源碼,問題總歸是可以解決的。
  3. 我發現這些開源項目其實都有issue,並且有些維護者也公開說了pull request is welcomed,所以有時間可以多讀一些源碼,找機會多做一些貢獻。

以上就是這次修復bug、貢獻源碼的全過程以及給我帶來的思考。只做了一點小小的工作,謝謝大家。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章