最近闲下来的时候,稍微想了想这个问题。关于Javascript模块化和命名空间管理

【关于模块化以及为什么要模块化】

先说说我们为什么要模块化吧。其实这还是和编码思想和代码管理的便利度相关(没有提及名字空间污染的问题是因为我相信已经考虑到模块化思想的编码者应该至少有了一套自己的命名法则,在中小型的站点中,名字空间污染的概率已经很小了,但也不代表不存在,后面会说这个问题)。 
其实模块化思想还是和面向对象的思想如出一辙,只不过可能我们口中所谓的“模块”是比所谓的“对象”更大的对象而已。我们把致力完成同一个目的的功能函数通过良好的封装组合起来,并且保证其良好的复用性,我们大概可以把这样一个组合代码片段的思想称为面向对象的思想。这样做的好处有很多,比如:易用性,通用性,可维护性,可阅读性,规避变量名污染等等。 
而模块化无非就是在面向对象上的面向模块而已,我们把和同一个项目(模块)相关的功能封装有机的组合起来,通过一个共同的名字来管理。就大概可以说是模块化的思想。所以,相比面向对象而言的话,我觉得在代码架构上贯彻模块化的思想其实比面向对象的贯彻还更为容易一些。 
不像c#,java等这种本身就拥有良好模块化和命名空间机制的强类型语言。JavaScript并没有为创建和管理模块而提供任何语言功能。正因为这样,我们在做js的编码的某些时候,对于所谓的命名空间(namespace)的使用会显得有些过于随便(包括我自己)。比如 :

var Hongru = {} // namespace 

(function(){
Hongru.Class1 = function () {
//TODO
}
...
Hongru.Class2 = function () {
//TODO
}
})();

如上,我们通常用一个全局变量或者全局对象就作为我们的namespace,如此简单,甚至显得有些随便的委以它这么重大的责任。但是我们能说这样做不好吗?不能,反而是觉得能有这种编码习惯的同学应该都值得表扬。。。

所以,我们在做一些项目的时候或者建一些规模不大的网站时,简单的用这种方式来做namespace的工作其实也够了,基本不会出什么大乱子。但是回归本质,如果是有代码洁癖或者是建立一个大规模的网站,抑或一开始就抱着绝对优雅的态度和逻辑来做代码架构的话。或许我们该考虑更好一些的namespace的注册和管理方式。 
在这个方面,jQuery相比于YUI,Mootool,EXT等,就显得稍逊一筹,(虽然jq也有自己的一套模块化机制),但这依然不妨碍我们对它的喜爱,毕竟侧重点不同,jq强是强在它的选择器,否则他也不会叫j-Query了。 
所以我们说jQuery比较适合中小型的网站也不无道理。就像豆瓣的开源的前端轻量级框架Do框架一样,也是建立在jQuery上,封装了一层模块化管理的思想和文件同步载入的功能。

【关于namespace】

好了,我们回归正题,如上的方式,简单的通过全局对象来做namespace已经能够很好的减少全局变量,规避变量名污染的问题,但是一旦网站规模变大,或者项目很多的时候,管理多个全局对象的名字空间依然会有问题。如果不巧发生了名字冲突,一个模块就会覆盖掉另一个模块的属性,导致其一或者两者都不能正常工作。而且出现问题之后,要去甄别也挺麻烦。所以我们可能需要一套机制或者工具,能在创建namespace的时候就能判断是否已有重名。

另一方面,不同模块,亦即不同namespace之间其实也不能完全独立,有时候我们也需要在不同名字空间下建立相同的方法或属性,这时方法或属性的导入和导出也会是个问题。

就以上两个方面,我稍微想了想,做了些测试,但依然有些纰漏。今天又重新翻了一下“犀牛书”,不愧是经典,上面的问题,它轻而易举就解决了。基于“犀牛书”的解决方案和demo,我稍微做了些修改和简化。把自己的理解大概分享出来。比较重要的有下面几个点:

