All posts in JS / DOM

AIR 的尝试

最近利用 Adobe AIR 做了一个饭否客户端:爱饭,并将之开源。使用 HTML, CSS 和 JavaScript 对着 API 文档照虎画猫,大概三个星期完工,有一些感想和总结。

  1. AIR 的开发对 Web 开发者非常友好,基本上不需要额外的程序知识了,甚至可以使用已有的 JS 库,爱饭就使用了 YUI。但是生成的程序有一通病,那就是占用内存高(爱饭在 Windows 下占用 40m 左右),而且不存在优化之说。做严肃的应用 AIR 还是上不了台面。很多时候觉得,打开一个 AIR 程序,其实就是打开了一个浏览器。
  2. absolute 的 CSS 布局方式非常灵活,对窗口缩放这种情况具有非常好的适应性。使用 webkit 引擎的 AIR 对 absolute 完全支持。如果是 IE 这种支持残缺的引擎,那得费非常多的 JS 代码。在 AIR 下写 CSS 有一种莫名的快感。正好 24ways 上发布了一篇关于 absolute 方式布局的文章,免却了我的罗嗦,见:Absolute Columns
  3. AIR 对 Linux 的支持还是存在缺失,比如无法给窗口加阴影,看来是 Linux 下的 Flash 支持跟不上。

完毕。

使用 iframe 获取网页片段的一个好处

异步操作数据的方式有两种常见的方式:XMLHttpRequestiframe. 孰优孰劣在此我们不争论,只是想举一个例子说明在获取网片片段上,使用 iframe 有一个比 XMLHttpRequest 更易企及的好处。

Ajax 常见的一种使用方法是加载网页片段填充某个区域。假设我们要在网页 foo.com/index 上请求 foo.com/partial。我们假设 partial 就是 HTML,不涉及 JSON 或 XML 格式。在这种情况下:

  1. 使用 XMLHttpRequest 直接访问 partial,获取 responseText 后赋予 index 页面上某个元素的 innerHTML 即可完成。partial 必须是一个纯粹的 HTML 片段(基于以上假设)。
  2. 在页面上创建一个隐藏的 iframe, 使用 iframe 的 src 请求 partial, partial 可以作为一个完整的页面,里面包含 JavaScript,由 partial 里的 JS 完成替换 index 上页面片段替换。

第二种看起来更繁琐,但能给我们更多控制权。例如,假如用户直接访问 foo.com/partial(这种情况很容易发生,假设您是 unobtrusive 的拥护者,机会更大,例如需要点击网页上的链接更新某部分内容时,用户使用新窗口打开了改链接), 你希望她看到的是什么内容呢?

在第一种情况中,用户看到的是代码片段,绝大部分下没有任何样式,也没有任何额外提示,导致用户体验的下降。因为只是一个 HTML 片段,你什么事都干不了。

但在第二种情况下,用户看到内容可能也只是 HTML 片段,但这却是一个完整的页面,一个可以执行 JS 的完整页面。我们只需检查这个页面的 parent 对象有没有我们预设的值,就可以判断它是不是在 iframe 之内了,然后我们可以让它跳转到正常的页面。

Demo: http://realazy.org/lab/xhrvsiframe/

Firebox 3 后退后按钮 diasabled 状态不恢复的一个解决方案

Firefox 3 有一个很让人讨厌的bug:基于某种目的,在表单提交时 disable 掉提交按钮,通过后退键回到这个页面后,这个提交按钮的状态依旧保持为 disabled 的状态,重新载入(软硬刷新)也无法改变。

google 良久,从 https://developer.mozilla.org/En/Using_Firefox_1.5_caching 中发现一个 window.onpageshow 事件,window.onload 事件无法在后退的页面中出发,但这个可以,所以解决方案就是它了。


window.addEventListener('pageshow', function(e){
    // 重置你不需要 disabled 的按钮
}, false);

更新:网友岁月如歌的解决方案比我的方案简易和正宗多了:给提交按钮加上 autocomplete="off" 的属性。

form 元素内的字段 name 不要跟 form 属性名称一致

