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

news2025/1/12 6:21:30

本文作者为 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 等多种发展方向供员工选择,并辅以提供相应的技术力、专业力、通用力、领导力等培训课程。奇舞团以开放和求贤的心态欢迎各种优秀人才关注和加入奇舞团。

6b5509e5e1847bcb8bca4f12625b25f4.png

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1244615.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

将对象转成URL参数

背景 有的时候前端跳转到其他平台的页面需要携带额外的参数,需要将对象转成用 & 连接的字符串拼接在路径后面。 实现方法

SquareCTF-2023 Web Writeups

官方wp:CTFtime.org / Square CTF 2023 tasks and writeups sandbox Description: I “made” “a” “python” “sandbox” “”“” nc 184.72.87.9 8008 先nc连上看看,只允许一个单词,空格之后的直接无效了。 flag就在当…

河北专升本(微机原理)

目录 第一章:计算机基础与数制转化 1. 进制运算基础 2. 常用编码形式 3. 计算机系统的组成及其工作原理 4. 微机系统主要技术指标 第二章:8086微处理器及其系统 1. 8086微处理器(CPU) 2. 8086的存储器及I/O组织 3. 8086系…

TikTok历史探秘:短视频中的时间之旅

在数字时代的浪潮中,TikTok崭露头角,成为社交媒体领域的一颗耀眼新星。这款短视频应用以其独特的创意、时尚和娱乐性质,吸引了全球数以亿计的用户。 然而,TikTok并非一夜之间的奇迹,它背后蕴藏着丰富而有趣的历史故事…

Course1-Week2-多输入变量的回归问题

Course1-Week2-多输入变量的回归问题 文章目录 Course1-Week2-多输入变量的回归问题1. 向量化和多元线性回归1.1 多维特征1.2 向量化1.3 用于多元线性回归的梯度下降法 2. 使梯度下降法更快收敛的技巧2.1 特征缩放2.2 判断梯度下降是否收敛2.3 如何设置学习率 3. 特征工程3.1 选…

管理时间的四象限法则

在管理工作和生活中,我们经常面临着各种各样的任务和事务。为了更好地处理这些任务,可以借鉴“重要紧急”、“重要不紧急”、“不重要紧急”以及“不重要不紧急”这四个象限的概念。 重要紧急:这类任务需要立刻行动,因为它们对目…

PPT思维导图怎么做?这2个思维导图工具墙裂推荐!

在日常学习和工作中,我们常常会面临需要处理大量信息的情况,这时候,一种叫做思维导图的工具可能会成为你的救星。 不同于传统的线性记录方式,思维导图以其独特的视觉表现力和结构化的信息处理方式,使得人们能够更加有…

实时截留抖音询价的用户:10个合规方法,让你的业务迅速增长!

先来看实操成果,↑↑需要的同学可看我名字↖↖↖↖↖,或评论888无偿分享 一、引言 随着抖音的普及度越来越高,越来越多的商家开始关注抖音询价用户。这些潜在客户对于企业的发展至关重要,如何实时截留这些用户成为商家关注的重点…

Python 基础【四】--数据类型-字符串【2023.11.23】

1 .定义 字符串是 Python 的一种数据类型,它可以通过单引号 ‘、双引号 "、三引号 ‘’’ 或 “”"来定义。 aabcd bacsdcd c"""accsfv""" print(a) print(b) print(c)2 .基本操作 访问单个字符 注意:从0开始…

detectron2安装

目录 macos安装windows安装提前准备下载detetron2文件激活你要安装的环境运行安装代码 windows下可能出现的问题 macos安装 建议直接照着官方文档,我当时记得是一步安装成功 网站 windows安装 略微麻烦,听我娓娓到来 提前准备 安装git 下载detetro…

C语言--三目运算符

一.介绍⭐ <表达式1>&#xff1f;<表达式2>&#xff1a;<表达式3> 它的含义是&#xff1a;如果表达式1的值为真&#xff08;非零&#xff09;&#xff0c;则整个表达式的值为表达式2的值&#xff1b;否则&#xff0c;整个表达式的值为表达式3的值。 三目运算…

网络异常检测

随着社交网络、视频流、点对点技术、云计算和 SaaS 的出现&#xff0c;可以肯定地说&#xff0c;现代企业的好坏取决于他们的网络&#xff0c;尤其是在它们提供的带宽和安全性方面。无论是银行保护其数据免遭盗窃&#xff0c;还是商业组织保护其网络免受安全威胁和攻击&#xf…

Javascript每天一道算法题(十四)——合并数组区间_中等

文章目录 1、问题2、示例3、解决方法&#xff08;0&#xff09;方法0——双指针&#xff08;错误思路&#xff09;&#xff08;1&#xff09;方法1——双指针&#xff08;正确&#xff09; 总结 1、问题 以数组 intervals 表示若干个区间的集合&#xff0c;其中单个区间为 inte…

openGL之纹理 :第二课

#include "glew.h" #include "glfw3.h" #include "SOIL2.h"//如何解析输入的数据流 0&#xff0c;1,2 分三部分 GLchar * vertextSrc " #version 330 core \n \layout (location 0) in vec3 position; \layout (location 1) in vec3 col…

MySQL-进阶

存储引擎 MySQL体系结构 连接层&#xff1a; 最上层是一些客户端和连接服务&#xff0c;主要完成一些类似于连接处理、授权认证、及相关的安全方案。服务器也会为安全接入的每个客户端验证它所具有的操作权限。服务层&#xff1a; 第二层架构主要完成大多数的核心服务功能&…

新的centos7.9安装jenkins(二)

更多ruoyi-nbcio功能请看演示系统 gitee源代码地址 前后端代码&#xff1a; https://gitee.com/nbacheng/ruoyi-nbcio 演示地址&#xff1a;RuoYi-Nbcio后台管理系统 接上一节文章。 这个版本默认git也安装好了&#xff0c;所以全局配置这个不需要了。 maven安装3.9.3版本…

C++基础(4)——类与对象(默认成员函数)

目录 1.拷贝构造函数&#xff1a; 1.1 为什么要引入拷贝构造&#xff1a; 1.2 拷贝构造函数的定义及特性&#xff1a; 1.3 什么类可以不用编写拷贝构造&#xff1a; 2. 赋值运算符重载&#xff1a; 2.1 为社么要引入运算符重载&#xff1a; 2.2运算符重载的定义以及特性…

EasyExcel 注解fillForegroundColor

EasyExcel 注解fillForegroundColor 对应的颜色值 /** * 样式的数据类 * * author leiyiDong **/ Data // 头背景设置成红色 IndexedColors.RED.getIndex() HeadStyle(fillPatternType FillPatternType.SOLID_FOREGROUND, fillForegroundColor 10) // 头字体设置成20 Hea…

qml ParticleSystem3D使用介绍

在 Qt Quick 3D 中,ParticleSystem3D 是用来创建和控制3D粒子系统的元素。粒子系统是图形编程中用于模拟液体、烟雾、火、星空等现象的技术,它通过生成大量小粒子来模拟这些效果。ParticleSystem3D 提供了一个框架,允许开发者定义粒子的各种属性,如生命周期、速度、颜色、大…

Vue3+vite 处理静态资源,解决服务器不显示动态循环img问题

注意&#xff1a; vue2webpack中&#xff0c;通常使用require来动态渲染静态资源。但在vue3vite中&#xff0c;不支持require语法&#xff0c;因此使用require会报undefined&#xff0c;所以官方推荐使用import来动态渲染静态资源。 实现方式动态渲染静态资源 vue2webpack 使…