[译]JavaScript中Base64编码字符串的细节

这篇具有很好参考价值的文章主要介绍了[译]JavaScript中Base64编码字符串的细节。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

本文作者为 360 奇舞团前端开发工程师

本文为翻译

原文标题:The nuances of base64 encoding strings in JavaScript

原文作者:Matt Joseph

原文链接:https://web.dev/articles/base64-encoding  

Base64编码和解码是一种常见的将二进制内容转换为适合Web的文本的形式。它通常用于data URLs,比如内嵌图片。

当你在JavaScript中对字符串应用base64编码和解码时会发生什么?这篇文章探讨了这些细节和需要避免的常见陷阱。

btoa() 和 atob() 函数

JavaScript中进行base64编码和解码的核心函数是btoa()atob()。btoa()用于将字符串转换为base64编码的字符串,而atob()则用于解码。

下面是一个快速示例:

// 一个非常简单的字符串,仅包含低于128的代码点。
const asciiString = 'hello';

// 这将会成功,它将打印:
// 编码后的字符串: [aGVsbG8=]
const asciiStringEncoded = btoa(asciiString);
console.log(`Encoded string: [${asciiStringEncoded}]`);

// 这也将会成功,它将打印:
// 解码后的字符串: [hello]
const asciiStringDecoded = atob(asciiStringEncoded);
console.log(`Decoded string: [${asciiStringDecoded}]`);

不幸的是,正如MDN文档所指出的,这只适用于包含ASCII字符的字符串,即可以用单个字节表示的字符。换句话说,这对于Unicode来说不起作用。

要理解发生了什么,请尝试以下代码:

// 示例字符串表示了小、中、大代码点的组合。
// 这个示例字符串是有效的UTF-16。
// 'hello' 的代码点都低于128。
// '⛳' 是一个16位代码单元。
// '❤️' 是两个16位代码单元,U+2764 和 U+FE0F(一个心形和一个变体)。
// '🧀' 是一个32位代码点(U+1F9C0),也可以表示为两个16位代码单元的替代对 '\ud83e\uddc0'。
const validUTF16String = 'hello⛳❤️🧀';

// 这将不会成功。它将打印:
// DOMException: Failed to execute 'btoa' on 'Window': The string to be encoded contains characters outside of the Latin1 range.
try {
  const validUTF16StringEncoded = btoa(validUTF16String);
  console.log(`Encoded string: [${validUTF16StringEncoded}]`);
} catch (error) {
  console.log(error);
}

字符串中的任何一个表情符号都会导致错误。为什么Unicode会引起这个问题?

为了理解,让我们先退后一步,深入了解计算机科学和JavaScript中的字符串。

Unicode和JavaScript中的字符串

Unicode是当前的全球字符编码标准,它是将数字分配给特定字符的实践,以便在计算机系统中使用。有关Unicode的更深入了解,请访问W3C的文章。

  • h - 104

  • ñ - 241

  • ❤ - 2764

  • ❤️ - 2764 带有一个隐藏的修改编号65039

  • ⛳ - 9971

  • 🧀 - 129472

表示每个字符的数字被称为“代码点”。您可以将“代码点”视为每个字符的地址。在红心表情符号中,实际上有两个代码点:一个用于心形,另一个用于“变化”颜色并使其始终为红色。

深入了解变体选择器的概念。

Unicode有两种常见的方法将这些代码点转换为计算机可以一致解释的字节序列:UTF-8和UTF-16。

一个过于简化的视角是:

  • 在UTF-8中,一个代码点可以使用一到四个字节(每个字节8位)。

  • 在UTF-16中,一个代码点始终是两个字节(16位)。

重要的是,JavaScript处理字符串时使用的是UTF-16。这破坏了像btoa()这样的函数,这些函数实际上是基于这样一个假设:字符串中的每个字符映射到一个单字节。MDN上明确说明了这一点:

The btoa() method creates a Base64-encoded ASCII string from a binary string (i.e., a string in which each character in the string is treated as a byte of binary data).

现在您知道JavaScript中的字符通常需要不止一个字节,下一部分将演示如何处理这种情况下的base64编码和解码。

btoa()和atob()与Unicode

正如您现在所知,抛出的错误是由于我们的字符串包含位于单个字节之外的UTF-16字符。

幸运的是,MDN关于base64的文章包含了一些有用的示例代码来解决这个“Unicode问题”。您可以修改这些代码以适应前面的示例:

// 来自https://developer.mozilla.org/en-US/docs/Glossary/Base64#the_unicode_problem。
function base64ToBytes(base64) {
  const binString = atob(base64);
  return Uint8Array.from(binString, (m) => m.codePointAt(0));
}

