本篇文章参考书籍《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 常用标签内的 < ; 和 > ; 分别转义成 < 和 > ,将三类空白符(回车符 \r ,制表符 \t ,换行符 \n )过滤掉。然后将 {%=test%} 转化成 ,typeof($l)===‘undefined’ ? ‘’ : $l, 形式。最后将 {% 替换成 ‘); ,将 %} 转化成 template_array.push(’ 。
_dealTpl = function(str) {
var _left = '{%', //左分隔符
_right = '%}', //右分隔符
//显示转化为字符串
return String(str)
//转义标签内的 < 如:<div>{%if(a<b)%}</div> -> <div>{%if(a<b)%}</div>
.replace(/</g, '<')
//转移标签 >
.replace(/>/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 架构模式使页面开发模块化,不仅仅是页面功能,甚至页面的每个组件模块都可以独立的开发,者更适合团队中多人开发。并且降低相互之间因功能或者视图创建的耦合影响概率。
一个组件即是一个文件,也让我们更好的管理一个页面,当然组件的多样化也会组建一个更丰富的页面,同样也会让组件的复用率更高,这是很有必要的,这就是组件开发的核心闪光点。