在開發一個備份七牛文件到本地的工具過程當中,使用到了阿里開源的 Egg.js 框架,在此過程當中遇到了一些利用 ES6 Generator 函數以及 Promise 進行流程控制和 NodeJS 流相關的問題,總結事後分享一下。
項目的需求以下:html
下載:利用七牛'資源列舉'接口獲取文件 key 名,獲得可下載的外鏈,進行下載並保存到本地;node
上傳:開放 Web 端多文件上傳接口,接受 HTML5 input[type="file"] 形式的文件上傳,上傳到業務服務器後保存本地一份,再由服務器直傳到七牛雲服務器git
在 Generator Function、Async Function 和 Promise 大行其道的今天,在網上搜索相關名詞,大部分都會搜到如何利用 Promise 改寫回調函數
這類文章,然而有時候咱們會碰到回調函數寫法和Promise、Generator Function 寫法並存的狀況,好比七牛的Node.js服務端 SDK,就是回調函數寫法的,而 Egg.js 基於 Koa 1.x 版本,大量使用 Generator Function,這裏就會有一些坑。github
首先介紹 Egg.js 約定的部分目錄以下
egg目錄結構約定web
app/router.js 用於配置 URL 路由規則數據庫
app/controller/** 用於解析用戶的輸入,處理後返回相應的結果api
app/service/** 用於編寫業務邏輯層數組
controller 是直接和 router 相關的,故controller職責主要是解析請求,調用service獲取數據並返回給客戶端。瀏覽器
service層主要作操做數據庫、上傳文件等業務邏輯操做緩存
假設咱們要從七牛服務器獲取文件信息,有以下接口:
// bucketManager 是構造的一個七牛資源管理對象 bucketManager.listPrefix(args, options, callback){} // 很顯然七牛的資源列舉對象是callback寫法的,而在Egg裏,咱們通常把獲取資源寫做一個service,再在controller裏調用 // 因而第一反應這麼寫 // app/controller/backup.js * save(options) { const result = yield this.ctx.service.qiniuOperation.listFiles(options); } // app/service/qiniu_Operation.js * listFiles (options) { bucketManager.listPrefix(args, options, (err, respBody, respInfo) => { if (err) throw err; if (respInfo.statusCode === 200) { // 異步數據庫操做 yield this.ctx.service.backup.databaseOperation(); } }); }
當咱們這麼寫的時候,很快會提示運行時錯誤Unexpected strict mode reserved word yield
。
究其緣由是 yield 是不能被用在一個非generator函數裏的,上面代碼中包裹yield 的環境是一個回調函數(匿名函數),故yield是不能使用的。因而就遇到一個問題,如何在callback寫法的sdk中使用generator函數進行異步流程控制。
因爲對generator的不熟悉,這個問題查了好久都沒有答案,直到在CNode的精華區看到一個帖子,第七部分講解 app/service 的例子給了我很大啓發,例子是這樣的
module.exports = app=>(class BaiduService extends app.Service { constructor(ctx) { super(ctx); this.config = this.app.config; } * getBaiduHomePage() { let data = yield new Promise((resolve, reject)=> { require('request').get('http://www.baidu.com', function (err, res, data) { if (err) return reject(err); return resolve(data); }) }); return data; } });
咱們能夠yield一個generator function,還能夠yield一個Promise。上面代碼中個人思惟停留在在每個異步的回調函數中處理下一步的操做,而例子中則巧妙的應用Promise,在回調函數獲取到數據後利用resolve將控制交還到controller,controller無需關心service發生了什麼,只須要yield service提供的函數便可獲取到數據。因而改寫代碼以下:
// app/controller/backup.js const result = yield this.ctx.service.qiniuOperation.listFiles(options); // app/service/qiniu_Operation.js * listFiles(options) { const files = yield new Promise((resolve, reject) => { bucketManager.listPrefix(args, options, (err, respBody, respInfo) => { if (err) throw err; if (respInfo.statusCode === 200) { return resolve(respBody); } else { return reject(); } }); }); return files; }
在項目開發中首先作了下載到本地功能,再去作的上傳功能。
在實現文件下載到本地時,一開始沒有認真看Egg文檔中HTTP Client一節,比較笨的使用了NodeJS原生的http模塊的request方法來下載雲端文件。獲取到文件buffer以後,採用fs.appendFile將buffer保存到本地文件。
再作上傳功能時,有個需求是在上傳前保存一份備份到本地。查閱Egg文檔,對於單文件提供了getFileStream*()方法,對於多文件上傳提供了multipart插件。經過log兩種方法的返回值,他們都返回一個FileSreanm對象。
這裏因爲慣性思惟,我選擇了直接讀取FileStream對象中的buffer,發現stream._readableState.buffer是一個長度爲1的數組。便直接複用下載文件的fs.appendFile那部分代碼,將buffer直接保存爲文件。
然然後來有個需求是要求限制上傳文件大小爲4mb,在測試的時候才發現別說4mb,超過60多k的文件就傳不上去了,一次請求服務端最多能收到64k左右的數據,因爲HTTP Client的30000ms timeout時間的限制,還會致使30s後服務進程退出。相似這個issue.
這讓我發現我對NodeJS裏流的概念理解的太過淺薄了,上傳時傳來的FileStream對象是一個Readable Stream,Node文檔告訴咱們經過stream._readableState.buffer能夠獲取到緩存數據,這個數據的大小是由highWaterMark選項指定的,在沒有被持續讀的時候,stream是暫停的,沒有被消費掉,這會致使瀏覽器卡死,並致使http timeout的問題。因此經過直接讀取buffer下載文件,在文件超過必定大小時,就行不通了,這在思路上就是有問題的。
在參考了egg-example中關於上傳的例子以後, 改成建立一個可寫流來接收上傳傳輸來的FileStream,並經過pipe()方法讓流持續被寫入。代碼以下:
// app/controller/backup.js * webMultiUpload () { const parts = this.ctx.multipart(); while((part = yield parts)) { yield this.ctx.service.backup.saveToLocal(keyName, part); } } // app/service/backup.js * saveToLocal (keyName, fileStream) { return new Promise((resolve, reject) => { mkdirp(dir, (err) => { resolve(fileStream); }); }) .then((fileStream) => { const ws = fs.createWriteStream(keyName); fileStream.pipe(ws); }) .catch(err => { console.log(err); }); }