65.9K
CodeProject 正在变化。 阅读更多。
Home

通过 AJAX 提交 Unicode 字符

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.50/5 (7投票s)

2009 年 3 月 26 日

CPOL

7分钟阅读

viewsIcon

40854

当网站只支持 Latin-1 时,如何让 AJAX 像真正的浏览器一样发布内容?

引言

本文展示了一种非常具体的技术,用于通过 AJAX 以与网络浏览器相同的方式提交 Unicode 文本。如果你认为这已经是这种情况了,那么你需要阅读本文。

本文不会解释标准的 AJAX 用法,因为你可以在任何地方找到这些信息。

背景 

我经常访问一个旧的网络论坛,它的页面字符集是“iso-8859-1”,也称为 Latin-1。它允许世界各地的人们使用他们想要的任何语言发布消息,但他们的 HTML 表单是纯粹的原生表单。有一个文本框和一个提交按钮。其他一切都取决于你。

它确实接受某些 HTML 代码子集,但你必须手动输入这些代码,而且错误会带来很大的麻烦。这对网站有利吗?肯定不是,但滥用很少发生,而且几乎总是由于错误造成的。用户总体上都是正派的人。

网站所有者对改变现状不感兴趣,所以我决定通过 Opera 的 UserJavaScript 和 Firefox 的 Greasemonkey 插件自行解决。简而言之,我编写了自己的代码来获取用户提交的内容并将其发布到网站,但我的 JavaScript 表单包含许多小的格式化按钮和安全检查。

此外,它通过 AJAX 提交,这样我就不必在每次评论后重新加载页面。当你的论坛有 400 多个带有图片和嵌入视频的帖子时,你真的不想在每次“伙计,这张照片真有趣”的评论后重新加载。

经过一番周折,我在两个浏览器中都使其运行得相当流畅。直到有一天,我尝试发布一些日文文本。有人知道“乱码”是什么意思吗?就是乱码字符。但为什么呢?我知道我可以在常规浏览器表单中发布完全相同的文本,并且它会完美显示,那么为什么 AJAX 会被搞砸呢?我之前看到的所有网络参考资料都说 AJAX 的工作方式与浏览器相同!

Using the Code

我将这段代码写在一个用户 JavaScript 文件中,它是纯 JavaScript,没有混合 HTML 或 CSS 或其他语言(除了嵌入在 JavaScript 中的),并且它以“*.js”结尾。你当然可以将代码用于任何你认为合适的地方,但只有我的特殊情况才让我发现问题确实存在,以及后来如何解决它。

我们将假设一个极其简单的 HTML 表单,如下所示

<form method="post" action="http://www.site.com/submit/path">
  <textarea id="userTxt"></textarea>
  <input type="submit" value="Submit">
</form>

正如我所说,非常简单。这与该网络论坛使用的内容大致相同。如果你在 Opera/Firefox/IE/Safari/任何浏览器中输入日文文本(例如,“ハロー、ワールド!”),你的评论将被提交并在页面重新加载时完全按照你输入的方式返回。

现在我们来编写一个 JavaScript 函数,通过 AJAX 提交相同的表单。这里有很多超出范围的细节,所以它们将被略过或省略。首先,假设表单已更改为调用 JavaScript 而不是直接提交

<form method="post">
  <textarea id="userTxt"></textarea>
  <input type="submit" value="Submit" onClick="sendByAJAX(); return false;">
</form>

我不会费心编写一个完美的、跨浏览器兼容的 AJAX 函数。就假装 Internet Explorer 在这部分遵循标准,或者在你的空闲时间自行调整。:-)

