在FeathersJS中上传文件¶
在过去的几个月里,我们在 ciancoders.com 一直在使用Feathers和React进行一个新的SPA项目,这两个项目的组合结果是 真是太棒了.
最近,我们一直在努力寻找一种上传文件的方法,而无需编写单独的Express中间件或必须(重新)编写复杂的Feathers服务.
我们的目标¶
我们希望实现上传服务来完成一些重要的事情:
它必须处理大文件(+ 10MB).
它需要使用应用程序的身份验证和授权.
需要验证文件.
目前没有涉及第三方存储服务,但这将在不久的将来发生变化,因此必须做好准备.
它必须显示上传进度.
计划是将文件上传到feat服务,以便我们可以利用钩子进行身份验证,授权和验证以及服务事件.
幸运的是,存在一个文件存储服务: feathers-blob. 有了它我们可以实现我们的目标,但(扰乱警报)它带来了自己的问题,我们将在下面讨论.
带 feathers-blob 和 feathers-client 的基本上传¶
为了简单起见,我们将使用上传服务在一个非常基本的Feathers服务器上工作.
让我们看一下服务器代码:
/* --- server.js --- */
const feathers = require('@feathersjs/feathers');
const express = require('@feathersjs/express');
const socketio = require('feathers-socketio');
// feathers-blob service
const blobService = require('feathers-blob');
// Here we initialize a FileSystem storage,
// but you can use feathers-blob with any other
// storage service like AWS or Google Drive.
const fs = require('fs-blob-store');
const blobStorage = fs(__dirname + '/uploads');
// Feathers app
const app = express(feathers());
// Parse HTTP JSON bodies
app.use(express.json());
// Parse URL-encoded params
app.use(express.urlencoded({ extended: true }));
// Add REST API support
app.configure(express.rest());
// Configure Socket.io real-time APIs
app.configure(socketio());
// Upload Service
app.use('/uploads', blobService({Model: blobStorage}));
// Register a nicer error handler than the default Express one
app.use(express.errorHandler());
// Start the server
app.listen(3030, function(){
console.log('Feathers app started at localhost:3030')
});
让我们看一下在 @feathersjs/cli 生成的服务器代码中实现的:
/* --- /src/services/uploads/uploads.service.js --- */
// Initializes the `uploads` service on path `/uploads'
// Here we used the nedb database, but you can
// use any other ORM database.
const createService = require('feathers-nedb');
const createModel = require('../../models/uploads.model');
const hooks = require('./uploads.hooks');
const filters = require('./uploads.filters');
// feathers-blob service
const blobService = require('feathers-blob');
// Here we initialize a FileSystem storage,
// but you can use feathers-blob with any other
// storage service like AWS or Google Drive.
const fs = require('fs-blob-store');
// File storage location. Folder must be created before upload.
// Example: './uploads' will be located under feathers app top level.
const blobStorage = fs('./uploads');
module.exports = function() {
const app = this;
const Model = createModel(app);
const paginate = app.get('paginate');
// Initialize our service with any options it requires
app.use('/uploads', blobService({ Model: blobStorage}));
// Get our initialized service so that we can register hooks and filters
const service = app.service('uploads');
service.hooks(hooks);
if (service.filter) {
service.filter(filters);
}
};
feathers-blob 适用于abstract-blob-store,它是各种存储后端的抽象接口,例如文件系统,AWS或Google Drive.它只接受和检索编码为dataURI字符串的文件.
就像我们已准备好后端一样,继续将一些东西发布到 localhost:3030/uploads,例如 postman:
{
'uri': 'data:image/gif;base64,R0lGODlhEwATAPcAAP/+//7/////+////fvzYvryYvvzZ/fxg/zxWfvxW/zwXPrtW/vxXvfrXv3xYvrvYvntYvnvY/ruZPrwZPfsZPjsZfjtZvfsZvHmY/zxavftaPrvavjuafzxbfnua/jta/ftbP3yb/zzcPvwb/zzcfvxcfzxc/3zdf3zdv70efvwd/rwd/vwefftd/3yfPvxfP70f/zzfvnwffvzf/rxf/rxgPjvgPjvgfnwhPvzhvjvhv71jfz0kPrykvz0mv72nvblTPnnUPjoUPrpUvnnUfnpUvXlUfnpU/npVPnqVPfnU/3uVvvsWPfpVvnqWfrrXPLiW/nrX/vtYv7xavrta/Hlcvnuf/Pphvbsif3zk/zzlPzylfjuk/z0o/LqnvbhSPbhSfjiS/jlS/jjTPfhTfjlTubUU+/iiPPokfrvl/Dll/ftovLWPfHXPvHZP/PbQ/bcRuDJP/PaRvjgSffdSe3ddu7fge7fi+zkuO7NMvPTOt2/Nu7SO+3OO/PWQdnGbOneqeneqvDqyu3JMuvJMu7KNfHNON7GZdnEbejanObXnOW8JOa9KOvCLOnBK9+4Ku3FL9ayKuzEMcenK9e+XODOiePSkODOkOW3ItisI9yxL+a9NtGiHr+VH5h5JsSfNM2bGN6rMJt4JMOYL5h4JZl5Jph3Jpl4J5h5J5h3KJl4KZp5Ks+sUN7Gi96lLL+PKMmbMZt2Jpp3Jpt3KZl4K7qFFdyiKdufKsedRdm7feOpQN2QKMKENrpvJbFfIrNjJL1mLMBpLr9oLrFhK69bJFkpE1kpFYNeTqFEIlsoFbmlnlsmFFwpGFkoF/////7+/v///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAANAALAAAAAATABMAAAj/AKEJHCgokKJKlhThGciQYSIva7r8SHPFzqGGAwPd4bKlh5YsPKy0qFLnT0NAaHTcsIHDho0aKkaAwGCGEkM1NmSkIjWLBosVJT6cOjUrzsBKPl54KmYsACoTMmk1WwaA1CRoeM7siJEqmTIAsjp40ICK2bEApfZcsoQlxwxRzgI8W8XhgoVYA+Kq6sMK0QEYKVCUkoVqQwQJFTwFEAAAFZ9PlFy4OEEiRIYJD55EodDA1ClTbPp0okRFxBQDBRgskAKhiRMlc+Sw4SNpFCIoBBwkUMBkCBIiY8qAgcPG0KBHrBTFQbCEV5EjQYQACfNFjp5CgxpxagVtUhIjwzaJYSHzhQ4cP3ryQHLEqJbASnu+6EIW6o2b2X0ISXK0CFSugazs0YYmwQhziyuE2PLLIv3h0hArkRhiCCzAENOLL7tgAoqDGLXSSSaPMLIIJpmAUst/GA3UCiuv1PIKLtw1FBAAOw=='
}
该服务将以这样的方式回应:
{
'id': '6454364d8facd7a88e627e4c4b11b032d2f83af8f7f9329ffc2b7a5c879dc838.gif',
'uri': 'the-same-uri-we-uploaded',
'size': 1156
}
或者我们可以使用 feathers-client 和 jQuery 实现一个非常基本的前端:
<!doctype html>
<html>
<head>
<title>Feathersjs File Upload</title>
<script src='https://code.jquery.com/jquery-2.2.3.min.js' integrity='sha256-a23g1Nt4dtEYOj7bR+vTu7+T8VP13humZFBJNIYoEJo=' crossorigin='anonymous'></script>
<script type='text/javascript' src='//cdnjs.cloudflare.com/ajax/libs/core-js/2.1.4/core.min.js'></script>
<script type='text/javascript' src='//unpkg.com/feathers-client@^2.0.0/dist/feathers.js'></script>
<script type='text/javascript'>
// feathers client initialization
const rest = feathers.rest('http://localhost:3030');
const app = feathers()
.configure(feathers.hooks())
.configure(rest.jquery($));
// setup jQuery to watch the ajax progress
$.ajaxSetup({
xhr: function () {
var xhr = new window.XMLHttpRequest();
// upload progress
xhr.addEventListener('progress', function (evt) {
if (evt.lengthComputable) {
var percentComplete = evt.loaded / evt.total;
console.log('upload progress: ', Math.round(percentComplete * 100) + '%');
}
}, false);
return xhr;
}
});
const uploadService = app.service('uploads');
const reader = new FileReader();
// encode selected files
$(document).ready(function(){
$('input#file').change(function(){
var file = this.files[0];
// encode dataURI
reader.readAsDataURL(file);
})
});
// when encoded, upload
reader.addEventListener('load', function () {
console.log('encoded file: ', reader.result);
var upload = uploadService
.create({uri: reader.result})
.then(function(response){
// success
alert('UPLOADED!! ');
console.log('Server responded with: ', response);
});
}, false);
</script>
</head>
<body>
<h1>Let's upload some files!</h1>
<input type='file' id='file'/>
</body>
</html>
此代码监视文件选择,然后对其进行编码并执行ajax发布以上载它,通过xhr对象观察上载进度.一切都按预期工作.
我们选择的每个文件都会上传并保存到 ./uploads 目录中.
完成工作!让我们称它为一天,好吗?
… 但是,嘿,有些事情感觉不太正确……对吗?
DataURI上传问题¶
感觉不对,因为事实并非如此.让我们想象如果我们尝试上传一个大文件,例如25MB或更多文件会发生什么:整个文件(加上一些额外的MB由于编码)必须保存在整个上传过程的内存中,这对于一台普通的电脑,但对于移动设备来说,这是一个大问题.
我们有一个很大的RAM消耗问题.更不用说我们必须在发送之前对文件进行编码…
解决方案是修改服务,添加支持将dataURI分成小块,然后一次上传一个,收集并重新组装服务器上的所有内容.但是,嘿,这可能不是浏览器和网络服务器一直在做的事情,因为可能是网络的早期阶段?也许是因为Netscape Navigator?
嗯,实际上它是,并且做一个 multipart/form-data 帖子仍然是上传文件最简单的方法.
具有多部分支持的Feathers-blob.¶
回到后端,为了接受分段上传,我们需要一种方法来处理Web服务器收到的 multipart/form-data.鉴于Feathers的行为类似Express,我们只需使用``multer``和一个自定义中间件来处理它.
/* --- server.js --- */
const multer = require('multer');
const multipartMiddleware = multer();
// Upload Service with multipart support
app.use('/uploads',
// multer parses the file named 'uri'.
// Without extra params the data is
// temporarely kept in memory
multipartMiddleware.single('uri'),
// another middleware, this time to
// transfer the received file to feathers
function(req,res,next){
req.feathers.file = req.file;
next();
},
blobService({Model: blobStorage})
);
请注意,我们将文件字段名称保留为 uri 以保持一致性,因为服务始终使用该名称.但如果您愿意,可以更改它.
Feathers-blob只能理解编码为dataURI的文件,因此我们需要先将它们转换.让我们为此做一个钩子:
/* --- server.js --- */
const dauria = require('dauria');
// before-create Hook to get the file (if there is any)
// and turn it into a datauri,
// transparently getting feathers-blob to work
// with multipart file uploads
app.service('/uploads').before({
create: [
function(context) {
if (!context.data.uri && context.params.file){
const file = context.params.file;
const uri = dauria.getBase64DataURI(file.buffer, file.mimetype);
context.data = {uri: uri};
}
}
]
});
Etvoilà!.现在我们有一个FeathersJS文件存储服务,支持传统的分段上传,以及各种存储选项供选择.
就是棒.
进一步改进¶
该服务始终将dataURI返回给我们,这可能没有必要,因为我们刚刚上传了文件,我们还需要验证文件并检查授权.
所有这些事情都可以通过更多的Hook轻松完成,这样可以保留所有内部的FeathersJS服务.我把它留给了你.
对于前端,客户端存在一个问题:为了显示上传进度,它只使用REST功能,而不是实时使用socket.io.
解决方案是将 feathers-client 从REST切换到 socket.io,只需使用你想要的任何地方上传文件,这是一个简单的任务,因为我们能够做一个传统的 form-multipart 上传.
以下是使用dropzone的示例:
<!doctype html>
<html>
<head>
<title>Feathersjs File Upload</title>
<link rel='stylesheet' href='assets/dropzone.css'>
<script src='assets/dropzone.js'></script>
<script type='text/javascript' src='socket.io/socket.io.js'></script>
<script type='text/javascript' src='//cdnjs.cloudflare.com/ajax/libs/core-js/2.1.4/core.min.js'></script>
<script type='text/javascript' src='//unpkg.com/feathers-client@^2.0.0/dist/feathers.js'></script>
<script type='text/javascript'>
// feathers client initialization
var socket = io('http://localhost:3030');
const app = feathers()
.configure(feathers.hooks())
.configure(feathers.socketio(socket));
const uploadService = app.service('uploads');
// Now with Real-Time Support!
uploadService.on('created', function(file){
alert('Received file created event!', file);
});
// Let's use DropZone!
Dropzone.options.myAwesomeDropzone = {
paramName: 'uri',
uploadMultiple: false,
init: function(){
this.on('uploadprogress', function(file, progress){
console.log('progresss', progress);
});
}
};
</script>
</head>
<body>
<h1>Let's upload some files!</h1>
<form action='/uploads'
class='dropzone'
id='my-awesome-dropzone'></form>
</body>
</html>
所有代码都可以通过github在这里找到: https://github.com/CianCoders/feathers-example-fileupload
希望你今天学到了一些东西,因为我学到了很多东西.
干杯!