在 JavaScript 中转换为 Base64 而没有 Deprecated 'Escape' 调用

Converting to Base64 in JavaScript without Deprecated 'Escape' call

我叫非斯图斯。

我需要通过 JavaScript 在浏览器中将字符串与 Base64 相互转换。这个主题在这个网站和 Mozilla 上都很好地涵盖了,建议的解决方案似乎是沿着这些路线:

function toBase64(str) {
    return window.btoa(unescape(encodeURIComponent(str)));
}

function fromBase64(str) {
    return decodeURIComponent(escape(window.atob(str)));
}

我做了更多研究,发现 escape()unescape() 已弃用,不应再使用。考虑到这一点,我尝试删除对已弃用函数的调用,这些函数会产生:

function toBase64(str) {
    return window.btoa(encodeURIComponent(str));
}

function fromBase64(str) {
    return decodeURIComponent(window.atob(str));
}

这似乎可行,但它回避了以下问题:

(1) 为什么最初提出的解决方案包括对 escape()unescape() 的调用?该解决方案是在弃用之前提出的,但大概这些功能在当时增加了某种价值。

(2) 在某些极端情况下,我删除这些已弃用的调用是否会导致我的包装函数失败?

注意:对于 string=>Base64 转换问题,Whosebug 上还有其他更冗长和复杂的解决方案。我确信它们工作得很好,但我的问题与这个 特别 流行的解决方案特别相关。

谢谢,

非斯都

TL;DR 原则上 escape()/unescape() 没有必要,没有弃用函数的第二个版本是安全的,但它生成的时间更长base64编码输出:

  • console.log(decodeURIComponent(atob(btoa(encodeURIComponent("€uro")))))
  • console.log(decodeURIComponent(escape(atob(btoa(unescape(encodeURIComponent("€uro")))))))

两者都创建输出 "€uro" 但没有 escape()/unescape() 的版本具有更长的 base64 表示形式

  • btoa(encodeURIComponent("€uro")).length // = 16
  • btoa(unescape(encodeURIComponent("€uro"))).length // = 8

escape()/unescape()步骤只有在对应的情况下才有必要(例如,不可调整的php-脚本期望以特定方式完成base64。)。

长版:

首先,为了更好地理解您上面建议的 toBase64()fromBase64() 两个版本之间的差异,让我们看一下位于问题的核心。文档说,btoa 的命名是助记符,因此

"b" can be considered to stand for "binary", and the "a" for "ASCII".

这有点误导,因为文档急于添加,

in practice, though, for primarily historical reasons, both the input and output of these functions are Unicode strings.

更不完美,btoa()确实只能接受

characters in the range U+0000 to U+00FF

坦率地说只有英语字母数字文本适用于 btoa()。

encodeURIComponent() 的目的,您在两个版本中都有,是为了帮助处理字符在 U+0000 到 U+00FF 范围之外的字符串。 一个例子是字符串 "uü€" 具有三个字符

  • a (U+0061)
  • ä (U+00E4)
  • (U+20AC)

这里只有前两个字符在范围内。 第三个字符,欧元符号,在外面并且 window.btoa("€") 引发了一个超出范围的错误。 为避免此类错误,需要一个解决方案来在 U+0000 到 U+00FF 的集合中表示“€”。这就是 window.encodeURIComponent 所做的:

window.encodeURIComponent("uü€")
创建以下字符串:
"a%C3%A4%E2%82%AC" 其中一些字符已被编码

  • a = a(保持不变)
  • ä = %C3%A4(改为它的utf8表示)
  • = %E2%82%AC(改为它的utf8表示)

(更改为其 utf8 表示形式)通过使用字符“%”和字符 utf8 表示形式的每个字节的两位数字来工作。 “%”是 U+0025,因此允许在 btoa() 范围内。的结果 window.encodeURIComponent("uü€") 然后可以被馈送到 btoa() 因为它不再有超出范围的字符:

btoa("a%C3%A4%E2%82%AC") \ = "YSVDMyVBNCVFMiU4MiVBQw=="

btoa()encodeURIComponent()之间使用unescape()的关键是utf8表示的所有字节用了3个字符%xx来存储所有字节 0x00 到 0xFF 的潜在值。这里是unescape() 可以起到可选作用的地方。这是因为 unescape() 占用了所有这样的 %xx 字节,并在允许的 U+0000 到 0+00FF 范围内在其位置创建了一个单一的 Unicode 字符。

检查:

  • btoa(encodeURIComponent("uü€"))).length // = 24
  • btoa(unescape(encodeURIComponent("uü€"))).length // = 8

主要区别是文本的 base64 表示长度减少,代价是通过可选的 escape()/unescape() 进行额外解析,主要是 ASCII 字符集文本反正是最小的。

