JavaScript实现上传前端包图片(基于Vue3)

lxf2023-02-17 01:51:40

开启AdminJS成长之旅!这是我参与「AdminJS日新计划 · 12 月更文挑战」的第1天,点击查看活动详情

背景

起因是后台管理系统要实现一个功能:

  1. 前端包可配置默认图片5张供用户选择(默认第一张),用户也可以上传自己本地的图片
  2. 当用户选中某张图片并做表单保存时,需要前端用JS实现上传前端包的图片资源

JavaScript实现上传前端包图片(基于Vue3)

需求分析

  1. 选中某张图片之后,可以得到该图片的 src,就是要通过图片 url 生成一个 image/jpeg 类型文件并上传

  2. 大致流程:

    • new Image() 生成一个图片对象 imageimage.src 赋为该图片的 src
    • image 添加 onload 事件(在图片加载完成后立即执行),当触发 onload 事件时
    • 通过 Canvas 对象将 image 绘制出来
    • 再通过 Canvas 对象的 toBlob(callback, fileType) 方法Canvas 对象转为 blob 文件流,就可以上传了

具体实现

由于想学一学 Vue3,我这次用的技术栈是:pnpm + Vite + Vue3 + Vue Router 4 + JavaScript + Axios + SCSS,搭建了一个简易版项目 github地址,一路举步维艰磕磕绊绊可算是搞出了一个丐版上传图片

用 Vite 搭建 Vue3 项目

1. 安装 pnpm

# 全局安装 pnpm
npm install pnpm -g
# 查看源
pnpm config get registry
# 切换淘宝源
pnpm config set registry http://registry.npm.taobao.org

2. 创建 Vite + Vue3 项目

# 快速创建
pnpm create vite vue3-test-app --template vue

# 常规创建
pnpm create vite
# 输入项目名称:vue3-test-app
# 选择框架:Vue
# 选择语言:JavaScript

3. 安装各种需要的依赖

# 安装 scss
pnpm add sass -D
# 安装 axios
pnpm add axios
# 安装 vue-router 4
pnpm add vue-router@4

4. 配置 vite.config.js

  • 引入全局 scss 样式文件

    • 配置项 css.preprocessorOptions 用来指定传递给 CSS 预处理器的选项
    • 这样就可以在全局中使用 variables.scss 文件中预定义的变量了
  • 配置 server.host0.0.0.0(监听所有地址,包括局域网和公网地址):这样可以在网络中暴露服务

    • 解决 vite 启动后出现 Network: use `--host` to expose

    • 同时也可以让局域网内的电脑和手机访问到网页

      JavaScript实现上传前端包图片(基于Vue3)

  • 配置 server.opentrue:可以在开发环境启动项目时自动在浏览器中打开应用程序

  • 配置 server.hmrtrue:配置 HMR 连接,实现热更新

    • 所谓热更新就是,开发过程中修改程序之后页面也能及时更新

    • 如果不配置热更新,就只能重启程序才能实现页面的更新

    • 如果配置了页面还是不更新,需要检查一下路由文件引入的页面名称大小写是不是正确(查了好久原因才发现这个大坑啊╰(艹皿艹 ) )

      JavaScript实现上传前端包图片(基于Vue3)

  • 配置 server.proxy 代理

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [vue()],
  css: {
    preprocessorOptions: {
      // 引入scss
      scss: {
        additionalData: '@import "./src/styles/variables.scss";'
      }
    }
  },
  server: {
    // 监听所有地址,包括局域网和公网地址
    host: '0.0.0.0',
    port: '8080',
    // 开发环境启动项目后自动在浏览器中打开一个网页
    open: true,
    cors: true,
    // 配置开启热更新 - 解决 修改了 .vue 文件保存后,视图没有更新,需要重新启动项目才行
    // 如果配置了还不生效,需要检查路由中文件名大小写是否正确
    hmr: true,
    // 配置代理
    proxy: {
      '/appointmentapi': {
        target: 'http://192.168.9.201:14084',
        changeOrigin: true
      }
    }
  }
})

5. 生成 vue-router 实例

// router.js
// 引入 vue-router
import * as VueRouter from 'vue-router'

const base = [
  { path: '', redirect: { name: 'home' } },
  { 
    path: '/home',
    name: 'home',
    component: () => import('./views/home/HomeIndex.vue'),
    meta: { title: '首页' }
  }
]

const upload = [
  {
    path: '/upload/auto',
    name: 'upload_auto',
    component: () => import('./views/upload/UploadAuto.vue'),
    meta: { title: '上传' }
  }
]


// routes配置项
const routes = base.concat(upload)
// 创建 router 实例
const router = VueRouter.createRouter({
  // 4. 内部提供了 history 模式的实现。为了简单起见,在这里使用 hash 模式
  history: VueRouter.createWebHashHistory(),
  routes
})

// 导出 router 实例
export default router
// main.js
import { createApp } from 'vue'
import './styles/index.scss'
import App from './App.vue'
import router from './router.js'

// 引入并使用 router
createApp(App).use(router).mount('#app')

上传图片页

1. 最终效果

  • 直接上传本地图片,使用 <input type="file"> 的原生文件上传

JavaScript实现上传前端包图片(基于Vue3) JavaScript实现上传前端包图片(基于Vue3)

  • JS模拟上传

JavaScript实现上传前端包图片(基于Vue3) JavaScript实现上传前端包图片(基于Vue3)

2. 图片 urlblob 文件流

