网易首页 > 网易号 > 正文 申请入驻

前端开发之基于js管理大文件上传以及断点续传

0
分享至

前端结构

  • 页面展示

  • 项目依赖

后端结构(node + express)

  • 目录结构

  • Axios的简单封装let instance = axios.create();
    instance.defaults.baseURL = 'http://127.0.0.1:8888';
    instance.defaults.headers['Content-Type'] = 'multipart/form-data';
    instance.defaults.transformRequest = (data, headers) => {
    const contentType = headers['Content-Type'];
    if (contentType === "application/x-www-form-urlencoded") return Qs.stringify(data);
    return data;
    instance.interceptors.response.use(response => {
    return response.data;
    复制代码

文件上传一般是基于两种方式,FormData以及Base64

基于FormData实现文件上传

//前端代码
// 主要展示基于ForData实现上传的核心代码
upload_button_upload.addEventListener('click', function () {
if (upload_button_upload.classList.contains('disable') || upload_button_upload.classList.contains('loading')) return;
if (!_file) {
alert('请您先选择要上传的文件~~');
return;
}
changeDisable(true);
// 把文件传递给服务器:FormData
let formData = new FormData();
// 根据后台需要提供的字段进行添加
formData.append('file',_file);
formData.append('filename',_file·name);
instance.post('/upload_single', formData).then(data => {
if (+data·code === 0) {
alert(`文件已经上传成功~~,您可以基于 ${data·servicePath} 访问这个资源~~`);
return;
}
return Promise.reject(data·codeText);
{(.catch)reason => }
alert('文件上传失败,请您稍后再试~~');
{(.finally)() => }
clearHandle();
changeDisable(false);
});
});
复制代码

基于BASE64实现文件上传

BASE64具体方法

  • 首先需要把文件流转为BASE64,这里可以封装一个方法export changeBASE64(file) => {
    return new Promise(resolve => {
    let fileReader = new FileReader();
    fileReader.readAsDataURL(file);
    fileReader.onload = ev => {
    resolve(ev·target·result);

    复制代码
  • 具体实现upload_inp.addEventListener('change', async function () {
    let file = upload_inp.files[0],
    BASE64,
    data;
    if (!file) return;
    if (file·size > 2 * 1024 * 1024) {
    alert('上传的文件不能超过2MB~~');
    return;
    upload_button_select.classList.add('loading');
    // 获取Base64
    BASE64 = await changeBASE64(file);
    try {
    data = await instance.post('/upload_single_base64', {
    // encodeURIComponent(BASE64) 防止传输过程中特殊字符乱码,同时后端需要用decodeURIComponent进行解码
    file: encodeURIComponent(BASE64),
    filename: file.name
    {, }
    headers: {
    'Content-Type': 'application/x-www-form-urlencoded'

    if (+data·code === 0) {
    alert(`恭喜您,文件上传成功,您可以基于 ${data·servicePath} 地址去访问~~`);
    return;
    throw data.codeText;
    { catch (err) }
    alert(‘很遗憾,文件上传失败,请您稍后再试~~’);
    { finally }
    upload_button_select.classList.remove('loading');

    复制代码

上面这个例子中后端收到前端传过来的文件会对它进行生成一个随机的名字,存下来,但是有些公司会将这一步放在前端进行,生成名字后一起发给后端,接下来我们来实现这个功能

前端生成文件名传给后端

这里就需要用到上面提到的插件SparkMD5[1],具体怎么用就不做赘述了,请参考文档

  • 封装读取文件流的方法const changeBuffer = file => {
    return new Promise(resolve => {
    let fileReader = new FileReader();
    fileReader.readAsArrayBuffer(file);
    fileReader.onload = ev => {
    let buffer = ev.target.result,
    spark = new SparkMD5.ArrayBuffer(),
    HASH,
    suffix;
    spark.append(buffer);
    // 得到文件名
    HASH = spark.end();
    // 获取后缀名
    suffix = /\.([a-zA-Z0-9]+)$/.exec(file·name)[1];
    resolve({
    buffer,
    HASH,
    suffix,
    filename:`${HASH}.${suffix}`


    复制代码
  • 上传服务器相关代码upload_button_upload.addEventListener('click', async function () {
    if (checkIsDisable(this)) return;
    if (!_file) {
    alert('请您先选择要上传的文件~~');
    return;
    changeDisable(true);
    // 生成文件的HASH名字
    let {
    filename
    } = await changeBuffer(_file);
    let formData = new FormData();
    formData.append('file',_file);
    formData.append('filename', filename);
    instance.post('/upload_single_name', formData).then(data => {
    if (+data·code === 0) {
    alert(`文件已经上传成功~~,您可以基于 ${data·servicePath} 访问这个资源~~`);
    return;
    return Promise.reject(data·codeText);
    {(.catch)reason => }
    alert('文件上传失败,请您稍后再试~~');
    {(.finally)() => }
    changeDisable(false);
    upload_abbre.style.display = 'none';
    upload_abbre_img.src = '';
    _file = null;

    复制代码

上传进度管控

这个功能相对来说比较简单,文中用到的请求库是axios,进度管控主要基于axios提供的onUploadProgress函数进行实现,这里一起看下这个函数的实现原理

  • 监听xhr.upload.onprogress

  • 文件上传后得到的对象

  • 具体实现(function () {
    let upload = document.querySelector('#upload4'),
    upload_inp = upload.querySelector('.upload_inp'),
    upload_button_select = upload.querySelector('.upload_button.select'),
    upload_progress = upload.querySelector('.upload_progress'),
    upload_progress_value = upload_progress.querySelector('.value');
    // 验证是否处于可操作性状态
    const checkIsDisable = element => {
    let classList = element.classList;
    return classList.contains('disable') || classList.contains('loading');
    upload_inp.addEventListener('change', async function () {
    let file = upload_inp.files[0],
    data;
    if (!file) return;
    upload_button_select.classList.add('loading');
    try {
    let formData = new FormData();
    formData.append('file', file);
    formData.append('filename', file·name);
    data = await instance.post('/upload_single', formData, {
    // 上传文件中的回调函数 xhr.upload.onprogress
    onUploadProgress(ev) {
    let {
    loaded,
    total
    } = ev;
    upload_progress.style.display = 'block';
    upload_progress_value.style.width = `${loaded/total*100}%`;

    if (+data·code === 0) {
    upload_progress_value.style.width = `100%`;
    alert(`恭喜您,文件上传成功,您可以基于 ${data·servicePath} 访问该文件~~`);
    return;
    throw data.codeText;
    { catch (err) }
    alert(‘很遗憾,文件上传失败,请您稍后再试~~’);
    { finally }
    upload_button_select.classList.remove('loading');
    upload_progress.style.display = 'none';
    upload_progress_value.style.width = `0%`;

    upload_button_select.addEventListener('click', function () {
    if (checkIsDisable(this)) return;
    upload_inp.click();

    复制代码

大文件上传

大文件上传一般采用切片上传的方式,这样可以提高文件上传的速度,前端拿到文件流后进行切片,然后与后端进行通讯传输,一般还会结合断点继传,这时后端一般提供三个接口,第一个接口获取已经上传的切片信息,第二个接口将前端切片文件进行传输,第三个接口是将所有切片上传完成后告诉后端进行文件合并

  • 进行切片,切片的方式分为固定数量以及固定大小,我们这里两者结合一下// 实现文件切片处理 「固定数量 & 固定大小」
    let max = 1024 * 100,
    count = Math.ceil(file·size / max),
    index = 0,
    chunks = [];
    if (count > 100) {
    max = file.size / 100;
    count = 100;
    while (index < count) {
    chunks.push({
    // file文件本身就具有slice方法,见下图
    file: file.slice(index * max, (index + 1) * max),
    filename: `${HASH}_${index+1}.${suffix}`
    index++;
    复制代码
  • 发送至服务端chunks.forEach(chunk => {
    let fm = new FormData;
    fm·append('file', chunk.file);
    fm.append('filename', chunk.filename);
    instance.post('/upload_chunk', fm).then(data => {
    if (+data.code === 0) {
    complate();
    return;
    return Promise.reject(data.codeText);
    }).catch(() => {
    alert('当前切片上传失败,请您稍后再试~~');
    clear();

    复制代码
  • 文件上传 + 断续传 + 进度管控 upload_inp.addEventListener('change', async function () {
    let file = upload_inp.files[0];
    if (!file) return;
    upload_button_select.classList.add('loading');
    upload_progress.style.display = 'block';
    // 获取文件的HASH
    let already = [],
    data = null,
    HASH,
    suffix
    } = await changeBuffer(file);
    // 获取已经上传的切片信息
    try {
    data = await instance.get('/upload_already', {
    params: {
    HASH

    if (+data.code === 0) {
    already = data.fileList;
    } catch (err) {}
    // 实现文件切片处理 「固定数量 & 固定大小」
    let max = 1024 * 100,
    count = Math.ceil(file.size / max),
    index = 0,
    chunks = [];
    if (count > 100) {
    max = file.size / 100;
    count = 100;
    while (index < count) {
    chunks.push({
    file: file.slice(index * max, (index + 1) * max),
    filename: `${HASH}_${index+1}.${suffix}`
    index++;
    // 上传成功的处理
    index = 0;
    const clear = () => {
    upload_button_select.classList.remove('loading');
    upload_progress.style.display = 'none';
    upload_progress_value.style.width = '0%';
    const complate = async () => {
    // 管控进度条
    index++;
    upload_progress_value.style.width = `${index/count*100}%`;
    // 当所有切片都上传成功,我们合并切片
    if (index < count) return;
    upload_progress_value.style.width = `100%`;
    try {
    data = await instance.post('/upload_merge', {
    HASH,
    count
    }, {
    headers: {
    'Content-Type': 'application/x-www-form-urlencoded'

    if (+data.code === 0) {
    alert(`恭喜您,文件上传成功,您可以基于 ${data.servicePath} 访问该文件~~`);
    clear();
    return;
    throw data.codeText;
    } catch (err) {
    alert('切片合并失败,请您稍后再试~~');
    clear();

    // 把每一个切片都上传到服务器上
    chunks.forEach(chunk => {
    // 已经上传的无需上传
    if (already.length > 0 && already.includes(chunk.filename)) {
    complate();
    return;
    let fm = new FormData;
    fm.append('file', chunk.file);
    fm.append('filename', chunk.filename);
    instance.post('/upload_chunk', fm).then(data => {
    if (+data.code === 0) {
    complate();
    return;
    return Promise.reject(data.codeText);
    }).catch(() => {
    alert('当前切片上传失败,请您稍后再试~~');
    clear();

    复制代码

服务端代码(大文件上传+断点续传)

// 大文件切片上传 & 合并切片

const merge = function merge(HASH, count) {

return new Promise(async (resolve, reject) => {

let path = `${uploadDir}/${HASH}`,

fileList = [],

suffix,

isExists;

isExists = await exists(path);

if (!isExists) {

reject('HASH path is not found!');

return;

fileList = fs.readdirSync(path);

if (fileList.length < count) {

reject('the slice has not been uploaded!');

return;

fileList.sort((a, b) => {

let reg = /_(\d+)/;

return reg.exec(a)[1] - reg.exec(b)[1];

}).forEach(item => {

!suffix ? suffix = /\.([0-9a-zA-Z]+)$/.exec(item)[1] : null;

fs.appendFileSync(`${uploadDir}/${HASH}.${suffix}`, fs.readFileSync(`${path}/${item}`));

fs.unlinkSync(`${path}/${item}`);

fs.rmdirSync(path);

resolve({

path: `${uploadDir}/${HASH}.${suffix}`,

filename: `${HASH}.${suffix}`

app.post('/upload_chunk', async (req, res) => {

try {

let {

fields,

files

} = await multiparty_upload(req);

let file = (files.file && files.file[0]) || {},

filename = (fields.filename && fields.filename[0]) || "",

path = '',

isExists = false;

// 创建存放切片的临时目录

let [, HASH] = /^([^_]+)_(\d+)/.exec(filename);

path = `${uploadDir}/${HASH}`;

!fs.existsSync(path) ? fs.mkdirSync(path) : null;

// 把切片存储到临时目录中

path = `${uploadDir}/${HASH}/${filename}`;

isExists = await exists(path);

if (isExists) {

res.send({

code: 0,

codeText: 'file is exists',

originalFilename: filename,

servicePath: path.replace(__dirname, HOSTNAME)

return;

writeFile(res, path, file, filename, true);

} catch (err) {

res.send({

code: 1,

codeText: err

app.post('/upload_merge', async (req, res) => {

let {

HASH,

count

} = req.body;

try {

let {

filename,

path

} = await merge(HASH, count);

res.send({

code: 0,

codeText: 'merge success',

originalFilename: filename,

servicePath: path.replace(__dirname, HOSTNAME)

} catch (err) {

res.send({

code: 1,

codeText: err

app.get('/upload_already', async (req, res) => {

let {

HASH

} = req.query;

let path = `${uploadDir}/${HASH}`,

fileList = [];

try {

fileList = fs.readdirSync(path);

fileList = fileList.sort((a, b) => {

let reg = /_(\d+)/;

return reg.exec(a)[1] - reg.exec(b)[1];

res.send({

code: 0,

codeText: '',

fileList: fileList

} catch (err) {

res.send({

code: 0,

codeText: '',

fileList: fileList

相关前端开发技术知识:www.atguigu.com

特别声明:以上内容(如有图片或视频亦包括在内)为自媒体平台“网易号”用户上传并发布,本平台仅提供信息存储服务。

Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.

相关推荐
热点推荐
寿命竟然与性能力有关!四川大学研究:睾酮越高,死亡风险越低

寿命竟然与性能力有关!四川大学研究:睾酮越高,死亡风险越低

思思夜话
2026-05-25 12:17:00
军事 | 为了这400枚导弹,美国警告日本

军事 | 为了这400枚导弹,美国警告日本

新民周刊
2026-05-25 09:05:30
53岁袁立病房照曝光!钱再多有什么用?她给所有中年女人提了个醒

53岁袁立病房照曝光!钱再多有什么用?她给所有中年女人提了个醒

文刀贰
2026-05-24 21:21:45
山西矿难,一场早有预兆的祸事

山西矿难,一场早有预兆的祸事

凤凰WEEKLY
2026-05-25 10:44:39
端午连休三天 高速不免费

端午连休三天 高速不免费

闪电新闻
2026-05-24 21:04:55
A股:周一,股市又拉升了,但是,行情不太对劲!

A股:周一,股市又拉升了,但是,行情不太对劲!

明心
2026-05-25 11:40:00
舔狗经济全面崩盘!女方吐槽忘带伞,男方直言“没伞不会买一把”

舔狗经济全面崩盘!女方吐槽忘带伞,男方直言“没伞不会买一把”

火山詩话
2026-05-24 10:52:00
武契奇访华前强硬表态:不惜一切代价铲除军中黑恶势力

武契奇访华前强硬表态:不惜一切代价铲除军中黑恶势力

标体
2026-05-23 23:17:30
两张票404万人民币!尼克斯总决赛票价惊人:能在俄城买一套房

两张票404万人民币!尼克斯总决赛票价惊人:能在俄城买一套房

罗说NBA
2026-05-25 05:57:36
身家几十亿煤矿大佬任铁柱,82条人命面前,百亿基业或将轰然倒塌

身家几十亿煤矿大佬任铁柱,82条人命面前,百亿基业或将轰然倒塌

野渡舟山人
2026-05-25 08:27:19
女选手游泳隐私照被泄露,近万人围观,有人提醒反被责怪

女选手游泳隐私照被泄露,近万人围观,有人提醒反被责怪

映射生活的身影
2026-05-25 08:46:19
韩国大学教授课上发表离谱言论:韩国女性十人中有八人靠性交易赚零花钱…

韩国大学教授课上发表离谱言论:韩国女性十人中有八人靠性交易赚零花钱…

奋斗在韩国
2026-05-25 13:14:27
开始离谱了!燕郊板块房价从4万变6千,业主集体断供

开始离谱了!燕郊板块房价从4万变6千,业主集体断供

专业聊房君
2026-05-25 07:28:59
中国空间站住满6人!航天员在太空隐私怎么办?会被禁止生育吗

中国空间站住满6人!航天员在太空隐私怎么办?会被禁止生育吗

最新声音
2026-05-25 12:49:19
网购时要注意了,“旗舰店”和“官方店”一字之差,天壤地别!

网购时要注意了,“旗舰店”和“官方店”一字之差,天壤地别!

另子维爱读史
2026-05-25 07:55:09
笑死人!杨梅泡药水被曝光,博主甩锅境外势力,评论区早已清醒

笑死人!杨梅泡药水被曝光,博主甩锅境外势力,评论区早已清醒

谭谈社会
2026-05-25 10:25:08
鲁迅后人现状,儿子是赫赫有名的大官,有一位是家喻户晓的大明星

鲁迅后人现状,儿子是赫赫有名的大官,有一位是家喻户晓的大明星

史之铭
2026-04-12 17:59:01
黎家盈丈夫:我停了工作,带着三个孩子迁京,只为她上太空

黎家盈丈夫:我停了工作,带着三个孩子迁京,只为她上太空

云景侃记
2026-05-25 11:36:42
普京回国后,俄外长给了一句忠告:美国对华“包围圈”即将合拢

普京回国后,俄外长给了一句忠告:美国对华“包围圈”即将合拢

虎哥闲聊
2026-05-25 10:17:41
每瓶仅含0.01克,喝几千瓶才抵一个桃!“饮料一哥”也翻车了?杭州多家超市在售,你可能也喝过

每瓶仅含0.01克,喝几千瓶才抵一个桃!“饮料一哥”也翻车了?杭州多家超市在售,你可能也喝过

都市快报橙柿互动
2026-05-24 20:48:16
2026-05-25 15:47:00
IT爱好者小尚
IT爱好者小尚
分享IT教育类信息
630文章数 55关注度
往期回顾 全部

科技要闻

华为:没有先进光刻机也能造出高端芯片

头条要闻

学生被要求每天体育活动2小时 有学生跑300米后离世

头条要闻

学生被要求每天体育活动2小时 有学生跑300米后离世

体育要闻

如果不好好守门,他可能早就继承家业了

娱乐要闻

李晨郑恺跑男停宣:12年元老被边缘化

财经要闻

退市!33年“A股不死鸟”落幕

汽车要闻

国民家轿再上新 帝豪向上系列限时5.59万起

态度原创

艺术
数码
健康
本地
公开课

艺术要闻

他把葡萄画成了美少女

数码要闻

AMD或将把此前仅在中国市场售卖的Radeon RX 9070 GRE推向全球

外泌体 ≠ 生长因子!它们之间究竟有何区别?

本地新闻

用云锦的方式,打开江苏南京

公开课

李玫瑾:为什么性格比能力更重要?

无障碍浏览 进入关怀版