要理解的主要教训是 btoa() 的命名具有误导性,并且需要 encodeURIComponent() 本身生成的 Unicode U+0000 到 U+00FF 字符。已弃用的 escape()/unescape() 只有 space 保存功能,这可能是可取的,但不是必需的。 Unicode 符号 > U+00FF 的问题在此处作为 btoa/atob Unicode problem 解决,其中甚至提到了将 "all UTF8 Unicode" 改进为现代浏览器中可能的 base64 编码的方法。

TL;DR/简短摘要

不要使用 btoa(encodeURIComponent(str))decodeURIComponent(atob(str)) - 那是“废话”。

convert string to Base64”通常表示“encode string as UTF-8 and encode the bytes as Base64”,这就是正是 btoa(unescape(encodeURIComponent(str))) 所做的。 btoa(encodeURIComponent(str)) 正在做 其他事情 对我能想象的任何情况都没有用,即使它永远不会像 humanityANDpeaces detailed .[=74 中解释的那样抛出错误=]



“将字符串转换为 Base64”是什么意思?

Base64 是一种二进制到文本的编码,字节序列被编码为 ASCII 字符序列。1因此不可能 直接将文本编码为 Base64。它在概念上始终是一个两步过程:

  1. 将字符串转换为字节(使用一些 character encoding
  2. 将字节编码为 Base64

你主要可以使用你想要的任何字符编码(也称为字符集2Encoding Scheme),它只需要能够表示所有需要的字符并且它两个方向必须相同(文本到 Base64 和 Base64 到文本)。 由于有 many different character encodings,协议或 API 应该定义使用哪一个。 如果 API 需要“通过 Base64 编码的字符串”并且没有提及字符编码,那么现在您通常可以假设需要 UTF-8 编码。3

对第 1 步中的字节进行 Base64 编码非常简单:
a) 取三个输入字节得到24位。
b) 分成四块,每块 6 位,得到 0...63 范围内的四个数字。
c) 通过 table 将数字转换为 ASCII 字符并将这些字符添加到输出
d) 转到 a)
有关 Base64 本身的更多详细信息超出了此答案的范围。

btoa 是做什么的?

现在您可能会想:“这个答案不可能是正确的。它声称,不可能将文本直接编码为 Base64,即使这正是 btoa 所做的——它获取文本并输出 Base64。

没有。它不采用 text 和 returns Base64,它采用 字符串类型的参数 和 returns Base64。但是那个字符串参数并不代表文本,它只是一个 strange way to store a sequence of bytes。每个字节由一个字符表示,其数字代码点值等于字节的值。4

HTML standard 中的注释说,“可以认为“b”代表“二进制”,而“a”代表“ASCII”。 ” 与流行观点相反,我不认为 btoa 的命名很糟糕。它不接受文本,它接受二进制数据并使用 Base64 生成 ASCII 字符串,因此“binary to ascii”的缩写形式是绝对正确的名称。这是参数类型,具有误导性。

HTML standardbtoa的定义简单地说:

[...] the user agent must convert that argument to a sequence of octets whose nth octet is the eight-bit representation of the code point of the nth character of the argument, and then must apply the base64 algorithm to that sequence of octets, and return the result.

我不知道,可能永远也不会知道,为什么他们不选择不同的参数类型,例如一组数字。也许在首次指定 btoa 时性能没有那么好?

unescape(encodeURIComponent(str)) 是做什么的?

现在您可能会想:“如果将文本转换为 Base64 的第一步是将文本编码为字节,那么 btoa(unescape(encodeURIComponent(str))) 是如何实现的? btoa 不会那样做,但是 unescapeencodeURIComponent 似乎都与字符编码没有任何关系?

实际上,<em>encode</em>URIComponent与字符encoding有关。 standard 表示:

The encodeURIComponent function computes a new [...] URI in which each instance of certain code points is replaced by [...] escape sequences representing the UTF-8 encoding of the code point.

现在我们有了百分比编码的 UTF-8 字节。要转换 percent-encoded bytes to a binary string suitable for btoa, one can use unescape, because the behavior description 状态等:

  • If c is the code unit 0x0025 (PERCENT SIGN), then
    • [... how to decode %uXXXX ...]
    • Else if k ≤ length - 3 and [... two hexdigits follow ...] then
      • Set c to the code unit whose value is the integer represented by [...] the two hexadecimal digits at indices k + 1 and k + 2 within string.

因此,在 encodeURIComponent 将 UTF-8 字节存储为 %XX 之后,unescape 将它们完全按照 btoa 的要求将它们转换为单个代码点。所以总而言之 btoa(unescape(encodeURIComponent(str))) 将文本编码为 UTF-8 字节,然后编码为 Base64。

