使用 JavaScript 处理 UTF-8 文本字符串或任意数据的 Base64 编解码

Base64 是一种将二进制数据编码为 ASCII 字符的编码方式. 使用 Base64 编码, 可以在媒介不支持传输或存储任意二进制数据的情况下, 避免数据在过程中发生损坏. 比如, 网站开发者常常会使用 Data URLs 来在 HTML 文档内嵌入一些小型文件.

Base64 的详情这里不作展开. 本文将主要介绍在 JavaScript 中处理 UTF-8 文本的 Base64 编解码的方式.

JavaScript 的字符串背景及 UTF-16 编码

在 JavaScript 中, 有 btoa() (binary to ASCII) 和 atob() (ASCII to binary) 这两个处理 Base64 编解码的函数. 如 MDN 关于 Base64 的术语解释页 所述, Base64 本身是对二进制数据进行编码, 而非针对于文本, 同时, 这两个函数的名称也示意着, 它们接收/返回的也是二进制 (binary) 数据. 只是在这两个函数加入 Web 平台时, 尚不存在适用于二进制数据的类型, 因此这两个函数使用字符串中每个字符的点位, 来表示二进制数据中每个字节的值.

这样的情况导致了一种常见误区: btoa 可以用来编码任意文本数据, atob 也可以用来解码文本数据. 事实上, 这种字符点位和字节之间的对应只限于 0x7f 以下的点位. 进一步来说, 0xff 以上的点位, 会令 btoa 函数抛出异常, 因为它超出了一个字节所能表示的最大数值. 所以, 在编码任意 Unicode 文本时, 需要针对该情况进行特殊处理.

JavaScript 中的字符串, 其内在是一串连续的 UTF-16 码元 (code units). 在 UTF-16 编码下, 每个码元都是 16 位长, 也就意味着, 使用单独的一个 UTF-16 码元, 可以表示 216 (或者说 65536) 种可能的字符. 但很显然, Unicode 中的字符总量远远超过这个数目; 在 UTF-16 下, 这之外的字符通过 surrogate pairs (代理对) 的形式, 也就是一对 16 位码元来表达. 为了避免歧义, 这一对码元必须位于 0xD8000xDFFF 之间, 这些点位也不能用来编码单码元的字符. 每个 Unicode 字符, 都有一个对应的 Unicode 代码点位 (Unicode code point), 可用 1 或 2 个 UTF-16 码元来表示. 每个 Unicode 代码点位可以使用 \u{xxxxxx} 以在字符串中表示, 其中 xxxxxx 表示 1-6 个十六进制数位.

Unicode 的编码空间从 U+0000 到 U+10FFFF, 一共有 1112064 个码位 (code point) 可以用来映射字符. Unicode 的编码空间可以划分为 17 个平面 (plane), 每个平面包含 216 (65536) 个码位. 17 个平面的码位对应到从 U+xx0000 开始到 U+xxFFFF 的码位 (其中 xx 是从 0x000x10 的十六进制值, 共 17 个). 第一个平面称为基本多语言平面 (Basic Multilingual Plane, BMP), 或第零平面 (Plane 0), 其它平面则称为辅助平面 (Supplementary Planes). 在基本多语言平面内, U+D800 和 U+DFFF 之间的码位永久保留, 不会映射到 Unicode 字符. UTF-16 可以利用这一区间内的码位来编码辅助平面内字符对应的码位.

具体的 UTF-16 编码方式这里不作展开, 这里仅简单介绍一下代理对中前导代理和后尾代理的概念. 起始的代理 (surrogates), 也被称作高位代理 (high-surrogate code units) 或 前导代理 (lead surrogates), 其取值应在 0xD8000xDBFF 之间 (包含起始), 而结尾的代理点位 (surrogates), 也称作低位代理点位 (low-surrogate code units) 或 后尾代理 (trail surrogates), 其取值应在 0xDC000xDFFF 之间 (包含起始).

可见, 在 UTF-16 中, 前导代理, 后尾代理 以及 BMP 中有效字符的码位互不重叠, 因此在进行字符串搜索等处理时, 不用担心不同字符编码之间会存在重叠的部分. 也可以说, UTF-16 是 自同步 (self-synchronizing) 的, 只给定一个码元, 即可判断其是否是一个字符的起始码元.

在编码领域中, 自同步代码是可以被唯一解码的代码; 从它的代码字 (code word) 中取出一部分符号流, 或者从两个相邻代码字中取出的重叠部分, 都不能作为合法的代码字. 或者用另一种方式来解释, 使用一个字母表构成的一个字符串 (即 “代码字”) 集合, 如果使用任意两个代码字组成的新的字符串中, 从第二个字符直至倒数第二个字符中, 不包含任何可作为代码字的子串, 则可以称其为自同步代码.