长话短说,看这个 form 元素:

<form method="post" action="_some_action_uri_" id="_form_id_">
<input type="hidden" name="method" value="1" />
</form>

试想一下,使用 document.getElementById('_form_id_').getAttribute('method') 会出现什么情况。Firefox 3, Safari 3, Opera 9.5 都会得到预期 “post”, 但是IE 6 和 7 就没有那么幸运了,得到的是一个 object: 其实就是 <input type="hidden" name="method" value="1" /> 这个元素。

因此,为避免混淆和挽救IE,最好是,as the title.

focus 进 textarea 元素后光标位置的修复

问题

一个已经有内容的 textarea 元素,在执行该元素的 .focus() 方法后,不同的浏览器有不同表现。我们的预期是能够出现在内容后面,但只有 gecko 浏览器能做到。

修复

注意:这个函数不能直接运行,函数内的 isIE, isOpera 和 isWebkit 需要你的库提供或你编写,这并不难,对吧。

function fixTextareaFocusCursorPosition(elTextarea){
    if (isIE || isOpera){
        var rng = elTextarea.createTextRange();
        rng.text = elTextarea.value;
        rng.collapse(false);
    } else if (isWebkit) {
        elTextarea.select();
        window.getSelection().collapseToEnd();
    }
}

跨浏览器使用剪贴板

一般情况下,访问或设置剪贴板,IE 只需使用 window.clipboardDatagetDatasetData 方法即可。Mozilla 家族的浏览器(如 Firefox)则比较麻烦,不仅开发者需要写一沱代码,用户也需要主动配合(就是需要设置允许访问剪贴板)才可以(参考 Using the Clipboard),以致几不可用。至于 Opera 则根本不提供剪贴板,Safari 可以在 onpaste 等非Dom 事件中访问剪贴板(参考 Using the Pasteboard From JavaScript)。

中国特色的网站上有一个很中国特色的应用就是,在一个输入框 focus 时自动帮你把内容复制到了剪贴板中。老实说访问剪贴板是个不安全的操作,因此即使是 IE, Windows 在后来的升级中都加入是否允许访问剪贴板的提醒。如果能够做到跨浏览器的“邪恶地悄无声息”地实现中国特色的剪贴板应用,确实是个不小的挑战。

遗憾的是老外在 2006 年就帮我们做到了:使用 Flash。参考 Clipboard Copy. 原版没有考虑不安转或禁止 Flash 的情况,我做了一个小改进:

function copy(inElement) {
    var get = function(id){
        return document.getElementById(id);
    },
        elId = 'flashcopier',
        embedId = 'flashembed';

    if(!get(elId)) {
        var divholder = Document.createElement('div');
        divholder.setAttribute('id', elId);
        document.body.appendChild(divholder);
    }

    var divholder = get(elId);
    divholder.innerHTML = '<embed src="http://static.hainei.com/swf/cp.swf"\
                    FlashVars="clipboard='+encodeURIComponent(inElement.value)+'"\
                    width="0" height="0" type="application/x-shockwave-flash"\
                    id="'+embedId+'"></embed>';

    // 检测是否安装了 Flash
    var flashObj = window[embedId] || document[embedId] || {};
    if (!flashObj.SetVariable){// 没有 flash
        try {
            return window.clipboardData.setData("Text", inElement.value);
        }
        catch(ex){
            return false;
        }
    }

    return true;
}

原版是 GPL 的,这个版本也请爱咋咋用……

富文本编辑器的基本原理与实践

富文本编辑器,Rich Text Editor, 简称 RTE, 它提供类似于 Microsoft Word 的编辑功能,容易被不会编写 HTML 的用户并需要设置各种文本格式的用户所喜爱。它的应用也越来越广泛。最先只有 IE 浏览器支持,其它浏览器相继跟进,在功能的丰富性来说,还是 IE 强些。虽然没有一个统一的标准,但对于最基本的功能,各浏览器提供的 API 基本一致,从而使编写一个跨浏览器的富文本编辑器成为可能。

