乐趣区

关于前端:JavaScript-中文或非-ASCII-字符与-base64-互转

因为浏览器和 Nodejs 中的接口等不统一,所以须要分类探讨

浏览器

此计划基于 utf-8 编码实现编 / 解码。通过集体摸索,除了 utf-8 外,JavaScript 字符串仅能够应用 utf-16 来编解码,原理相似,仅在取二进制值时不同,不在此赘述,留作思考题。

实现

如果仅仅只是疾速采纳轮子,能够间接 cv 上面的代码。

javascript 版:

function encode64(text) {return btoa(String.fromCharCode(...new TextEncoder().encode(text)))
}

function decode64(text) {return new TextDecoder().decode(Uint8Array.from(atob(text), (c) => c.charCodeAt(0)))
}

typescript 版:

function encode64(text: string): string {return btoa(String.fromCharCode(...new TextEncoder().encode(text)))
}

function decode64(text: string): string {return new TextDecoder().decode(Uint8Array.from(atob(text), (c) => c.charCodeAt(0)))
}

原理解说

浏览器中用于将字符串和 base64 互转的 api 为 atobbtoa,然而这两个 API 只反对 Latin-1 字符集。如果须要对中文进行编码,btoa 则会呈现如下谬误:

Uncaught DOMException: Failed to execute 'btoa' on 'Window': The string to be encoded contains characters outside of the Latin1 range.

atob 则无奈解码出正确的字符串

// '5Lit5paH' 为 '中文' 的 utf-8 的 base64 编码
console.log(atob('5Lit5paH'))
// 输入: '中æ\x96\x87'

mdn 中的解释如下

依据设计,Base64 仅将二进制数据作为其输出。而在 JavaScript 字符串中,这意味着每个字符只能应用一个字节示意。所以,如果你将一个字符串传递给 btoa(),而其中蕴含了须要应用超过一个字节能力示意的字符,你就会失去一个谬误,因为这个字符串不能被看作是二进制数据

所以对于中文编码成 base64,咱们的思路就很简略:取字符串的二进制(utf-8 也好 utf-16 也好),取每一字节当做一个字符的码点,从新生成一个字符串,再拿这个字符串去调用 btoa,就能够胜利转进去 base64 了。

那么第一步就是拿到这个字符串的 utf-8 的二进制流。尽管 JavaScript 默认应用 utf-16,然而其提供了 TextEncoder.encode()TextDecoder.decode() 用于字符串和 utf-8 字节流转换。接下来以 中文 这个字符串为例,分 编码 解码 进行解说。

编码

对于编码,咱们能够通过上面的代码拿到一个字符串的 utf-8 字节流

new TextEncoder().encode('中文')
// 此字节流的值为 Uint8Array: [228, 184, 173, 230, 150, 135]

当初咱们就拿到了 中文 的二进制,为 228, 184, 173, 230, 150, 135,一共 6 位。

接下来就能够拿这 6 个字节来结构 6 个字符,造成一个字符串。这些二进制的值又被称为一个字符的码点,所以咱们能够应用 String.fromCharCode() 这个静态方法将这 6 个值转换成 6 个字符

动态 String.fromCharCode() 办法返回由指定的 UTF-16 代码单元序列创立的字符串。

String.fromCharCode(num1[, ...[, numN]])

—— mdn

String.fromCharCode() 的函数签名和 Uint8Array 的数组个性可知,咱们能够间接应用上面的代码将失去的二进制字节流转换成字符串

String.fromCharCode(...new TextEncoder().encode('中文'))
// 值: '中æ\x96\x87'

当初咱们将一个 utf-16 的字符串胜利转成了 utf-8 字节流对应的字符串,当初咱们就能够应用 btoa() 将这个字符串转换成 base64 编码了。

btoa(String.fromCharCode(...new TextEncoder().encode('中文')))
// 值: '5Lit5paH'

解码

对于解码,首先咱们应用 atob() 将下面失去的 base64 编码转换成字符串。

atob('5Lit5paH')
// 值: '中æ\x96\x87'

接下来咱们须要将这个字符串转换成一个 Uint8Array 二进制字节流,这里咱们能够应用 Uint8Array.from() 这个 api 来将字符串转换成 Uint8Array 字节流。

TypedArray.from() 办法 从一个类数组或者可迭代对象中创立一个新类型数组。这个办法和 Array.from() 相似。

TypedArray.from(arrayLike, mapFn)
TypedArray.from(arrayLike, mapFn, thisArg)

—— mdn

然而如果咱们如下代码间接传递给 Uint8Array 的话,会发现这个二进制字节流的每一位都是 0

Uint8Array.from(atob('5Lit5paH'))
// 值: Uint8Array: [0, 0, 0, 0, 0, 0]

此处显著为 Uint8Array.from 没有正确取到该字符串每一位的码点(即便是应用 Uint16Array 也不行),然而察看他的函数签名,咱们能够发现有 mapFn 这个参数,所以咱们能够通过这个参数取到每一个字符的码点。

对于取字符的码点,有 String.prototype.charCodeAt() 这个 api。所以转换成二进制字节流的代码如下:

Uint8Array.from(atob('5Lit5paH'), (c) => c.charCodeAt(0))
// 值: Uint8Array: [228, 184, 173, 230, 150, 135]

当初咱们就能够将其传递给 TextDecoder.decode() 转换成字符串了!

new TextDecoder().decode(Uint8Array.from(atob(text), (c) => c.charCodeAt(0)))

思考题

下面的实现为通过 utf-8 编码的二进制流与 base64 互转的实现,那么如果通过 JavaScript 默认的 utf-16 又该怎么实现呢?

提醒:能够参考 mdn 中的此局部进行实现,并优化到一行代码。

Nodejs

Nodejs 中因为有原生的 api 用于转换 base64,在 vscode 中如果把鼠标放到 atob() 或者 btoa() 上,能够失去如下阐明

This function is only provided for compatibility with legacy web platform APIs and should never be used in new code, because they use strings to represent binary data and predate the introduction of typed arrays in JavaScript. For code running using Node.js APIs, converting between base64-encoded strings and binary data should be performed using Buffer.from(str, 'base64') andbuf.toString('base64').

据此实现就很简略了。

实现

JavaScript 版:

function encode64(text) {return Buffer.from(text).toString('base64')
}

function decode64(text) {return Buffer.from(text, 'base64').toString()}

typescript 版:

function encode64(text: string): string {return Buffer.from(text).toString('base64')
}

function decode64(text: string): string {return Buffer.from(text, 'base64').toString()}
退出移动版