前端模块化的作用

  • Post author:
  • Post category:其他


相信很多人都用过 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 都会自动处理好。对模块开发者来说,这是一种很好的 关注度分离,能让程序员更多地享受编码的乐趣。

小结

除了解决命名冲突和依赖管理,使用模块化开发还可以带来很多好处:

  1. 模块的版本管理。通过别名等配置,配合构建工具,可以比较轻松地实现模块的版本管理。
  2. 提高可维护性。模块化可以让每个文件的职责单一,非常有利于代码的维护。Sea.js 还提供了 nocache、debug等插件,拥有在线调试等功能,能比较明显地提升效率。
  3. 前端性能优化。RequireJS 和 SeaJS 都是通过异步加载模块,这对页面性能非常有益。Sea.js 还提供了 combo、flush等插件,配合服务端,可以很好地对页面性能进行调优。
  4. 跨环境共享模块。CMD 模块定义规范与 Node.js 的模块规范非常相近。通过 Sea.js 的 Node.js版本,可以很方便实现模块的跨服务器和浏览器共享。



版权声明:本文为lilythy2016原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。