[重构倒数第07天] - 不用靠後端的 client 端上传图片预览图

前言

该系列是为了让看过Vue官方文件或学过Vue但是却不知道怎麽下手去重构现在有的网站而去规画的系列文章,在这边整理了许多我自己使用Vue重构很多网站的经验分享给读者们。

我们在开发平台的时候常常会有需要上传商品图片的需求,但是往往我们会需要确定我们上传的图片到底对不对,或是要做一些修改确认,这时候预览的功能就很重要,在以前,前端的预览功能会需要後端先把图片存起来後,在前端在显示出来,或是透过已经死去的 Flash 来达成预览的功能,但是当html5的崛起,又出现了可以透过 canvas 的方式来做预览的动作,但是相对来说 canvas 相对复杂一点,所以後来就出现了新的 API 可以帮我们完成前端预览的功能。

  • URL.createObjectURL() : 可以将我们 input 所选取的 FIle 物件转换成 Blob 物件给浏览器读取。

MDN 文件:https://developer.mozilla.org/zh-TW/docs/Web/API/URL/createObjectURL

我们先来看一下我们要完成的样子长什麽样

01

当我今天 click 画面上的 button 的时候,会选取我的图片,然後确认会把这些图片变成预览图放到画面上。

首先我们来看一下我们要怎麽做,我首先会需要一个 button 的物件以及 type 是 file 的 input 元件

<template>
  <div>
    <input
      type="file"
      class="upload"
      name="imgUpload"
      multiple="multiple"
    />
    <button>上传照片</button>
  </div>
</template>

这边要记得把 input 加上 multiple="multiple"不然没法多选档案

