代码要写成别人看不懂的样子(二十五)

本篇文章参考书籍《JavaScript设计模式》–张容铭

前言

  大家如果熟悉组件,那么学习本节内容会比较又感触,上一节我们学习了模块化开发,可以把功能细化,逐一实现每个微功能,这是一种很好的变成实践。不过对于页面视图的开发,这种思想应该怎么实现?

Widget模式

  (Web Widget 指的是一块可以在任意页面中执行的代码块)Widget 模式是指借用 Web Widget 思想,将页面分解成部件,针对部件开发,最终组合成完整的页面

  多人合作开发的时候,我们的页面视图有可能时常掺和在一起。这个时候我们就需要把页面粒度化,分解成一个个组件,当然一个组件也对应一个模块,一个完整的组件包含该模块的完整视图和一套完整功能。

  针对这一特点,我们就需要对视图进行分割,将功能融合形成一套完整的组件,来解决试图开发中的耦合问题。

  所以 Widget 模式开发中一个组件要对应一个文件,而不是某个功能或者某个视图,因此在这个组件中需要做两件事:

  创建视图。

  添加相应功能。

  对于添加功能,上一节我们已经讨论过了,本节我们学习如何分解与创建视图。

  既然是创建视图,我们就需要个模板渲染方法,才能使我们的开发更高效。大伙还记得我们那个简单模板模式的那个 formateString 模板渲染方法不?需要复习的同学 戳这里

  创建视图需要借助简单模板模式的思想,用服务器端请求来的数据格式化我们视图模板,实现对视图的创建,不过之前的方法太简单了,不适合复杂视图的创建,这里我们进行一下拓展。封装成一个模板功能组件 template 让它可以对页面元素模板、 script 模板、表单模板、字符串模板格式化,甚至可以编译并执行 JavaScript 语句。

   template 组件的实现思路如下:

  处理数据。

  获取模板。

  处理模板。

  编译执行。

  开始之前我们还需要明白一件事,那就是我们需要做出什么效果,可以参考下面的例子。

模板:<a href="#" class="data-lang {%if(is_selected) {%}%}" value="{%=value%}">{%=test%}</a>

数据:{is_selected:true, value:'zh', text:'zh-text'}

=>输出结果:<a href="#" calss="data-lang selected" value="zh">zh-text</a>

  有了上面的模板渲染样品做参考,我们就可以按部就班的完成模板引擎中的渲染四部了。首先先把我们的组件环境搭起来。

//模板引擎模块
F.module('lib/template', function() {
		//模板引擎 处理数据与编译模板入口
	var _TplEngine = function() {},
		//获取模板
		_getTpl = function() {},
		//处理模板
		_dealTpl = function() {},
		//编译执行
		_compileTpl = function() {};
	return _TplEngine;
});

  上面代码不知道各位熟不熟悉,我们之前学模块化开发的时候,就是这样开发的。这样做的好处是以后使用时只需要引用模板引擎模块依赖即可。

F.module(['lib/template'], function(template) {
	//do something
})

  接下来我们就该实现模板引擎设计的四个方法了。第一步的处理数据方法要做两件事,如果传入的数据是对象,则直接渲染,如果数据是数组,则要遍历数组,之一渲染,并将返回的字符串拼接在一起。

/**
* 模板引擎 处理数据与编译模板入口
* @param str   模板容器 id 或者模板字符串
* @param data  渲染数据
*/
_TplEngine = function(str, data) {
	//如果数据是数组
	if(data instanceof Array) {
			//缓存渲染模板结果
		var html = '',
			//数据索引
			i = 0,
			//数据长度
			len = data.length;
		//遍历数据
		for(; i < len; i++) {
			//缓存模板渲染结果,也可写成 html += arguments.callee(str, data[i]);
			html += _getTpl(str)(data[i]);
		}
		//返回模板渲染最终结果
		return html;
	} else {
		//返回模板渲染结果
		return _getTpl(str)(data);
	}
}

  第二步,获取模板方法 _getTpl 实现起来更容易,如果 str 是一个 id ,并且可获得该 id 对应的元素,那么我们获取元素的内容或值(针对于表单元素),否则将 str 看作模板字符串直接处理。