出现在字串末位或非末尾代理之前的前导代理 (位于 0xD8000xDBFF 范围 (含起始) 间的码元), 以及出现在字串首位或非前导代理之后的末尾代理 (位于 0xD8000xDBFF 范围 (含起始) 间的码元), 被称作 “lone surrogate” (孤独代理), 本身不能表示任何 Unicode 字符. JavaScript 的大多数函数都是基于 UTF-16 码元工作, 因此能够处理这些码元. 但如果要和其它系统进行交互, 比如使用 encodeURI() 将特定字符根据 UTF-8 编码替换为若干转义序列 (escape sequences) 时, 由于 UTF-8 编码不对 “long surrogate” 进行编码, 因此会在遇到 “lone surrogate” 时发生异常.

不包含 “lone surrogate” 的 JavaScript 字符串被称作 “well-formed” (格式良好的), 可以安全地被 encodeURI()TextEncoder 使用. 可以使用 isWellFormed() 判断字符串是否包含 “lone surrogate” (以便进行进一步处理) 或使用 toWellFormed() 将字符串中的 “lone surrogate” 替换为 U+FFFD (Unicode replacement character).

需要注意的是, 在一些情况下 (比如使用 TextEncoder 进行编码), “ill-formed” (格式不良好的) 的字符串, 也就是包含 “lone surrogate” 的字符串, 其中的 “lone surrogate” 会被 自动 替换为 U+FFFD; 在被渲染显示时, “lone surrogate” 也通常被渲染为 U+FFFD, 也就是 这个中间带有问号的菱形符号.

尽管 Unicode 标准中, 包括 UTF-16 在内的各种 UTF 编码, 不会对 “surrogate pair” 对应的点位进行编码, 对 “lone surrogate” 的编码应视作错误, 但是 UCS-2, UTF-8, 以及 UTF-32 都能很自然地编码这些点位; 而在 UTF-16 中, 直接使用不成对的代理码元, 就能无歧义地编码它的对应点位, 虽然结果并不是合法的 UTF-16. 因此, 使用这些点位记录数据从理论上可行, 但是在处理时需要特别注意, 不然可能会造成数据损失.

除了 Unicode 字符之外, 还有一些特定的 Unicode 字符序列, 应当被视作一个可视单元 (visual unit), 这样的序列也被称作 “grapheme cluster”.

“grapheme” 称作 “字位” 或 “字素”, 是最小的有意义的书写符号单位; 比如小写英文字母 a 和大写英文字母 A 是两个 “glyph (字形)”, 但是同属一个 “字位” <a>. 而在中文中, 同一个字可以有不同写法 (如正体字, 简化字, 异体字), 但是都计算为同一个字位.

在 emoji 中便存在很多 “grapheme cluster”, 属于常见的场景了. 比如, 很多 emoji 存在一系列变体, 其事实上由多个 emoji 组成, 通常使用 <ZWJ> (U+200D, Zero Width Joiner, 零宽连字) 这一控制字符连接在一起.

一些书写系统中, 字素的位置和形态取决于其和其他字素之间的关系, 比如阿拉伯文字和婆罗米系文字, 或者德文尖角体 (Fraktur, 亦称破碎体) 下的拉丁文字 (也称罗马字体); 在计算机中排版这些书写系统下的文字时, 就会用到这些控制字符. ZWJ 的具体用法, 取决于 “conjunct consonant” 或 “ligature” (译作 “合字” “连字” 或 “连体字”) 的使用是否是默认的. 比如在天城文 (Devanagari) 中, 它被用来阻碍 conjunct 的形成, 而在僧家罗文中则相反 (其默认不使用合字). Emoji 中的使用和后者类似, 通过在两个或多个 emoji 之间放置 ZWJ, 来形成展示出来的单个字形, 比如表示家庭的 emoji, 就是由两个表示成人的 emoji 和一到两个表示孩子的 emoji 所组成的.

因此在对字符串进行迭代访问时, 需要注意所访问的 “字符” (“character”) 层级. 这里使用引号来强调 “字符”, 是因为比如使用 split("") 分割字符串时, 所得到的是一系列 UTF-16 码元, 而非用户所能感知的单个字符 (比如 “grapheme clusters”) 或单个 Unicode 字符 (即 code point, 代码点位), 也就是说, 字符串中的 “surrogate pair” 会被破坏. 而使用字符串的 @@iterator() 方法时, 比如 ... 展开语法或者 for...of 循环, 则会按照 Unicode 代码点位进行迭代.

