JavaScript模块化发展

简介

在最开始学习前端的时候只需要一个js文件就能玩转一个小的练手应用,但是随着自己不断的学习,ajax、jQuery等广泛应用,使得我们的代码量变得巨大,代码变得格外的混乱。现在迫切的需要我们将大段的代码分离开来。

前端最开始并没有像java中package概念以及import那样的引包工具。JavaScript源生代码是在ES6的时候才正式的引入import这个API,来调用其他文件。在这之前也同样出现了很多社区来实现模块化开发。


发展历程

注意下面会讲历史上面出现的一些类库,有一些现在已经没有人用了,所以建议知道有过就行。


原始写法

function fn1() {}
function fn2() {}

将函数挂载到全局上,通过函数名就可以直接调用。但是这种方式污染全局,容易发生命名冲突。

对象写法

var math = {
    _addFir: 2,
    add: function(addSec) {
        return _addFir + addSec;
    },
};

对象写法相对来说减少了全局变量,但是一点也不安全。 例如:

math._addFir = 10;
console.log(math.add(2)); // 12

对象内部的变量可以被外面修改。

使用立即执行

var math = (function() {
    var _addFir = 2;
    
    function add(addSec) {
        return _addFir + addSec;
    }
    
    return {
        add: add,
    };
})();

这样就无法修改函数内部的_addFir参数了

_addFir = 10;
console.log(math.add(2)); // 4

引入依赖

var get = (function($) {
    var $p = $('input');
    
    function getVal() { 
        return $p.val();
    }
    
    return {
        getVal: getVal,
    };
})(jQuery);

使用立即执行的方式写模块,当模块非常大的时候,我们就需要将这个模块分成几部分来进行编写。下面将会展示廖雪峰老师所说的放大模式和宽放大模式(我暂时想不出其他的名字,就使用了现有的)。

放大模式

var module = (function(mod) {
    mod.add = function(a, b) {
        return a + b;
    };
    
    return mod;
})(module);

上面的方式实现了将已存在的对象添加方法,使之放大,但是如果module起初还没有被加载到文件中怎么办,下面就用到了宽放大模式。

宽放大模式

var module = (function(mod) {
    mod.add = function(a, b) {
        return a + b;
    };
    
    return mod;
})(module || {});

这样就解决了module模块没有加载出来,报错的问题。

LABjs

起初script标签引入文件

我们最初使用html中的<script>标签来引入js文件。当项目不断变大以后,我们的项目的依赖也开始变多,就像下面。

<body>
    ...
    <script src="jQuery.js"></script>
    <script src="zepto.js"></script>
    <script src="iScroll.js"></script>
    <script src="math.js"></script>
    <script src="dom.js"></script>
    ...
</body>

大量的script标签排列在我们的html文件中。

缺点

  • 这些文件引入必须按照循序进行加载,当文件依赖过多,当我们编写一个新类库来替换以前的通用组件时,那就不只是改几句代码就行得通的。
  • 浏览器需要停止响应,来进行这些文件的加载。

LABjs

LABjs它是一个文件加载器,使用script和wait实现文件异步和同步加载,解决文件之间的相互依赖,使的文件加载的性能大大提高。有了它我们的html中引脚本文件可以成下面这样。

<script src="LAB.js"></script>
<script>
$LAB
    .script('jQuery.js').wait() // .wait是等待此文件的加载完成,当所有的文件都需要依赖jQuery中的api,必须等jQuery文件加载好以后才能调用jQuery
    .script('a.js')
    .script('b.js')
    .script('c.js')
    .script('math.js')
    // .script(['a.js', 'b.js', 'c.js', 'math.js']) // 同时加载所有的js文件
    .wait(function() {  // 等所有的js文件加载完成以后,执行这里的代码块
        math.add(2, 2);
    })
</script>

同时LABjs也可以解决所有文件之间都相互依赖的问题

