实验前提是了解amd规范:http://wiki.commonjs.org/wiki/Modules/AsynchronousDefinition,国内很多社区或个人或公司或开源项目都已有一些具体实现,不再赘述。

另外,这里只有概念,不涉及具体算法和代码。

版本控制

土豆网现有的js版本控制逻辑是为每个更新发布的js文件自动更新版本号,它以下划线+版本号数字为文件名结尾(不包括后缀名):

而在amd中,我们自己实现的异步加载模块机制,譬如说是个use方法,需要使用a.js这个模块。那么不可能每更新一次a.js,所有使用到它的地方都手动改为use(‘a_2.js’)

实际情况是:源代码里一直写的是use(‘a.js’),至于版本控制,它是在服务器端自动追加的:

红色字体部分需要特殊解释一下,它并不是正则替换掉所有use(‘a.js’)的地方,而是做了一个映射或者说路由。以a.js为key、a_2.js为value存入一个HashMap中,然后use(‘a.js’)方法执行时读取到映射关系,将key转换为value后再加载。

这样做的好处是既不影响规范的id语义,又屏蔽了版本号对开发人员的细节可见度,甚至当迫不得已时想使用某个古老版本亦可:use(‘a_1.js’)——因为匹配不到映射关系,所以会直接加载a_1.js这个模块。

智能依赖构建

有时候一个页面上的js要使用到多个模块,为了节省请求数量,我们希望将多个模块合并到这一个js中。为此引入了@import语法,它以注释形式出现在文件头部,服务器端构建工具会自动将导入的js文件合并到一起。这个过程是递归的,也就是说会导入可能所有需要导入的子孙文件。

事实上,这个方法早已屡见不鲜,很多构建工具都是这样做的,只是大部分都是客户端构建,由开发人员手动打包文件后发布。将这一过程移植到服务器端可省却许多不必要的麻烦。

值得一提的是,打包构建会影响到版本映射时的逻辑。譬如我们use(‘a.js’),但a.js已经被导入到当前文件中了,因此映射逻辑会忽视掉它——这个id的模块已经存在,不会再去异步加载了,映射还有什么用呢?

更进一步

手动导入其实不仅仅限于导入依赖或者导入模块,导入其它不相关的东西都行。有时候,在使用一个模块时,这个模块有依赖、依赖的模块还会继续依赖……我们希望一下子全部导入所有依赖到这个文件,而不想手动@import。较好的做法是增加一个关键字比如@build,它会递归将所有依赖打包到一个文件中。

这个步骤我还没有做。

是否需要final

经过讨论有这样一个观点:a.js被b.js和c.js所依赖,a.js更新后原本b和c都会自动追加版本映射(如果是导入则自动更新导入),但是c并不想随之自动更新,c想等自己文件有更新后才去更新导入和依赖的东西,因此需要增加一个@final关键字指定。

经过思考我觉得它带来的好处和带来的坏处一样多:构建和版本映射系统将变得复杂; 阅读维护上增加歧义;等c下次更新时你可能已经忘记了依赖的哪些模块上次没有自动更新。所以这个指令还是不要的好,每次更新一个模块后,所有依赖和导入的都随之自动更新,由测试人员在测试环境根据文件列表全部测试到位。

实际上我们也应该这样做,一个底层的被大量依赖的模块必须经过严格的测试,它被设计成被大量依赖的说明它是全局性的,即使不存在于amd中也是被写成全局代码,在更新时一样要测试到位。

允许并存

开发人员手动合并还存在一种冲突现象:这个页面用到了a.js和b.js,一个人改了a另外一个人改了b,他俩在发布测试环境时会互相覆盖对方的结果。服务器端智能构建可以很好的解决这一问题:一个人发a的源文件而另外一个人发b的源文件,同时在测试环境测试;a测试完成后a发布上线即可,不会发布到b,反之亦然。

甚至某个模块想要设计时也可以是“平滑过渡”的。试想由于某些原因,底层模块a要升级了,但并不是所有地方都兼容新版本,此时我们可以新建个底层模块a2作为新版本,兼容的地方手动替换a2,不兼容的继续使用a。这得益于模块化的松耦合特性。

单元测试

