将一个url图片资源转为base64资源会出现的问题!

2,852 阅读9分钟

一、前言

在日常的开发工作中,我们通常会有这样的需求

产品经理:给你一个URL,将这个URL对应的图片资源转为base64

本文尝试探讨这个过程中出现的问题,以及该如何解决这些问题!

二、知识

在了解具体的业务之前我们需要了解几个前置的知识点:

1.base64

什么是base64呢?

百度百科:Base64是网络上最常见的用于传输8Bit字节码的编码方式之一,Base64就是一种基于64个可打印字符来表示二进制数据的方法。

一个标准的base64长这个样子

data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAApYAAAI+CAYAAAAGvUqJAAAAAXNSR0IArs4c6QAAIABJREFUeF7sXQd0VcXW/m56SEijSJEgKKJ0QoeASuhSxKeAjY5ISBCkqbQo8AtYn9SngvSq1NBEQJAWkITmk44CKr2XkEDuv/aemVNubiBIQtA3Z731iPfec/aZPXv2fLOrIzy8qBNwQl0O/sOBNDgh/haXwwmkeTjgoN/S/5wOOOhDiLs9HE7+zPIBkObkG+lJ1mfRZw4nP4lpafqa/1r+5DrT60/rH61/9f6j91+NP/7G+MsRHh7OqMYG/wj...

它的格式是

data:image/type;base64,xxxx...

xxx的每一位有64种字符,分别是大小写的a-z、0-9、+、/构成 ,他们代表的含义如下:

索引对应字符索引对应字符索引对应字符索引对应字符
0A17R34i51z
1B18S35j520
2C19T36k531
3D20U37l542
4E21V38m553
5F22W39n564
6G23X40o575
7H24Y41p586
8I25Z42q597
9J26a43r608
10K27b44s619
11L28c45t62+
12M29d46u63/
13N30e47v
14O31f48w
15P32g49x
16Q33h50y

所以base64可以和二进制互相转换,并且常用来表示图片等资源,我们的img标签的src属性也可以直接渲染base64格式的图片。

因此base64可以理解为更好的在web端表示二进制的信息资源,并且web端也配备了很多可以解析base64编码的接口,比如img标签,在 JavaScript 中,有两个函数被分别用来处理解码和编码 Base64 字符串。

  • btoa():从二进制数据“字符串”创建一个 Base-64 编码的 ASCII 字符串(“btoa”应读作“binary to ASCII”)
  • atob():解码通过 Base-64 编码的字符串数据(“atob”应读作“ASCII to binary”)

4.CORS(Cross-Origin Resource Sharing)

百度百科:CORS,全称Cross-Origin Resource Sharing ,是一种允许当前域(domain)的资源(比如html/js/web service)被其他域(domain)的脚本请求访问的机制,通常由于同域安全策略(the same-origin security policy)浏览器会禁止这种跨域请求。

上面是比较官方的说法,实际上通俗来说,我们可以理解为:在互联网的世界里,请求资源有很多种规范和方式,就像我们买东西可以选择支付宝、微信、现金、paypal、赊账等等一样,我们选择使用什么方式请求资源就得遵守人家的规范,选择用支付宝你就得先下载支付宝这个软件,然后再绑定银行卡。

而CORS就是一种请求资源的方式,它的规范就是你如果请求一个资源,那么这个资源必须满足和你是同源的,如果不同源,而你也想要请求的话,那就得这个资源的提供人员声明这个资源是可以被CORS通过的,声明的方式就是在响应头加上这个字段:

Access-Control-Allow-Origin:"*" // 代表可以被任何客户端请求到

3.资源分类

对于给定的一个URL,如果与当前站点的协议、域名、端口一致,那么这样的资源就和当前站点同源、我们可以称为内部资源

对于给定的一个URL,如果与当前站点的协议、域名、端口任何一种不一致,那么这样的资源就和当前站点费同源、我们可以称为外部资源


// 例如:假设我们的站点叫做https://new-story.cn 事实上这就是我的个人网站哈哈

