来自:《javascript设计模式》(张容铭),自己留作备用,勿转。
同步模块模式
test.html文件:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<script src="smd.js"></script>
</head>
<body>
<div id="test">test</div>
</body>
</html>
smd.js文件:
var F = F||{};
//定义模块管理器
F.define = function (str, fn) {
var parts = str.split('.'),
old = parent = this,
i = len = 0;
if (parts[0] === 'F') {
parts = parts.slice();
}
if (parts[0] === 'define' || parts[0] === 'modules') {
return;
}
for (len = parts.length; i < len; i++) {
if (typeof parent[parts[i]] === 'undefined') {
parent[parts[i]] = {};
}
old = parent;
parent = parent[parts[i]];
}
if (fn) {
old[parts[--i]] = fn();
}
return this;
}
// 创建模块
// F.string模块
F.define('string', function() {
// 接口方法
return {
// 清除字符串两边空白
trim : function(str) {
return str.replace(/^\s+|\s+$/g, '') ;
}
}
}) ;
F.define('dom', function() {
// 简化获取元素方法(重复获取可被替代,此设计用于演示模块添加)
var $ = function(id) {
$.dom = document.getElementById(id) ;
// 返回构造函数对象
return $ ;
}
// 获取或者设置元素内容
$.html = function(html) {
// 如果传参则设置元素内容,否则获取元素内容
if(html) {
this.dom.innerHTML = html ;
return this ;
}else {
return this.dom.innerHTML ;
}
}
// 返回构造函数
return $ ;
}) ;
/*******************************************************************/
/*
对于模块的创建,也可以先声明后创建,比如添加addClass()为元素添加class方法。
*/
// 为dom模块添加addClass方法
// 注意:此种添加模式之所以可行,是因为将模块添加到F对象上,模块化开发中只允许上面的添加方式
F.define('dom.addClass') ;
F.dom.addClass = (function(type, fn) {
return function(className) {
// 如果不存在该类
if(!~this.dom.className.indexOf(className)) {
// 简单添加类
this.dom.className += ' ' + className ;
}
}
})() ;
/*******************************************************************/
//模块调用方法---创建一个“使用”模块方法:module
F.module = function() {
// 将参数转化为数组
var args = [].slice.call(arguments) ;
// 获取回调执行函数
var fn = args.pop() ;
// 获取依赖模块,如果args[0]是数组,则依赖模块args[0]。否则依赖模块arg
var parts = args[0] && args[0] instanceof Array ? args[0] : args ;
// 依赖模块列表
var modules = [] ;
// 模块路由
var modIDs = '' ;
// 依赖模块索引
var i = 0 ;
// 依赖模块长度
var ilen = parts.length ;
// 父模块,模块路由层级索引,模块路由层级长度
var parent, j, jlen ;
// 遍历依赖模块
while(i < ilen) {
// 如果是模块路由
if(typeof parts[i] === 'string') {
// 设置当前模块父对象(F)
parent = this ;
// 解析模块路由,并屏蔽掉模块父对象
modIDs = parts[i].replace(/^F./, '').split('.') ;
// 遍历模块路由层级
for(j = 0, jlen = modIDs.length; j < jlen; j++) {
// 重置父模块
parent = parent[modIDs[j]] || false ;
}
// 将模块添加到依赖模块列表中
modules.push(parent) ;
// 如果是模块对象
}else {
// 直接加入依赖模块列表中
modules.push(parts[i]) ;
}
// 取下一个依赖模块
i++ ;
}
// 执行回调执行函数
fn.apply(null, modules) ;
}
console里的测试代码:
// 调用模块
// 引用dom模块与document对象(注意:依赖模块对象通常为已创建的模块对象)
F.module(['dom', document], function(dom, doc) {
// 通过dom模块设置元素内容
dom('test').html('new add!') ;
// 通过document设置body元素背景色
doc.body.style.background = 'red' ;
}) ;
异步模块模式:
html文件:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<script src="amd.js"></script>
</head>
<body>
<button id="demo">aaa</button>
<button id="bbb">bbb</button>
</body>
</html>
amd.js文件:
/**
* 闭包环境
**/
// 向闭包中传入模块管理其对象F(~屏蔽雅素偶文件时,前面漏写;报错)
~(function(F) {
// 模块缓存器。存储已创建模块
var moduleCache = {} ;
}) ((function() {
// 创建模块管理器对象F,并保存在全局作用域中
return window.F = {} ;
}) ()) ;
/**
* 创建或调用模块方法
* @param url 参数为模块URL
* @param deps 参数为依赖模块
* @param callback 参数为模块主函数
**/
F.module = function(url, modDeps, modCallback) {
// 将参数转化为数组
var args = [].slice.call(arguments) ;
// 获取模块构造函数(参数数组中最后一个参数成员)
var callback = args.pop() ;
// 获取依赖模块(紧邻回调函数参数,且数据类型为数组)
var deps = (args.length && args[args.length - 1] instanceof Array) ? args.pop() : [] ;
// 该模块URL(模块ID)
var url = args.length ? args.pop() : null ;
// 依赖模块序列
var params = [] ;
// 未加载的依赖模块数量统计
var depsCount = 0 ;
// 依赖模块序列中索引值
var i = 0 ;
// 依赖模块序列长度
var len ;
// 获取依赖模块长度
if(len = deps.length) {
// 遍历依赖模块
while(i < len) {
// 闭包保存i
(function(i) {
// 增加未加载依赖模块数量统计
depsCount++ ;
// 异步加载依赖模块
loadModule(deps[i], function(mod) {
// 依赖模块序列中添加依赖模块接口引用
params[i] = mod ;
// 依赖模块加载完成,依赖模块数量统计减一
depsCount-- ;
// 如果依赖模块全部加载
if(depsCount === 0) {
// 在模块缓存器中矫正该模块,并执行构造函数
setModule(url, params, callback) ;
}
}) ;
}) (i) ;
// 遍历下一依赖模块
i++ ;
}
// 无依赖模块,直接执行回调函数
}else {
// 在模块缓存器中矫正该模块,并执行构造函数
setModule(url, [], callback) ;
}
}
// 加载模块
var moduleCache = {} ;
/***
* 异步加载依赖模块所在文件
* @param moduleName 模块路径(id)
* @param callback 模块加载完成回调函数
**/
var loadModule = function(moduleName, callback) {
// 依赖模块
var _module ;
// 如果依赖模块被要求加载过
if(moduleCache[moduleName]) {
// 获取该模块信息
_module = moduleCache[moduleName] ;
// 如果模块加载完成
if(_module.status === 'loaded') {
// 执行模块加载完成回调函数
setTimeout(callback(_module.exports), 0) ;
}else {
// 缓存该模块所处文件加载完成回调函数
_module.onload.push(callback) ;
}
// 模块第一次被依赖引用
}else {
// 缓存该模块初始化信息
moduleCache[moduleName] = {
moduleName : moduleName, // 模块id
status : 'loading', // 模块对应文件加载状态(默认加载中)
exports : null, // 模块接口
onload : [callback] // 模块对应文件加载完成回调函数缓冲器
};
// 加载模块对应文件
loadScript(getUrl(moduleName)) ;
}
} ;
// 获取文件路径
var getUrl = function(moduleName) {
// 拼接完整的文件路径字符串,如'lib/ajas' => 'lib/ajax.js'
return String(moduleName).replace(/\.js$/g, '') + '.js' ;
} ;
// 加载脚本文件
var loadScript = function(src) {
var _script = document.createElement('script') ;
// 文本类型
_script.type = 'text/JavaScript' ;
// 确认编码
_script.charset = 'UTF-8' ;
// 异步加载
_script.async = true ;
// 文件路径
_script.src = src ;
// 插入页面中
document.getElementsByTagName('head')[0].appendChild(_script) ;
};
/***
* 设置模块并执行模块构造函数
* @param moduleName 模块id名称
* @param params 依赖模块
* @param callback 模块构造函数
**/
var setModule = function(moduleName, params, callback) {
// 模块容器,模块文件加载完成回调函数
var _module, fn ;
// 如果模块被调用过
if(moduleCache[moduleName]) {
// 获取模块
_module = moduleCache[moduleName] ;
// 设置模块已经加载完成
_module.status = 'loaded' ;
// 矫正模块接口
_module.exports = callback ? callback.apply(_module, params) : null ;
// 执行模块文件加载完成回调函数
while(fn = _module.onload.shift()) {
fn(_module.exports) ;
}
}else {
// 模块不存在(匿名函数),则直接执行构造函数
callback && callback.apply(null, params) ;
}
};
lib/dom.js文件
F.module('lib/dom', function() {
return {
// 获取元素方法
g : function(id) {
return document.getElementById(id) ;
},
// 获取或者设置元素内容方法
html : function(id, html) {
if(html) {
this.g(id).innerHTML = html ;
}else {
return this.g(id).innerHTML ;
}
}
}
}) ;
lib/event.js文件
F.module('lib/event', ['lib/dom'], function(dom) {
var events = {
// 绑定事件
on : function(id, type, fn) {
dom.g(id)['on' + type] = fn ;
}
}
return events;
}) ;
Console里的测试代码:
F.module(['lib/event', 'lib/dom'], function(events, dom) {
events.on('demo', 'click', function() {
dom.html('demo', 'success') ;
}) ;
}) ;
id为“demo”的button按钮被点击后,text变成了success。
widget设计模式
html页面代码如下:其中id=“demo_script”最为模板,di=“test”的div作为容器。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<script src="amd.js"></script>
</head>
<body>
<div id="test"></div>
<!-- srcpt模板内容 -->
<script type="text/template" id="demo_script">
<div id="tag_cloud">
{% for(var i = 0; i < tagCloud.length; i++){
var ctx = tagCloud[i] ; %}
<a href="#" class="tag_item{% if(ctx['is_selected']){ %}selected{% } %}" title="{%=ctx['title']%}">{%=ctx['text']%}</a>
{% } %}
</div>
</script>
</body>
</html>
amd.js文件同上,dom.js文件也同上。
另外需要一个处理模板的文件。放在lib/template.js
// 模板引擎模块
F.module('lib/template', function() {
/***
* 模板引擎,处理数据的编译模板入口
* @param str 模块容器id或者模板字符串
* @param data 渲染数据
**/
var _TplEngine = function(str, data) {
// 如果数据是数组
if(data instanceof Array) {
// 缓存渲染模板结果
var html = '' ;
// 数据索引
var i = 0 ;
// 数据长度
var 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) ;
}
} ;
/***
* 获取模板
* @param str 模板容器id,或者模板字符串
**/
var _getTpl = function(str) {
// 获取元素
var ele = document.getElementById(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) ;
}
} ;
// 处理模板
var _dealTpl = function(str) {
// 左分隔符
var _left = '{%' ;
// 右分隔符
var _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($1) === 'undefined' ? '' : $1, '")
// 替换左分隔符
.replace(new RegExp(_left, 'g'), "');")
// 替换右分隔符
.replace(new RegExp(_right, 'g'), "template_array.push('") ;
} ;
/***
* 编译执行
* @param str 模板数据
**/
var _compileTpl = function(str) {
// 编译函数体
var fnBody = "var template_array=[];\nvar fn=(function(data){\nvar template_key='';\nfor(key in data){\ntemplate_key +=('var '+key+'=data[\"'+key+'\"];');\n}\neval(template_key);\ntemplate_array.push('"+_dealTpl(str)+"');\ntemplate_key=null;\n})(templateData);\nfn=null;\nreturn template_array.join('') ;" ;
// 编译函数
return new Function('templateData', fnBody) ;
} ;
// 返回
return _TplEngine ;
}) ;
最后,console里的测试代码:
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'},
]
}
F.module(['lib/template', 'lib/dom'], function(template, dom) {
// 服务器端获取到data数据逻辑
// 创建组件视图逻辑
var str = template('demo_script', data) ;
dom.html('test', str) ;
// 组件其他交互逻辑
}) ;
所以要用widget的话,只需要修改id=“demo_script”的script文件,然后提供对应的数据data,以及修改console里的function即可使用widget。