--测试每一个子模块的可用性

由于我们的名字空间是一个对象,拥有对象应该有的层级关系,所以在检测名字空间的可用性时,需要基于这样的层级关系去判断和注册,这在注册一个子名字空间(sub-namespace)时尤为重要。比如我们新注册了一个名字空间为Hongru,然后我们需要再注册一个名字空间为Hongru.me,亦即我们的本意就是me这个namespace是Hongru的sub-namespace,他们应该拥有父子的关系。所以,在注册namespace的时候需要通过‘.'来split,并且进行逐一对应的判断。所以,注册一个名字空间的代码大概如下:

// create namespace --> return a top namespace
Module.createNamespace = function (name, version) {
if (!name) throw new Error('name required');
if (name.charAt(0) == '.' || name.charAt(name.length-1) == '.' || name.indexOf('..') != -1) throw new Error('illegal name'); var parts = name.split('.'); var container = Module.globalNamespace;
for (var i=0; i<parts.length; i++) {
var part = parts[i];
if (!container[part]) container[part] = {};
container = container[part];
} var namespace = container;
if (namespace.NAME) throw new Error('module "'+name+'" is already defined');
namespace.NAME = name;
if (version) namespace.VERSION = version; Module.modules[name] = namespace;
return namespace;
};

注:上面的Module是我们来注册和管理namespace的一个通用Module,它本身作为一个“基模块”,拥有一个modules的模块队列属性,用来存储我们新注册的名字空间,正因为有了这个队列,我们才能方便的判断namespace时候已经被注册:

var Module;
//check Module --> make sure 'Module' is not existed
if (!!Module && (typeof Module != 'object' || Module.NAME)) throw new Error("NameSpace 'Module' already Exists!"); Module = {}; Module.NAME = 'Module';
Module.VERSION = 0.1; Module.EXPORT = ['require',
'importSymbols']; Module.EXPORT_OK = ['createNamespace',
'isDefined',
'modules',
'globalNamespace']; Module.globalNamespace = this; Module.modules = {'Module': Module};

上面代码最后一行就是一个namespace队列,所有新建的namespace都会放到里面去。结合先前的一段代码,基本就能很好的管理我们的名字空间了,至于Module这个“基模块”还有些EXPORT等别的属性,等会会接着说。下面是一个创建名字空间的测试demo

Module.createNamespace('Hongru', 0.1);//注册一个名为Hongru的namespace,版本为0.1

上面第二个版本参数也可以不用,如果你不需要版本号的话。在chrome-debugger下可以看到我们新注册的名字空间

可以看到新注册的Hongru命名空间已经生效:再看看Module的模块队列:

可以发现,新注册的Hongru也添进了Module的modules队列里。大家也发现了,Module里还有require,isDefined,importSymbols几个方法。
由于require(检测版本),isDefined(检测namespace时候已经注册)这两个方法并不难,就稍微简略点:

--版本和重名检测

// check name is defined or not
Module.isDefined = function (name) {
return name in Module.modules;
};
// check version
Module.require = function (name, version) {
if (!(name in Module.modules)) throw new Error('Module '+name+' is not defined');
if (!version) return;
var n = Module.modules[name];
if (!n.VERSION || n.VERSION < version) throw new Error('version '+version+' or greater is required');
};

上面两个方法都很简单,相信大家都明白,一个是队列检测是否重名,一个检测版本是否达到所需的版本。也没有什么特别的地方,就不细讲了,稍微复杂一点的是名字空间之间的属性或方法的相互导入的问题。 
--名字空间中标记的属性或方法的导出 
由于我们要的是一个通用的名字空间注册和管理的tool,所以在做标记导入或导出的时候需要考虑到可配置性,不能一股脑全部导入或导出。所以就有了我们看到的Module模板中的EXPORT和EXPORT_OK两个Array作为存贮我们允许导出的属性或方法的标记队列。其中EXPORT为public的标记队列,EXPORT_OK为我们可以自定义的标记队列,如果你觉得不要分这么清楚,也可以只用一个标记队列,用来存放你允许导出的标记属性或方法。 
有了标记队列,我们做的导出操作就只针对EXPORT和EXPORT_OK两个标记队列中的标记。