function sendByAJAX() {
   // get the user text and make it safe for HTTP transmission
   var userTxt = encodeURIComponent( document.getElementById('userTxt').value );
   // create the AJAX object
   var xmlhttp = new XMLHttpRequest();
   // assume successful response -- do NOT actually make this assumption in real code
   xmlhttp.onreadystatechange = function() {
      if (xmlhttp.readyState==4 && xmlhttp.status>=200 && xmlhttp.status<300) {
         // You'll probably want to do something more meaningful than an alert dialog
         alert('POST Reply returned: status=[' + xmlhttp.status + 
	    ' ' + xmlhttp.statusText + ']\n\nPage data:\n' + xmlhttp.responseText);
      }
   }
   xmlhttp.open('POST', 'http://www.site.com/submit/path');
   // here we are overriding the default AJAX type, 
   // which is UTF-8 -- this probably seems like a stupid thing to do
   xmlhttp.setRequestHeader('Content-type', 
	'application/x-www-form-urlencoded; charset=ISO-8859-1;');
   xmlhttp.setRequestHeader('User-agent'  , 'Mozilla/4.0 (compatible) Naruki');
   xmlhttp.send(userTxt);
}

当您添加日文文本、俄语脚本、特殊数学符号等时,上述代码将会失败。嗯,从技术上讲,它会成功,并且不会抛出任何错误,但您会从服务器收到乱码字符。

原因是 Unicode。事实上,如果您强制浏览器将页面编码更改为 Unicode,您的字符现在将变得可读。它们根本不是真正的乱码字符,而是网站指定的页面编码与用户提供的文本冲突,您对此无能为力……哦,等等,有办法。

我知道有些不对劲,因为浏览器本身可以发布完全相同的文本,并且它能够正确返回,而无需通过浏览器设置强制使用新的编码。这个技巧是谷歌无法告诉我的,但一位非常有用的 Opera 员工可以。

当浏览器在要发布的文本中遇到 Unicode 字符时,它会悄悄地将它们转换为 HTML 实体。

你可能知道,Unicode 标准可以容纳 1,114,112 个字符,这几乎足以涵盖我们人类所需的一切。前 65,535 个字符与 HTML 数字实体有一对一的映射。从技术上讲,所有字符都有,但有一个陷阱,所以暂时忽略它。

那对我意味着什么?嗯,假设你非常渴望在你的帖子中包含一些时髦的数学字符。你在 Unicode 书籍中查找它们的数字,但你找不到与之匹配的 HTML 命名实体。比如说 Unicode 字符 0x24EA。你如何发布它?嗯,只需输入数字实体引用 `⓪`,你就搞定了。

由于 HTML 实体中的字符都是标准的 Latin-1 字符,因此传输该文本不需要特殊的编码。但是,如果我使用我的 IME(输入法编辑器)输入日文文本,它将不会生成 HTML 实体。它将直接将 Unicode 字符放入表单中。

浏览器知道如何预解析,所以让我们教 JavaScript 同样的事情。我们需要添加一个新函数,然后更改我们读取 `userTxt` 字段的行。

   // get the user text and make it safe for HTTP transmission
   var userTxt = encodeURIComponent
		( uni2ent( document.getElementById('userTxt').value ) );
...
function uni2ent1stTry(snippet) {
  var uSnip = '';
  for (var c=0, val; val = snippet.charCodeAt(c); c++) {
    if (val < 256) {
      uSnip += snippet.charAt(c);
    }
    else {
      uSnip += "&#" + val + ";"
    }
  }
  return uSnip;
}

函数 `uni2ent()` [注意代码示例中的第一次尝试 - 尚未准备好] 使用 JavaScript 内置的 `string` 函数 `charCodeAt()` 逐个字符地解析文本。这给出了该字符的 Unicode 值。

如果该值低于 256,则可以安全地按原样使用原始字符,因为 Unicode 和 Latin-1 在这一点上使用相同的集合。如果该值更高,则需要将其转换为 HTML 数字实体。实体可以写成十进制 (`叶`) 或十六进制 (`叶`)。

JavaScript 本身使用的是 Unicode,而不是 Latin-1,在处理互操作性问题时,您应该注意这一点。

现在来说说陷阱!这个函数几乎是完美的,而且大多数人永远不会遇到问题。但它确实存在,它被称为代理对。