<script src="LAB.js"></script>
<script>
$LAB
    .setOptions({AlwaysPreserveOrder:true}) // 下面需要加载的这些文件之间都相互依赖
    .script(['a.js', 'b.js', 'c.js', 'math.js'])
    .wait(function() {
        math.add(2, 2);
    })

YUI

YUI用来基于模块的依赖管理。

YUI.add('module1', function(Y) {...}, '1.0.0', requires: []);

其中YUI是全局变量,就像是jQuery;第一个参数是此模块的名字;第二个参数中函数的内容就是此模块的内容;第三个参数是此模块的版本号;第四个参数是此模块需要依赖的模块有哪些。 下面将展示如何使用YUI添加和使用一个模块

// hello.js
YUI.add('hello', function(Y) {
    Y.sayHello = function() {
        Y.DOM.set(el, 'innerHTML', 'hello!');
    }
}, '1.0.0', 
    requires: ['dom']);
// index.html
<div id="entry"></div>
<script>
    YUI().use('hello', function(Y) {
        Y.sayHello('entry'); // <div id="entry">hello!</div>
    })
</script>

我不想花太多时间在这个上面,所以后面只会写生成模块和使用模块。如果对这个有兴趣可以到:YUI3

CommonJS

the spec does not define a standard library that is useful for building a broader range of applications. 该规范没有定义一个标准库,可用于构建更广泛的应用程序。

上面这段话来自CommonJS官网中的自我定位,它本质上面是一个规范,需要其他的JavaScript类库、框架等自行实现它定义的API。

CommonJS使得JavaScript不仅仅只适用于浏览器,他让js可以编写更多应用程序,如:

commonjs中的模块加载时同步加载,在服务器端,模块存在服务器本地的,加载速度很快。但是,当程序运行在浏览器端的时候要从服务器端去加载模块会导致性能、可用性、调试、跨域等问题,所以commonjs不适用与浏览器端。

node应用程序就是根据CommonJS规范实现的,下面我将直接使用node来讲解CommonJS中module和require两个API。

在node中每一个文件就是一个模块,每个模块中变量、函数、对象、类都是私有的,除非将这些放入global中去。

// math.js
var count1 = 2;
global.count2 = 5;

// use.js
console.log(count1); // count1 is not defined
console.log(count2); // 5

module

node中有一个Module构造函数(node中的lib/module.js),在node应用程序中每个模块都含有一个Module实例,用来存放此模块的信息。

// Module 构造函数
function Module(id, parent) {
  this.id; // String,模块标识,为该模块文件在系统中的绝对路径
  this.exports; // Object,模块导出的对象
  this.parent; // Object,调用此模块的模块信息
  this.filename; // String,模块文件的绝对路径
  this.loaded; // Boolean,表示模块是否加载完成
  this.children;  // Array,此模块调用了的模块
  this.path; // Array,此模块加载的路径
}
module.exports

module.exports中的属性就是模块对外输出的接口。

// math.js
var count = 5;
function add(val) {
    return count + val;
}
module.exports = { count, add };
// use.js
var math = require('math.js');
console.log(math.count); // 5
math.add(5); // 10

注意module.exports只会输出对象的自身属性,prototype上面的方法是私有方法

// math.js
function math() {}; // 函数即对象
math.count1 = 5;
math.prototype.count2 = 10;
module.exports = math;
// use.js
var math = require('math.js');
console.log(math); // { [Function: math] count1: 5 }
console.log(math.count2); // undefined
exports

exports是node提供的一个变量,用来指向module.exports的引用,相当于每个node文件前面有一段这样的代码exports = module.exports = something。(something是一个对象)

// math.js
var count = 5;
function add(val) {
    return count + val;
}

exports.count = count;
exports.add = add;
// use.js
var math = require('math.js');
console.log(math.count); // 5
math.add(5); // 10

注意模块最终输出的是module.exports,而不是exports。

// math.js
var count = 5;
function add(val) {
    return count + val;
}
module.exports = { count, add }; // 此时module的exports指向另一个对象
exports.count = 10; // exports依旧指向的是最开始module.exports指向的对象something
// use.js
var math = require('math.js');
console.log(math.count); // 注意,这里打印的还是5
math.add(5); // 10