/ import module
Module.importSymbols = function (from) {
if (typeof form == 'string') from = Module.modules[from];
var to = Module.globalNamespace; //dafault
var symbols = [];
var firstsymbol = 1;
if (arguments.length>1 && typeof arguments[1] == 'object' && arguments[1] != null) {
to = arguments[1];
firstsymbol = 2;
}
for (var a=firstsymbol; a<arguments.length; a++) {
symbols.push(arguments[a]);
}
if (symbols.length == 0) {
//default export list
if (from.EXPORT) {
for (var i=0; i<from.EXPORT.length; i++) {
var s = from.EXPORT[i];
to[s] = from[s];
}
return;
} else if (!from.EXPORT_OK) {
// EXPORT array && EXPORT_OK array both undefined
for (var s in from) {
to[s] = from[s];
return;
}
}
}
if (symbols.length > 0) {
var allowed;
if (from.EXPORT || form.EXPORT_OK) {
allowed = {};
if (from.EXPORT) {
for (var i=0; i<form.EXPORT.length; i++) {
allowed[from.EXPORT[i]] = true;
}
}
if (from.EXPORT_OK) {
for (var i=0; i<form.EXPORT_OK.length; i++) {
allowed[form.EXPORT_OK[i]] = true;
}
}
}
}
//import the symbols
for (var i=0; i<symbols.length; i++) {
var s = symbols[i];
if (!(s in from)) throw new Error('symbol '+s+' is not defined');
if (!!allowed && !(s in allowed)) throw new Error(s+' is not public, cannot be imported');
to[s] = form[s];
}
} 这个方法中第一个参数为导出源空间,第二个参数为导入目的空间(可选,默认是定义的globalNamespace),后面的参数也是可选,为你想导出的具体属性或方法,默认是标记队列里的全部。
下面是测试demo:
复制代码 代码如下: Module.createNamespace('Hongru');
Module.createNamespace('me', 0.1);
me.EXPORT = ['define']
me.define = function () {
this.NAME = '__me';
}
Module.importSymbols(me, Hongru);//把me名字空间下的标记导入到Hongru名字空间下

可以看到测试结果:

本来定义在me名字空间下的方法define()就被导入到Hongru名字空间下了。当然,这里说的导入导出,其实只是copy,在me名字空间下依然能访问和使用define()方法。

好了,大概就说到这儿吧,这个demo也只是提供一种管理名字空间的思路,肯定有更加完善的方法,可以参考下YUI,EXT等框架。或者参考《JavaScript权威指南》中模块和名字空间那节。

最后贴下源码:

/* == Module and NameSpace tool-func ==
* author : hongru.chen
* date : 2010-12-05
*/
var Module;
//check Module --> make sure 'Module' is not existed
if (!!Module && (typeof Module != 'object' || Module.NAME)) throw new Error("NameSpace 'Module' already Exists!");
Module = {};
Module.NAME = 'Module';
Module.VERSION = 0.1;
Module.EXPORT = ['require',
'importSymbols'];
Module.EXPORT_OK = ['createNamespace',
'isDefined',
'modules',
'globalNamespace'];
Module.globalNamespace = this;
Module.modules = {'Module': Module};
// create namespace --> return a top namespace
Module.createNamespace = function (name, version) {
if (!name) throw new Error('name required');
if (name.charAt(0) == '.' || name.charAt(name.length-1) == '.' || name.indexOf('..') != -1) throw new Error('illegal name');
var parts = name.split('.');
var container = Module.globalNamespace;
for (var i=0; i<parts.length; i++) {
var part = parts[i];
if (!container[part]) container[part] = {};
container = container[part];
}
var namespace = container;
if (namespace.NAME) throw new Error('module "'+name+'" is already defined');
namespace.NAME = name;
if (version) namespace.VERSION = version;
Module.modules[name] = namespace;
return namespace;
};
// check name is defined or not
Module.isDefined = function (name) {
return name in Module.modules;
};
// check version
Module.require = function (name, version) {
if (!(name in Module.modules)) throw new Error('Module '+name+' is not defined');
if (!version) return;
var n = Module.modules[name];
if (!n.VERSION || n.VERSION < version) throw new Error('version '+version+' or greater is required');
};
// import module
Module.importSymbols = function (from) {
if (typeof form == 'string') from = Module.modules[from];
var to = Module.globalNamespace; //dafault
var symbols = [];
var firstsymbol = 1;
if (arguments.length>1 && typeof arguments[1] == 'object' && arguments[1] != null) {
to = arguments[1];
firstsymbol = 2;
}
for (var a=firstsymbol; a<arguments.length; a++) {
symbols.push(arguments[a]);
}
if (symbols.length == 0) {
//default export list
if (from.EXPORT) {
for (var i=0; i<from.EXPORT.length; i++) {
var s = from.EXPORT[i];
to[s] = from[s];
}
return;
} else if (!from.EXPORT_OK) {
// EXPORT array && EXPORT_OK array both undefined
for (var s in from) {
to[s] = from[s];
return;
}
}
}
if (symbols.length > 0) {
var allowed;
if (from.EXPORT || form.EXPORT_OK) {
allowed = {};
if (from.EXPORT) {
for (var i=0; i<form.EXPORT.length; i++) {
allowed[from.EXPORT[i]] = true;
}
}
if (from.EXPORT_OK) {
for (var i=0; i<form.EXPORT_OK.length; i++) {
allowed[form.EXPORT_OK[i]] = true;
}
}
}
}
//import the symbols
for (var i=0; i<symbols.length; i++) {
var s = symbols[i];
if (!(s in from)) throw new Error('symbol '+s+' is not defined');
if (!!allowed && !(s in allowed)) throw new Error(s+' is not public, cannot be imported');
to[s] = form[s];
}
}

关于Javascript模块化和命名空间管理的问题说明的更多相关文章

  1. 《前端之路》之 Javascript 模块化管理的来世今生

    目录 第二章 - 04: Javascript 模块化管理的来世今生 一.什么是模块化开发 1-1.模块化第一阶段 1-2.封装到对象 1-3. 对象的优化 二.模块化管理的发展历程 2-1.Comm ...

  2. Javascript模块化开发,使用模块化脚本加载工具RequireJS,提高你代码的速度和质量。

    随着前端JavaScript代码越来越重,如何组织JavaScript代码变得非常重要,好的组织方式,可以让别人和自己很好的理解代码,也便于维护和测试.模块化是一种非常好的代码组织方式,本文试着对Ja ...

  3. Javascript模块化编程之路——(require.js)

    转自:http://www.ruanyifeng.com/blog/2012/10/javascript_module.html Javascript模块化编程(一):模块的写法 随着网站逐渐变成&q ...

  4. JavaScript 模块化简述

    JavaScript 模块化简述 前言 关于模块化,最直接的表现就是我们写的 require 和 import 关键字,如果查阅相关资料,就一定会遇到 CommonJS .CMD AMD 这些名词,以 ...

  5. JavaScript 模块化

    当项目越来越大时,会遇到一些问题: 1.命名冲突 2.文件依赖 所有就有了javascript模块化开发概念. 模块化开发的演变: 1.函数块:最开始用全局函数将代码块包括在函数体内,然后把很多函数写 ...

  6. JavaScript 模块化简析

    关于模块化,最直接的表现就是我们写的 require 和 import 关键字,如果查阅相关资料,就一定会遇到 CommonJS .CMD AMD 这些名词,以及 RequireJS.SeaJS 等陌 ...

  7. JavaScript模块化演变 CommonJs,AMD, CMD, UMD(一)

    原文链接:https://www.jianshu.com/p/33d53cce8237 原文系列2链接:https://www.jianshu.com/p/ad427d8879cb 前端完全手册: h ...

  8. Javascript模块化编程-require.js

    转自:https://www.cnblogs.com/digdeep/p/4607131.html Javascript模块化编程(一):模块的写法 随着网站逐渐变成"互联网应用程序&quo ...

  9. 看完我的笔记不懂也会懂----javascript模块化

    JavaScript模块化 模块化引子 模块化的历史进化 模块化规范 CommonJS规范 Node.js(服务器端) 下项目的结构分析 browerify(浏览器端) 下项目的结构分析 AMD规范 ...