在很多开发者看来,富文本编辑器的编写是一件很神秘或者复杂的事情。神秘倒没有,复杂的话,确实如此。但是它的基本原理并不复杂,入门也不难。今天我们的主题是讲述基本原理,并逐步演示一个简单富文本编辑器的产生。这是我在 D2 上的一个分享内容,在台上的演讲效果不佳,固写下来,希望能够对感兴趣的读者有所帮助。

富文本编辑器的基本原理

这个原理实在是太简单了!对于支持富文本编辑的浏览器来说,其实就是设置 documentdesignMode 属性为 on 后,再通过执行 document.execCommand('commandName'[, UIFlag[, value]]) 即可。commandNamevalue 可以在 MSDN 上MDC 上找到,它们就是我们创建各种格式的命令,比方说,我们要加粗字体,执行 document.execCommand('bold', false) 即可。很简单是吧?但是值得注意的是,通常是选中了文本后才执行命令,被选中的文本才被格式化。对于未选中的文本进行这个命令,各浏览器有不同的处理方式,比方 IE 可能是对位于光标中的标签内容进行格式化,而其它浏览器不做任何处理,这超出本文的内容,不细述。同时需要注意的是,UIFlag 这个参数设置为 true 表示 display any user interface triggered by the command (if any), 在我们今天的教程中都是 false, 而 value 也只在某些 commandName 中才有,具体参考以上刚给出的两个链接。

为了不影响当前 document, 通常的做法是在页面中嵌入一个 iframe 元素,然后对这个 iframe 内的 document(通过 iframe.contentWindow.document 获得)进行操作。

十分简单,是吧?下面我们来动手做一个。

编写一个简单的富文本编辑器

这个例子使用了 YUI. 即使你对它不是很熟悉也没有关系,我在这里只使用了它的 DOM 和 Event 的一些跨平台基本方法。

搭架

在此强调一下很久未曾提及的 unobtrusive. 我们的编辑器是对 textarea 元素的一个增强(enhencement),就是说,即使 JavaScript 被禁用了,用户还可以通过 textarea 编辑内容。

在这个例子中,我们将使用 YAHOO.realazy 的命名空间,在之下实现一个 RTE 的类。我们今天的编辑器很简单,因此构造器(constructor) 的参数也只有 textarea 一个。我们使用一个实例变量来保存工具条的各个项目。实例初始化放到一个叫 render 的方法中。这一步的页面和代码见第 1 步

创建 iframe 并替换 textarea

搭好架子,正如我在前面所说,建立一个 iframe, 编辑器的所有操作都在 iframedocument 内执行。并且把 textarea 隐藏起来。从第 2 步中可以看到,我们已经有了一个 iframe, 但不能输入任何东西,很正常,我们没有打开它的 designMode 嘛。

开启 designMode

这一步涉及的东西挺多,也是关键。我们会创建获取 iframedocument 的方法,并通过程序的方式向 iframe 写入空页而非使用一个外接的 blank.html. 我们使用一个类属性 YAHOO.realazy.RTE.htmlContent 来保存空页的 html. 在准备好一切后,就可以开启 designMode 了。页面和代码详见第 3 步。看,我们已经可以在 iframe 里输入东西了。

构建工具条

我们需要操作的工具条!这样才可以控制 iframe 里的内容,才能称之为编辑器。在此我并不打算实现太多的功能,只是选择字形、字号、加粗、斜体、下划线、居左、居中、居右、超链接和插图作为示例。对于跨平台,Mozilla Midas Specification 是不错的参考。ok, 请看第 4 步,我们的工具条出来了,虽然很丑。我同时用 CSS 对 iframe 的宽度做出了一些调整。

给工具条加上事件

嗯,工具条出来了,编辑器看起来也“人模狗样”了,你兴奋的点啊点,没什么效果……意料中嘛。我们接着给工具条绑定一些事件,让编辑器内容能够响应工具条。在这一步,我们把 execCommand 再封一层,前面说过,我们用不上 UIFlag,让它永远是 false 好了。好,有代码就有真相,请看第 5 步。如果是正使用 IE, 请先暂时转移到其它浏览器。看到了吧,工具条生效了!

