在FeathersJS中上传文件

在过去的几个月里,我们在 ciancoders.com 一直在使用Feathers和React进行一个新的SPA项目,这两个项目的组合结果是 真是太棒了.

最近,我们一直在努力寻找一种上传文件的方法,而无需编写单独的Express中间件或必须(重新)编写复杂的Feathers服务.

我们的目标

我们希望实现上传服务来完成一些重要的事情:

  1. 它必须处理大文件(+ 10MB).

  2. 它需要使用应用程序的身份验证和授权.

  3. 需要验证文件.

  4. 目前没有涉及第三方存储服务,但这将在不久的将来发生变化,因此必须做好准备.

  5. 它必须显示上传进度.

计划是将文件上传到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-clientjQuery 实现一个非常基本的前端:

<!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

希望你今天学到了一些东西,因为我学到了很多东西.

干杯!