/**
* 获取模板
* @param str 模板容器 id,或者模板字符串
*/
_getTpl = function(str) {
	//获取元素
	var ele = document.getElementBuId(str);
	//如果元素存在
	if(ele) {
		//如果是 input 或者 textarea 表单元素,则获取该元素的 value 值,否则获取元素的内容
		var html = /^(textarea|input)$/i.test(ele.nodeName) ? ele.value : ele.innerHTML;
		return _compileTpl(html);
	} else {
		//编译模板
		return _compileTpl(str);
	}
}

  第三步,处理模板方法 _dealTpl ,需要完成功能如下,首先要明确那些内容是要被替换的,确定替换内容的左右分隔符。接下来要对模板字符串处理,将模板字符串分割并传入编译环境中的 template_array 数组中。处理流程比较复杂,下面举例说明一下:

例如:模板:<a>{%=test%}</a>
处理后的形式为:template_array.push('<a>', typeof(test)==='undefined' ? '' : test,'</a>')

  首先显性的将传入的内容转化为字符串,这是容错处理。然后将 html 常用标签内的 &lt ; &gt ; 分别转义成 < > ,将三类空白符(回车符 \r ,制表符 \t ,换行符 \n )过滤掉。然后将 {%=test%} 转化成 ,typeof($l)===‘undefined’ ? ‘’ : $l, 形式。最后将 {% 替换成 ‘); ,将 %} 转化成 template_array.push(’

_dealTpl = function(str) {
	var _left = '{%',  //左分隔符
		_right = '%}', //右分隔符
	//显示转化为字符串
	return String(str)
		//转义标签内的 < 如:<div>{%if(a&lt;b)%}</div> -> <div>{%if(a<b)%}</div>
		.replace(/&lt;/g, '<')
		//转移标签 >
		.replace(/&gt;/g, '>')
		//过滤回车符,制表符,换行符
		.replace(/[\r\t\n]/g, '')
		//替换内容
		.replace(new RegExp(_left+'=(.*?)'+_right, 'g'), "',typeof($l)==='undefined'? '': $l,'")
		//替换左分隔符
		.replace(new RegExp(_left, 'g'), "'};")
		//替换右分隔符
		.replace(new RegExp(_right, 'g'), "template_array.push('");
}

  第四步,编译执行方法是要将模板处理方法 _dealTpl 得到的模板字符串编译成最终的模板,所以我们要实现模板编译方法 _compileTpl ,这个方法是模板引擎的核心,也是最复杂的,它应用了函数声明技巧,通过 new Function 将我们模板字符串转化成函数体执行的语句,编译原理是先声明数据变量,然后用数据变量替换 template_array 内的变量,并得到结果。

  大伙别被吓到,这东西只是看着有点唬人。

/**
* 执行编译
* @param str  模板数据
*/
_compileTpl = function(str) {
	//编译函数体(超长字符串)
	var fnBody = "var template_array=[];\n var fn=(function(data){\n var template_key='';\n
		for(key in data){\n template_key+=('var'+key+'data[\"'+key+'\"];');\n}\n eval(template_key);\n
		template_array.push('"+_dealTpl(str)+"');\n template_key=null;\n})(templateData);\n fn = null;\n
		return template_array.join('');";
	//编译函数
	return new Function('templateData', fnBody);
}

  没有晕的同学举个爪 o(=•ェ•=)m

  其实编译过程不难,我们只需要了解编译流程即可。