这里摘选 MDN 上给出的几个代码示例:

"😄".split(""); // ['\ud83d', '\ude04']; splits into two lone surrogates

// "Family: Man, Boy"
[..."👨‍👦"]; // [ '👨', '‍', '👦' ]
// splits into the "Man" and "Boy" emoji, joined by a ZWJ

在 JavaScript 中 Bases64 编解码字符串

如前文所述, 若要在 JavaScript 中不进行另行实现的情况下使用 Base64 算法编码 (即 btoa 函数), 需要使用各个字符均在 0x00 - 0xff (一个字节) 以内的字符串, 亦即 “bytes string” (字节串) 或者 “binary string” (二进制字符串). 类似地, atob 解码后得到的 “字符串”, 实际上也是一个 “bytes string”, 仅当每个字节都恰好对应 UTF-16 编码下的字符时, 这个 “字符串” 才有字符内容上的意义. 而涉及到这之外的字符, 就需要进行恰当的编解码了.

一些文章中使用 decodeURIencodeURI 来将字符串中不符合 URI 规范的字符替换为 UTF-8 编码的转义序列, 这样得到的结果中仅包含 ASCII 字符, 因此在使用 btoaatob 进行编解码时是安全的. 也有的方案会进一步将 %xx 这样的转义序列中的十六进制数对, 替换为对应数码下的字符, 去掉了冗余数据, 可以提升存储和传输的效率; 这样的字符串中, 也是只包含 0xff 以内的字符. (参见 Using Javascript’s atob to decode base64 doesn’t properly decode utf-8 strings - Stack Overflow)

atobbtoa 的若干年后, JavaScript 不仅加入了可以用于处理二进制数据的 TypedArray, 也提供了将字符串编码到 UTF-8 的 TextEncoder, 以及可以解码多种字符集编码的 TextDecoder. 使用这些功能可以进一步简化代码的编写. MDN 关于 Base64 的术语解释页 上就给出了这样的例子:

function base64ToBytes(base64) {
  const binString = atob(base64);
  return Uint8Array.from(binString, (m) => m.codePointAt(0));
}

function bytesToBase64(bytes) {
  const binString = Array.from(bytes, (byte) =>
    String.fromCodePoint(byte),
  ).join("");
  return btoa(binString);
}

// Usage
bytesToBase64(new TextEncoder().encode("a Ā 𐀀 文 🦄")); // "YSDEgCDwkICAIOaWhyDwn6aE"
new TextDecoder().decode(base64ToBytes("YSDEgCDwkICAIOaWhyDwn6aE")); // "a Ā 𐀀 文 🦄"

不同于 charCodeAt() 方法返回对应下标处 UTF-16 码元的数值, codePointAt() 方法返回的是对应下标位置处, 字符的 Unicode 点位数值. 不过, 由于 codePointAt() 方法会在下标越界时返回 undefined, 因此 TypeScript 会在 base64ToBytes 调用 Uint8Array.from() 时提示类型错误. 可以通过添加 as 指定 codePointAt() 调用处返回的类型. 笔者尝试为这些代码添加了类型提示, TypeScript 代码示例如下:

function base64ToBytes(base64: string) {
  const binString = atob(base64);
  return Uint8Array.from(binString, (m) => m.codePointAt(0) as number);
}

function bytesToBase64(bytes: Uint8Array) {
  const binString = Array.from(bytes, (byte) =>
    String.fromCodePoint(byte),
  ).join("");
  return btoa(binString);
}

读者也可以 在 TypeScript Playground 上尝试这些代码.

这些方法在浏览器中是普遍支持的, 而 Node.js 也在 8.3.0 版本 (2017 年 8 月) 支持了 TextEncoder. 这样, 同样的代码, 既可以在 Web 前端实现功能, 也能在服务器端使用.

在 JavaScript 中 Bases64 编解码任意二进制数据

上文中的 base64ToBytesbytesToBase64 即可实现从 Base64 到 Uint8Array 的转换, 只是使用 TextEncoderTextDecoder 将其应用到了字符串之上.

如果为了更高的效率, MDN 的页面上还给出了使用 FileReaderfetch API 的 Base64 解决方案.

async function bytesToBase64DataUrl(bytes, type = "application/octet-stream") {
  return await new Promise((resolve, reject) => {
    const reader = Object.assign(new FileReader(), {
      onload: () => resolve(reader.result),
      onerror: () => reject(reader.error),
    });
    reader.readAsDataURL(new File([bytes], "", { type }));
  });
}