有了模块化之后,单元测试便有了前提。具体怎么测试就不探讨了,这是属于测试人员的范畴。至于为什么说有了前提,那是因为所有模块文件都是异步加载的,在线上环境可能被合并构建成了一个文件,但是只要将全部js代理成源代码(源代码里只写了类似@import的东西,并没有合并,合并是在服务器端最后发布做的),所有导入都变成异步加载了,这有利于分离调试并定位错误。

以前写过一个旧版本的,现在做了不少变化,最主要修改是分离出2个文件:html5formcore.js和html5form.js。前者用以模拟不支持的低版本浏览器行为,后者则是在其基础上与ui相关的部分。另外也增加了submit类型的各种属性,取消了浏览器默认的表现行为。

以下便是2个版本,第一个没有ui,第二个基于前者小弄了个ui上去。说明和源码都在链接页面里。

http://army8735.org/Ai/test/html5formcore.html

http://army8735.org/Ai/test/html5form.html

已经修改为晚绑定,这意味着即使元素是后添加进来的,也可以完全模拟html5元素的特性。另外改写使得其对jq1.4版本也有兼容。

八月 17th, 2011

jssc5.1中的匹配模式

No Comments, jssc, by army.

预览:http://jssc.googlecode.com/svn/trunk/jssc5/bin/index.html

下载:http://code.google.com/p/jssc/downloads/list

源码:http://jssc.googlecode.com/svn/trunk/jssc5/src/

好久不动,都生疏了。离上次5.1alpha版有好几个月了,今天释出beta版,添加了Python语法。

之前说过5.1版本最大的变动是使得添加新的语法变得更加容易,老的5.0版每个新语言都得写个词法分析器太累了,于是乎花大精力修改这一部分,添加了“匹配模式”类,用以方便添加新语法解析。

匹配模式的概念

简单地说,匹配模式就是描述某个语言基本词法单元(Token)的规则。以javascript举例,需要高亮的Token大致有注释、字符串、正则表达式、数字、关键字。其中关键字属于Word中的保留字。注释的匹配模式就是以/*开头*/结尾的代码、字符串是指引号之间的内容等等。

现有的几种匹配模式

所有匹配模式都实现IMatch接口,我目前定义了以下几种:CharacterSet、CompleteEqual、LinearSearch、LinearParse、IDMatch、RegularMatch。

CharacterSet 是个字符集匹配,它定义了Token以什么字符集为开头并以什么字符集为组成内容。比如js中的Word是以下划线、美元符号、字母为开头,下划线、美元符号、字母和数字为组成内容。出于初期设计的原因,字符集不能完全自定义,只能使用已经定义的若干常量。这个缺陷会在以后考虑。

CompleteEqual 是最简单的全等匹配,只有完全相等时才会匹配成功。在js中我用全等来查找花括号和圆括号,因为它们要参与计算深度(折叠功能)。

LinearSearch 线性查找,寻找以什么字符串为开头和结尾的内容。js中注释是以此实现的。注释是以/*开头*/结尾、或者//开头行末结尾。

LinearParse 线性分析和线性查找很像,只是它多了判断转义的逻辑,性能稍稍差一些。js中字符串是以此实现的。字符串以引号开始引号结束,但是结尾的引号可能存在转义的情况,字符串中也可能出现转义符,需要特殊对待。

IDMatch ID匹配和线性查找也有点相似,它定义了以什么字符串为开头,然后匹配一个正则表达式结果。js中并没有直接用到,但其实注释部分也可以用它来实现:以//为开头,匹配的正则表达式是//[^\n]*。至于为什么不用ID匹配来实现呢,那是因为正则的消耗比较大,为性能考虑才使用的线性查找。事实上只要是能用线性查找替换的地方,都应该这样做。

RegularMatch 看名字就知道这是终极解决方案了,和IDMatch的差异也只在于定义开头也是个正则。最消耗的方案。我根本没有用到它,放在那里有备无患而已。

ecmascript的示例

以下是用来解析js、as等语言的定义类文件:

