编解码

JavaScript 内置编码函数

escape

Javascript 语言用于编码的函数,一共有三个,最古老的一个就是 escape()。该方法不会对 ASCII 字母和数字进行编码,也不会对下面这些 ASCII 标点符号进行编码: - _ . ! ~ _ ’ ( )。其他所有的字符都会被转义序列替换。所有的空格符、标点符号、特殊字符以及其他非 ASCII 字符都将被转化成 %xx 格式的字符编码(xx 等于该字符在字 符集表里面的编码的 16 进制数字)。比如,空格符对应的编码是 %20。不会被此方法编码的字符: @ _ / +。实际上,escape() 不能直接用于 URL 编码,它的真正作用是返回一个字符的 Unicode 编码值。比如 " 王下邀月熊 " 的返回结果 是 %u738B%u4E0B%u9080%u6708%u718A,也就是说在 Unicode 字符集中," 王 " 是第 738B 个(十六进制)字符,后面的以此类推。

> escape("王下邀月熊")
'%u738B%u4E0B%u9080%u6708%u718A'

其对应的解码函数为 unescape:

> unescape('%u738B%u4E0B%u9080%u6708%u718A')
'王下邀月熊'

encodeURI

encodeURI() 是 Javascript 中真正用来对 URL 编码的函数。它着眼于对整个 URL 进行编码,因此除了常见的符号以外,对其他一些在网址中有特殊含义的符号 “; / ? : @ & = + $, #",也不进行编码。编码后,它输出符号的 utf-8 形式,并且在每个字节前加上 %。

> encodeURI("http://王下邀月熊.com")
'http://%E7%8E%8B%E4%B8%8B%E9%82%80%E6%9C%88%E7%86%8A.com'

它对应的解码函数是 decodeURI()。

> decodeURI('http://%E7%8E%8B%E4%B8%8B%E9%82%80%E6%9C%88%E7%86%8A.com')
'http://王下邀月熊.com'

DOM 下 GBK 编码

在 node 环境下我们可以使用node-urlencode方便地进行各种格式的编解码,但是在 DOM 下 GBK 的编码却是个小麻烦。另一方面,如果看过笔者之前的浏览器跨域方法与基于 Fetch 的 Web 请求最佳实践这篇文章会发现,因为希望能在 Node 环境下测试,而后在 Browser 环境中无缝运行,所以笔者封装了isomorphic-urlencode,其保证了接口风格是与node-urlencode保持一致,但是因为基于 DOM 的解码是异步进行的,因此最后是设置了 Promise 作为异步的返回对象。在 Browser 中其核心的对于 GBK 的编码方式分为两步,首先是在当前的页面中创建隐藏的 form 表单与隐藏的 iframe:

//创建form通过accept-charset做encode
const form = document.createElement("form");
form.method = "get";
form.style.display = "none";
form.acceptCharset = "gbk";

//创建伪造的输入
const input = document.createElement("input");
input.type = "hidden";
input.name = "str";
input.value = url;

//将输入框添加到表单中
form.appendChild(input);
form.target = "_urlEncode_iframe_";

document.body.appendChild(form);

//隐藏iframe截获提交的字符串
if (!window["_urlEncode_iframe_"]) {
  const iframe = document.createElement("iframe");
  //iframe.name = '_urlEncode_iframe_';
  iframe.setAttribute("name", "_urlEncode_iframe_");
  iframe.style.display = "none";
  iframe.width = "0";
  iframe.height = "0";
  iframe.scrolling = "no";
  iframe.allowtransparency = "true";
  iframe.frameborder = "0";
  iframe.src = "about:blank";
  document.body.appendChild(iframe);
}

//
window._urlEncode_iframe_callback = callback;

//设置回调编码页面的地址,这里需要用户修改
form.action = window.location.href;

//提交表单
form.submit();

//定时删除两个子Element
setTimeout(function () {
  form.parentNode.removeChild(form);
  iframe.parentNode.removeChild(iframe);
}, 100);

即将 form 表单的提交结果异步显示在 iframe 中,因为笔者是基于 React 进行的开发,因此只有一个 HTML 文件作为入口,因此笔者是提交到了自身,并且需要在 HTML 文件首部添加如下回调控制代码

if (parent._urlEncode_iframe_callback) {
  parent._urlEncode_iframe_callback(location.search.split("=")[1]);

  //直接关闭当前子窗口
  window.close();
}

在原文中还有关于 IE 的 Bug 的讨论,这里暂时不做详细介绍。总结而言,isomorphic-urlencode 简单的用法为

const urlencode = require("isomorphic-urlencode");

urlencode("王下邀月熊").then(function (data) {
  console.log(data);
});

urlencode("王下邀月熊", "gbk").then(function (data) {
  console.log(data);
});

在笔者自己以流式风格基于 fetch 封装的fluent-fetch中,建议是将所有的非 UTF-8 编码的操作提取到网络请求之外,即可以如下使用:

//测试需要以GBK编码方式发起的请求
const urlencode = require("isomorphic-urlencode");

urlencode("左盼", "gbk").then((data) => {
  fluentFetcher = new FluentFetcher({
    host: "ggzy.njzwfw.gov.cn",
    responseContentType: "text",
  });

  //http://ggzy.njzwfw.gov.cn/njggzy/consultant/showresault.aspx?ShowLsh=0&Mlsh=123456&Name=%D7%F3%C5%CE
  //测试以代理模式发起请求
  fluentFetcher
    .parameter({ ShowLsh: "0", Mlsh: "123456", Name: data })
    .get({ path: "/njggzy/consultant/showresault.aspx" })
    .proxy({ proxyUrl: "http://app.truelore.cn:11499/proxy" })
    .build()
    .then((data) => {
      console.log(data);
    })
    .catch((error) => {
      console.log(error);
    });
});