graph LR
A[图片url] -->|Image构造函数| B[Image对象img] -->|img.onload => Canvas画图|C[Canvas对象canvas]
-->|canvas.toBlob|D[Blob对象file]
  1. 图片url → Image对象img
    • const img = new Image(): 会创建一个HTMLImageElement实例 img,等同于const img = document.createElement('img')。我们会用到img 的一些属性和事件:
      • img.src: 设置或返回图像的 URL
      • img.width,img.height:设置或返回图像的宽高
      • img.onload:设置图像加载完毕后的操作
    • img.onload = () => { // 后续转blob文件流及上传的操作 }: 先设置onload事件一方面是设置了img.src后,会马上进行异步图片加载,如果在达到onload之前加载,则onload将不会触发;另一方面也是兼容某些低版本浏览器(IE)onload无效(onload写到src前面,先告诉浏览器图片加载完要怎么处理,再让它去加载图片=>不是IE浏览器不会触发onload事件,而是因为加载缓冲区的速度太快,在没有告诉它加载完要怎么办时,它已经加载完了)
    • img.src = '图片url': 定义 Image 对象的 src,相当于给浏览器缓存了一张图片

在网上查找onload 兼容浏览器相关资料时看到一篇还不错的文章:图片onload事件详解,兼容所有浏览器! - 众里寻它 - 博客园 (cnblogs.com)

  1. Image对象 → Canvas对象

    • const canvas = document.createElement('canvas'): 创建一个Canvas元素,它公开了一个或多个渲染上下文,可以用来绘制和处理要展示的内容
    • const ctx = canvas.getContext('2d'): 用来获得渲染上下文(2D)和它的绘画功能
    • canvas.width=img.width,canvas.height=img.height: 设置画布宽高为图片的宽高
    • ctx.drawImage(img, 0, 0, canvas.width, canvas.height): 用前面获取到的2D渲染上下文来绘制图片
  2. Canvas对象 → Blob对象

    • canvas.toBlob(callback(file), mimeType, quality): 通过Canvas对象直接生成 blob 文件流,这个blob图片文件可以被缓存或保存到本地(由用户代理自行决定)
      • callback回调会入参生成的blob 文件流
      • mimeType代表图像的 MIMEType 类型(字符串),默认是 image/png
      • quality代表图片质量,0到1之间,=只对image/jpegimage/webp类型有效

3. blob 文件流上传

前端发起请求可以有很多种方式,这里我们用的是 Axios(关于Axios的内容可以参考官方文档);而上传给后端接口的文件数据需要以FormData格式存储

const data = new FormData()
// 参数名,参数值,文件名称
data.append('file', file, 'image1.png')
  • 由于我们前面已经得到的是blob文件流,可以直接向 FormData 对象附加 File 或 Blob 类型的文件(如下)
  • 如果不指定文件名称(第三个参数),文件名称就会是'blob',这需要根据后端具体接口来决定是否必须传文件名称

JavaScript实现上传前端包图片(基于Vue3) JavaScript实现上传前端包图片(基于Vue3)

第三个参数是:传给服务器的文件名称 (一个 USVString),当一个 Blob 或 File 被作为第二个参数的时候, Blob 对象的默认文件名是 "blob"。 File 对象的默认文件名是该文件的名称

参考文章:浅析FormData.append()的使用、FormData对象常用方法、如何使用FormData传文件流传json对象传list数组、如何使用FormData传多个文件、如何打印FormData对象的内容 - 古兰精 - 博客园 (cnblogs.com)

4. 核心代码

详细代码请转至GitHub,后面也可能会继续完善代码,这里就只展示出JS部分代码

<script setup>
import { ref, reactive } from 'vue'
import axios from 'axios'
// 图片列表
const bannerList = [
  { src: 'config/upload/banner1.jpg', name: 'banner1.jpg' },
  { src: 'config/upload/banner2.jpg', name: 'banner2.jpg' },
  { src: 'config/upload/banner3.jpg', name: 'banner3.jpg' },
  { src: 'config/upload/banner4.jpg', name: 'banner4.jpg' },
  { src: 'config/upload/banner5.jpg', name: 'banner5.jpg' },
]
// 选中的图片
let active = ref(0)
// 最终上传的图片文件
let attach = ref(null)

/** 选择一张图片 */
const select = (index) => {
  active.value = index
}
/** 上传选中图片 */
const uploadSelect = () => {
  if (active.value < 0) {
    return
  }
  // 生成图片
  const item = bannerList[active.value] // 选中的选项
  const image = new Image()
  image.onload = () => { // 图片加载完毕后
    // 图片绘制到Canvas对象
    const canvas = document.createElement('canvas')
    const ctx = canvas.getContext('2d')
    canvas.width = image.width
    canvas.height = image.height
    ctx.drawImage(image, 0, 0, canvas.width, canvas.height)
    // Canvas对象转blob文件流
    canvas.toBlob((file) => {
      upload(file, item.name)
    }, 'image/jpeg')
  }
  image.src = item.src
}
/** 普通上传图片 */
const uploadImg = (e) => {
  const files = e.target.files
  if (!files.length) { // 没有选择文件
    return
  }
  active.value = -1
  upload(files[0])
}
/** 接口上传 */
const upload = (file, fileName = undefined) => {
  // 创建axios实例
  const service = axios.create({
    baseURL: 'http://192.168.9.201:14084/appointmentapi',
    timeout: 30000
  })
  // 请求拦截
  service.interceptors.request.use(config => {
    config.headers.Authorization = '一个token'
    return config
  }, error => {
    return Promise.reject(error)
  })

  const data = new FormData()
  data.append('file', file, fileName)

  service.post('/common/upload', data, {
    headers: { 'Content-Type': 'multipart/form-data' }
  }).then(response => {
    console.log(response)
    const res = response.data
    if (res.success) { // 成功
      attach.value = res.data
    }
  })
}
</script>