关于什么是base64及其工作原理
,请看链接base64及base64的工作原理
前端任务
在项目中,使用了vant
中的uploader
实现图片上传:
<uploader upload-text="上传头像" :after-read="afterRead">
<input class="avatar" type="image" :src="avatar">
</uploader>
import {uploadAvatar} from '@/api/mine'
methods:{
afterRead(file){
const params = {
name: file.file.name,
content: file.content
}
uploadAvatar(params).then((val)=>{
this.avatar = val.data.message
})
},
}
使用console.log(params.content)
我们可以得到类似以下的输出
data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAioAAAEGCAIAAADE1Ck0AAAgAElEQVR4AezB6Y9l52Hn9+/vec45997aq/eF7GZ3kxT3TYsl2bKt8TpJPN4UOJgAARIjk8x/ECAvojdBkkEyQJAXAYIYHjh2HGdGnpFseSzZlmTJskRKFMWlufXC3veqrr3uveec55dzby2s6oWiFFs25P589K0LpZ3qlMoyTY+EwzuKsSJI3MOGZG6s+PgNz3fJgs06G4MtGkKAEBbrDGIbs06sMwNmwIAx4Aa2ARvbGA+QkgdSsklOeA
可以看出content
分为两部分,第一部分是内容描述,第二部分是内容详情。显然内容详情才是图片二进制流转换成base64
后的真正载体。
uploaderAvatar的结构如下
import axios from 'axios'
import {BASE_URL} from './route'
export function uploadAvatar(params){
return axios.post(`${BASE_URL}/uploadAvatar`,params) //图片上传是一个post请求,所以我们使用axios.post
}
前端的任务比较简单,只需要发送像普通post
请求一样就能实现图片上传的效果。
后端任务
在后端koa2
服务器中,我们的工作就是获取前端传递过来的图片,然后保存到本地,并将其转换成网络地址,最后将其网络地址保存到数据库,并将网络地址返回给前端页面。
- 前端请求方式是
post
,所以我们在获取请求参数时是在ctx.request.body
中获取。回头再看前端代码,发现参数名是:name
和content
,参数值分别是file.file.name
文件名和file.content
文件内容(此时的文件内容是基于base64
编码过后的)
const params = {
name: file.file.name,
content: file.content
}
后端获取参数
const extend_name = ctx.request.body.name.slice(-4) // 取出文件后缀名
let bitmap = ctx.request.body.content.split(',')[1]
- 在前面,我们提到过前端传过来的
content
分为两部分,一部分是内容描述,一部分是内容详情。所以ctx.request.body.content.split(',')[1]
在这里,我们取后一部分,也就是图片的真正内容。 - 这里存在一个致命的遗漏点,很重要!!!因为图片大小是不确定的,编码的二进制数可能是
3
的倍数,也可能不是3
的倍数。Base64
用\x00
字节在末尾补足后,再在编码的末尾加上1
个或2
个=
号,表示补了多少字节.
因此在这里,我们需要进行以下判断。
let index = bitmap.lastIndexOf('=')
if(index!==-1){ // 如果最后一位是`#`
index = bitmap.charAt(index-1)==='#'?-2:-1 # 看看倒数第二位是不是'#',如果是则`index=-2`,如果不是`index=-1`
bitmap = bitmap.slice(0,index)
}
bitmap
就是我们得到的基于base64
编码的文件了,接下来,我们要将其转换成二进制流
const buffer = Buffer.from(bitmap,'base64') // 将其转换成二进制流
- 在将二进制流写成文件之前,我们首先要确定文件的路径和名称。明确路径和名称时应该明确两点:
- 文件名称应该与用户相关联,这样有利于后期维护
- 文件名称不应该有重复,因为同一个用户可能存在多次上传头像的行为。
基于以上两点考虑,我们的文件名称设计为用户id+时间戳+文件后缀名
,文件名确定了,文件路径也就明确了。(usersAvatar文件夹需要事先创建)
const path =`./public/images/usersAvatar/${user_id}${timeStamp}${extend_name}`
- 将文件写入本地。如果文件写入成功,则同时将其网络地址写入数据库。在前面我们已经得到了图片的本地地址,将其转换成网络地址也变得十分简单。
let imgUrl = `http://127.0.0.1:3000/images/usersAvatar/${user_id}${extend_name}`
await fs.writeFile(path,buffer,async function(err){
if(err){
ctx.body = {message:err}
return;
}else{
let sqlStr = `UPDATE users SET avatar=? WHERE id ='${user_id}'`
let res = await query(sqlStr,[imgUrl])
}
})
后端接口完整代码:
router.post('/uploadAvatar', async (ctx,next) => {
let token = ctx.request.headers["authorization"];
let payload = jwt.verify(token,serect);
let user_id = payload.id;
const extend_name = ctx.request.body.name.slice(-4)
let bitmap = ctx.request.body.content.split(',')[1].slice(0,-2)
let index = bitmap.lastIndexOf('=')
if(index!==-1){
index = bitmap.charAt(index-1)==='#'?-2:-1
bitmap = bitmap.slice(0,index)
}
const buffer = Buffer.from(bitmap,'base64')
let date = new Date()
let timeStamp = date.valueOf()
const path =`./public/images/usersAvatar/${user_id}${timeStamp}${extend_name}`
let imgUrl = `http://127.0.0.1:3000/images/usersAvatar/${user_id}${timeStamp}${extend_name}`
// console.log(path)
await fs.writeFile(path,buffer,async function(err){
if(err){
ctx.body = {message:err}
return;
}else{
let sqlStr = `UPDATE users SET avatar=? WHERE id ='${user_id}'`
let res = await query(sqlStr,[imgUrl])
}
})
ctx.body = {success_code:200,
message: imgUrl
}
})
到现在,也就基本完成了图片上传的任务。
在写这篇文章时,也上网搜了一些资料,学到了一些关于get
和post
的新东西,在这里做一下笔记:
- 对于
get
请求,以前存在错误的认知:get
请求参数的长度是有限制的。其实,这种说法是不准确的。HTTP
协议从未规定GET/POST
的请求长度限制是多少。所谓的长度限制,长度限制,也是限制的是整个 URI 长度,而不仅仅是你的参数值数据长度。 - 多数浏览器对于
POST
采用两阶段发送数据的,先发送请求头,再发送请求体,即使参数再少再短,也会被分成两个步骤来发送(相对于GET
),也就是第一步发送header
数据,第二步再发送body
部分。