前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >JavaScript模块化发展

JavaScript模块化发展

作者头像
pitaojin
发布2018-05-25 16:53:07
1.7K0
发布2018-05-25 16:53:07
举报
文章被收录于专栏:前端萌媛的成长之路

简介

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

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


发展历程

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


原始写法

代码语言:javascript
复制
function fn1() {}
function fn2() {}

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

对象写法

代码语言:javascript
复制
var math = {
    _addFir: 2,
    add: function(addSec) {
        return _addFir + addSec;
    },
};

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

代码语言:javascript
复制
math._addFir = 10;
console.log(math.add(2)); // 12

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

使用立即执行

代码语言:javascript
复制
var math = (function() {
    var _addFir = 2;
    
    function add(addSec) {
        return _addFir + addSec;
    }
    
    return {
        add: add,
    };
})();

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

代码语言:javascript
复制
_addFir = 10;
console.log(math.add(2)); // 4
引入依赖
代码语言:javascript
复制
var get = (function($) {
    var $p = $('input');
    
    function getVal() { 
        return $p.val();
    }
    
    return {
        getVal: getVal,
    };
})(jQuery);

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

放大模式
代码语言:javascript
复制
var module = (function(mod) {
    mod.add = function(a, b) {
        return a + b;
    };
    
    return mod;
})(module);

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

宽放大模式
代码语言:javascript
复制
var module = (function(mod) {
    mod.add = function(a, b) {
        return a + b;
    };
    
    return mod;
})(module || {});

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

LABjs

起初script标签引入文件

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

代码语言:javascript
复制
<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中引脚本文件可以成下面这样。

代码语言:javascript
复制
<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也可以解决所有文件之间都相互依赖的问题

代码语言:javascript
复制
<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用来基于模块的依赖管理。

代码语言:javascript
复制
YUI.add('module1', function(Y) {...}, '1.0.0', requires: []);

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

代码语言:javascript
复制
// hello.js
YUI.add('hello', function(Y) {
    Y.sayHello = function() {
        Y.DOM.set(el, 'innerHTML', 'hello!');
    }
}, '1.0.0', 
    requires: ['dom']);
代码语言:javascript
复制
// 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中去。

代码语言:javascript
复制
// 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实例,用来存放此模块的信息。

代码语言:javascript
复制
// 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中的属性就是模块对外输出的接口。

代码语言:javascript
复制
// math.js
var count = 5;
function add(val) {
    return count + val;
}
module.exports = { count, add };
代码语言:javascript
复制
// use.js
var math = require('math.js');
console.log(math.count); // 5
math.add(5); // 10

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

代码语言:javascript
复制
// math.js
function math() {}; // 函数即对象
math.count1 = 5;
math.prototype.count2 = 10;
module.exports = math;
代码语言:javascript
复制
// 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是一个对象)

代码语言:javascript
复制
// math.js
var count = 5;
function add(val) {
    return count + val;
}

exports.count = count;
exports.add = add;
代码语言:javascript
复制
// use.js
var math = require('math.js');
console.log(math.count); // 5
math.add(5); // 10

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

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

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

代码语言:javascript
复制
// math.js
var count = 5;
function add(val) {
    return count + val;
}
exports = module.exports = { count, add };
exports.count = 10; 
代码语言:javascript
复制
// 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使用手册》

代码语言:javascript
复制
当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)。 首先会确定文件的绝对路径,并依此去寻找每个目录

代码语言:javascript
复制
/home/test/node_modules/math
/home/node_modules/math
/node_modules/math

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

代码语言:javascript
复制
math
math.js
math.json
math.node

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

代码语言:javascript
复制
package.json(main字段)
index.js
index.json
index.node

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

node中模块缓存机制

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

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

require函数

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

代码语言:javascript
复制
Module.prototype.require = function(path) {
    return Module._load(path, this);
}

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

下面是Module._load的源码

代码语言:javascript
复制
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模块没有依赖

代码语言:javascript
复制
// math.js
define({
    add: function(x, y) {
        return x + y;
    },
});

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

math模块需要依赖其他模块

代码语言:javascript
复制
// math.js
define(['other'], function(oth) {
    function add(x, y) {
        return oth(x, y);
    }
    return {
        add: add
    };
});
require.js

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

代码语言:javascript
复制
define([module], callback);
require([module], callback, errCallback);

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

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

代码语言:javascript
复制
<script data-main="scripts/main" src="scripts/require.js"></script>

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

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

代码语言:javascript
复制
require(['mod1', 'mod2'], function(mod1, mod2) {
    ...
})

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

配置require.js

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

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

代码语言:javascript
复制
require.config({
    bathUrl: './lib'
    paths: {
        mod1: ['mod', 'mod1'],
        mod2: 'mod2'
    },
})
require([], function)
paths

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

baseUrl

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

shim

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

代码语言:javascript
复制
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模块,和字符串模块。

代码语言:javascript
复制
define({ newborn: 'hello' });
define('welcome, newborn!');

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

代码语言:javascript
复制
define(function(require, exports, module) {...});

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

require

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

代码语言:javascript
复制
define(function(require) {
    var $ = require('jquery'); // 注意这里是同步执行的
    ...
})

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

require.async([module], callback)

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

代码语言:javascript
复制
define(function(require) {
    require.async(['jquery'], function($) {
        ...
    });
    
    require.async(['a'], function(a) {
        ...
    });
})
对外提供模块接口

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

代码语言:javascript
复制
define(function(require, exports) {
    exports.add = function(x, y) {
        return x + y;
    };
    
    exports.count = 5;
})

return 方法

代码语言:javascript
复制
define(function() {
    return {
        add: function() {},
        count: 5,
    }
})

module.exports方法

代码语言:javascript
复制
define(function(require, exports, module) {
    module.exports = {
        add: function() {},
        count: 5,
    }
})

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

代码语言:javascript
复制
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. 可维护性:如果想要想要修改个部分的代码,不用去到所有代码中去修改代码,仅仅只需要到引用的模块中修改。

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

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2018.05.22,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 简介
  • 发展历程
    • 原始写法
      • 对象写法
        • 使用立即执行
          • 引入依赖
          • 放大模式
          • 宽放大模式
        • LABjs
          • 起初script标签引入文件
          • LABjs
        • YUI
          • CommonJS
            • module
            • require
          • AMD
            • require.js
            • 配置require.js
          • CMD
            • require
            • require.async([module], callback)
            • 对外提供模块接口
        • 模块化的优点
        相关产品与服务
        云服务器
        云服务器(Cloud Virtual Machine,CVM)提供安全可靠的弹性计算服务。 您可以实时扩展或缩减计算资源,适应变化的业务需求,并只需按实际使用的资源计费。使用 CVM 可以极大降低您的软硬件采购成本,简化 IT 运维工作。
        领券
        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档