package lexer.rule {
	/**
	 * ...
	 * @author army8735
	 */
	import lexer.*;
	import lexer.depth.*;
	import lexer.match.*;

	public class EcmascriptRule extends LanguageRule {

		public function EcmascriptRule() {
			var keywords:Array = "if else for break case continue function true use \
switch default do while int float double long short char null public super in false \
abstract boolean Boolean byte class const debugger delete static void synchronized this import \
enum export extends final finally goto implements protected throw throws transient \
instanceof interface native new package private try typeof var volatile Vector with \
document window return Function String Date Array Object RegExp Event Math Number \
decodeURI decodeURIComponent encodeURI encodeURIComponent escape isFinite isNaN namespace \
isXMLName parseFloat parseInt trace uint unescape XML XMLList undefined Infinity NaN".split(" ");
			super(keywords, true);

			addMatch(new CompleteEqual(Token.DEPTH, "{", LanguageLexer.IS_PERL_REG));
			addMatch(new CompleteEqual(Token.DEPTH, "}", LanguageLexer.IS_PERL_REG));
			addMatch(new CompleteEqual(Token.DEPTH, "[", LanguageLexer.IS_PERL_REG));
			addMatch(new CompleteEqual(Token.DEPTH, "]", LanguageLexer.IS_PERL_REG));
			addMatch(new CompleteEqual(Token.DEPTH, "(", LanguageLexer.IS_PERL_REG));
			addMatch(new CompleteEqual(Token.DEPTH, ")", LanguageLexer.NOT_PERL_REG));
			addMatch(new LinearSearch(Token.COMMENT, "//", "\n", false));
			addMatch(new LinearSearch(Token.COMMENT, "/*", "*/", true));
			addMatch(new LinearParse(Token.STRING, "'", "'", true, LanguageLexer.IS_PERL_REG));
			addMatch(new LinearParse(Token.STRING, "\"", "\"", true, LanguageLexer.IS_PERL_REG));
			addMatch(new CharacterSet(Token.ID, [
				CharacterSet.LETTER,
				CharacterSet.UNDERLINE,
				CharacterSet.DOLLAR
			], [
				CharacterSet.LETTER,
				CharacterSet.UNDERLINE,
				CharacterSet.DOLLAR,
				CharacterSet.DIGIT
			], LanguageLexer.NOT_PERL_REG));

			addDep(new TokenDepth(Token.DEPTH, "{", "}"));
			addDep(new TokenDepth(Token.DEPTH, "[", "]"));
			addDep(new TokenDepth(Token.DEPTH, "(", ")"));
		}

	}

}

super()构造函数的2个参数分别为保留字数组和语言本身是否支持Perl风格的正则表达式(perl风格的判别被直接集成在了父层解析器中)。

可以看到下方一系列的addMatch()方法,它按照出现顺序来添加匹配模式并排序优先级,先出现的匹配模式具有高优先级。

addDep()方法则是解析深度用的,有兴趣的可以看看——它也很容易被猜出来是怎么做的。

python的示例

python语言可能是比较特殊的例子了,因为它的深度折叠不是靠Token识别,而是看每行开始的空格?!我对python语法不大了解,所以简单搜索写了下,这也能体现出添加新语法文件方便多了的特性。

package lexer.rule {
	/**
	 * ...
	 * @author army8735
	 */
	import lexer.*;
	import lexer.depth.*;
	import lexer.match.*;

	public class PythonRule extends LanguageRule {

		public function PythonRule() {
			var keywords:Array = "and assert break class continue def del elif else \
except exec finally for from global if import in is lambda not or pass print raise \
return try yield while __import__ abs all any apply basestring bin bool buffer callable \
chr classmethod cmp coerce compile complex delattr dict dir divmod enumerate eval \
execfile file filter float format frozenset getattr globals hasattr hash help hex id \
input int intern isinstance issubclass iter len list locals long map max min next \
object oct open ord pow print property range raw_input reduce reload repr reversed \
round set setattr slice sorted staticmethod str sum super tuple type type unichr \
unicode vars xrange zip".split(" ");
			super(keywords, true);

			addMatch(new LinearSearch(Token.COMMENT, "#", "\n", false));
			addMatch(new LinearSearch(Token.STRING, "'''", "'''", true, LanguageLexer.IS_PERL_REG));
			addMatch(new LinearSearch(Token.STRING, '"""', '"""', true, LanguageLexer.IS_PERL_REG));
			addMatch(new LinearParse(Token.STRING, "'", "'", true, LanguageLexer.IS_PERL_REG));
			addMatch(new LinearParse(Token.STRING, "\"", "\"", true, LanguageLexer.IS_PERL_REG));
			addMatch(new CharacterSet(Token.ID, [
				CharacterSet.LETTER,
				CharacterSet.UNDERLINE
			], [
				CharacterSet.LETTER,
				CharacterSet.UNDERLINE,
				CharacterSet.DIGIT
			], LanguageLexer.NOT_PERL_REG));
		}

	}

}