// 那么百度的图片 https://baidu.com/a.png 对于我这个网站就是外部资源

// https://new-story.cn/a.png 对于我这个网站就是内部资源

在互联网的世界里资源的浏览的深度都是会受某些限制的,因此开发过程中会出现一些问题,下面的内容就是来探讨这些问题!

三、问题

我们先来把常规的将一个url转为本地base64的方案实现一下!

思路

截屏2023-05-27 下午1.37.29.png

代码


function urlToBase64 (url){
  const image = new Image();
  image.src = url;
  image.onload = ()=>{
    const canvas = document.createElement("canvas");
    canvas.width = image.width;
    canvas.height = image.height;
    const ctx = canvas.getContext("2d")
    ctx.drawImage(image , 0 , 0 , canvas.width , canvas.height)
    return canvas.toDataURL();
  }
}

以上的解决方案对于内部资源来说没有什么问题,但是对于外部资源就会存在问题,我们可以通过以下实验验证一下:

准备

准备一个vue项目,一个node服务,分别托管两张同样的图片,确保他们都可以访问的通。

截屏2023-05-27 下午2.04.58.png

截屏2023-05-27 下午2.05.07.png

web站点端口为8080,实验组件如下。

// vue组件
</template>
  <div>
      <h1>上传</h1>
      <input type="file" @input="onInput" />
      <button @click="onUpload">上传</button>

     <h1>转base64</h1>
     <div v-for="url in urlList" :key="url" class="img-box">
      <img :src="url" alt="" />
      <span>{{ url }}</span>
      <button @click="toBase64(url)">ToBase64</button>
     </div>
  </div>
</template>

<script>
import axios from "axios";
export default {
  name: "App",
  data() {
    return {
      file: null,
      urlList: [
        "http://localhost:8080/logo.png",
        "http://localhost:3000/logo.png",
      ],
    };
  },
  methods: {
    onUpload() {
      const formData = new FormData();

      formData.append("file", this.file);

      axios({
        method: "post",
        data: formData,
        url: "/api/upload",
      }).then((res) => console.log(res));
    },

    onInput(e) {
      const [file] = e.target.files;
      this.file = file;
    },

    toBase64(url) {
      const image = new Image();
      image.src = url;
      image.onload = () => {
        const canvas = document.createElement("canvas");
        canvas.width = image.width;
        canvas.height = image.height;
        const ctx = canvas.getContext("2d");
        ctx.drawImage(image, 0, 0, canvas.width, canvas.height);
        // return canvas.toDataURL();
        console.log(canvas.toDataURL());
      };
    },
  },
};
</script>

演示

屏幕录制2023-05-27 下午2.10.26.gif

可以看到,内部资源成功转成了base64,但是外部资源失败了,报错如下:

caught DOMException: Failed to execute 'toDataURL' on 'HTMLCanvasElement': 

接下来的内容是重要核心,敲黑板!!!

我看过很多解决这个方案帖子当中,会直接告诉你,在创建image标签的时候,加一行代码就可以了,就像下面这样:

image.crossOrigin = "anonymous";

其实我可以直接告诉你,在本实验中,就算加了这行代码也是不行的。为什么不行呢?

我们要透过现象看本质,我们看一看crossOrigin这个字段的意义到底是什么?

前置知识我们已经了解到请求资源有很多种方式,其实如果没有加上这个属性,那么img就是以普通的方式请求的资源,这个方式几乎没有限制,请求什么图片资源都可以,但是有一点是你只能在标签中浏览,而不能拿到具体的二进制数据,所以通过这种方式请求的资源是不能通过canvas的toDataURL方式拿到具体的二进制信息。如果我们选择加上这个CorssOrigin属性之后,那就意味着img标签会以CORS的方式加载图片,还记得之前我们提到过的条件么?要么必须是同源,要么资源的提供方允许你的站点可访问。而这两个条件都不满足,就会报CORS错误。

这里我强调一下为什么不满足这两个条件,非同源很好理解:

