Node层模拟实现multipart表单的文件上传 进入全屏
line

有时候就是有这样的需求,Nodejs做webserver,从浏览器端上传文件到后端服务器,Node层只是做一个数据中转,如果在这个过程中,Node webserver需要对数据进行适当加工,然后再Post到后端,那么就得在Node层模拟文件上传了。


首先,通过浏览器上传文件,PostData格式是长着个样子的:

屏幕快照 2014-11-22 下午9.18.45.png

如图,每一组数据其实就是用“-----WebkitFormBoundary.....”分隔开的,最后再用这个分隔符结束,而且,这个分隔符完全是可自定义的。

每一段提交数据,则通过Content-Disposition来描述,未指定Content-Type,则默认text/plain,如果是上传的二进制文件,指定其mime-type即可。

简单封装一个方法,实现Node层的文件上传:


/**
 * 上传文件
 * @param files     经过formidable处理过的文件
 * @param req        httpRequest对象
 * @param postData    额外提交的数据
 */
function uploadFile(files, req, postData) {
    var boundaryKey = Math.random().toString(16);
    var endData = '\r\n----' + boundaryKey + '--';
    var filesLength = 0, content;

    // 初始数据,把post过来的数据都携带上去
    content = (function (obj) {
        var rslt = [];
        Object.keys(obj).forEach(function (key) {
            arr = ['\r\n----' + boundaryKey + '\r\n'];
            arr.push('Content-Disposition: form-data; name="' + key + '"\r\n\r\n');
            arr.push(obj[key]);
            rslt.push(arr.join(''));
        });
        return rslt.join('');
    })(postData);

    // 组装数据
    Object.keys(files).forEach(function (key) {
        if (!files.hasOwnProperty(key)) {
            delete files.key;
            return;
        }
        content += '\r\n----' + boundaryKey + '\r\n' +
            'Content-Type: application/octet-stream\r\n' +
            'Content-Disposition: form-data; name="' + key + '"; ' +
            'filename="' + files[key].name + '"; \r\n' +
            'Content-Transfer-Encoding: binary\r\n\r\n';
        files[key].contentBinary = new Buffer(content, 'utf-8');
        filesLength += files[key].contentBinary.length + fs.statSync(files[key].path).size;
    });

    req.setHeader('Content-Type', 'multipart/form-data; boundary=--' + boundaryKey);
    req.setHeader('Content-Length', filesLength + Buffer.byteLength(endData));

    // 执行上传
    var allFiles = Object.keys(files);
    var fileNum = allFiles.length;
    var uploadedCount = 0;
    allFiles.forEach(function (key) {
        req.write(files[key].contentBinary);
        var fileStream = fs.createReadStream(files[key].path, {bufferSize: 4 * 1024});
        fileStream.on('end', function () {
            // 上传成功一个文件之后,把临时文件删了
            fs.unlink(files[key].path);
            uploadedCount++;
            if (uploadedCount == fileNum) {
                // 如果已经是最后一个文件,那就正常结束
                req.end(endData);
            }
        });
        fileStream.pipe(req, {end: false});
    });
}

思路就这样,代码也不复杂,可能额外需要注意的是,在http.request的response处理中,response.headers可能是gzip的,这个时候buffer不能直接toString,需要通过zlib解码再转换为string,大概思路:


var result = [];
response.on('data', function (chunk) {
    result.push(chunk);
});

// 处理response
var _dealResponse = function (data) {
    var buffer = data;
    try {
        data = data.toString('utf8');
        data = data ? (JSON.parse(data) || data) : false;
    } catch (err) {
        // 接口返回数据格式异常,解析失败
        console.log(err);
    }

    self.res.writeHead(response.statusCode, 'OK', {
        'content-type': 'text/plain; charset=utf-8',
        'content-length': buffer.length
    });
    self.res.write(buffer);
    self.res.end();
};

response.on('end', function () {
    result = Buffer.concat(result);
    // gzip 的数据,需要zlib解码
    if (response.headers['content-encoding'] == 'gzip') {
        zlib.gunzip(result, function (err, dezipped) {
            var data = err ? new Buffer('{}') : dezipped;
            _dealResponse(data);
        });
    } else {
        _dealResponse(result);
    }
});

Mark一下,也许你路过正好需要~~~

阿里巴巴-钉钉-开放平台,能力开放&开发者运营岗位招聘中, 期待你的加入!
钉钉开放,让应用开发更简单
充分开放,是钉钉的重要方向!除致力于为开发者打造丰富的开放API, 更易接入的场景化能力包, 完备的应用开发工具之外, 还需要持续构建开放能力的布道、开发者生态运营体系,包括培训、沙龙、大会、社区合作等等。业务在快速发展,我们也还需要更多优秀的小伙伴加入!

评论区域

line