由上面代码可以看出,当module.exports发生改变的时候,exports失效,这就很正常了。如果想让后面的exports的操作能改变输出的话,使exports的指向module.exports新的引用就行了。

// math.js
var count = 5;
function add(val) {
    return count + val;
}
exports = module.exports = { count, add };
exports.count = 10; 
// use.js
var math = require('math.js');
console.log(math.count); // 10
math.add(5); // 10

在讲exports的最后,提醒大家,想要使用exports对外输出的时候不是对exports赋值。如果大家看了多次还是不懂exports的用法,那就去看下这篇module.exports与exports??关于exports的总结

require

node中require是的顺序

下面是来自廖雪峰的require() 源码解读翻译翻译自《Node使用手册》

当Node 遇到 require(X) 时,按下面的顺序处理。
(1)如果 X 是内置模块(比如 require('http')) 
  a. 返回该模块。 
  b. 不再继续执行。
  
(2)如果 X 以 "./" 或者 "/" 或者 "../" 开头 
  a. 根据 X 所在的父模块,确定 X 的绝对路径。 
  b. 将 X 当成文件,依次查找下面文件,只要其中有一个存在,就返回该文件,不再继续执行。
        X
        X.js
        X.json
        X.node
  c. 将 X 当成目录,依次查找下面文件,只要其中有一个存在,就返回该文件,不再继续执行。
        X/package.json(main字段)
        X/index.js
        X/index.json
        X/index.node
        
(3)如果 X 不带路径 
  a. 根据 X 所在的父模块,确定 X 可能的安装目录。 
  b. 依次在每个目录中,将 X 当成文件名或目录名加载。
  
(4) 抛出 "not found"

当文件/home/test/use.js中使用require('math'),这种情况属于上面的(3)。 首先会确定文件的绝对路径,并依此去寻找每个目录

/home/test/node_modules/math
/home/node_modules/math
/node_modules/math

在寻找每个目录中的文件的时候,node会现将math当成一个文件。当依此寻找到一个以后就会立马返回。

math
math.js
math.json
math.node

把math当成文件并没有找到的时候,就会将math当成文件夹,并去依此寻找他下面的这些文件。

package.json(main字段)
index.js
index.json
index.node

require会按照上面的顺序依次去查询是否含有这个文件,如果找到了就会立马加载此文件,并停止去遍历那些路径。 如果将确定好的绝对路径目录都寻找了一遍没有找到目标文件时,就会抛出一个错误。

node中模块缓存机制

node中模块不会被重复加载,node会将加载过的文件名缓存下来,以后再次访问时就不会重复加载模块了。

注意这里缓存的文件名并不是require中的参数,require('math')和require('./node_modules/math')只会去解析一次此模块。

require函数

node中的每个模块实例都有一个require方法。

Module.prototype.require = function(path) {
    return Module._load(path, this);
}

从上面的代码可以看出,require并不是全局变量,而是模块内部的一个方法。

下面是Module._load的源码