async function dataUrlToBytes(dataUrl) {
  const res = await fetch(dataUrl);
  return new Uint8Array(await res.arrayBuffer());
}

// Usage
await bytesToBase64DataUrl(new Uint8Array([0, 1, 2])); // "data:application/octet-stream;base64,AAEC"
await dataUrlToBytes("data:application/octet-stream;base64,AAEC"); // Uint8Array [0, 1, 2]

同样, 读者也可以 在 TypeScript Playground 上尝试上述代码.

另见: The nuances of base64 encoding strings in JavaScript | Articles | web.dev

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

37.WEB渗透测试-信息收集-企业信息收集(4)

免责声明&#xff1a;内容仅供学习参考&#xff0c;请合法利用知识&#xff0c;禁止进行违法犯罪活动&#xff01; 内容参考于&#xff1a; 易锦网校会员专享课 上一个内容&#xff1a;36.WEB渗透测试-信息收集-企业信息收集&#xff08;3&#xff09;-CSDN博客 关于主域名收…

c#学习入门2

十、运算符 1&#xff09;算术运算符是用于数值类型变量计算的运算符&#xff0c;它返回的结果是数值 1.赋值符号 2.算数运算符 加 减- 乘* 除/ 取余% 3.算数运算符的优先级 4.算术运算符的复合运算 5.算术运算符的自增减 2&#xff09;字符串拼接 1.字符串拼接方式1 注意&…

编写一个函数fun,它的功能是:实现两个字符串的连接(不使用库函数strcat),即把p2所指的字符串连接到p1所指的字符串后。

本文收录于专栏:算法之翼 https://blog.csdn.net/weixin_52908342/category_10943144.html 订阅后本专栏全部文章可见。 本文含有题目的题干、解题思路、解题思路、解题代码、代码解析。本文分别包含C语言、C++、Java、Python四种语言的解法完整代码和详细的解析。 题干 编写…

day01黑马头条小bug合集及解决办法

问题1.初始构造heima-leadnews-user这个模块 触发此bug Command line is too long 解决办法&#xff1a; 问题2&#xff1a;构建网关模块启动时 报数据库错误 Failed to configure a DataSource: url attribute is not specified and no embedded datasource could be confi…

电磁仿真--基本操作-CST-(4)

目录 1. 简介 2. 建模过程 2.1 基本的仿真配置 2.2 构建两个圆环体和旋转轴 2.3 切分圆环体 2.4 衔接内外环 2.5 保留衔接部分 2.6 绘制内螺旋 2.7 绘制外螺旋 2.8 查看完整体 2.9 绘制引脚 2.10 设置端口 2.11 仿真结果 3. 使用Digilent AD2进行测试 3.1 进行…

《HCIP-openEuler实验指导手册》1.2Apache主页面配置

一、配置服务器监听IP及端口 注释主配置文件“监听IP及端口”部分 cd /etc/httpd/conf cp httpd.conf httpd.conf.bak vim httpd.conf可以在普通模式下搜索Listen关键字 :/Listen按n键继续向后搜索 在/etc/httpd/conf.d中新建子配置文件port.conf&#xff1a; touch /etc…

QT——简易计算器(从0开始)

目录 一、题目描述&#xff1a; 二、创建工程&#xff1a; 1. ​编辑 2. 3. 4. 默认 5. 6. 7. 8. 默认 9. 创建完成 三、UI界面设计&#xff1a; 1. 添加按钮 1. 2. 按钮界面 3. 按钮绑定快捷键 2. 文本框添加 1. 文本框字体 2. 默认文本 3. 文本对齐方式…

【介绍下IDM的实用功能】

&#x1f3a5;博主&#xff1a;程序员不想YY啊 &#x1f4ab;CSDN优质创作者&#xff0c;CSDN实力新星&#xff0c;CSDN博客专家 &#x1f917;点赞&#x1f388;收藏⭐再看&#x1f4ab;养成习惯 ✨希望本文对您有所裨益&#xff0c;如有不足之处&#xff0c;欢迎在评论区提出…

ptyhon画图显示中文

import matplotlib.pyplot as plt import matplotlib# 设置中文字体 matplotlib.rcParams[font.sans-serif] [SimHei] matplotlib.rcParams[font.family]sans-serifplt.plot([1, 2, 3, 4]) plt.xlabel(这是x轴) plt.ylabel(这是y轴) plt.title(这是标题) plt.show()用这个代码…

66、二分-搜索旋转排序数组

