简历中如何写项目经验&技术
切片上传相关面试题
实现核心:秒传?断点续传?切片?……?如何实现的
最基本的视图
加上拖拽
加上拖拽事件。监听drop事件,event.dataTransfer.files文件对象。其他dragenter?dragover?dragleave事件event.stopPropagation?event.preventDefault
文件预览。URL.createObjectUrl
根据文件内容得到加密文件名。
file->arrayBuffer->加密buffer->buffer.toString
优化点:放到webworker里不会使用js内存
文件切片?blob.slice
并行上传每个分片。优化点:并发管控
node层:核心的2个接口
上传单个切片的接口
合并切片的接口
const?express?= require('express');
const?logger?= require('morgan');
const {?StatusCodes?} = require('http-status-codes');
const?cors?= require('cors');
const?fs?= require('fs-extra');
const?path?= require('path');
const PUBLIC_DIR =?path.resolve(__dirname, 'public');
const TEMP_DIR =?path.resolve(__dirname, 'temp');
const CHUNK_SIZE = 100 * 1024 * 1024;
//存放上传并合并好的文件
fs.ensureDirSync(PUBLIC_DIR);
//存放分片的文件
fs.ensureDirSync(TEMP_DIR);
const?app?= express();
app.use(logger('dev'));
app.use(cors());
app.use(express.json());
app.use(express.urlencoded({?extended: true }));
app.use(express.static(path.resolve(__dirname, 'public')));
/**
?*?上传分片
?*/
app.post('/upload/:filename', async (req,?res,?next) => {
//通过路径参数获取文件名
const {?filename?} =?req.params;
//通过查询参数获取分片名
const {?chunkFileName?} =?req.query;
//写入文件的起始位置
const?start?= isNaN(req.query.start)?0:parseInt(req.query.start,10);
//创建用户保存此文件的分片的目录
const?chunkDir?=?path.resolve(TEMP_DIR,?filename);
//分片的文件路径
const?chunkFilePath?=?path.resolve(chunkDir,?chunkFileName);
//先确定分片目录存在
await?fs.ensureDir(chunkDir);
//创建此文件的可写流?,可以指定写入的起始位置
const?ws?=?fs.createWriteStream(chunkFilePath, {start,flags:'a'});
//后面会实现暂停操作,如果客户端点击了暂停按钮,会取消上传的操作,取消之后会在服务器触发请求择象的
//aborted事件,关闭可定流
????req.on('aborted', () => {?ws.close() });
//使用管道的方式把请求中的请求体流数据写入到文件中
try {
await pipeStream(req,?ws);
????????res.json({?success: true });
} catch (error) {
next(error);
}
});
app.get('/merge/:filename', async (req,?res,?next) => {
//通过路径参数获取文件名
const {?filename?} =?req.params;
try {
await mergeChunks(filename);
????????res.json({?success: true });
} catch (error) {
next(error)
}
});
app.get('/verify/:filename',async (req,res,next)=>{
const {filename} =?req.params;
//先获取文件在服务器的路径
const?filePath?=?path.resolve(PUBLIC_DIR,filename);
//判断是文件在服务器端是否存在
const?isExist?= await?fs.pathExists(filePath);
//如果已经存在了,则直接返回不需要上传了
if(isExist){
return?res.json({success:true,needUpload:false});
}
const?chunksDir?=?path.resolve(TEMP_DIR,filename);
const?existDir?= await?fs.pathExists(chunksDir);
//存放已经上传的分片的对象数组
let?uploadedChunkList?=[];
if(existDir){
//读取临时目录里面的所有的分片对应的文件
const?chunkFileNames?= await?fs.readdir(chunksDir);
//读取每个分片文件的文件信息,主要是它的文件大小,表示已经上传的文件的大小
????????uploadedChunkList?= await?Promise.all(chunkFileNames.map(async function(chunkFileName){
const {size} = await?fs.stat(path.resolve(chunksDir,chunkFileName));
return {chunkFileName,size};
}));
}
//如果没有此文件,则意味着服务器还需要你上传此文件
//返回,上传尚未完成,但是已经上传了一部分了,我把已经上传的分片名,以及分片的大小给客户端
//客户端可以只上传分片剩下的数据部就可以了
????res.json({success:true,needUpload:true,uploadedChunkList});
});
async function mergeChunks(filename) {
const?mergedFilePath?=?path.resolve(PUBLIC_DIR,?filename);
const?chunkDir?=?path.resolve(TEMP_DIR,?filename);
const?chunkFiles?= await?fs.readdir(chunkDir);
//对分片按索引进行升序排列
????chunkFiles.sort((a,?b) => Number(a.split('-')[1]) - Number(b.split('-')[1]));
try {
//为了提高性能,我们在这时可以分片并行写入
const?pipes?=?chunkFiles.map((chunkFile,?index) => {
return pipeStream(
????????????????fs.createReadStream(path.resolve(chunkDir,?chunkFile), {?autoClose: true }),
????????????????fs.createWriteStream(mergedFilePath, {?start:?index?* CHUNK_SIZE })
);
});
//并发把每个分片的数据写入到目标文件中
await?Promise.all(pipes);
//删除分片的文件和文件夹
await?fs.rmdir(chunkDir, {?recursive: true })
//合并完文件之后可以重新在这里计算合并后的文件的hash值,和文件中的hash值进行对比
//如果值是一样的,说明肯定内容是对的,没有被修改
} catch (error) {
next(error)
}
}
function pipeStream(rs,?ws) {
return new Promise((resolve,?reject) => {
//把可读流中的数据写入可写流中
????????rs.pipe(ws).on('finish',?resolve).on('error',?reject);
});
}
app.listen(8080, () =>?console.log('Sever?started?on?port?8080'));
进度是每个切片的进度
秒传:已经存在就不传了
取消上传:cancelToken
断点续传:用户点了暂停/网络断开的情况。
优化点:
把耗时的操作放到worker里,不要阻塞主进程
let?worker=new?Worker('./fileCalculate')
worker.postMessage?self.on('message')
如果失败重试3次:但是做的是整个失败重试
待考虑问题:
如何确定切片大小?
如何做文件校验?