const image = new Image()
image.src = "http://localhost:3000/logo.png"

上面这段代码产生的请求是在http://localhost:8080 这个站点下发起的,所以不同源。并且http://localhost:3000/logo.png 是由express静态托管的,并没有添加那个特殊的响应头,因此默认就是不允许跨域的。所以两个条件都不满足,就会报CORS错误。

下面是实证

截屏2023-05-27 下午3.13.02.png

四、解决

既然我们知道了问题的本质,我们就可以尝试解决这个问题:

方案一

我们既然选择使用CORS的方式请求,那么就让资源满足CORS的规则就好了,我们就让资源的提供方,一般来说是后端的同学在我们想要的所有资源的响应头添加上那个特殊的响应头就好了:

access-control-allow-origin:"*"

互联网上有很多添加了这个响应头的图片,不信我随便给你找一张试一下:

截屏2023-05-27 下午3.22.25.png

这张图片其实就是资源的提供方告诉全世界,你们都可以随便来访问我这个资源,假设这个资源就是我们让后端同学加了这个响应头的,那么这个时候再回到我们之前的vue项目中,我们把这个资源添加到我们的测试用例中:

截屏2023-05-27 下午3.25.10.png

这种图片显然是不同源的,但是因为服务端同学设置了这个特殊的响应头,所以可以以CORS的方式加载到img标签中,其中的信息也可以被canvas使用toDataURL读取出来。

结论:所以如果我们要使用canvas的思路去解决这个需求,要么该图片是内部资源的URL,我们直接转没有问题,要么这个图片是外部资源,我们就要求这个图片是允许跨域的。否则这个图片就不适合使用canvas的思路去转base64。

方案二

但是问题就是如果每一次我们都舔着个脸去求后端同学,那就太没面子了,有些事情呀,能够自己掌控还是得自己掌控才好!

我们可以借助我们之前解决跨域问题的思路来实现,方案一,不就是因为跨域么!我们把跨域问题解决不就可以了么,跨域的解决方案有很多,我们直接使用代理就可以:

思路

截屏2023-05-27 下午3.44.32.png

我们在vue项目中的vue.config.js配置代理:


module.exports = {
  devServer: {
    proxy: {
      "/api": {
        target: "http://localhost:3000/",
        changeOrigin: true,
        pathRewrite: {
          "^/api": "",
        },
      },
    },
  },
};

然后在组件中添加一个方法:

proxyToBase64() {
  axios({
    url: "/api/logo.png",
    method: "get",
    responseType: "blob",
  }).then((res) => {
    const reader = new FileReader();
    reader.readAsDataURL(res.data);
    reader.onload = () => {
      console.log(reader.result);
    };
  });
},

然后我们看一看效果:

屏幕录制2023-05-27 下午3.47.53.gif

可以看到,实际上通过canvas的思路还是会存在跨域问题,但是通过代理就可以转为base64,没有任何问题,但值得注意的是本案例中是本地的代理服务器做的,生产环境中需要使用nginx进行代理的转发。

后续方案。。。

但是其实方案二,也有一个问题,我们希望对于任何一个域名的资源都可以转base64,但是代理手段则需要提前将想要代理哪一个域名提前配置好,所以理论上也是存在缺陷的,我也正在寻找更好的方案,如果掘金大佬们有更好的方案,欢迎评论区留言,我们好好唠唠!

五、资源

参考

一个关于image访问图片跨域的问题

我的

保姆级讲解JS精度丢失问题(图文结合)

shell、bash、zsh、powershell、gitbash、cmd这些到底都是啥?

从0到1开发一个浏览器插件(通俗易懂)

用零碎时间个人建站(200+赞)

更多精彩内容请访问我的个人网站 new-story.cn

创作不易,如果您觉得文章有任何帮助到您的地方,或者触碰到了自己的知识盲区,请帮我点赞收藏一下,或者关注我,我会产出更多高质量文章,最后感谢您的阅读,祝愿大家越来越好。