起因 為什麼做這個東西,是突然間聽一後端同事說起 "Annie" 這個東西,發現這個東西下載視頻挺方便的,會自動爬取網頁中的視頻,然後整理成列表。發現用命令執行之後是下麵的樣子: 心裡琢磨了下,整一個界面玩一下吧。然後就做成下麵這個樣子了。 列表 下載列表 本文地址倉庫: "https://gith ...
起因
為什麼做這個東西,是突然間聽一後端同事說起Annie這個東西,發現這個東西下載視頻挺方便的,會自動爬取網頁中的視頻,然後整理成列表。發現用命令執行之後是下麵的樣子:
心裡琢磨了下,整一個界面玩一下吧。然後就做成下麵這個樣子了。
列表
下載列表
本文地址倉庫:https://github.com/Rynxiao/yh-tools,如果喜歡,歡迎star.
涉及技術
- Express 後端服務
- Webpack 模塊化編譯工具
- Nginx 主要做文件gzip壓縮(發現Express添加gzip有點問題,才棄坑nginx)
- Ant-design 前端UI庫
- React + React Router
- WebSocket 進度回傳服務
其中還有點小插曲,最開始是使用docker
起了一個nginx
服務,但是發現內部轉發一直有問題,同時獲取宿主主機IP也出現了點問題,然後折磨了好久放棄了。(docker
研究不深,敬請諒解^_^)
下載部分細節
首先瀏覽器會連接WebSocket
伺服器,同時在WebSocket
伺服器上存在一個所有客戶端的Map,瀏覽器端生成一個uuid
作為瀏覽器客戶端id
,然後將這個鏈接作為值存進Map中。
客戶端:
// list.jsx
await WebSocketClient.connect((event) => {
const data = JSON.parse(event.data);
if (data.event === 'close') {
this.updateCloseStatusOfProgressBar(list, data);
} else {
this.generateProgressBarList(list, data);
}
});
// src/utils/websocket.client.js
async connect(onmessage, onerror) {
const socket = this.getSocket();
return new Promise((resolve) => {
// ...
});
}
getSocket() {
if (!this.socket) {
this.socket = new WebSocket(
`ws://localhost:${CONFIG.PORT}?from=client&id=${clientId}`,
'echo-protocol',
);
}
return this.socket;
}
服務端:
// public/javascript/websocket/websocket.server.js
connectToServer(httpServer) {
initWsServer(httpServer);
wsServer.on('request', (request) => {
// uri: ws://localhost:8888?from=client&id=xxxx-xxxx-xxxx-xxxx
logger.info('[ws server] request');
const connection = request.accept('echo-protocol', request.origin);
const queryStrings = querystring.parse(request.resource.replace(/(^\/|\?)/g, ''));
// 每有連接連到websocket伺服器,就將當前連接保存到map中
setConnectionToMap(connection, queryStrings);
connection.on('message', onMessage);
connection.on('close', (reasonCode, description) => {
logger.info(`[ws server] connection closed ${reasonCode} ${description}`);
});
});
wsServer.on('close', (connection, reason, description) => {
logger.info('[ws server] some connection disconnect.');
logger.info(reason, description);
});
}
然後在瀏覽器端點擊下載的時候,會傳遞兩個主要的欄位resourceId
(在代碼中由parentId
和childId
組成)和客戶端生成的bClientId
。這兩個id
有什麼用呢?
- 每次點擊下載,都會在
Web
伺服器中生成一個WebSocket
的客戶端,那麼這個resouceId
就是作為在伺服器中生成的WebSocket
伺服器的key
值。 bClientId
主要是為了區分瀏覽器的客戶端,因為考慮到同時可能會有多個瀏覽器接入,這樣在WebSocket
伺服器中產生消息的時候,就可以用這個id
來區分應該發送給哪個瀏覽器客戶端
客戶端:
// list.jsx
http.get(
'download',
{
code,
filename,
parent_id: row.id,
child_id: childId,
download_url: url,
client_id: clientId,
},
);
// routes/api.js
router.get('/download', async (req, res) => {
const { code, filename } = req.query;
const url = req.query.download_url;
const clientId = req.query.client_id;
const parentId = req.query.parent_id;
const childId = req.query.child_id;
const connectionId = `${parentId}-${childId}`;
const params = {
code,
url,
filename,
parent_id: parentId,
child_id: childId,
client_id: clientId,
};
const flag = await AnnieDownloader.download(connectionId, params);
if (flag) {
await res.json({ code: 200 });
} else {
await res.json({ code: 500, msg: 'download error' });
}
});
// public/javascript/annie.js
async download(connectionId, params) {
//...
// 當annie下載時,會進行數據監聽,這裡會用到節流,防止進度回傳太快,websocket伺服器無法反應
downloadProcess.stdout.on('data', throttle((chunk) => {
try {
if (!chunk) {
isDownloading = false;
}
// 這裡主要做的是解析數據,然後發送進度和速度等信息給websocket伺服器
getDownloadInfo(chunk, ws, params);
} catch (e) {
downloadSuccess = false;
WsClient.close(params.client_id, connectionId, 'download error');
this.stop(connectionId);
logger.error(`[server annie download] error: ${e}`);
}
}, 500, 300));
}
服務端收到進度以及速度的消息後,回傳給客戶端,如果進度達到了100%,那麼就刪除掉存在server
中的伺服器中起的websocket
的客戶端,並且發送一個客戶端被關閉的通知,通知瀏覽器已經下載完成。
// public/javascript/websocket/websocket.server.js
function onMessage(message) {
const data = JSON.parse(message.utf8Data);
const id = data.client_id;
if (data.event === 'close') {
logger.info('[ws server] close event');
closeConnection(id, data);
} else {
getConnectionAndSendProgressToClient(data, id);
}
}
function getConnectionAndSendProgressToClient(data, clientId) {
const browserClient = clientsMap.get(clientId);
// logger.info(`[ws server] send ${JSON.stringify(data)} to client ${clientId}`);
if (browserClient) {
const serverClientId = `${data.parent_id}-${data.child_id}`;
const serverClient = clientsMap.get(serverClientId);
// 發送從web伺服器中傳過來的進度、速度給瀏覽器
browserClient.send(JSON.stringify(data));
// 如果進度已經達到了100%
if (data.progress >= 100) {
logger.info(`[ws server] file has been download successfully, progress is ${data.progress}`);
logger.info(`[ws server] server client ${serverClientId} ready to disconnect`);
// 從clientsMap將當前的這個由web伺服器創建的websocket客戶端移除
// 然後關閉當前連接
// 同時發送下載完成的消息給瀏覽器
clientsMap.delete(serverClientId);
serverClient.send(JSON.stringify({ connectionId: serverClientId, event: 'complete' }));
serverClient.close('download completed');
}
}
}
整體來說就這麼多,有一點需要指出,annie
在解析的時候有時候可能消息處理不是很穩定,導致我數據解析的時候出現了一些問題,但是我用mock
的數據以及mock
的進度條回傳是不會出現問題的。
最後總結
多讀書,多看報,少吃零食,多睡覺