回到原来的问题

如果你忘记了,问题是:

(1) Why did the originally proposed solution include calls to escape() and unescape()? The solution was proposed prior to deprecation but presumably these functions added some kind of value at the time.

(2) Are there certain edge cases where my removal of these deprecated calls will cause my wrapper functions to fail?

如果没有 unescape,您将无法获得 UTF-8 编码字符串的 Base64 表示形式。 btoa(encodeURIComponent(str)) 将文本编码为一些奇怪的字节(不是标准化的 Unicode 编码方案,而是通过将 URI 编码的字符串存储为 ASCII 可以获得的字节),然后将其编码为 Base64。所以 unescape 是标准一致性所必需的——好的,encodeURIComponent 和 ASCII 也是标准化的,但没有人会想到这种奇怪的组合。

如果只有你自己在 Base64 之间转换,那么是的,你可以使用 btoa(encodeURIComponent(str)) 并且它永远不会抛出错误,如 humanityANDpeaces detailed 中所解释的那样(问题 (2) 已得到充分回答我想想)。

但是在那种情况下你可以直接使用encodeURIComponent的结果更好。它已经是纯 ASCII,总是btoa(encodeURIComponent(str)) 短。如果你想要比 encodeURIComponent(str) 更小的大小,你可以使用 btoa(unescape(encodeURIComponent(str)))(如果输入字符串包含更多非 ASCII 字符,则更小)。

如果您转换为 Base64,因为其他方 API 或协议需要 Base64,那么您根本无法使用 btoa(encodeURIComponent(str)),因为没有人理解结果。

哦,而且 btoa(unescape(encodeURIComponent(str))) 不可能真正成为 unescape 的“ 在弃用 之前提出”:
unescape 在版本 3 中从标准中删除,同一版本添加了 encodeURIComponentunescape在文档中仍然有解释,但被移至附件B.2,其介绍stated, that it “suggests uniform semantics [...] without making the properties or their semantics part of this standard.” But as browsers have to be backwards compatible, it probably won't be removed any time soon


自己试试:

function run(){
    let Base64Function=new Function("str", $("#algorithm").val());
    let base64=Base64Function($("#input").val());
    $("#Base64Text").text("Output: "+base64);
    let charset=$('#charset').val();
    let uri="data:text/plain"
           +(charset?";charset="+charset:'')
           +($("#interpret").prop('checked')?";base64":'')
           +","+base64;
    $("#dataURI").text(uri);
    $("#dataURI").attr('href', uri);
    $("#Base64iframe").attr('src',uri);
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>

<label for="input">Text to encode:</label>
<input type="text" id="input" value="abc€"/><br />

<label for="algorithm">Encode function:</label>
<input type="text" id="algorithm" size="50"/><br />

<button type="button" onclick="run();">Run</button>
Defaults:
<button type="button" onclick='
    $("#algorithm").val("return btoa(unescape(encodeURIComponent(str)))");
    $("#charset").val("UTF-8");
    $("#interpret").prop("checked",true);
'>UTF-8 Base64</button>
<button type="button" onclick='
    $("#algorithm").val("return btoa(encodeURIComponent(str))");
    $("#charset").val(""); //I don't know - it's not UTF-8
    $("#interpret").prop("checked",true);
'>wrong</button>
<button type="button" onclick='
    $("#algorithm").val("return encodeURIComponent(str)");
    $("#charset").val("UTF-8");
    $("#interpret").prop("checked",false);
'>without btoa (not Base64)</button>
<br />

<div id="Base64Text">Output:</div>

<label for="charset">Interpret as this character encoding:</label>
<input type="text" id="charset" /><br />

<label for="interpret">Interpret as Base64:</label>
<input type="checkbox" id="interpret" /><br />

<div><a id="dataURI"></a></div>
<iframe id="Base64iframe"></iframe>

此代码段通过创建 dataURI 来测试 Base64 结果,但该概念也适用于 Base64 的其他应用程序。


注:

在某些引文中,我使用 [] 来省略或缩短我认为不重要的内容。
[... some text ...] 显然不是来源的一部分。

脚注:

1 标准说Base64“is designed to represent arbitrary sequences of octets”(八位字节表示由八位组成的字节)

2 A character set is not exactly the same as a character encoding. However a coded character set can always be considered to implicitly define a character encoding, therefore "character set" and "character encoding" are often used as synonyms. Maybe it once was the same? Sometimes the term charset 明确用作字符编码而不是字符集的简称。

3 至少UTF-8对于websites. Also see UTF-8 Everywhere

来说是很占优势的

4 这实际上是 ISO_8859-1 编码,但我不会这样想。最好想想 bytes[i]==str.charCodeAt(i).