本篇文章参考书籍《JavaScript设计模式》–张容铭
前言
同学们好,技巧类的设计模式我们告一段落了,本届开始,我们一起学习一个新的种类,架构型设计模式,之前我们学习的都是解决具体问题的相关设计模式,本节开始,我们学习项目框架,这就涉及多人共同开发大型复杂项目了。
架构型设计模式是一类框架结构,通过提供一些子系统,指定他们的职责,并将他们条理清晰的组织在一起。
同步模块模式
模块化:将复杂的系统分解成高内聚、低耦合的模块,使系统开发变得可控,可维护,可扩展,提高模块的复用率。
同步模块模式—SMD:请求发出后,无论模块是否存在,立即执行后续的逻辑,实现模块开发中对模块的立即引用。
当我们在开发一些大型复杂项目的时候,不能可等一个人完成了,然后下一个人再继续开发,一定是所有人同时开发的,那这就会涉及一个问题,比如我需要写一个导航栏功能,而别人需要写导航栏提示,正常来开发的话,需要先完成导航栏才能再开发导航提示,但这会很影响效率。
那么我们引入模块化就是为了解决上面的问题。
模块化的重点在于需要一个模块管理器,它管理着模块的创建与调度,对于模块的调用分为两类,第一类同步模块的调度实现比较简单,不需要考虑模块间的异步加载。第二类异步模块调度的实现就比较繁琐了。它可以实现对模块的加载调度。
首先我们先实现同步模块模式。
//定义模块管理器单体对象
var F = F || {};
/**
* 定义模块方法(理论上模块方法应该放在闭包中实现,可以隐藏内部信息,这里我们简化一下)
* @param str 模块路由
* @param fn 模块方法
*/
F.define = function(str, fn) {
//解析模块路由
var parts = str.split('.'),
//old 当前模块的祖父模块, parent 当前模块父模块
//如果在闭包中,为了屏蔽对模块直接访问,建议将模块添加给闭包内部私有变量
old = parent = this,
//i 模块层级,len 模块层级长度
i = len = 0;
//如果第一个模式是模块管理器单体对象,则移除
if(parts[0] === 'F') {
parts = parts.slice(1);
}
//屏蔽对 define 与 module 模块方法的重写
if(parts[0] === 'define' || parts[0] === 'moudle') {
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) {
//此时 i 等于parts.length, 固减一
old[parts[--i]] = fn();
}
//返回该模块管理器单体对象
return this;
}
创建模块的方法 define 我们实现了,下面我们创建一些模块,首先我们创建 String 模块,对于 String 模块,要为我们提供 trim 方法。
//F.string 模块
F.define('string', function() {
//接口方法
return {
//清除字符串两边空白
trim: function(str) {
return str.replace(/^\s+|\s+$/g, '');
}
}
});
我们简单测试一下。
/**
* 注意:在真正的模块开发中,是不允许这样直接调用的,有两点原因:
* 1.技术上
*
* 模块通常保存在闭包内部的私有变量里,而不会保存在 F 上
* 因此是获取不到的,而我们这里简化了闭包,也为了方便测试。
*
* 2.类似如下调用不符合模块开发规范
*/
F.string.trim('测试用例。')
对于模块的回调函数,我们也可以以构造函数的形式返回接口,比如我们创建 DOM 模块,其中包括 dom() 获取元素方法、 html() 获取或者设置元素 innerHTML 内容方法等。
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 $;
});
//测试用例(页面元素: <div id="test"> test </div>)
F.dom('test').html(); //"test"
对于模块的创建,我们也可以先声明后创建,如添加 addClass() 为元素添加 calss 方法。
//为 dom 模块添加 addClass 方法
//注意,此种添加模式之所以可行,是因为将模块添加到 F 对象上,模块化开发中只允许上面的添加方式
F.define('dom.addClass');
F.dom.addClass = function(type, fn) {
return function(className) {
//如果不存在该类
if(!~this.dom,calssName.indexOf(calssName)) {
//简单添加类
this.dom.calssName += ' ' + className;
}
}
} ();
//测试用例
F.dom('test').addClass('test')
接下来我们就需要用已有的模块去完成需求,使用模块的话我们需要创建一个“使用”模块方法—— moudle 。
//模块调用方法
F.moudle = function() {
//将参数转化为数组
var args = [].slice.call(arguments),
//获取回调执行函数
fn = args.pop(),
//获取依赖模块,如果 args[0] 是数组,则依赖模块为 args[0]。否则依赖模块为 arg
parts = args[0] && args[0] instanceof Array ? args[0] : args,
//依赖模块列表
moudles = [];
//模块路由
modIDs = '',
//依赖模块索引
i = 0,
//依赖模块长度
ilen = parts.length,
//父模块,模块路由层级索引,模块路由层级长度
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);
}
对于模块调用方法,参数可分为两部分,依赖模块与回调执行函数(最后一个参数), 它的实现原理是,首先遍历并获取所有的依赖模块,并一次保存在依赖模块列表中,然后将这些依赖模块作为参数传入执行函数中执行。
//引用 dom 模块与 document 模块对象(注意,依赖模块对象通常为已创建的模块对象)
F.module(['dom', document], function(dom, doc) {
//通过 dom 模块设置元素内容
dom('test').html('new add!');
//通过 document 设置 body 元素背景色
doc.body.style.background = 'red';
});
上面我们通过数组来声明了依赖模块,其实我们在定义 module 方法时,依赖模块还可以以字符串形式传入。比如:
//依赖引用 dom 模块,string,trim 方法
F.module('dom', 'string.trim', function(dom, trim) {
//测试元素<div id="test"> test </div>
var html = dom('test').html(); //获取元素内容
var str = trim(html); //去除字符串两边空白符
console.log(`*${html}*`,`*${str}*`); // * test * *test*
});
这种变成方式,卸载 module 回调函数里面的功能安全可靠,模块间的组织条例清晰,这样以后我们自己负责自己的模块就可以了。
总个小结
模块发开发是以分而治之的思想,实现对复杂系统的分解,使系统随着其功能的增加而变得可控、可拓展、可维护。这就要求我们对模块细化。随着系统功能的增加模块的数量也随之增加。模块开发的成本随之减少,但是模块的接口数量却随之增加,接口的使用成本和开发成本与维护成本也随之增加,所以合理的模块分割显得尤为重要。
模块化开发其实也像以组合模式对模块的组合。因此这也使得系统中的问题一般出现在局部,使得开发人员处理相应模块即可,而不用考虑整个系统。因此相对于整个复杂系统,对局部模块的升级、改造甚至是替换所需成本要小得多。组合的灵活性也使得我们可以实现更复杂、多样化的功能。
同步模块模式是模块化开发的一种最简单的形式,这种模式使得以来的模块无论加载,无论有无,模块创建即执行,这就要求依赖的模块必然是创建过的。这就限制了同步模块的发展,不过这种模式却很适合服务端如 nodejs 等,服务端文件都存在本地,可以很方便访问到。