python的深度解析直接内嵌集成了,所以没有出现addDep()。其中的疏漏欢迎指出。

想要添加新的语法文件更加欢迎,即使不了解as或者flash的,你也只需告诉我组成规则即可,譬如:python代码的注释部分需要高亮,它由#开头到行末结尾;java的Word由字母下划线开头、字母下划线数字组成……

感慨万千啊,mmorpg,聊天、任务、战斗……刚开始随即夭折,只保留有这2段视频和1张截图了。《Fantasy Sky》——纪念那些逝去的代码,还有青春。

二月 28th, 2011

jquery的html5form插件

1 Comment, 前端开发, by army.

http://www.matiasmancini.com.ar/jquery-plugin-ajax-form-validation-html5.html

前阵子在这个地址发现了jq的html5form插件,api设计得不错,功能上稍有欠缺,于是乎借鉴了思想重新写了一个:

http://army8735.org/Ai/html5form.html

暂时提取了较为常用的html5表单属性做成了jq插件,样式自己先凑合的,如此可以节省和统一大量的重复校验和表现工作。那些错误以及提示信息均为新生成的绝对定位节点。

input类型有:email、url、number、date、time、search、color几种类型,这些类型根据不同浏览器的支持表现各不相同。

  • 比如number在webkit下支持非常好,输入框尾部有微调按钮;
  • color和date在opera下支持非常好,直接内置弹出一个选择框用鼠标点一下即可选择; 在不支持的比如ie下,它会默认解读为type=”text”,无任何影响。

较为常用的属性有:placeholder、autofocus、novalid、required、pattern、maxlength。

  • placeholder在chrome下支持最好,占位符和输入文字自动用2种颜色表示,其它的webkit用同一种颜色表示;
  • autofocus也是chrome支持最好,ie等需模拟,多个autofocus将以最后一个为准;
  • novalid指明这个表单无需验证便可提交;
  • required表明这个input必须填写;
  • pattern是自定义正则验证,比如示例中自定义的中文输入框,必须输入中文;
  • maxlength指定最大输入文字长度,在webkit中超过了就无法输入(safari没校验输入法,chrome没校验回车),其它的不行。textarea上使用了这一属性,在输入时会自动提示长度;
  • step、max、min等均为新特性,高级浏览器中会体现到,低级浏览器中不出现,渐进增强吧。

这些属性可以组合起来使用,用firebug查看下源码,看看input上的属性即可理解。

API: $(‘form’).html5form(callback);

Callback: 一个回调function,可省略,在form的submit通过html5校验之后触发,this指向form本身,返回值为form.submit的返回值(return false阻止默认提交行为)。

源码,还只是个雏形:

(function() {

	var body = $(document.body),
		PLACE_HOLDER_CLASS = 'td_placeholder',
		ERROR_CLASS = 'td_error',
		TIP_CLASS = 'td_tip',
		TYPE_VALID = {
			'url': {
				pattern: /^[a-zA-z]+:\/\/[^\s]*$/,
				message: 'url格式不合法'
			},
			'email': {
				pattern: /^\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$/,
				message: 'email格式不合法'
			},
			'number': {
				pattern: /^\d+$/,
				message: '这不是一个数字'
			},
			'date': {
				pattern: /^\d{2,4}-\d{1,2}-\d{1,2}$/,
				message: '日期格式不合法'
			},
			'time': {
				pattern: /^\d{1,2}:\d{1,2}(:\d{1,2}(\.\d{1,3})?)?$/,
				message: '时间格式不合法'
			},
			'color': {
				pattern: /^#[a-z\d]{3,6}$/,
				message: '颜色格式不合法'
			}
		},
		tipBox,
		tpl = [
			'<table class="td_mesbox" cellspacing="0" cellpadding="0"><tbody>',
				'<tr>',
					'<td class="round left_top"></td>',
					'<td class="line top"></td>',
					'<td class="round right_top"></td>',
				'</tr><tr>',
					'<td class="left"></td>',
					'<td class="center">',
						'<div class="container"><%=text%></div>',
					'</td>',
					'<td class="right"></td>',
				'</tr><tr>',
					'<td class="round left_bottom"></td>',
					'<td class="line bottom"></td>',
					'<td class="round right_bottom"></td>',
				'</tr>',
			'</tbody></table>'
		].join('');

	function showError(node, message) {
		var s = $$.render(tpl, { text: message }),
			box = $('<div>').addClass(ERROR_CLASS).html(s).hide().appendTo(body),
			pos = getPos(node, box);
		//最初的坐标保存起来
		box.data('html5form_left', pos.left).data('html5form_top', pos.top);
		//以及对:input的引用
		box.data('html5form_node', node);
		//设置位置
		box.css({
			left: pos.left + 'px',
			top: pos.top + 'px'
		}).fadeIn(200).click(function() {
			node.focus().val(node.val());
		});
		shake(box);
		return box;
	}
	function hideError(box) {
		clearShake(box);
		box.remove();
	}
	function shake(box) {
		clearShake(box);
		var offset = 1,
			count = 0;
		//颤动动画存在自身的interval变量上
		box.data('html5form_interval', setInterval(function() {
			if(count++ > 10) {
				clearShake(box);
			}
			box.css({
				left: box.data('html5form_left') + offset + 'px',
				top: box.data('html5form_top') + offset + 'px'
			});
			offset *= -1;
		}, 50));
	}
	function clearShake(box) {
		var interval = box.data('html5form_interval');
		if(interval) {
			clearInterval(interval);
		}
	}
	function showTip(node, current, maxLength) {
		if(!tipBox) {
			var s = $$.render(tpl, { text: '' });
			tipBox = $('<div>').addClass(TIP_CLASS).html(s);
		}
		//focus时显示并设置位置
		if(node) {
			tipBox.hide().appendTo(body);
			var pos = getPos(node, tipBox);
			tipBox.css({
				left: pos.left + 'px',
				top: pos.top + 'px'
			}).fadeIn(200);
		}
		else {
			tipBox.show();
		}
		//更新说明
		current = current > maxLength ? '<strong>' + current + '</strong>' : current;
		tipBox.find('div.container').html(current + ' / ' + maxLength);
	}
	function hideTip() {
		if(tipBox) {
			tipBox.hide();
		}
	}
	function getPos(node, box) {
		var left = node.offset().left + node.outerWidth() + 10,
			top = node.offset().top;
		//不够放在node后面的时候,放在前面,top暂不考虑,很少遇到
		if(left + box.outerWidth() > body.innerWidth()) {
			left = Math.max(1, node.offset().left - box.outerWidth() - 10);
		}
		return {
			left: left,
			top: top
		};
	}

	$.fn.html5form = function(cb) {
		this.each(function() {
			var form = $(this),
				novalidate = !$.isUndefined(form.attr('novalidate')),
				inputs = form.find(':input:visible:not(:button, :submit, :radio, :checkbox)'),
				input = document.createElement('input'),
				autofocus = 'autofocus' in input,
				placeholder = 'placeholder' in input,
				required = 'required' in input,
				maxlength = 'maxlength' in input,
				validArray = [];

			//有novalidate属性的话无需验证表单
			if(novalidate) {
				return;
			}

			inputs.each(function(index) {
				var item = $(this),
					type = (this.getAttribute('type') || '').toLowerCase(),
					interval;
				//placeholder占位符
				if(!placeholder && this.getAttribute('placeholder') != null) {
					var ph = item.attr('placeholder'),
						place; //开关,标明input当前是否是占位符状态。
					//占位符为空字符串无效
					if(ph.length) {
						function focus() {
							//打开状态下认为是占位符
							if(place) {
								item.val('').removeClass(PLACE_HOLDER_CLASS);
							}
						}
						function blur() {
							//离开时如有输入数据开关关闭,否则打开
							if(item.val() == '') {
								item.val(ph).addClass(PLACE_HOLDER_CLASS);
								place = true;
							}
							else {
								place = false;
							}
						}
						item.focus(focus).blur(blur);
						//初始化判断,因为ie和ff会在刷新页面后可能autocomplete遗留表单数据,此时占位符就成为遗留的默认数据;也可能在js执行前有用户输入。唯一的缺点是假如在js执行前用户输入的和占位符相同,会被误认为占位符,可忽视。
						if(ph == item.val() || item.val() == '') {
							place = true;
							item.val(ph).addClass(PLACE_HOLDER_CLASS);
						}
					}
				}
				//maxlength
				var maxLength = parseInt(this.getAttribute('maxlength'));
				if(!isNaN(maxLength)) {
					function input() {
						showTip(null, item.val().length, maxLength);
					}
					item.focus(function() {
						if(!validArray[index]) {
							showTip(item, item.val().length, maxLength);
						}
					}).blur(function() {
						hideTip();
						var v = item.val().length;
						if(!validArray[index] && v > maxLength) {
							validArray[index] = showError(item, '最多只允许输入<strong>' + maxLength + '</strong>个字符');
						}
					});
					//input事件除了ie都支持,可以用onpropertychange代替
					if(window.addEventListener) {
						this.addEventListener('input', input, false);
					}
					else if(window.attachEvent) {
						this.attachEvent('onpropertychange', input);
					}
				}
				//autofocus自动聚焦
				if(!autofocus && this.getAttribute('autofocus') != null) {
					item.focus();
				}
				//required
				if(this.getAttribute('required') != null) {
					item.blur(function() {
						if(validArray[index]) {
							shake(validArray[index]);
						}
						else if($.trim(item.val()) == '') {
							validArray[index] = showError(item, '此项必填');
						}
					});
				}

				//默认的校验
				var typeValid = TYPE_VALID[type];
				if(this.nodeName.toLowerCase() == 'input' && typeValid) {
					item.blur(function() {
						if(validArray[index]) {
							shake(validArray[index]);
						}
						else {
							var v = item.val().trim();
							if(v.length && !typeValid.pattern.test(v)) {
								validArray[index] = showError(item, typeValid.message || '格式不正确');
							}
						}
					});
				}
				//number类型另附验证范围
				if(type == 'number') {
					var max = parseFloat(item.attr('max')),
						min = parseFloat(item.attr('min'));
					if(!isNaN(max) || !isNaN(min)) {
						item.blur(function() {
							if(validArray[index]) {
								shake(validArray[index]);
							}
							else {
								var v = item.val().trim();
								if(v.length) {
									v = parseFloat(v);
									if(!isNaN(max) && v > max) {
										validArray[index] = showError(item, '超出范围,不能大于' + max);
									}
									if(!isNaN(min) && v < min) {
										validArray[index] = showError(item, '超出范围,不能小于' + max);
									}
								}
							}
						});
					}
				}
				//自定义pattern
				var pattern = this.getAttribute('pattern');
				if(pattern != null && pattern.length) {
					pattern = new RegExp(pattern);
					item.blur(function() {
						if(validArray[index]) {
							shake(validArray[index]);
						}
						else {
							var v = item.val().trim();
							if(v.length && !pattern.test(v)) {
								validArray[index] = showError(item, '格式不正确');
							}
						}
					});
				}

				//所有的:input输入时都要隐藏可能存在的错误提示框
				function removeErrorInput() {
					var error = validArray[index];
					if(error) {
						hideError(error);
						validArray[index] = null;
					}
				}
				if(window.addEventListener) {
					this.addEventListener('input', removeErrorInput, false);
				}
				else {
					this.attachEvent('onpropertychange', removeErrorInput);
				}
			});

			form.submit(function() {
				inputs.blur(); //全部触发可能存在的校验
				var validResult = true,
					first;
				validArray.forEach(function(item) {
					if(item) {
						validResult = false;
						shake(item);
						//focus到第一个错误:input
						if(!first) {
							first = true;
							item.data('html5form_node').focus();
						}
					}
				});
				//本身通过html5校验,如有传入callback,返回callback的值
				if(validResult && $.isFunction(cb)) {
					validResult = cb.call(this) !== false;
				}
				return validResult;
			});
		});
		return this;
	}

})();