思路&#xff1a; 不断二分&#xff0c;首先判断左侧有序还是右侧有序&#xff0c;如果左侧有序那么就在左侧寻找&#xff0c;如果右侧有序那就在右侧寻找。假设左侧有序&#xff0c;那就判断目标值在不在左侧&#xff0c;如果在左侧继续左侧二分。如果不在左侧&#xff0c;那么…

使用aqua data studio进行mysql、oracle、syabse等等debug调试

1、在aqua data studio界面 右击左边空白位置&#xff0c;选择”注册服务器“&#xff0c;弹出框如下&#xff1a; 2、在”一般“里选择使用的数据库&#xff0c;如sybase、mysql, 3、登录成功后&#xff0c;会显示数据库&#xff0c;点击要debug的存储过程

WIFI信号状态信息 CSI 特征提取篇之活动片段提取上(五)

在之前的数据处理环节中&#xff0c;用CSI Tool收集到的原始数据信号&#xff0c;经历了数据解析、降噪、插值的处理步骤&#xff0c;变成了干净、完整的信号片段&#xff0c;这是后续做更进一步分析的基础。 在开始阅读本篇博客前&#xff0c;需要说明两个重要的点&#xff1…

基于SpringBoot + Vue实现的家政服务管理系统设计与实现+毕业论文+答辩PPT+指导搭建视频(包运行成功)

目录 项目介绍 论文展示 资源获取 项目介绍 家政服务管理平台是一个管理信息系统&#xff0c;为了宣传的需要&#xff0c;为了给用户提供方便快捷的服务&#xff0c;从而设计了家政服务管理平台。管理员可以通过这个系统把家政服务信息发布出去&#xff0c;可以方便用户快…

RK3568平台开发系列讲解(Linux系统篇)芯片手册的使用:GPIO的寄存器说明

🚀返回专栏总目录 文章目录 一、查找复用寄存器二、查找方向寄存器三、查找数据寄存器沉淀、分享、成长,让自己和他人都能有所收获!😄 📢寄存器GPIO 进行配置, 一般情况下需要对 GPIO 的复用寄存器, 方向寄存器, 数据寄存器进行配置。 GPIO0_B0 配置为例: 一、查…

《十一》Qt各种对话框之QInputDialog

QInputDialog QInputDialog 用于方便快捷地获取一个用户输入数据&#xff0c;支持整数 int、浮点数 double、文本 QString 三种数据。按照 QInputDialog 内部的输入控件&#xff0c;又可以分为整数输入控件 QSpinBox、浮点数输入控件 QDoubleSpinBox、单行文本输入控件 QLineE…

C++|stack-queue-priority_queue(适配器+模拟实现+仿函数)

目录 一、容器适配器 1.1容器适配器概念的介绍 1.2stack和queue的底层结构 1.3deque容器的介绍 1.3.1deque的缺陷及为何选择他作为stack和queue的底层默认实现 二、stack的介绍和使用 2.1stack的介绍 2.2stack的使用 2.3stack的模拟实现 三、queue的介绍和使用 …

mysql download 2024

好久没在官网下载 mysql server 安装包。今天想下载发现&#xff1a; 我访问mysql官网的速度好慢啊。mysql server 的下载页面在哪里啊&#xff0c;一下两下找不到。 最后&#xff0c;慢慢悠悠终于找到了下载页面&#xff0c;如下&#xff1a; https://dev.mysql.com/downlo…

Qt:学习笔记一

一、工程文件介绍 1.1 main.cpp #include "widget.h" #include <QApplication> // 包含一个应用程序类的头文件 //argc&#xff1a;命令行变量的数量&#xff1b;argv&#xff1a;命令行变量的数组 int main(int argc, char *argv[]) {//a应用程序对象&…

揭示C++设计模式中的实现结构及应用——行为型设计模式

简介 行为型模式&#xff08;Behavioral Pattern&#xff09;是对在不同的对象之间划分责任和算法的抽象化。 行为型模式不仅仅关注类和对象的结构&#xff0c;而且重点关注它们之间的相互作用。 通过行为型模式&#xff0c;可以更加清晰地划分类与对象的职责&#xff0c;并…

使用Umbrello学习工厂模式

工厂方法模式之所以有一个别名叫多态性工厂模式是因为具体工厂类都有共同的接口&#xff0c; 或者有共同的抽象父类。 当系统扩展需要添加新的产品对象时&#xff0c;仅仅需要添加一个具体对象以及一个具体工厂对 象&#xff0c;原有工厂对象不需要进行任何修改&#xff0c;也不…