还记得我说过前 65535 个字符与 HTML 数字实体有一对一的映射吗?用十六进制表示,那是 0xFFFF,占用两个字节。一旦超过这个范围,你需要额外的两个字节(取决于特定的 Unicode 编码方案)来组成一个字符。JavaScript 和大多数 Unicode 程序通常使用 UTF-16 变体,所以它们需要这些额外的字节来表示更高编号的字符。

关于这个问题有很多网络讨论,但在你不知道“代理对”这个神奇短语之前很难找到。本质上,当一个 Unicode 字符必须表示为代理对时,你使用一个巧妙的小公式将 Unicode 数字转换为两个 UTF-16 数字。这很复杂,你需要花一段时间思考它才能开始理解。

我使用了两个主要参考资料,它们帮助我最终解决了这个难题。第一个是 维基百科的 UTF-16 编码过程示例。这与我想要的方向相反,所以我将其反向工程为以下内容:

function uni2ent2ndTry(srcTxt) {
   var entTxt = '';
   var c, hi, lo;
   var len = 0;
   for (var i=0, code; code=srcTxt.charCodeAt(i); i++) {
      // need to convert to HTML entity
      if (code > 255) {
         // values in this range are surrogate pairs
         if (0xD800 <= code && code <= 0xDBFF) {
            hi = code;
            lo = srcTxt.charCodeAt(i+1);
            lo &= 0x03FF;
            hi &= 0x03FF;
            hi = hi << 10;
            code = (lo + hi) + 0x10000;
         }
         // wrap it up as a Hex entity
         c = "&#x" + code.toString(16).toUpperCase() + ";";
      }
      // smaller values can be used raw
      else {
         c = srcTxt.charAt(i);
      }
      entTxt += c;
   }
   return entTxt;
}

不要太喜欢那个函数。它没有错误检查,它假设输入是完美的,而且位移位仍然让我感到不安。

后来,我发现 Mozilla 发布了一个有趣的 关于如何从代理对中获取完整 Unicode 字符的示例,这正是我需要的!稍作修改,然后……

function uni2ent(srcTxt) {
  var entTxt = '';
  var c, hi, lo;
  var len = 0;
  for (var i=0, code; code=srcTxt.charCodeAt(i); i++) {
    var rawChar = srcTxt.charAt(i);
    // needs to be an HTML entity
    if (code > 255) {
      // normally we encounter the High surrogate first
      if (0xD800 <= code && code <= 0xDBFF) {
        hi  = code;
        lo = srcTxt.charCodeAt(i+1);
        // the next line will bend your mind a bit
        code = ((hi - 0xD800) * 0x400) + (lo - 0xDC00) + 0x10000;
        i++; // we already got low surrogate, so don't grab it again
      }
      // what happens if we get the low surrogate first?
      else if (0xDC00 <= code && code <= 0xDFFF) {
        hi  = srcTxt.charCodeAt(i-1);
        lo = code;
        code = ((hi - 0xD800) * 0x400) + (lo - 0xDC00) + 0x10000;
      }
      // wrap it up as Hex entity
      c = "" + code.toString(16).toUpperCase() + ";";
    }
    else {
      c = rawChar;
    }
    entTxt += c;
    len++;
  }
  return entTxt;
}

这就是最终版本!你真的应该努力理解它,因为我希望别人和我一样痛苦。但如果你有任何问题,别找我。我一无所知。

如果你仔细查看 Mozilla 的示例,你会发现我放弃了错误检查,因为我是一个糟糕的程序员。照我说的做,不要照我做的做。

关注点

这是我的第一篇文章,所以我肯定会进行很多编辑(并收到很多错误报告)。请大家多多包涵。

我还没有做过太多正式测试,所以如果你发现任何严重的错误或不准确的陈述,请告诉我。谢谢。

历史

  • 2009 年 3 月 26 日:首次发布
© . All rights reserved.