//声明 template_array 模板容器组
var template_array = [];\n
//闭包,模板容器组添加成员
var fn = (function(data) {\n
	//渲染数据变量的执行函数体
	var template_key = '';\n
	//遍历渲染数据
	for(key in data) {\n
		//为渲染数据变量的执行函数体添加赋值语句
		template_key += ('var' + key + '= data[\"' + key + '\"];');\n
	}\n
	//执行渲染数据变量函数
	eval(template_key);\n
	//为模板容器数组添加成员(注意,此时渲染数据将替换容器中的变量)
	template_array.push('" + _dealTpl(str) + "');\n
	//释放渲染数据变量函数
	template_key = null;\n
//为闭包传入数据
})(templateData);\n
//释放闭包
fn = null;\n
//返回渲染后的模板容器组,并拼接成字符串
return template_array.join('');"

  这回是不是清晰一点了,其实编译函数就是执行一遍变量赋值语句,并用渲染数据替换 template_array 容器内的变量,最终得到渲染后的模板字符串。

  接下来我们实现一个简单的云模块如下图,模块容器( div )里面有一些标签( a 元素),后端传输的数据控制我们前端标签云模块的展示内容,我们通过 Widget 模式实现它。首先我们要创建模板,有以下几种方式:
在这里插入图片描述

//页面元素内容
<div id="demo_tag" class="template">
	<div id="tag_cloud">
		{% for(var i = 0, len = tagCloud.length; i < len; i++) {
			var ctx = tagCloud[i];%}
			<a href="#" class="tag_item
			{% if(ctx['is_selected']) { %}
				selected
			{% } %}
			" title="{%=ctx["title"]%}">{%=ctx["text"]%}</a>
		{% } %}
	</div>
</div>
//表单元素内的内容
<textarea id="demo_textarea" class="template">
	<div id="tar_cloud">
		{% for(var i = 0, len = tagCloud.length; i < len; i++) {
			var ctx = tagCloud[i];%}
			<a href="#" class="tag_item
			{% if(ctx['is_selected']) { %}
				selected
			{% } %}
			" title="{%=ctx["title"]%}">{%=ctx["text"]%}</a>
		{% } %}
	</div>
</textarea>
//script 模板内容
<script type="text/template" id="demo_script">
	<div id="tag_cloud">
		{% for(var i = 0, len = tagCloud.length; i < len; i++) {
			var ctx = tagCloud[i];%}
			<a href="#" class="tag_item
			{% if(ctx['is_selected']) { %}
				selected
			{% } %}
			" title="{%=ctx["title"]%}">{%=ctx["text"]%}</a>
		{% } %}
	</div>
</script>
//自定义模板
var demo_tpl = ['<div id="tag_cloud">',
		'{% for(var i = 0, len = tagCloud.length; i < len; i++) {',
			'var ctx = tagCloud[i];%}'',
			'<a href="#" class="tag_item',
			'{% if(ctx["is_selected"]) { %}',
				'selected',
			'{% } %}',
			'" title="{%=ctx["title"]%}">{%=ctx["text"]%}</a>',
		'{% } %}',
'</div>'].join('');

  假设我们已经通过异步请求从服务器端获取到格式良好的数据。

var data = {
	tagCloud: {
		{is_selected: true, title: '设计模式', text: '设计模式'},
		{is_selected: false, title: 'HTML', text: 'HTML'},
		{is_selected: null, title: 'CSS', text: 'CSS'},
		{is_selected: '', title: 'JavaScript', text: 'JavaScript'},
	}
}

  接下来完成我们的云标签很容易。

/*widget/tag_cloud.js*/
F.module(['lib/template', 'lib/dom'], function(template, dom) {
	//服务器端获取到 data 数据逻辑
	//创建组件视图逻辑
	var str = template('demo_script', data);
	//组件其他交互逻辑
})

  这样就完成了一个组件,如果页面还有其他组件我们只需要为组件创建一个页面即可。

总个小结

   Widget 架构模式使页面开发模块化,不仅仅是页面功能,甚至页面的每个组件模块都可以独立的开发,者更适合团队中多人开发。并且降低相互之间因功能或者视图创建的耦合影响概率。

  一个组件即是一个文件,也让我们更好的管理一个页面,当然组件的多样化也会组建一个更丰富的页面,同样也会让组件的复用率更高,这是很有必要的,这就是组件开发的核心闪光点。




评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值