最近有视频上传的需求,所以决定写几篇文章来讲解一下。这些文章能写到的内容如下:
- 前端如何拿到文件对象。
- 前端处理文件对象有哪些方式。
- axios进行前后端通信时,文件数据应该如何传输?
- node拿到数据后,应该如何处理?
- 如果是大文件,如何提高文件上传的效率?
- 如果是大文件,前端下载时,如何提高下载的效率?
在写这篇文章的时候,它的基调还没有确定下来,总想把视频处理上升到文件处理。但是文件处理这个话题涉及的面还比较广,但是2023年11月19日 晚上23:29分,我决定了,不扩展了,就只写视频,如果我有精力去写其他文件的处理,再把他们合并到一个专栏里也不晚。
一、环境搭建
初始化项目
前端搭建就不说了,使用create-react-app脚手架可以快速的帮你搭建好一个react项目,当然你也可以使用我写出来的工具lazy-create-react-app
去搭建一个非常简单的react项目。如果你真的想用我这个工具,请运行:npx lazy-create-react-app [自定义项目名称]
。
后端我们使用express来搭建后端工程。具体操作如下:
javascript
// 1、第一步(安装express脚手架)
npm install -g express-generator
// 2、第二步(使用脚手架创建一个名为"end"的项目)
express end
// 3、第三步(进入项目目录)
cd end
// 4、第四步(安装项目依赖)
npm install
// 5、修改后端项目的端口号
// 6、第五步(启动项目)
npm run start
修改后端的端口号如下,否则前后端项目同时启动的时候,他们两个会因为使用相同的端口号而导致冲突:
接着,我们来为本次的内容注册专门的路由:
routes目录下新增video.js文件,video.js文件内容如下:
javascript
var express = require('express');
var router = express.Router();
router.get('/', (req, res, next) => {
res.send({ msg: '路由注册成功' });
});
module.exports = router;
紧接着,我们来修改下app.js文件内容:
javascript
// 其余文件内容不变 =====
var videoRouter = require('./routes/video');
app.use('/video', videoRouter);
// 其余文件内容不变 =====
至此,初始化的工作就完毕了。
前后端联调
后端需要安装cors依赖,请执行如下命令:
javascript
npm install cors --save
依赖安装成功后,继续修改app.js文件内容:
javascript
// 其余文件内容不变 =====
var cors = require('cors');
app.use(cors());
// 其余文件内容不变 =====
接下来,我们在前端项目里新增一个Video页面,但是在操作前,我们还需要安装2个依赖,分别是react-router、axios。请执行以下命令:
javascript
npm install react-router axios --save
修改前端项目里的index.js文件,并新增Video文件。
jsx
// 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文件内容如下:
jsx
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页面,并且点击按钮,如果你的界面是下面这样,那么恭喜你,环境搭建算是成功了。
二、前端拿到上传的视频文件
首先来思考一下,前端有哪些方式可以上传视频?似乎只有input[type='file']
标签才能上传视频吧,那顺着这个思路走,接下来该如何拿到上传的文件呢?
2种方式
,并且这2种方式是等价的。
- 通过event.target.files来拿到
- 通过InputElement.files来拿到
那这两种方式应该在代码里的哪个位置去写呢?
答案是在input标签的change事件里写。但是需要注意一下,如果你上传的是同一个文件,那么第二次上传视频时是不会再触发change事件的。
接下来我们来具体看一下代码,修改Video.js文件如下:
jsx
// 其余文件内容不变=======
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对象。
下图可能会更清晰:
三、前端处理视频的几种方式
上一小节里,我们知道了如何拿到上传的文件,这一节我们来讲一下如何处理视频数据。
2种方式如下:
- 不做处理。直接将File对象传给后端。
- 只传递文件内容。具体方式是先用FileReader读取文件内容,然后再根据文件内容的大小、实际场景是怎样的,动态决定是用base64编码来承载数据,还是使用二进制数据来承载数据。
这2种方式的区别如下:
- 第二种方式扩展性更强。
- 第二种方式处理数据更灵活。
- 第一种方式更暴力。
现在来具体解释一下:
1、扩展性强。这代表着第二种方式可以做出亲民的效果。因为fileReader提供了很多的API、事件,所以它能让用户在程序里去预览上传后的文件。再比如本地文件上传中的loading效果。
2、处理数据更灵活。它可以将数据处理为base64,或者二进制数据。只需要调用相应的API即可。
3、第一种方式更暴力。这个主要是针对那些对本地预览没有要求的场景。细粒度肯定不如第二种方式。
我们这里决定使用更暴力的那一方,就是File对象的方式。
javascript
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>
}
}
这里需要注意的是,只要请求体是一个对象就可以,不一定非要是formData对象
。并且我们还需要设置请求头里的Content-Type为"multipart/form-data"来表明我们的post body是一个表单。
四、前后通信过程中应该注意什么?
- 如果是不做处理,直接传递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都干了哪些事的话可以去看看官网。这里只能保证你可以实现这个功能。
此时我们来修补一下后端对应的接口:
node
// 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目录下,成功的保存了上传的视频。
六、敬请期待
好啦,我们本篇文章就结束了。这里面我们只是实现了一个最最基本普通的视频上传保存功能,接下来的文章会基于这篇文章来做讲解,比如我们还要实现文件的分片上传、断点续传、分片下载等功能。如果上述过程中出现了失误,还请各位大佬指点迷经。
希望我的文章对你有所帮助,我们下期再见啦~~