相信很多人都用过 seajs、 requirejs 等这些模块加载器,他们都是十分便捷的工程管理工具,简化了代码的结构,更重要的是消除了各种文件依赖和命名冲突问题,并利用 AMD / CMD 规范统一了格式。然而你了解模块化的作用吗?下面主要讲述模块化能解决哪些问题。
命名冲突
做项目时,常常会将一些通用的功能抽象出来,独立成一个个函数,比如
//util.js
function formate(arr) {
// 实现代码
}
function isEmpty(str) {
// 实现代码
}
把这些函数统一放在 util.js 里。需要用到时,引入该文件就行。这一切工作得很好,同事也很感激提供了这么便利的工具包。
直到团队越来越大,开始有人抱怨:我想定义一个 formate 方法格式化时间,但 util.js 里已经定义了一个。还有的同学不知道util里定义了formate方法,自定义了一个formate函数,然后这个页面其他地方用到formate的地方都报错了。。。
大团队下使用这种方式封装公用工具就会变成
全局变量的灾难
,一不小心就会有
变量命名冲突
的问题。
后面有人提出了采用的用
自执行函数
来包装代码或jQuery风格的
匿名自执行函数
来试图解决这个问题,代码如下
//util.js
//用自执行函数来包装代码
var Util = function(){
return {
formate : function(c){
//......
},
isEmpty: function(){
//......
}
}
}()
//jQuery风格的匿名自执行函数
(function(window){
//代码
window.jQuery = window.$ = jQuery;//通过给window添加属性而暴漏到全局
})(window);
这样function内部的变量就对全局隐藏了,达到是封装的目的。但是这样还是有缺陷的:所需依赖还是得外部提前提供、还是增加了全局变量。
作为前端业界的标杆,YUI 团队下定决心解决这一问题。在 YUI3 项目中,引入了一种新的
命名空间机制
。于是 util.js 里的代码变成了
//util.js
YUI().use('util', function (Y) {
// Node 模块已加载好
// 下面可以通过 Y 来调用
var foo = Y.one('#formate');
});
//YUI 通过沙箱机制,很好的解决了命名空间过长的问题。然而,也带来了新问题。
YUI().use('util1', 'util2', function (Y) {
Y.formate(); // 如果util1 和util2里都有formate方法,
// 那么formate 方法究竟是模块 util1 还是 util12 提供的?
});
看似简单的命名冲突,实际解决起来并不简单,再来看另一个常见问题。
繁杂的文件依赖
基于 util.js,同事写了一个很实用的组件 component.js,使用方式很简单,先引入 util.js,再引入 component.js,因为 component.js 依赖于 util.js。
<script src="util.js"></script>
<script src="component.js"></script>
<script>
Component.init({ /* 传入初始化参数 */ });
</script>
新来的同事在使用 component.js 时,不知道它依赖于 util.js ,总会来询问为什么 component.js 有问题。通过一番排查,发现导致错误的原因经常是在 component.js 前没有引入 util.js,因此 component.js 无法正常工作。上面的文件依赖还在可控范围内。当项目越来越复杂,众多文件之间的依赖经常会让人抓狂。
在前端页面里,大部分脚本的依赖目前依旧是通过人肉的方式保证。当团队比较小时,这不会有什么问题。当团队越来越大,公司业务越来越复杂后,依赖问题如果不解决,就会成为大问题。
模块化加载解决方案
随着Node.js的到来,服务器上通过require加载资源是直接读取文件的,因此中间所需的时间可以忽略不计,但是在浏览器这种需要依赖HTTP获取资源的就不行了,资源的获取所需的时间不确定,这就导致必须使用异步机制,原理即利用script标签的async属性来异步加载js文件,代表主要有2个:
基于 AMD 的RequireJS
基于 CMD 的SeaJS
它们分别在浏览器实现了define、require及module的核心功能,虽然两者的目标是一致的,但是实现的方式或者说是思路,还是有些区别的,AMD偏向于依赖前置,CMD偏向于用到时才运行的思路,从而导致了依赖项的加载和运行时间点会不同。前面例子中的 util.js 变成
// CMD
define(function (require) {
var util1 = require('./util1'); // <- 运行到此处才开始加载并运行模块util1
var util2 = require('./util2'); // <- 运行到此处才开始加载并运行模块util2
util1.formate();
// more code ..
})
// AMD
//util1.js
define(function(require, exports) {
exports.formate = function (arr) {
// 实现代码
};
exports.isEmpty = function (str) {
// 实现代码
};
});
//main.js
define(
['./util1', './util2'], // <- 前置声明,也就是在主体运行前就已经加载并运行了模块a和模块b
function (util1, util2) {
util1.formate();
// more code ..
}
)
我们通过 require(‘./util1.js’) 就可以拿到 util1.js 中通过 exports 暴露的接口,使用util1里的formate方法即
util1.formate();
,并不会有 util1.js 和 util2.js 方法名冲突问题。这里的 require 可以认为是给 JavaScript 语言增加的一个
语法关键字
,通过 require 可以获取其他模块提供的接口。
好好琢磨以上代码,我相信你已经看到了 RequireJS 和 SeaJS 带来的两大好处:
1、通过 exports 暴露接口。这意味着不需要命名空间了,更不需要全局变量。这是一种彻底的命名冲突解决方案。
2、通过 require 引入依赖。这可以让依赖内置,开发者只需关心当前模块的依赖,其他事情 RequireJS 和 SeaJS 都会自动处理好。对模块开发者来说,这是一种很好的 关注度分离,能让程序员更多地享受编码的乐趣。
小结
除了解决命名冲突和依赖管理,使用模块化开发还可以带来很多好处:
- 模块的版本管理。通过别名等配置,配合构建工具,可以比较轻松地实现模块的版本管理。
- 提高可维护性。模块化可以让每个文件的职责单一,非常有利于代码的维护。Sea.js 还提供了 nocache、debug等插件,拥有在线调试等功能,能比较明显地提升效率。
- 前端性能优化。RequireJS 和 SeaJS 都是通过异步加载模块,这对页面性能非常有益。Sea.js 还提供了 combo、flush等插件,配合服务端,可以很好地对页面性能进行调优。
- 跨环境共享模块。CMD 模块定义规范与 Node.js 的模块规范非常相近。通过 Sea.js 的 Node.js版本,可以很方便实现模块的跨服务器和浏览器共享。