当前位置: 首页 > 后端技术 > Node.js

Egg.js开发七牛云备份项目总结

时间:2023-04-03 23:23:28 Node.js

起因在开发七牛文件备份到本地的工具过程中,使用了阿里开源的Egg.js框架。过程中,一些ES6Generator函数和Promise用于过程控制和NodeJS流相关的问题,总结之后,分享一下。项目需求如下:下载:使用七牛的“资源列表”接口获取文件键名,获取可下载的外部链接,下载并保存到本地;上传:打开web端多文件上传界面,接受HTML5input[type="file"]格式文件上传,上传到业务服务器并保存一份本地副本,然后从服务器直接上传到七牛云服务器。使用GeneratorFunction和Promise解决多层异步回调。今天流行GeneratorFunction、AsyncFunction和Promise,在网上搜索相关术语,大多会找到关于如何使用Promise重写回调函数的文章。但是,有时候我们会遇到回调函数的编写与Promise和GeneratorFunction的编写并存的情况,比如七牛的Node.js服务端SDK是在回调函数中编写的,而Egg.js是基于Koa1.x版本的,这大量使用GeneratorFunction,所以这里会有一些坑。先介绍一下Egg.js约定的一些目录如下egg目录结构约定app/router.js用于配置URL路由规则app/controller/**用于解析用户输入,处理后返回相应结果app/service/**用于编写业务逻辑层的controller与router直接相关,所以controller的主要职责是解析请求,调用服务获取数据返回给客户端。服务层主要进行操作数据库、上传文件等业务逻辑操作。异步操作继承自Koa1.x。ManagementobjectbucketManager.listPrefix(args,options,callback){}//很明显七牛的资源枚举对象是写在callback里的,但是在Egg中,我们一般把资源获取写成service,然后在controller中调用//所以第一反应是这样写//app/controller/backup.js*save(options){constresult=yieldthis.ctx.service.qiniuOperation.listFiles(options);}//app/service/qiniu_Operation.js*listFiles(options){bucketManager.listPrefix(args,options,(err,respBody,respInfo)=>{if(err)throwerr;if(respInfo.statusCode===200){//异步数据库操作yieldthis.ctx.service.backup.databaseOperation();}});}这样写的时候,很快就会提示runtimeerrorUnexpectedstrictmodereservedwordyield。原因是yield不能用在非生成器函数中。上面代码中的环境包裹yield是一个回调函数(匿名函数),所以不能使用yield。于是就遇到了一个问题,如何在callback写sdk中使用generator函数进行异步流程控制。由于对generator不熟悉,这个问题查了很久都没有答案,直到看到CNode本质中的一篇帖子,第七部分讲解app/service的例子给了我很大的启发,例子是这个module.exports=app=>(classBaiduServiceextendsapp.Service{constructor(ctx){super(ctx);this.config=this.app.config;}*getBaiduHomePage(){letdata=yieldnewPromise((resolve,reject)=>{require('request').get('http://www.baidu.com',function(err,res,data){if(err)returnreject(err);return解析(数据);})});返回数据;}});我们可以生成一个生成器函数,也可以生成一个Promise。在上面的代码中,我的思路停留在每个异步回调函数中去处理接下来的操作。在示例中,巧妙地使用了Promise。回调函数获取数据后,使用resolve将控制权返回给controller。控制器不需要关心服务。怎么回事,只需要yield服务提供的函数就可以拿到数据了。于是重写代码如下://app/controller/backup.jsconstresult=yieldthis.ctx.service.qiniuOperation.listFiles(options);//app/service/qiniu_Operation.js*listFiles(options){constfiles=yieldnewPromise((resolve,reject)=>{bucketManager.listPrefix(args,options,(err,respBody,respInfo)=>{if(err)throwerr;if(respInfo.statusCode===200){returnresolve(respBody);}else{returnreject();}});});returnfiles;}上传文件在项目开发中,先实现下载到本地的功能,再进行上传的功能。在本地下载文件的时候,一开始没有仔细看Egg文档中的HTTPClient部分,而是使用了NodeJS原生http模块的request方法来下载云端文件。获取文件缓冲区后,使用fs.appendFile将缓冲区保存到本地文件。再次做上传功能时,有需求在上传前先备份到本地。查看Egg文档,单文件提供getFileStream*()方法,多文件上传提供multipart插件。通过log的两个方法的返回值,都返回一个FileSream对象。这里由于惯性思维,我选择了直接读取FileStream对象中的buffer,发现stream._readableState.buffer是一个长度为1的数组。那么直接复用下载的fs.appendFile部分代码文件,并将缓冲区直接保存为文件。不过后来有要求限制上传文件的大小为4mb。测试的时候发现,别说4mb了,超过60k的文件都上传不了。对服务器的一次请求最多可以接收到64k的数据。由于HTTPClient的30000ms超时时间限制,也会导致服务进程在30s后退出。类似这个问题。这让我意识到我对NodeJS中流的概念理解太浅了。上传时传输的FileStream对象是一个ReadableStream。Node文档告诉我们可以通过stream._readableState.buffer获取缓存数据。此数据的大小由highWaterMark选项指定。当不连续读取时,流被挂起不消费,会导致浏览器卡顿,造成http超时问题。因此,通过直接读取缓冲区来下载文件,当文件超过一定大小时,是行不通的。这是思想上的问题。参考了egg-example中上传的例子,改为创建一个可写流来接收上传传输的FileStream,并使用pipe()方法保持流的写入。代码如下://app/controller/backup.js*webMultiUpload(){constparts=this.ctx.multipart();while((part=yieldparts)){yieldthis.ctx.service.backup.saveToLocal(keyName,part);}}//app/service/backup.js*saveToLocal(keyName,fileStream){returnnewPromise((resolve,reject)=>{mkdirp(dir,(err)=>{resolve(fileStream);});}).then((fileStream)=>{constws=fs.createWriteStream(keyName);fileStream.pipe(ws);}).catch(err=>{console.log(err);});}