视频上传那些事 -- 基础篇

最近有视频上传的需求,所以决定写几篇文章来讲解一下。这些文章能写到的内容如下:

  • 前端如何拿到文件对象。
  • 前端处理文件对象有哪些方式。
  • axios进行前后端通信时,文件数据应该如何传输?
  • node拿到数据后,应该如何处理?
  • 如果是大文件,如何提高文件上传的效率?
  • 如果是大文件,前端下载时,如何提高下载的效率?

那接下来,咱们就开始进入正题吧:

一、环境搭建

初始化项目

前端搭建就不说了,使用create-react-app脚手架可以快速的帮你搭建好一个react项目,当然你也可以使用我写出来的工具lazy-create-react-app去搭建一个非常简单的react项目。如果你真的想用我这个工具,请运行:npx lazy-create-react-app [自定义项目名称]。

后端我们使用express来搭建后端工程。具体操作如下:

// 1、第一步(安装express脚手架) 
npm install -g express-generator 

// 2、第二步(使用脚手架创建一个名为“end”的项目) 
express end 

// 3、第三步(进入项目目录) 
cd end 

// 4、第四步(安装项目依赖) 
npm install 

// 5、修改后端项目的端口号

// 6、第五步(启动项目) 
npm run start

修改后端的端口号如下,否则前后端项目同时启动的时候,他们两个会因为使用相同的端口号而导致冲突:

视频上传那些事 -- 基础篇_第1张图片

接着,我们来为本次的内容注册专门的路由:

routes目录下新增video.js文件,video.js文件内容如下:

var express = require('express');
var router = express.Router();

router.get('/', (req, res, next) => {
  res.send({ msg: '路由注册成功' });
});

module.exports = router;

视频上传那些事 -- 基础篇_第2张图片

紧接着,我们来修改下app.js文件内容:

// 其余文件内容不变 =====
var videoRouter = require('./routes/video');

app.use('/video', videoRouter);

// 其余文件内容不变 =====

至此,初始化的工作就完毕了。

前后端联调

后端需要安装cors依赖,请执行如下命令:

npm install cors --save

依赖安装成功后,继续修改app.js文件内容:

// 其余文件内容不变 =====
var cors = require('cors');

app.use(cors());
// 其余文件内容不变 =====

接下来,我们在前端项目里新增一个Video页面,但是在操作前,我们还需要安装2个依赖,分别是react-router、axios。请执行以下命令:

npm install react-router axios --save

修改前端项目里的index.js文件,并新增Video文件。

// app.js文件如下:
import { createBrowserRouter, RouterProvider, Route } from 'react-router-dom';
import Video from './Video';

const router = createBrowserRouter([
  {
    path: '/',
    element: <App />,
  },
  {
    path: '/video',
    element: <Video/>
  }
]);

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <React.StrictMode>
    <RouterProvider
      router={router}
    />
  </React.StrictMode>
);

Video文件内容如下:

import React from 'react';
import axios from 'axios';

let axiosInstance = axios.create({
    baseURL: 'http://127.0.0.1:8888',
});

class Video extends React.Component {
    constructor(props){
        super(props);
    }
    
    testConnect = async () => {
        let result = await axiosInstance.get('/video');
        if (result?.data?.code == 200){
            alert(result?.data.msg);
        }
    }
    
    render(){
        return <div>
            <button onClick={this.testConnect}>测试连接</button>
        </div>
    }
}

export default Video;

访问Video页面,并且点击按钮,如果你的界面是下面这样,那么恭喜你,环境搭建算是成功了。

视频上传那些事 -- 基础篇_第3张图片

二、前端拿到上传的视频文件

首先来思考一下,前端有哪些方式可以上传视频?似乎只有input[type='file']标签才能上传视频吧,那顺着这个思路走,接下来该如何拿到上传的文件呢?

2种方式,并且这2种方式是等价的。

  • 通过event.target.files来拿到
  • 通过InputElement.files来拿到

那这两种方式应该在代码里的哪个位置去写呢?

答案是在input标签的change事件里写。但是需要注意一下,如果你上传的是同一个文件,那么第二次上传视频时是不会再触发change事件的。

接下来我们来具体看一下代码,修改Video.js文件如下:

// 其余文件内容不变=======

class Video extends React.Component {

    // 其余文件内容不变=======

    inputChange = async (event) => {
        console.log('uploadFileObj:', event.target.files)
        console.log('input-files:', document.querySelector('input').files)
    }

    render(){
        return <div>
            <input
                type='file'
                onChange={(event) => this.inputChange(event)}
            />
        </div>
    }
}

// 其余文件内容不变=======

此时我们上传一个视频,看看控制台里的打印:

在这里插入图片描述

我们会发现,这2种方式拿到的都是fileList对象。

什么是fileList?

它是一个类数组对象,类数组对象里的每一项都是File对象。File对象又继承Blob对象。

下图可能会更清晰:

视频上传那些事 -- 基础篇_第4张图片