然後把这个 input 物件给隐藏起来,因为毕竟原生的物件样式没有很好看,我这边用了很多种藏起来的方式 (笑。

.upload {
  position: fixed;
  top: -500px;
  left: -500;
  z-index: -100;
  opacity: 0;
}

接下来才是重点,我们要点击 button 来触发 input 的 click 这个动作,才能开启系统的选档案视窗,所以我先新增一个 ref 变数来抓取 input 的实体。


<script>
import { ref } from "vue";
export default {
  setup() {
    // input DOM
    const inputDOM = ref(null);

    const fileChange = (e) => {
      console.log(e.target.files);
    };

    const uploadImages = () => {
      inputDOM.value.click();
    };

    return {
      inputDOM,
      fileChange,
      uploadImages,
    };
  },
};
</script>

<template>
  <div>
    <input
      ref="inputDOM"
      type="file"
      class="upload"
      name="imgUpload"
      multiple="multiple"
      @change="fileChange"
    />
    <button @click="uploadImages">上传照片</button>
  </div>
</template>

当我点击 button 的时候去触发 input 的 click 函式,这样就可以开启系统的选取档案视窗,然後当我选取的档案後,它就会触发 change 事件,然後我们就可以取得的到我们 input 的 file 物件,长成这样。

https://ithelp.ithome.com.tw/upload/images/20210924/20125854TWGVpl2CyB.jpg

可以拿到物件之後我们就可以开始转换成预览用的 Blob 物件。

写一个专门转换的 Composition API 吧 !

因为我们的 file 专换成 Blob 是一个 input 与 output 的动作,而且可能会依照不同的需求转换的中间过程会需要不同的处理,所以这边我们就很适合把整个转换的动作逻辑包成 Composition API。

首先我要检查 URL 这个物件有没有 在window 之中,因为不同的浏览器可能名字不一样,这点很重要。

window.URL = window.URL || window.webkitURL;

在来我们要新增我们的 function

export function useFileUpdate() {
  // 预览用档案
  const previewMap = ref({});

  // 初始化
  const initData = () => {
    previewMap.value = {};
  };

  // 选择多个档案
  const setFile = async (file = []) => {
    initData();
    previewMap.value = useQueuePreview(file);
  };

  return { setFile, previewMap };
}

这个 Composition API 主要会丢出两个东西,一个是负责接收File档案转换的 function,一个是转换好的档案物件,让我们可以直接跑一个 v-for render的物件,所以你看这边我 return 的两个东西出去。

setFile这个 function 里面有两个东西,一个是 initData 函式,一个是 useQueuePreview 的 Composition API ,我们先来看 initData 函式,这是为了每一次在选取要预览的图片的时候,先去清空前一次选取的图片所需要做的,让整个上传的行为变得正常。

至於 useQueuePreview 呢 ? 因为我们传入的 File 物件是多张图片,所以它是一个阵列,所以我需要透过回圈,一张张的做转换,所以我在这个档案里面写了一个新的 Composition API,因为这个只会用在 useFileUpdate 里面,所以我就不拆出去了。

// 本地预览
function useQueuePreview(fileArr) {
  // 多图多影片列表
  const previewMap = {};
  // 排序索引
  let idx = 0;
  for (const file of fileArr) {
    const fileData = useImageFilePreview(file);
    previewMap[idx] = fileData;
    idx++;
  }
  return previewMap;
}

// 读取 image 资料
function useImageFilePreview(file) {
  return window.URL.createObjectURL(file);
}

我跑了一个回圈,然後把资料格式重组成 Map 格式,至於为什麽不直接用成阵列就好,请参考前几个章节的内容,回圈的执行内容我又在另外的写了一个useImageFilePreview 函式去做处理,把每个细节的动作都另外拆开,我可以很清楚的知道每个转换的阶段做了什麽事情。

完整的 useFileUpdate.js 的 code

import { ref } from "vue";

window.URL = window.URL || window.webkitURL;

// 读取 image 资料
function useImageFilePreview(file) {
  return window.URL.createObjectURL(file);
}

// 本地预览
function useQueuePreview(fileArr) {
  // 多图多影片列表
  const previewMap = {};

  // 排序索引
  let idx = 0;
  for (const file of fileArr) {
    const fileData = useImageFilePreview(file);
    previewMap[idx] = fileData;
    idx++;
  }

  return previewMap;
}

export function useFileUpdate() {
  // 预览用档案
  const previewMap = ref({});

  // 初始化
  const initData = () => {
    previewMap.value = {};
  };

  // 选择多个档案
  const setFile = async (file = []) => {
    initData();
    previewMap.value = useQueuePreview(file);
    console.log(previewMap.value);
  };

  return { setFile, previewMap };
}

这样一来,我们就可以在使用 useFileUpdate 的地方取得转换成预览图的物件,直接使用。

我们来看一下使用了 setFile 後,取得 previewMap,加一个 <img /> 跑一下 v-for

<script>
import { ref } from "vue";
import { useFileUpdate } from "./composition-api/useFileUpdate.js";
export default {
  setup() {
    const { setFile, previewMap } = useFileUpdate();

    // input DOM
    const inputDOM = ref(null);

    const fileChange = (e) => {
      console.log(e.target.files);
      setFile(e.target.files);
    };

    const uploadImages = () => {
      inputDOM.value.click();
    };

    return {
      inputDOM,
      fileChange,
      uploadImages,
      previewMap,
    };
  },
};
</script>

<template>
  <div>
    <input
      ref="inputDOM"
      type="file"
      class="upload"
      name="imgUpload"
      multiple="multiple"
      @change="fileChange"
    />
    <button @click="uploadImages">上传照片</button>
  </div>
  <div
    v-show="Object.values(previewMap).length !== 0"
    class="img_box"
    v-for="item in previewMap"
    :key="item"
  >
    <img :src="item" alt="" />
  </div>
</template>

01

codesandbox 完成范例 : https://codesandbox.io/s/vue3-upload-img-preview-c629o?file=/src/App.vue:0-938

大功告成......了吗 !?

老板或是客户的脑袋永远是你无法掌握的,我们虽然可以看到预览图了,但是图片不足的地方就会露出黑色,毕竟使用者上传图片的时候我们不能去限制它上传的图片大小,所以我们需要把现在的黑底变成这样有模糊的样子。

https://ithelp.ithome.com.tw/upload/images/20210924/20125854LGLgjan9dU.jpg

这样是看起来比较美观啦,只是今天的篇幅够长了,我想还是留一点到明天再说吧,那我们明天一起在来完成它,帮它加上模糊的效果。

QRcode

那如果对於Vue3不够熟的话呢?

Ps. 购买的时候请登入或注册该平台的会员,然後再使用下面连结进入网站点击「立即购课」,这样才可以让我获得更多的课程分润,还可以帮助我完成更多丰富的内容给各位。

我有开设了一堂专门针对Vue3从零开始教学的课程,如果你觉得不错的话,可以购买我课程来学习
https://hiskio.com/bundles/9WwPNYRpz?s=tc

那如果对於JS基础不熟的朋友,我也有开设JS的入门课程,可以参考这个课程
https://hiskio.com/bundles/b9Rovqy7z?s=tc

订阅Mike的频道享受精彩的教学与分享

Mike 的 Youtube 频道
Mike的medium
MIke 的官方 line 帐号,好友搜寻 @mike_cheng


<<:  Day09 - 使用PopupWindow显示搜寻结果

>>:  [Day23]C# 鸡础观念- 物件导向(oop)~属性(Property)

Day17:【技术篇】SQL之其它常用语法

一、前言   上一篇文章有稍微带到简单的SQL基本CRUD操作方式,但实际玩起来我觉得就和GIT一样...

来说说有哪些逻辑结构吧 - DAY 2

资料结构的逻辑结构 集合 逻辑:资料元素(紫色球)除了属於相同集合之外没有其他关系 类似结构 书:封...

D11 - 「数位×IN×OUT」:数位功能

数位 I/O 视窗当然是要有数位讯号相关功能啦。 数位 I/O 功能 在 Supported Mod...

Material UI in React [ Day 19 ] Surface

在这边我会一起讲解这一 part 里面的组件,由於 App Bar 的部分之前已先讲解这边就不再提及...

随身碟无法读取,在磁碟管理中显示为No Media

本文将向您说明修复随身碟无法读取,在磁碟管理中显示为No Media错误的详细步骤。如何修复USB在...