likes
comments
collection
share

表单上传图片可不止选择这一种方式

作者站长头像
站长
· 阅读数 11

大家好,我是苏先生,一名热爱钻研、乐于分享的前端工程师,跟大家分享一句我很喜欢的话:人活着,其实就是一种心态,你若觉得快乐,幸福便无处不在

github与好文

前言

通过表单上传图片发送到后台是挺常见的需求,一般来说有两种交互方式:

1-点击按钮,到文件夹内选取指定的图片后上传

2-将文件夹拖拽到展示区域后上传

最近产品解锁了新的交互方式,在我看来这纯属脱裤子放屁,多此一举,不过我的经验告诉我,可行!!!

需求说明

对应需求在原型的第七条,要求从剪切板读取用户复制的图片并进行上传

表单上传图片可不止选择这一种方式

头脑风暴

该功能的实现由以下部分构成:

1-复制图片

复制图片是发生在操作系统的,因此这一个交互动作不需要我们关心

2-读取剪切板

这一定涉及到浏览器的权限问题和版本兼容性问题:对于版本问题,由于是内部系统可以强制要求更新到chrome最新版所以不用考虑。而权限问题则需要进行交互上的优化处理,比如什么时间去请求权限;用户赋权导致了粘贴中断应该如何处理;如果用户拒绝了又该如何处理等

表单上传图片可不止选择这一种方式

3-过滤剪切板

由于我们不参与图片复制的过程,也就无法保证用户规规矩矩的复制的是图片,如果是非图片的文件我们应该进行过滤

4-监听剪切

实现粘贴的方式无外乎两种:ctrl+v、鼠标右键。如果存在差异,需要进行特殊处理

调研

  • 读取剪切板

读取剪切板有4种方式:

1-使用navigator的Clipboard对象

2-使用document.execCommand

3-使用window.clipboardData对象

4-使用第三方库

由于方式3是ie浏览器独有的,方式4会导致项目体积变大,故这俩我们直接不考虑。至于方式2,虽然也是原生的,且对浏览器的兼容性比较好,但是由于其是同步行为,所以我们也不考虑。因此我们选择方式一来进行实现

  • 监听剪切

通过监听元素的paste事件可以监听到用户的粘贴行为,这包括ctrl+v和鼠标右键

  • 过滤剪切板

paste事件的回调参数e中的clipboardData下的files可以拿到用户复制的文件,每个file的type可以对非图片进行过滤

实现

  • 封装剪切板读取授权函数

考虑到文本和图片的授权方式不同,我们通过type来进行区分

export default (that, cb, type = 'read') => {
  const env = process.env.NODE_ENV;
  const errTip = '当前浏览器不支持(或用户拒绝授权)读取剪切板';
  if (!window.isSecureContext || ['development', 'test'].includes(env)) {
    that.$Message.warning(errTip);
    return;
  }
  navigator.clipboard[type]()
    .then((text) => {
      if (typeof cb === 'function') {
        cb(text);
      }
    })
    .catch(() => {
      that.$Message.warning(errTip);
    });
};
  • 在组件的created中初始化拉取授权
import getClipboardAuth from '@/utils/getClipboardAuth';
getClipboardAuth(this,null,'read')
  • 在组件的mounted中为图片区域绑定事件

1-将这部分逻辑提取到utils中,避免影响主流程观感

export const bindEvent = (event) => {
  const boxId = 'img-box';
  const box = document.getElementById(boxId);
  box.addEventListener(
    'click',
    (e) => {
      let target = e.target;
      while (target.id !== boxId) {
        target = target.parentNode;
      }
      target.style.border = '1.5px solid green';
      target.addEventListener('paste', event);
    },
    true
  );
  box.addEventListener('mouseleave', (e) => {
    if (e.target.id !== boxId) return;
    e.target.style.border = '1px solid #ccc';
    e.target.removeEventListener('paste', event);
  });
};

2-引入并注册监听

import { bindEvent } from './utils';
bindEvent(this.handlePaste)
  • 读取图片并展示到指定区域

这里我们通过type属性对非图片进行过滤,然后使用FileReader将其作为base64展示到页面中,其实是可以直接通过oss上传得到线上地址的,但是我感觉这样有点浪费资源

methods: {
        handlePaste(e) {
            const clipboardData = e.clipboardData || {}
            let files = clipboardData.files || []
            files = [...files]
            files = files.filter((v) => v.type.startsWith('image'))
            if (files.length) {
                for (let i = 0; i < files.length; i++) {
                    const reader = new FileReader()
                    reader.index = i
                    reader.onload = (ev) => {
                        const url = ev.target.result
                        this.previewImgs.push(url)
                        files[reader.index].base64Url = url
                        this.uploadImgs.push(files[reader.index])
                        this.formModel.prove = 'have'
                    }
                    reader.readAsDataURL(files[i])
                }
                return
            }

            this.$Message.warning("剪切板中不存在图片")
        },
    },
  • 预览图片时处理上传oss

我选择在用户预览时再使用oss进行上传,且只会处理一次,后续再次预览就直接从缓存中读取,这样能一定程度避免浪费资源

methods: {
        async handlePreview(id) {
            const previewItem = this.uploadImgs.find((v) => v.base64Url === id)
            if (previewItem && previewItem.httpUrl) {
                previewImg(previewItem.httpUrl)
                return
            }
            for (let i = 0; i < this.uploadImgs.length; i++) {
                const v = this.uploadImgs[i]
                const base64Url = v.base64Url
                delete v.base64Url
                // eslint-disable-next-line no-loop-func
                this.uploadImg([v], i).then((imgs) => {
                    const urlDas = norImgs(imgs)
                    this.uploadImgs[urlDas[0].index].httpUrl = urlDas[0].url
                    this.uploadImgs[urlDas[0].index].base64Url = base64Url
                    if (base64Url === id) {
                        previewImg(urlDas[0].url)
                    }
                })
            }
        },
},
  • 提交表单时,上传oss

由于业务员可能不会进行预览,所以在提交表单时还需要进行校验是否需要上传到oss,我个人习惯将处理过程提取出来,如下,在处理参数阶段,我对uploadImgs中的httpUrl进行了校验

export async function processParams() {
  const { prove, orderNumber } = this.formModel;
  const params = {
    ...this.formModel,
  }; 
  if (prove === 'have') {
    if (!this.uploadImgs[0].httpUrl) {
      let imgs = await this.uploadImg(this.uploadImgs);
      imgs = norImgs(imgs);
      imgs = imgs.map((v) => v.url);
      params.prove = imgs.join('|');
    } else {
      params.prove = this.uploadImgs.map((v) => v.httpUrl).join('|');
    }
  }
  ...
  return params;
}

总结

本文从需求分析到技术调研再到功能落地,一步步带领大家实现了从剪切板读取图片的完整示例


如果本文对您有用,希望能得到您的点赞和收藏

订阅专栏,每周更新2-3篇类型体操,每月1-3篇vue3源码解析,等你哟😎