随机推荐

  1. 【01】循序渐进学 docker:到底是啥

    写在前面的话 首先说一下,我本身是做运维的,4 年工作,多家公司.所以可能接下来谈到的更多的是一些在工作过程中积累的个人看法.且有些并不具备普遍性,有不合适的地方,全当我在吹牛逼就行. 一开始我们得谈 ...

  2. leetcode 106. 从中序与后序遍历序列构造二叉树(Construct Binary Tree from Inorder and Postorder Traversal)

    目录 题目描述: 示例: 解法: 题目描述: 根据一棵树的中序遍历与后序遍历构造二叉树. 注意: 你可以假设树中没有重复的元素. 示例: 给出 中序遍历 inorder = [9,3,15,20,7] ...

  3. saltstack平台基础

    saltstack概述saltstack是基于python开发的一套C/S架构配置管理工具,使用SSL证书签方的方式进行认证管理底层使用ZeroMQ消息队列pub/sub方式通信   号称世界上最快的 ...

  4. 【转】C# datagridview大小跟随窗口动态改变

    源地址:https://blog.csdn.net/fengxing11/article/details/52527715

  5. Navicat 远程连接 MySQL

    Navicat 远程连接 MySQL 相信大家都有在远程服务器上进行开发吧,其中 MySQL 的使用率应该也会挺高,如果使用 Navicat 等可视化工具来操作远程数据库不失为一种很好的选择,避免了在 ...

  6. vmware vSphere克隆与快照技术

    通过Web vCenter我们可以很容易的对虚拟机进行管理,通过克隆技术或创建模板,我们可以迅速的创建虚拟机,我们也可以通过快照技术去捕获虚拟机的一些状态,比如说虚拟机的内存.设置或者虚拟磁盘的情况等 ...

  7. Django template的html明明改了,前端页面居然没有对应变化?!---Django的小坑

    写django的时候,我有个模板的名字叫detail.html,被detail视图函数渲染 因为那个detail写乱了,但是里面有东西要参考,我没删掉它,改名为detail_old.html,又在目录 ...

  8. 蓝牙4.0BLE抓包(三) – 扫描请求和扫描响应

    版权声明:本文为博主原创文章,转载请注明作者和出处.    作者:强光手电[艾克姆科技-无线事业部] 1. 扫描请求和扫描响应 广播包含扫描请求SCAN_REQ和扫描响应SCAN_RSP. 扫描请求: ...

  9. DHCP协议及基本实现原理

    DHCP(Dynamic Host Configuration Protocol):动态主机配置协议. DHCP的优缺点 DHCP服务优点:网络管理员可以验证IP地址和其它配置参数,而不用去检查每个主 ...

  10. <转> django模板语言filter方法

    Django的模板是一个简单的文本文件,它可以生成任何文本格式(HTML.XML.CSV等),在本文开始之前先看一个Django模板的例子: 从这个例子中可以看到,模板标签可以是单独出现的,如exte ...