egg-multipart

egg-multipart是一個(gè)處理文件上傳的插件,下面我根據(jù)自己理解分析下這個(gè)插件源碼久橙。

egg默認(rèn)配置egg-multipart茵肃,在app.js會(huì)調(diào)用中間件處理,注意這是是講插件的file模式

// src/app.js
app.coreLogger.info('[egg-multipart] will save temporary files to %j, cleanup job cron: %j',
      options.tmpdir, options.cleanSchedule.cron);
// enable multipart middleware
app.config.coreMiddleware.push('multipart');

上面就是導(dǎo)入egg-multipart的入口代碼辐烂,他會(huì)自動(dòng)調(diào)用multipart插件遏插,你可以參考官方文檔

中間件的核心代碼

module.exports = options => {
  // normalize
  const matchFn = options.fileModeMatch && pathMatching({ match: options.fileModeMatch });

  return async function multipart(ctx, next) {
    if (!ctx.is('multipart')) return next();
    if (matchFn && !matchFn(ctx)) return next();

    await ctx.saveRequestFiles();
    return next();
  };
};

其實(shí)就是調(diào)用 ctx.saveRequestFiles()這個(gè)方法

這個(gè)方法就是將傳遞的file文件

 async saveRequestFiles(options) {
    options = options || {};
    const ctx = this;

    const multipartOptions = {
      autoFields: false,
    };
    if (options.defCharset) multipartOptions.defCharset = options.defCharset;
    if (options.limits) multipartOptions.limits = options.limits;
    if (options.checkFile) multipartOptions.checkFile = options.checkFile;

    let storedir;

    const requestBody = {};
    const requestFiles = [];

    const parts = ctx.multipart(multipartOptions);
    let part;
    do {
      try {
        part = await parts();
      } catch (err) {
        await ctx.cleanupRequestFiles(requestFiles);
        throw err;
      }

      if (!part) break;

      if (part.length) {
        ctx.coreLogger.debug('[egg-multipart:storeMultipart] handle value part: %j', part);
        const fieldnameTruncated = part[2];
        const valueTruncated = part[3];
        if (valueTruncated) {
          await ctx.cleanupRequestFiles(requestFiles);
          return await limit('Request_fieldSize_limit', 'Reach fieldSize limit');
        }
        if (fieldnameTruncated) {
          await ctx.cleanupRequestFiles(requestFiles);
          return await limit('Request_fieldNameSize_limit', 'Reach fieldNameSize limit');
        }

        // arrays are busboy fields
        requestBody[part[0]] = part[1];
        continue;
      }

      // otherwise, it's a stream
      const meta = {
        field: part.fieldname,
        filename: part.filename,
        encoding: part.encoding,
        mime: part.mime,
      };
      // keep same property name as file stream
      // https://github.com/cojs/busboy/blob/master/index.js#L114
      meta.fieldname = meta.field;
      meta.transferEncoding = meta.encoding;
      meta.mimeType = meta.mime;

      ctx.coreLogger.debug('[egg-multipart:storeMultipart] handle stream part: %j', meta);
      // empty part, ignore it
      if (!part.filename) {
        await sendToWormhole(part);
        continue;
      }

      if (!storedir) {
        // ${tmpdir}/YYYY/MM/DD/HH
        storedir = path.join(ctx.app.config.multipart.tmpdir, moment().format('YYYY/MM/DD/HH'));
        const exists = await fs.exists(storedir);
        if (!exists) {
          await mkdirp(storedir);
        }
      }
      const filepath = path.join(storedir, uuid.v4() + path.extname(meta.filename));
      const target = fs.createWriteStream(filepath);
      await pump(part, target);
      // https://github.com/mscdex/busboy/blob/master/lib/types/multipart.js#L221
      meta.filepath = filepath;
      requestFiles.push(meta);

      // https://github.com/mscdex/busboy/blob/master/lib/types/multipart.js#L221
      if (part.truncated) {
        await ctx.cleanupRequestFiles(requestFiles);
        return await limit('Request_fileSize_limit', 'Reach fileSize limit');
      }
    } while (part != null);

    ctx.request.body = requestBody;
    ctx.request.files = requestFiles;
  },

可以看出代碼量非常小,但最主要過(guò)程是ctx.multipart方法創(chuàng)建一個(gè)實(shí)例纠修,將上傳文件存儲(chǔ)在臨時(shí)目錄中胳嘲,然后封裝requestBody和requestFiles這個(gè)2個(gè)方法。所以只要搞明白ctx.multipart這個(gè)方法的實(shí)現(xiàn)扣草,基本上你就明白egg-multipart是怎么處理上傳文件了牛。