// 来自https://developer.mozilla.org/en-US/docs/Glossary/Base64#the_unicode_problem。
function bytesToBase64(bytes) {
  const binString = String.fromCodePoint(...bytes);
  return btoa(binString);
}

// 示例字符串表示了小、中、大代码点的组合。
// 这个示例字符串是有效的UTF-16。
// 'hello' 的代码点都低于128。
// '⛳' 是一个16位代码单元。
// '❤️' 是两个16位代码单元,U+2764 和 U+FE0F(一个心形和一个变体)。
// '🧀' 是一个32位代码点(U+1F9C0),也可以表示为两个16位代码单元的替代对 '\ud83e\uddc0'。
const validUTF16String = 'hello⛳❤️🧀';

// 这将会成功。它将打印:
// 编码后的字符串: [aGVsbG/im7PinaTvuI/wn6eA]
const validUTF16StringEncoded = bytesToBase64(new TextEncoder().encode(validUTF16String));
console.log(`Encoded string: [${validUTF16StringEncoded}]`);

// 这将会成功。它将打印:
// 解码后的字符串: [hello⛳❤️🧀]
const validUTF16StringDecoded = new TextDecoder().decode(base64ToBytes(validUTF16StringEncoded));
console.log(`Decoded string: [${validUTF16StringDecoded}]`);The following steps explain what this code does to encode the string:
  1. 使用TextEncoder接口将UTF-16编码的JavaScript字符串转换为UTF-8编码的字节流,可通过TextEncoder.encode()实现。

  2. 这将返回一个Uint8Array,这是JavaScript中较少使用的数据类型,是TypedArray的子类。

  3. 将这个Uint8Array提供给bytesToBase64()函数,该函数使用String.fromCodePoint()将Uint8Array中的每个字节作为代码点处理,并从中创建一个字符串,其结果为一个可以全部用单个字节表示的代码点的字符串。

  4. 使用btoa()对该字符串进行base64编码。

解码过程与此相同,但顺序相反。

这有效的原因是,Uint8Array和字符串之间的步骤保证了虽然JavaScript中的字符串是以UTF-16的两字节编码表示的,但每两个字节代表的代码点始终小于128。

这段代码在大多数情况下都工作良好,但在其他情况下会悄悄地失败。

静默失败的案例

使用相同的代码,但使用不同的字符串:

// 来自https://developer.mozilla.org/en-US/docs/Glossary/Base64#the_unicode_problem。
function base64ToBytes(base64) {
  const binString = atob(base64);
  return Uint8Array.from(binString, (m) => m.codePointAt(0));
}

//  来自https://developer.mozilla.org/en-US/docs/Glossary/Base64#the_unicode_problem。
function bytesToBase64(bytes) {
  const binString = String.fromCodePoint(...bytes);
  return btoa(binString);
}

// 示例字符串表示了小、中、大代码点的组合。
// 这个示例字符串是无效的UTF-16。
// 'hello' 的代码点都低于128。
// '⛳' 是一个16位代码单元。
// '❤️' 是两个16位代码单元,U+2764 和 U+FE0F(一个心形和一个变体)。
// '🧀' 是一个32位代码点(U+1F9C0),也可以表示为两个16位代码单元的替代对 '\ud83e\uddc0'。
// '\uDE75' 是代理对中的一半。
const partiallyInvalidUTF16String = 'hello⛳❤️🧀\uDE75';

// 这将会成功。它将打印:
// 编码后的字符串: [aGVsbG/im7PinaTvuI/wn6eA77+9]
const partiallyInvalidUTF16StringEncoded = bytesToBase64(new TextEncoder().encode(partiallyInvalidUTF16String));
console.log(`Encoded string: [${partiallyInvalidUTF16StringEncoded}]`);

// 这也将会成功。它将打印:
// 解码后的字符串: [hello⛳❤️🧀�]
const partiallyInvalidUTF16StringDecoded = new TextDecoder().decode(base64ToBytes(partiallyInvalidUTF16StringEncoded));
console.log(`Decoded string: [${partiallyInvalidUTF16StringDecoded}]`);

如果您查看解码后的最后一个字符(�)的十六进制值,您会发现它是\uFFFD而不是原来的\uDE75。它没有失败或抛出错误,但输入和输出数据已经悄悄地改变了。为什么会这样?

JavaScript API中的字符串变化

如前所述,JavaScript将字符串处理为UTF-16。但是UTF-16字符串有一个独特的属性。