解决 IE 的问题

well, 如果你没有听我的劝告,依然使用 IE, 你会发现除了字型和字号其它的都不能用。为什么呢?你观察一下,有没有发现,其它浏览器选择文本后,再点击工具条上的项目,被选中的文本是否依然选中的?而 IE 呢,在点击工具条时,选中的文本马上失去选中的状态,所以它们就失败了。所以,如果我们能够保证点击工具条文本保持选中状态,就可以解决 IE 的问题了。

Microsoft 给 HTML 标签一个很奇怪的属性 unselectable, 只要设置为 on, 焦点不会转移到点击的元素上,从而保证文本的选中状态。

请看第 6 步。这也是解决 IE 头痛问题的关键所在。我曾经在这上面费了很大脑筋。

高级主题展望

good, 看看我们现在的代码,224 行。相比其它动辄上万行的编辑器,你可能会觉得不可思议。因为我们这个最基本的编辑器,连 selection 都没有用到。很多很酷的效果,比如 Google Doc 里能够动态改变链接文本,使用页内层而非弹出的 prompt 来操作等高级功能,基本上都要用到 TextRange(IE) 或者 Range(W3C). 要命的是这两个东西互补兼容,只是相似而已。入门推荐看PPK 的 Introduction to Range.

在此我们就不深入了,等我有时间我会总结一些奇技淫巧(呜呼,前端开发需要的奇技淫巧太多了,这不是好事情)出来。

JavaScript Memoization

Memoization 是一种将函数返回值缓存起来的方法,在 Lisp, Ruby, Perl, Python 等语言中使用非常广泛。随着 Ajax 的兴起,客户端对服务器的请求越来越密集(经典如 autocomplete),如果有一个良好的缓存机制,那么客户端 JavaScript 程序的效率的提升是显而易见的。

Memoization 原理非常简单,就是把函数的每次执行结果都放入一个散列表中,在接下来的执行中,在散列表中查找是否已经有相应执行过的值,如果有,直接返回该值,没有才真正执行函数体的求值部分。很明显,找值,尤其是在散列中找值,比执行函数快多了。现代 JavaScript 的开发也已经大量使用这种技术。

我通过 Google 寻找了好几种 JavaScript Memoization 的实现,都不太如人愿,有的实现不能缓存递归函数,有的需要修改函数的 prototype,于是自己实现一个:

/**
 * JavaScript Momoization
 * @param {string} func name of function / method
 * @param {object} [obj] mothed's object or scope correction object
 *
 * MIT / BSD license
 */

function Memoize(func, obj){
    var obj = obj || window,
        func = obj[func],
        cache = {};
    return function(){
        var key = Array.prototype.join.call(arguments, "");
        var key = Array.prototype.join.call(arguments, "_")
        if (!(key in cache))
            cache[key] = func.apply(obj, arguments);
        return cache[key];
    }
}

并写了一个测试案例,空口无凭,让大家亲自看看 Memoization 的威力。

见:http://realazy.org/lab/memoization.html

另,例子中的 fibonacci 函数有很多更有效率的实现方法,在此我使用最无效率的递归实现,只是为了更直达地演示 memoization.

又,longwosion 留言所提到的 key 唯一性问题,我略作修正,但应该还有更好的办法,欢迎您留言探讨。

测试 Google App Engine

一不小心从某 blog 中第一时间发现 Google App Engine 发布,立马注册一个。当天晚上抽空看了看文档,做了做hello world,第二天晚上开始写一个聊天室程序,第三天晚上拿出 Python 技术手册,捣腾到今天才把一个简单的东西弄出来。去年学了一段时间的 Python, 还用 webpy 写了一个不成型的 blog 系统。某段时间认识自己不足,苦读了两个月的 C,后来买了 macbook, 又一头扎进 Objective-C 和 Cocoa, 哈哈哈,总之,一事无成吧。现在 GAE 出来了,突然发现一直寻找的 Python hosting 就这么从天上掉下来了,而且还是馅饼……重新激起 Python 兴趣,却发现又忘得差不多了……