const parse = require('co-busboy');
/**
   * create multipart.parts instance, to get separated files.
   * @function Context#multipart
   * @param {Object} [options] - override default multipart configurations
   *  - {Boolean} options.autoFields
   *  - {String} options.defCharset
   *  - {Object} options.limits
   *  - {Function} options.checkFile
   * @return {Yieldable} parts
   */
  multipart(options) {
    // multipart/form-data ctx.is() 檢查傳入請(qǐng)求是否包含 Content-Type 消息頭字段, 并且包含任意的 mime type辰妙。
    if (!this.is('multipart')) {
      this.throw(400, 'Content-Type must be multipart/*');
    }
    // 避免重復(fù)處理
    if (this[HAS_CONSUMED]) throw new TypeError('the multipart request can\'t be consumed twice');

    this[HAS_CONSUMED] = true;
    const parseOptions = Object.assign({}, this.app.config.multipartParseOptions);
    options = options || {};
    if (typeof options.autoFields === 'boolean') parseOptions.autoFields = options.autoFields;
    if (options.defCharset) parseOptions.defCharset = options.defCharset;
    if (options.checkFile) parseOptions.checkFile = options.checkFile;
    // merge and create a new limits object
    if (options.limits) parseOptions.limits = Object.assign({}, parseOptions.limits, options.limits);
    return parse(this, parseOptions);
  },

這里代碼也非常簡(jiǎn)單鹰祸,其實(shí)看到這里你就明白了,egg-multipart是封裝co-busboy的插件密浑。但在繼續(xù)分析下去我沒(méi)什么動(dòng)力了蛙婴,等有機(jī)會(huì)我看看co-busboy插件源碼。

總結(jié)

整體來(lái)看尔破,egg-multipart做事情都很簡(jiǎn)單街图,就封裝了三個(gè)主要方法背传,當(dāng)mode是file模式時(shí),中間件多了一步調(diào)用await ctx.saveRequestFiles()方法台夺,其實(shí)本事也是調(diào)用ctx.multipart方法去二次封裝處理request.body 和 request.files径玖。相信到這里你跟我一樣有大概思路,但是啥也寫(xiě)不出來(lái)颤介,哈哈哈梳星。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市滚朵,隨后出現(xiàn)的幾起案子冤灾,更是在濱河造成了極大的恐慌,老刑警劉巖辕近,帶你破解...
    沈念sama閱讀 219,188評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件韵吨,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡移宅,警方通過(guò)查閱死者的電腦和手機(jī)归粉,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,464評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)漏峰,“玉大人糠悼,你說(shuō)我怎么就攤上這事∏城牵” “怎么了倔喂?”我有些...
    開(kāi)封第一講書(shū)人閱讀 165,562評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀(guān)的道長(zhǎng)靖苇。 經(jīng)常有香客問(wèn)我席噩,道長(zhǎng),這世上最難降的妖魔是什么贤壁? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,893評(píng)論 1 295
  • 正文 為了忘掉前任悼枢,我火速辦了婚禮,結(jié)果婚禮上芯砸,老公的妹妹穿的比我還像新娘萧芙。我一直安慰自己给梅,他們只是感情好假丧,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,917評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著动羽,像睡著了一般包帚。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上运吓,一...
    開(kāi)封第一講書(shū)人閱讀 51,708評(píng)論 1 305
  • 那天渴邦,我揣著相機(jī)與錄音疯趟,去河邊找鬼。 笑死谋梭,一個(gè)胖子當(dāng)著我的面吹牛信峻,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播瓮床,決...
    沈念sama閱讀 40,430評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼盹舞,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了隘庄?” 一聲冷哼從身側(cè)響起踢步,我...
    開(kāi)封第一講書(shū)人閱讀 39,342評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎丑掺,沒(méi)想到半個(gè)月后获印,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,801評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡街州,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,976評(píng)論 3 337
  • 正文 我和宋清朗相戀三年兼丰,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片唆缴。...
    茶點(diǎn)故事閱讀 40,115評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡地粪,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出琐谤,到底是詐尸還是另有隱情蟆技,我是刑警寧澤,帶...
    沈念sama閱讀 35,804評(píng)論 5 346
  • 正文 年R本政府宣布斗忌,位于F島的核電站质礼,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏织阳。R本人自食惡果不足惜眶蕉,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,458評(píng)論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望唧躲。 院中可真熱鬧造挽,春花似錦、人聲如沸弄痹。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,008評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)肛真。三九已至谐丢,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背乾忱。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,135評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工讥珍, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人窄瘟。 一個(gè)月前我還...
    沈念sama閱讀 48,365評(píng)論 3 373
  • 正文 我出身青樓衷佃,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親蹄葱。 傳聞我的和親對(duì)象是個(gè)殘疾皇子纲酗,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,055評(píng)論 2 355