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)颤介,哈哈哈梳星。