三、前端处理视频的几种方式

上一小节里,我们知道了如何拿到上传的文件,这一节我们来讲一下如何处理视频数据。

2种方式如下:

  • 不做处理。直接将File对象传给后端。
  • 只传递文件内容。具体方式是先用FileReader读取文件内容,然后再根据文件内容的大小、实际场景是怎样的,动态决定是用base64编码来承载数据,还是使用二进制数据来承载数据。

这2种方式的区别如下:

  • 第二种方式扩展性更强。
  • 第二种方式处理数据更灵活。
  • 第一种方式更暴力。

现在来具体解释一下:

1、扩展性强。这代表着第二种方式可以做出亲民的效果。因为fileReader提供了很多的API、事件,所以它能让用户在程序里去预览上传后的文件。再比如本地文件上传中的loading效果。
2、处理数据更灵活。它可以将数据处理为base64,或者二进制数据。只需要调用相应的API即可。
3、第一种方式更暴力。这个主要是针对那些对本地预览没有要求的场景,直接以FormData的数据格式发送到后端。细粒度肯定不如第二种方式。

我们这里决定使用更暴力的那一方,就是File对象的方式。


let axiosInstance = axios.create({
    baseURL: 'http://127.0.0.1:8888',
});

class Video extends React.Component {

    uploadChunkReq = async (fileBlob) => {
        let result = await axiosInstance.post(
            '/video/uploadChunk',
            {videoDict: fileBlob},
            {
                headers: {
                    'Content-Type': 'multipart/form-data'
                }
            }
        );
        return result;
    }

    // 上传文件后触发
    inputChange = async (event) => {
        let self = this;
        let uploadFileObj = event.target.files[0] || {};
        await self.uploadChunkReq(uploadFileObj);
    }

    render(){
        return <div>
            <input
                type='file'
                onChange={(event) => this.inputChange(event)}
            />
        </div>
    }
}

特别注意!!!

这里对content-type类型值为FormData做一个解释,这个点也是很多人遗漏的点。我们如何告诉后端,我们的请求体是一个表单呢?需要满足以下2点:

  • 只要请求体是一个对象就可以,不一定非要是formData对象
  • 我们还需要设置请求头里的Content-Type为“multipart/form-data”。

四、前后通信过程中应该注意什么?

  • 如果是不做处理,直接传递File对象,那此时请求头里的Content-Type的值要设置为“multipart/form-data”。
  • 如果是使用FileReader来读取文件,如果视频的体积较小,则可以考虑base64来承载数据。虽然这种方法会让数据的体积增大约1/3,但是谁让它小呢,多出的那一部分也无伤大雅。
  • 如果是使用FileReader来读取文件,如果文件的体积较大,则可以考虑使用readAsBinaryString等方法来以二进制的方式读取文件内容,然后将二进制的数据传给后端,此时请求头里的Content-Type的值要设置为“application/octet-stream”。

五、后端如何处理视频?

后端在接收到这样请求后,需要去处理Content-Type类型为“multipart/form-data”的数据,但是express并没有预制处理它的能力。所以我们只能是下载一个第三方库multer。multer只是其中的一个代表,你也可以选择其他第三方库,比如:connect-multiparty

接下来就是multer库的用法,想知道这些API都干了哪些事的话可以去看看官网。这里只能保证你可以实现这个功能。

此时我们来修补一下后端对应的接口:

// video.js文件内容如下:
var express = require('express');
var multer = require("multer");
var router = express.Router();

// 定义文件的存放路径
var tempChunkPosition = multer({
  storage: multer.diskStorage({
    destination: function (req, file, cb) {
      cb(null, path.join(__dirname, '../tempChunk'))
    },
    filename: function (req, file, cb) {
      cb(null, file.fieldname + '-' + Date.now() + '.mp4')
    }
  })
});


router.post('/uploadChunk', tempChunkPosition.single("videoDict"),(req, res, next) => {
  let fileInfo = req.file;
  return res.send({
    success: true,
    msg: '上传成功',
    data: {
      filePath: fileInfo.path
    }
  });
});


module.exports = router;

当我们在前端上传一个mp4的视频时,此时在本地我们就可看到在tempChunk目录下,成功的保存了上传的视频。

视频上传那些事 -- 基础篇_第5张图片

六、敬请期待

好啦,我们本篇文章就结束了。这里面我们只是实现了一个最最基本普通的视频上传保存功能,接下来的文章会基于这篇文章来做讲解,比如我们还要实现文件的分片上传、断点续传、分片下载等功能。如果上述过程中出现了失误,还请各位大佬指点迷经。

本人最近建立了一个交流群,这个交流群不仅局限于前端,你想要探讨任何一项技术都可以,因为是刚刚建立,所以群里会比较清净,想要加入群聊的,欢迎添加我的微信:18845097791

最后,希望我的文章对你有所帮助,我们下期再见啦~~

你可能感兴趣的:(音视频)