以奶酪表情为例。这个表情(🧀)的Unicode代码点是129472。不幸的是,16位数的最大值是65535!那么UTF-16是如何表示这个更高的数字的呢?

UTF-16有一个称为代理对的概念。您可以这样想:

  • 对中的第一个数字指定要搜索的“书籍”。这被称为 "surrogate"。

  • 对中的第二个数字是“书籍”中的条目。

您可以想象,有时仅拥有代表书籍的数字而没有实际书籍中的条目可能是有问题的。在UTF-16中,这被称为 lone surrogate

这在JavaScript中尤其具有挑战性,因为一些API尽管存在单独代理也能工作,而其他API则会失败。

在前面的例子中,您在从base64解码回来时使用了TextDecoder。特别是,TextDecoder的默认设置指定了以下内容:

它默认为false,这意味着解码器用替代字符替换格式错误的数据。

您之前观察到的那个�字符,用十六进制表示为\uFFFD,就是那个替代字符。在UTF-16中,带有单独代理的字符串被视为“格式错误的”或“不规范的”。

有各种Web标准(示例1, 2, 3, 4)准确指定了格式错误的字符串何时影响API行为,但值得注意的是TextDecoder是这些API之一。在进行文本处理之前确保字符串格式规范是一个好习惯

检查格式良好的字符串

最近版本的浏览器现在具有用于此目的的函数:isWellFormed().

浏览器支持: isWellFormed().

您可以通过使用encodeURIComponent()来实现类似的结果,如果字符串包含单独代理,则会抛出URIError错误。

以下函数在可用时使用isWellFormed(),如果不可用则使用encodeURIComponent()。类似的代码可用于创建isWellFormed()的polyfill。

// 由于旧版浏览器不支持isWellFormed(),可以快速创建polyfill。
// encodeURIComponent()对于单独代理会抛出错误,这本质上是相同的。
function isWellFormed(str) {
  if (typeof(str.isWellFormed)!="undefined") {
    // 使用更新的isWellFormed()功能。
    return str.isWellFormed();
  } else {
    // 使用较老的encodeURIComponent()。
    try {
      encodeURIComponent(str);
      return true;
    } catch (error) {
      return false;
    }
  }
}

将所有内容整合在一起

现在您已经知道如何处理Unicode和单独代理,您可以将所有内容整合在一起,创建能够处理所有情况并且不会进行静默文本替换的代码。

// 来自https://developer.mozilla.org/en-US/docs/Glossary/Base64#the_unicode_problem.
function base64ToBytes(base64) {
  const binString = atob(base64);
  return Uint8Array.from(binString, (m) => m.codePointAt(0));
}

// 来自https://developer.mozilla.org/en-US/docs/Glossary/Base64#the_unicode_problem.
function bytesToBase64(bytes) {
  const binString = String.fromCodePoint(...bytes);
  return btoa(binString);
}

// 由于旧版浏览器不支持isWellFormed(),可以快速创建polyfill。
// encodeURIComponent()对于单独代理会抛出错误,这本质上是相同的。
function isWellFormed(str) {
  if (typeof(str.isWellFormed)!="undefined") {
    // Use the newer isWellFormed() feature.
    return str.isWellFormed();
  } else {
    // Use the older encodeURIComponent().
    try {
      encodeURIComponent(str);
      return true;
    } catch (error) {
      return false;
    }
  }
}

const validUTF16String = 'hello⛳❤️🧀';
const partiallyInvalidUTF16String = 'hello⛳❤️🧀\uDE75';

if (isWellFormed(validUTF16String)) {
  // 这将会成功。它将打印:
  // 编码后的字符串: [aGVsbG/im7PinaTvuI/wn6eA]
const validUTF16StringEncoded = bytesToBase64(new TextEncoder().encode(validUTF16String));
console.log(`Encoded string: [${validUTF16StringEncoded}]`);

  // 这将会成功。它将打印:
  // 解码后的字符串: [hello⛳❤️🧀]
  const validUTF16StringDecoded = new TextDecoder().decode(base64ToBytes(validUTF16StringEncoded));
  console.log(`Decoded string: [${validUTF16StringDecoded}]`);
} else {
  // 忽略
}

if (isWellFormed(partiallyInvalidUTF16String)) {
  // 忽略
} else {
  // 这不是一个格式良好的字符串,因此我们要处理这种情况。
  console.log(`Cannot process a string with lone surrogates: [${partiallyInvalidUTF16String}]`);
}

