一、需求分析
在日常使用计算机的过程中,看到喜欢的资源不可避免地想把它下载到我们的设备上保存下来,比如图片,音视频资源,文档资源等,基于这种应用场景,现在来看看在爱智设备上可以如何实现呢? 因为是离线下载,所以爱智 APP Web 端向爱智 APP Server 端发送一个请求,爱智后端启动多任务线程完成下载任务,这时候前端就可以关闭了。 需要将下载的进度实时返回到前端,使用 Socket.IO 将进度推送到前端,下载任务和 Socket.IO 之间需要异步通信,使用 SigSlot。
二、什么是 SigSlot?
SigSlot 是一个事件驱动的异步通信组件,支持多任务和多进程,这也是为什么选择使用 SigSlot 在 Task 中进行通信的原因,它继承自 EventEmitter,是一个典型的订阅和发布通信机制。 SigSlot 的功能还远不止于此,当应用申请开启 GSS 支持后,来自同一开发供应商的应用程序可以通过 GSS 的功能互相订阅和发布消息。在这里,只在同一应用中的多个线程中使用它的异步通信功能。
三、WebGet
WebGet 模块用于获取 http 数据,它支持分段请求数据,以及断点续传等,通过调用 WebGet 上的 file 方法来将数据保存到文件中,可以在选项中指定数据的起始位置、每段数据的大小以及并行请求的数量。 如果文件存在并且设置了 reload 为 true,WebGet 对象将检查日志并启动断点恢复过程。
四、实现过程
① 前端实现
前端部分是正常的发送请求,以及监听进度部分,这里给出大致的代码段:
import { defineComponent, ref, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { download } from '../apis/index'
export default defineComponent ( {
name: 'Home' ,
setup ( props, ctx) {
const router = useRouter ( )
const downloadList = ref ( [ ] )
onMounted ( ( ) = > {
socketio. socket. on ( 'progress' , data = > {
downloadList. value. forEach ( item = > {
if ( item. hash == = data. hash) {
item. progress = data. data
item. name = data. name
}
} )
if ( data. data == = 100 ) {
console. log ( '>>>> download over <<<<' )
}
} )
} )
}
} )
这里在 onMounted 的生命周期中接收 Socket.IO 传递过来的下载进度的数据,然后更新到对应的 DataView 中。
② Server 端实现
Server 端部分主要看三个部分的内容,router 中触发下载任务:
const Router = require ( 'webapp' ) . Router;
const SigSlot = require ( 'sigslot' ) ;
const fs = require ( 'fs' )
const sigslot = new SigSlot ( 'download' ) ;
const router = Router. create ( ) ;
const task = new Task ( './download-task.js' , 'download-task' , {
directory: module. directory
} )
router. post ( '/download' , function ( req, res) {
sigslot. emit ( 'download' , {
url: req. body. url,
hash: req. body. hash
} )
res. json ( {
code: 0 ,
msg: 'success'
} )
} ) ;
router. get ( '/download-list' , function ( req, res) {
res. json ( {
code: 0 ,
msg: 'ok' ,
data: fs. dumpdir ( './public/download' )
} )
} )
module. exports = router;
在路由一开始就将下载的线程运行了起来,在其内部监听一个 download 的 Sigslot 任务,路由这里接收到下载的请求后,就将参数通过 Sigslot 发送到了 Task 线程。 接下来看一下 Task 线程中的内容:
const WebGet = require ( 'webget' ) ;
const SigSlot = require ( 'sigslot' ) ;
const fs = require ( 'fs' )
const sigslot = new SigSlot ( 'download' ) ;
fs. mkdir ( './public/download' , 0 o666, true)
sigslot. slot ( 'download' , ( msg) = > {
const url = msg. url;
const fileNameArr = url. split ( '/' )
const fileName = fileNameArr[ fileNameArr. length - 1 ]
const path = `. / public/ download/ ${ fileName} `;
let totalSize = 0
WebGet. file ( url, path, {
limits: 512 ,
lines: 2 ,
reload: true
} , ( loader) = > {
loader. on ( 'response' , ( info) = > {
totalSize = info. requestSize;
} ) ;
loader. on ( 'data' , ( chunk, info) = > {
const progress = info. completeSize / totalSize * 100 ;
sigslot. emit ( 'progress' , {
data: Math. round ( progress) ,
hash: msg. hash,
name: fileName
} )
} ) ;
loader. on ( 'end' , ( ) = > {
console. log ( `Download finish, file: ${ path} `) ;
} ) ;
loader. on ( 'error' , ( e) = > {
console. log ( 'Download error:' , e. message) ;
} ) ;
} )
} ) ;
require ( 'iosched' ) . forever ( ) ;
Task 中只做一件事,外层 SigSlot 订阅下载任务,然后使用 WebGet.file 进行资源下载,按照需求配置分片以及断点续传等功能。WebGet.file 的回调函数返回 WebGet 对象,可以在 WebGet 对象的 data 事件中获取计算进度的数据,最后再通过 SigSlot 发布到 Socket.IO 的订阅事件中。 最后看返回给前端的下载进度:
const WebApp = require ( 'webapp' ) ;
const io = require ( 'socket.io' ) ;
const SigSlot = require ( 'sigslot' ) ;
const bodyParser = require ( 'middleware' ) . bodyParser;
const sigslot = new SigSlot ( 'download' ) ;
const myRouter = require ( './routers/rest' ) ;
const app = WebApp. createApp ( ) ;
app. use ( WebApp. static ( './public' ) ) ;
app. use ( bodyParser. json ( ) ) ;
app. use ( bodyParser. urlencoded ( ) ) ;
app. use ( '/api' , myRouter) ;
app. get ( '/temp.html' , function ( req, res) {
res. render ( 'temp' , { time: Date. now ( ) } ) ;
} ) ;
app. start ( ) ;
const socketio = io ( app, {
serveClient: false,
pingInterval: 10000 ,
pingTimeout: 5000
} )
socketio. on ( 'connection' , socket = > {
console. log ( '>> socket connected <<' )
sigslot. slot ( 'progress' , ( msg) = > {
socket. emit ( 'progress' , msg)
} ) ;
} )
require ( 'iosched' ) . forever ( ) ;
在入口文件中建立 Socket 连接,在连接建立后,开始订阅在上一步的 Task 中发布的下载进度数据,再用 Socket 发布到前端。到这里,就已经将所有环节串联了起来,整个离线下载任务就基本上完成了,只需要发送下载请求,即可把任务交个爱智来完全掌握。
还有很多不同的复杂场景,在解决问题之前,先将复杂的任务简单化,然后灵活运用 JSRE 提供的丰富的 API 接口,逐个解决小任务,最终将各个环节串联合并完成整个功能。