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

張偉傑發表於2018-07-12

本文主要記錄筆者在使用開源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-mp4.jpg

而可以下載的視訊請求,內容則是: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)
複製程式碼

重啟服務,重新整理後,發現效果如下:

content-type-mpeg4.jpg

果然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按鈕,選擇自己想提交的分支即可。

pull-request.jpg

結論

發起請求後,專案維護者愉快的採納了,於是我也有了對Node.js生態開源圈的第一次貢獻,心裡還是很高興的。

Pull Request的地址在這裡

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

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

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

相關文章