这段代码可以进行许多优化,比如将其泛化为一个polyfill,将TextDecoder的参数更改为在单独代理处抛出而不是默默替换,以及其他。有了这些知识和代码,您还可以明确决定如何处理格式不正确的字符串,比如拒绝数据或明确启用数据替换,或者为以后分析而抛出错误。除了作为base64编码和解码的一个有价值的例子外,本文还提供了一个例子,说明仔细处理文本数据尤其重要,特别是当文本数据来自用户生成或外部来源时。

- END -

关于奇舞团

奇舞团是 360 集团最大的大前端团队,代表集团参与 W3C 和 ECMA 会员(TC39)工作。奇舞团非常重视人才培养,有工程师、讲师、翻译官、业务接口人、团队 Leader 等多种发展方向供员工选择,并辅以提供相应的技术力、专业力、通用力、领导力等培训课程。奇舞团以开放和求贤的心态欢迎各种优秀人才关注和加入奇舞团。

[译]JavaScript中Base64编码字符串的细节,javascript,前端,开发语言,ecmascript文章来源地址https://www.toymoban.com/news/detail-755765.html

到了这里,关于[译]JavaScript中Base64编码字符串的细节的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • elementui图片上传转为base64字符串

    场景:后端需要将上传的图片文件作为base64字符串的方式传参给他。 html代码 js代码

    2024年02月09日
    浏览(60)
  • 将 base64 字符串转换为 Blob 对象

    在 JavaScript 中,可以使用以下代码将 base64 字符串转换为 Blob 对象: 其中, base64 是待转换的 base64 字符串, type 是 Blob 对象的 MIME 类型,默认值为 \\\'application/octet-stream\\\' 。该函数返回一个 Blob 对象。 可以像下面这样使用该函数: 其中, base64Str 是待转换的 base64 字符串, ima

    2024年02月16日
    浏览(65)
  • postman获取验证码图片(base64字符串格式)

    在 Tests 里编写脚本 然后,在响应体的 Visualize 里查看

    2024年02月12日
    浏览(58)
  • 解决因base64字符串过长,报500的问题

    提示:后端用nodejs的express,前端是vue 当上传的图片小(base64字符串长度小)时,上传成功 当上传的图片大(base64字符串长度过长)时,上传失败,接口报500,服务器也报了一大堆的错误。 如果直接把base64字符串复制到数据库发现报错,提示数据太长,很明显是因为base64字符

    2024年02月11日
    浏览(48)
  • 图片文件和 Base64 字符串互转(Java 实现)

      项目中,有些场景下,客户端需要将本地图片传输到服务方存储,此时客户端可以将图片文件转为 Base64 字符串传输到服务方,服务方收到后再将 Base64 字符串还原为图片。以下是一些图片文件和 Base64 字符串互转的工具类,以及校验图片大小的工具。 一、依赖包 二、工

    2024年02月04日
    浏览(62)
  • 将html字符串中的base64图片转换成file并上传

    目的 解决富文本编辑器中复制粘贴的图片 base64 字符串过长导致无法存储到数据库的问题 思路 通过正则 获取html字符串中里面的所有图片 base64 数组 然后每个图片base64 转成file 使用上传文件的函数 上传到服务器上. 将上传后获取到的图片访问url 替换成 数据里面的 img 的 src

    2024年01月23日
    浏览(55)
  • [虚幻引擎] UE DTBase64 插件说明 使用蓝图对字符串或文件进行Base64加密解密

    本插件可以在虚幻引擎中使用蓝图对字符串,字节数组,文件进行Base64的加密和解密。 目录 1. 节点说明 String To Base64 Base64 To String Binary To Base64 Base64 To Binary File To Base64 Base64 To File 2. 案例演示 3. 插件下载 String To Base64 对字符串进行Base64加密,字符串会自动转换成UTF8的格式,这

    2024年02月13日
    浏览(83)
  • uniapp 将base64字符串保存为图片、Word、Excel、音频、视频等文件

     uniapp 将base64字符串保存为图片、Word、Excel、音频、视频等文件 index.vue,复制运行此代码看效果,支持Android、iOS 参考文档: 1、uniapp中拿到base64转blob对象,或base64转bytes字节数组,io操作写入字节流文件bytes 2、录音文件与Base64编码相互转换的方法 3、关于base64保存为文件 4、

    2024年02月11日
    浏览(58)
  • 【golang】go获取腾讯云cos对象存储 并转为base64字符串输出

    需要引入腾讯云cos的sdk https://github.com/tencentyun/cos-go-sdk-v5 配置yaml如下: go代码编写如下:

    2024年02月11日
    浏览(53)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包