Module._load = function(request, parent, isMain) {

  //  计算绝对路径
  var filename = Module._resolveFilename(request, parent);

  //  第一步:如果有缓存,取出缓存
  var cachedModule = Module._cache[filename];
  if (cachedModule) {
    return cachedModule.exports;

  // 第二步:是否为内置模块
  if (NativeModule.exists(filename)) {
    return NativeModule.require(filename);
  }

  // 第三步:生成模块实例,存入缓存
  var module = new Module(filename, parent);
  Module._cache[filename] = module;

  // 第四步:加载模块
  try {
    module.load(filename);
    hadException = false;
  } finally {
    if (hadException) {
      delete Module._cache[filename];
    }
  }

  // 第五步:输出模块的exports属性
  return module.exports;
};

AMD

AMD:异步模块定义规范。模块和模块的依赖可以通过异步加载。前面说了程序运行在浏览器端的时候,如果同步的去加载服务器中模块会导致性能、可用性、调试、跨域等问题。AMD推荐依赖前置,require.js从2.0开始也支持依赖就近了。

AMD规范现在给出了define和require两个全局函数。 define(id, [module], callback);第一个参数id字符串,指的是这个模块的名字,可选,如果使用了这个参数,加载此模块时填写的模块名应该默认为此id;第二个参数[module],此模块的依赖列表,异步加载这些依赖。第三个参数callback,此模块的所要执行的函数或者对象,如果此模块有依赖的模块,那么callback参数的顺序应该和[module]顺序一致。

如下一个math模块没有依赖

// math.js
define({
    add: function(x, y) {
        return x + y;
    },
});

// 同下
define(function() {
    function add(x, y) {
        return x + y;
    }
    return {
        a: a,
    };
});

math模块需要依赖其他模块

// math.js
define(['other'], function(oth) {
    function add(x, y) {
        return oth(x, y);
    }
    return {
        add: add
    };
});

require.js

require.js是基于AMD规范的模块加载器。基本思想是使用define来定义模块,使用require来加载模块。

define([module], callback);
require([module], callback, errCallback);

define和require的前两个参数是一样的,第一个参数是[module]是此模块依赖的模块的加载数组,第二个参数是当依赖模块加载完成后调用的回调函数。require支持第三个参数errCallback, 是处理错误的函数。

首先将require.js文件嵌入网页中。

<script data-main="scripts/main" src="scripts/require.js"></script>

data-main的作用是定义网页的主模块,所以scripts中的main.js是第一个被require的脚本文件。require默认文件扩展名是.js

主模块中一般会依赖其他的文件。

require(['mod1', 'mod2'], function(mod1, mod2) {
    ...
})

require()加载模块的时候浏览器不会失去响应,它会现将所要依赖的模块准备好,然后依赖前置的方式将模块引用进来。

配置require.js

上面主模块中加载的mod1和mod2,默认是这两个依赖模块和主模块在同一个目录下。

如果mod1和mod2都位于主模块目录中的lib目录下:

require.config({
    bathUrl: './lib'
    paths: {
        mod1: ['mod', 'mod1'],
        mod2: 'mod2'
    },
})
require([], function)
paths

paths参数指定各个模块的位置。这个位置可以是同一个服务器上的相对位置,也可以是外部网址。可以为每个模块定义多个位置,如果第一个位置加载失败,则加载第二个位置,上面的例子中的mod1的加载中第一个位置加载出错后,会自动加载数组中的第二个地址。当模块的路径指定本地文件路径时,可以省略文件最后的js后缀名。

baseUrl

改变基准目录,本地的模块相对于哪个目录。

shim

此属性帮助require.js来加载非AMD规范的库。(没验证)

require.config({
    paths: {
        "backbone": "vendor/backbone",
        "underscore": "vendor/underscore"
    },
    shim: {
        "backbone": {
            deps: [ "underscore" ],
            exports: "Backbone"
        },
        "underscore": {
            exports: "_"
        }
    }
});

CMD

CMD规范是用来规定程序在浏览器环境下的模块开发。CMD推崇一个模块就是一个文件,依赖就近。注意:CMD依赖就近并不是在那个时候才开始加载模块,它会事先将模块准备好,依赖就近是引用就近。

CMD提供了一个全局函数define(factory),define是用来定义模块用的。它以factory为参数,其中factory可以是函数、对象、字符串。如果factory是对象或者字符串的时候,那表示此模块对外的接口就是这个对象或者字符串。

下面分别定义的一个JSON模块,和字符串模块。

define({ newborn: 'hello' });
define('welcome, newborn!');

当factory是函数的时候,这个函数就是这个模块的构造函数。factory默认会传入三个参数依次是require|exports|module

define(function(require, exports, module) {...});

define(id, [module], factory)同样也可以支持三个参数,和AMD规范中的参数一样,但是当define带有id和[module]参数的时候,就已经不属于CMD规范了

require

require是factory的默认的参数。它使得在模块内部同步的引用依赖模块。

define(function(require) {
    var $ = require('jquery'); // 注意这里是同步执行的
    ...
})

引用模块是需要时间的,当引用多个模块并分别对这些模块进行调用,如果还是同步的去执行,会消耗很多不必要的时间。

require.async([module], callback)

require.async表示的是在模块内部执行异步操作。

define(function(require) {
    require.async(['jquery'], function($) {
        ...
    });
    
    require.async(['a'], function(a) {
        ...
    });
})

对外提供模块接口

exports、return、module.exports,这三种方法都行。 exports方法

define(function(require, exports) {
    exports.add = function(x, y) {
        return x + y;
    };
    
    exports.count = 5;
})

return 方法

define(function() {
    return {
        add: function() {},
        count: 5,
    }
})

module.exports方法

define(function(require, exports, module) {
    module.exports = {
        add: function() {},
        count: 5,
    }
})

注意: exports是module.exports的一个引用,如果给exports进行赋值,不会影响到模块对外接口

define(function(require, exports) {
    // 错误用法
    exports = {
        add: function() {},
        count: 5,
    }
    
    // 正确用法
    module.exports = {
        add: function() {},
        count: 5,
    }
})

这里就不重复讲解这是为什么了,CMD中的exports和module.exports和前面前面讲解使用CommonJS规范的node中exports和module.exports的使用方式一样。但是,注意这里的module和node中的module不是一个东西.

sea.js是CMD规范的最佳实践,在这里也不去讲述了。

想要了解AMD和CMD的区别可以去: JavaSript模块规范 - AMD规范与CMD规范介绍

本篇中前面的LABjs和YUIjs都已经成为历史,个人觉得只需要知道有过就行了,因为篇幅问题,还有很多关于模块化的内容没有写,比如UMD、es6等。

模块化的优点

  1. 代码复用:我们平常有的时候有块业务相似,通过Ctrl+v这样没问题,但是如果通过模块的引用岂不更简单。
  2. 命名空间:模块将变量封装起来,这样避免污染全局环境,就减少了命名冲突的可能性。
  3. 可维护性:如果想要想要修改个部分的代码,不用去到所有代码中去修改代码,仅仅只需要到引用的模块中修改。

下面是文章中所引用的连接

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏贾老师の博客

常用 Bash Shell 整理

1004
来自专栏友弟技术工作室

bash及其特性

1273
来自专栏智能算法

Python学习(十)---- python中的进程与协程

原文地址: https://blog.csdn.net/fgf00/article/details/52790360 编辑:智能算法,欢迎关注! 上期我们一起学...

792
来自专栏用户2442861的专栏

JavaWeb工程中web.xml基本配置

        先说下我记得xml规则,必须有且只有一个根节点,大小写敏感,标签不嵌套,必须配对。

1601
来自专栏运维小白

10.7 free命令

监控系统状态 free 查看内存使用情况 free -m / -g / -h buffer/cache区别 公式:total=used+free+buff/ca...

2257
来自专栏用户2442861的专栏

Linux下动态库(.so)和静态库(.a) 的区别

动态库(共享库)的代码在可执行程序运行时才载入内存,在编译过程中仅简单的引用,因此代码体积比较小。

1.4K1
来自专栏Golang语言社区

Go 语言系统调用简析

一、系统调用概述 系统调用是受控的内核入口,借助于这一机制,进程可以请求内核以自己的名义去执行某些动作。Linux 内核以 C 语言语法 API 接口形式(头文...

4738
来自专栏跟着阿笨一起玩NET

.NET简谈静态事件链

在我们日常开发过程中经常会遇到多个类实例之间的关联,不管是B/S还是C/S的项目,在对实例的使用是一样的;只不过C/S的项目比较好控制,不管是UI层的对象都能很...

671
来自专栏十月梦想

不同函数间的数据传递

        小程序不想mvc的框架一样,获取dom进行操作,只是依靠数据绑定,数据有限原则进行数据传输.

742
来自专栏程序员同行者

django基础之二

1324

扫码关注云+社区

领取腾讯云代金券