废话太多了……这个测试 demo 叫 chatlazy, 位于 http://chatlazy.appspot.com. 是一个简易聊天室,后台部分,就是 Python 了,具体一点,是 webpy 0.3 (开发版,未发布)。机制十分简单,就是前端使用 JavaScript 隔 5 秒去提取后台的最新消息。有几个小细节还是值得总结一下的:

  1. 由于 GAE 的数据 ID 使不能用在 Gql 中的,我只能通过时间戳来比对消息状态。把 datetime 和秒数 + 毫秒数的互转,还是比较繁琐的。Python 技术手册帮了我很大忙。解决方案大致如此:
    str(time.mktime(d.timetuple()))[:-1] + str(d.microsecond)

    反过来则是:

    p = str(t).split('.')
    tp = time.localtime(float(p[0]))
    dt = datetime(tp[0], tp[1], tp[2], tp[3], tp[4], tp[5]+1, int(p[1]))
  2. 对于 iterable 的对象, 先要 list 它转成列表,才可以使用 reversed 等相关方法。
  3. 需要取最新的 n 条信息,即数据库末尾的 n 条,但是又要顺序,可以先按逆序取 n 条,再反向排序(由此引发上条启示)。
  4. 对于任何用户输入的东西都要做过滤,一开始我在用户名那块忽略了,结果马上有人 XSS 了。这应该是基本常识,应铭记于心。
  5. 一定要处理异常。

由于 GAE 这个天上掉的馅饼,我想我近期的精力会放到 Python 上了,有计划地把 blog 迁徙到 GAE 上,并开发一些有趣地程序。GAE rocks. 老实说,这是搜索、Gmail 后,对我而言可以排到第三的 Google 服务了。

认识延迟时间为 0 的 setTimeout

由 John Resig 的 How JavaScript Timers Work 可以知道,现有的 JavaScript 引擎是单线程处理任务的。它把任务放到队列中,不会同步去执行,必须在完成一个任务后才开始另外一个任务。

让我们看看我之前的文章:JavaScript的9个陷阱及评点,在第 9 点 Focus Pocus 中提到的问题。原作者对这个认识有所偏差,其实不只是 IE 的问题,而是现有 JavaScript 引擎对于线程实现的问题(关于线程,我的概念其实不多,如果不对,希望读者多多指教)。我们通过一个例子来说明,请访问 http://realazy.org/lab/settimeout.html. 我们来看 1 和 2。如果你能看看源代码,会发现我们的任务很简单,就是给文档增加一个 input 文本框,并聚焦和选中。请现在分别点击一下,可以看到,1 并没有能够聚焦和选中,而 2 可以。它们之间的区别在于,在执行

input.focus();
input.select();

时, 2 多了一个延迟时间为 0 的 setTimeout 的外围函数,即:

setTimeout(function(){
	input.focus();
	input.select();
}, 0);

按照 JavaScript: The Definitive Guide 5th 的 14.1 所说:

在实践中,setTimeout 会在其完成当前任何延宕事件的事件处理器的执行,以及完成文档当前状态更新后,告诉浏览器去启用 setTimeout 内注册的函数。

其实,这是一个把需要执行的任务从队列中跳脱的技巧。回到前面的例子,JavaScript 引擎在执行 onkeypress 时,由于没有多线程的同步执行,不可能同时去处理刚创建元素的 focusselect 事件,由于这两个事件都不在队列中,在完成 onkeypress 后,JavaScript 引擎已经丢弃了这两个事件,正如你看到的例子 1 的情况。而在例子 2 中,由于setTimeout可以把任务从某个队列中跳脱成为新队列,因而能够得到期望的结果。

这才是延迟事件为 0 的setTimeout的真正目的。在此,你可以看看例子 3,它的任务是实时更新输入的文本,现在请试试,你会发现预览区域总是落后一拍,比如你输 a, 预览区并没有出现 a, 在紧接输入 b 时, a 才不慌不忙地出现。其实我们是有办法让预览区跟输入框同步地,在此我没有给出答案,因为上面所说的,就是解决思路,try it yourself!