基于Base64实现图片上传(KOA2)[项目笔记]

1,713 阅读2分钟

关于什么是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中获取。回头再看前端代码,发现参数名是:namecontent,参数值分别是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
    }
})

到现在,也就基本完成了图片上传的任务。


在写这篇文章时,也上网搜了一些资料,学到了一些关于getpost的新东西,在这里做一下笔记:

  • 对于get请求,以前存在错误的认知:get请求参数的长度是有限制的。其实,这种说法是不准确的。HTTP协议从未规定GET/POST的请求长度限制是多少。所谓的长度限制,长度限制,也是限制的是整个 URI 长度,而不仅仅是你的参数值数据长度。
  • 多数浏览器对于POST采用两阶段发送数据的,先发送请求头,再发送请求体,即使参数再少再短,也会被分成两个步骤来发送(相对于GET),也就是第一步发送header数据,第二步再发送body部分。

参考链接

base64及base64的工作原理
关于 HTTP GET/POST 请求参数长度最大值的一个理解误区