js模块化规范

什么是模块化?

模块化是组织代码的一种方式。将全部的js业务逻辑代码写在一个文件里面,不只致使文件庞大,并且难以管理和维护。javascript

好比:html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>js</title>
</head>
<body>
    <script> ...// 一些业务逻辑 </script>
</body>
</html>
复制代码

为了方便维护,能够经过外部引入的方式:前端

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>js</title>
</head>
<body>
    ...
<script type="javascript" src="...(file path)"></script>
</body>
</html>
复制代码

这种方式我的看来也是一种模块化的方式,只不过这种方式存在许多弊端。vue

  • 文件必须顺序引入。在大型项目开发中,因为引入的文件较多,文件之间的依赖关系也较为复杂,文件引入顺序难以明确。
// test.js是基于jQuery.js开发的,也就是说test.js是依赖于jQuery.js的,因此jQuery必须先于test.js引入。
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>js</title>
</head>
<body>
    ...
<script type="javascript" src="jQuery.js"></script>
<script type="javascript" src="test.js"></script>
</body>
</html>
复制代码
  • 命名空间污染问题。
// a.js
var a=1;

// b.js
var a=2;

//test.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <script src="a.js"></script>
    <script src="b.js"></script>
    <title>js</title>
</head>
<body>
    <script> alert('a的值为'+a); </script>
</body>
</html>
复制代码

执行结果以下:java

执行结果

为了解决这些问题,一些模块化的规范就出现了。jquery

模块的发展历程

  先想想,为何模块很重要?git

  由于有了模块,咱们就能够更方便地使用别人的代码,想要什么功能,就加载什么模块。github

  可是,这样作有一个前提,那就是你们必须以一样的方式编写模块,不然你有你的写法,我有个人写法,岂不是乱了套!考虑到 Javascript模块如今尚未官方规范,这一点就更重要了。面试


  • js不像其余高级语言有模块系统,标准库较少和更缺少包管理系统express

  • js 起初只有全局对象的形式,经过一个个小函数来实现不一样的模块功能

    function m1(){
    &emsp;&emsp;//...
    }
    function m2(){
    &emsp;&emsp;//...
    } 
    复制代码
  • 渐渐发展,经过构建对象的形式,来武装不一样的功能

    var module1 = new Object ({
    &emsp;&emsp;_count : 0,
    &emsp;&emsp;m1 : function (){
    &emsp;&emsp;&emsp;&emsp;//...
    &emsp;&emsp;},
    &emsp;&emsp;m2 : function (){
    &emsp;&emsp;&emsp;&emsp;//...
    &emsp;&emsp;}
    }); 
    复制代码

    上面的函数 m1()m2(),都封装在 module1 对象里。使用的时候,就是调用这个对象的属性:

    module1.m1();

    可是,这样的写法会暴露全部模块成员,内部状态能够被外部改写。好比,外部代码能够直接改变内部计数器的值。

    module1._count = 5; 
    复制代码
  • 继续发展,经过当即执行函数和闭包的形式来分离一个又一个的小组件

    3、当即执行函数写法

      使用"当即执行函数"(Immediately-Invoked Function Expression,IIFE),能够达到不暴露私有成员的目的。

    var module1 = (function(){
    &emsp;&emsp;var _count = 0;
    &emsp;&emsp;var m1 = function(){
    &emsp;&emsp;&emsp;&emsp;//...
    &emsp;&emsp;};
    &emsp;&emsp;var m2 = function(){
    &emsp;&emsp;&emsp;&emsp;//...
    &emsp;&emsp;};
    &emsp;&emsp;return {
    &emsp;&emsp;m1 : m1,
    &emsp;&emsp;m2 : m2
    &emsp;&emsp;};
    })(); 
    复制代码

      使用上面的写法,外部代码没法读取内部的_count 变量。

    console.info (module1._count); //undefined
    复制代码

      module1就是Javascript模块的基本写法。

     下面,再对这种写法进行加工。

    放大模式( augmentation)

      若是一个模块很大,必须分红几个部分,或者一个模块须要继承另外一个模块,这时就有必要采用"放大模式"(augmentation)。

    var module1 = (function (mod){
    &emsp;&emsp;mod.m3 = function () {
    &emsp;&emsp;&emsp;&emsp;//...
    &emsp;&emsp;};
    &emsp;&emsp;return mod;
    })(module1); 
    复制代码

      上面的代码为 module1 模块添加了一个新方法 m3(),而后返回新的 module1 模块。

    宽放大模式(Loose augmentation)

      在浏览器环境中,模块的各个部分一般都是从网上获取的,有时没法知道哪一个部分会先加载。若是采用上一节的写法,第一个执行的部分有可能加载一个不存在的空对象,这时就要采用"宽放大模式"。

    var module1 = ( function (mod){
    &emsp;&emsp;//...
    &emsp;&emsp;return mod;
    })(window.module1); 
    复制代码

      与"放大模式"相比,"宽放大模式"就是"当即执行函数"的参数能够是空对象。

  • 当对象多起来的时候,又开始经过命名空间,来实现分级管理

    输入全局变量

  独立性是模块的重要特色,模块内部最好不与程序的其余部分直接交互。

  为了在模块内部调用全局变量,必须显式地将其余变量输入模块。

var module1 = (function ($, YAHOO) {
&emsp;&emsp;//...
})(jQuery, YAHOO); 
复制代码

  上面的 module1 模块须要使用 jQuery库和 YUI库,就把这两个库(实际上是两个模块)看成参数输入 module1。这样作除了保证模块的独立性,还使得模块之间的依赖关系变得明显

  • 最终,历经十多年,社区渐渐发展壮大,commonJS规范的提出成了javascript历史上最重要的里程碑。
  • Best Wishes : hope javascript can run everywhere!

主流的模块化的规范有:

  • commonJs 规范
  • AMD 规范(Asynchronous Module Definition) (异步模块定义)
  • CMD 规范
  • ES6 module

四大主流规范

CommonJS规范

CommonJS规范的使用

Node.jscommonJS规范的主要实践者,它有四个重要的环境变量为模块化的实现提供支持:moduleexportsrequireglobal。实际使用时,用module.exports定义当前模块对外输出的接口(不推荐直接用exports),用require加载模块。

// 定义模块math.js
var basicNum = 0;
function add(a, b) {
  return a + b;
}
module.exports = { //在这里写上须要向外暴露的函数、变量
  add: add,
  basicNum: basicNum
}

// 引用自定义的模块时,参数包含路径,可省略.js
var math = require('./math');
math.add(2, 5);

// 引用核心模块时,不须要带路径
var http = require('http');
http.createService(...).listen(3000);
复制代码

commonJS用**同步**的方式加载模块。在服务端,模块文件都存在本地磁盘,读取很是快,因此这样作不会有问题。可是在浏览器端,限于网络缘由,更合理的方案是使用异步加载。

commomJS的实现原理

commonJS简化版源码

function Module(id, parent){
    this.id = id;
    this.exports = {};
    this.parent = parent;
    this.filename = null;
    this.loaded = false;
    this.children = []
}

Module.prototype.require = function(path){
  return Module._load(path, this)  
}
//由此可知,require 并非全局命令,而是每一个模块提供的一个内部方法,也就是说,只有在模块内部才能使用require命令,(惟一的例外是REPL 环境)。另外,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;
};
module.exports = Module;
复制代码
  • 每一个文件就是一个模块,每一个模块都是Module类的一个实例。

1561816770931

  • 从上面图中,能够知道moduleglobal全局对象的一个属性。
  • 能够观察到在global对象也有一个全局函数require(),global对象的require()是对module.require()函数的进一步抽象和封装。

AMD规范

AMD是"Asynchronous Module Definition"的缩写,意思就是"异步模块定义"。它采用异步方式加载模块,模块的加载不影响它后面语句的运行。全部依赖这个模块的语句,都定义在一个回调函数中,等到加载完成以后,这个回调函数才会运行。


为何会有AMD规范

有了服务器端模块之后,很天然地,你们就想要客户端模块。并且最好二者可以兼容,一个模块不用修改,在服务器和浏览器均可以运行。

可是,因为一个重大的局限,使得CommonJS规范不适用于浏览器环境。仍是上一节的代码,若是在浏览器中运行,会有一个很大的问题。

var math = require('math');
math.add(2, 3);
复制代码

第二行math.add(2, 3),在第一行require('math')以后运行,所以必须等math.js加载完成。也就是说,若是加载时间很长,整个应用就会停在那里等。

这对服务器端不是一个问题,由于全部的模块都存放在本地硬盘,能够同步加载完成,等待时间就是硬盘的读取时间。可是,对于浏览器,这倒是一个大问题,由于模块都放在服务器端,等待时间取决于网速的快慢,可能要等很长时间,浏览器处于"假死"状态。

所以,浏览器端的模块,不能采用"同步加载"(synchronous),只能采用"异步加载"(asynchronous)。这就是AMD规范诞生的背景。

AMD规范的使用

AMD也采用require()语句加载模块,可是不一样于CommonJS,它要求两个参数:

require([module], callback);
复制代码

第一个参数[module],是一个数组,里面的成员就是要加载的模块;第二个参数callback,则是加载成功以后的回调函数。若是将前面的代码改写成AMD形式,就是下面这样:

require(['math'], function (math) {
    math.add(2, 3);
});
复制代码

math.add()math模块加载不是同步的,浏览器不会发生假死。因此很显然,AMD比较适合浏览器环境。

目前,主要有两个Javascript库实现了AMD规范:require.jscurl.js

AMD规范的写法

require.js 加载的模块,采用 AMD规范。也就是说,模块必须按照 AMD的规定来写。 具体来讲,就是模块必须采用特定的 define()函数来定义。若是一个模块不依赖其余模块。那么能够直接定义在 define() 函数之中。 假定如今有一个 math.js 文件,它定义了一个math模块。那么,math.js 就要这样写:

// math.js
define(function (){
&emsp;var add = function (x,y){
&emsp;&emsp;return x+y;
&emsp;};
&emsp;return {
&emsp;&emsp;add: add
&emsp;};
});
复制代码

加载方法以下:

// main.js
require(['math'], function (math){
&emsp;alert(math.add(1,1));
});
复制代码

若是这个模块还依赖其余模块,那么define() 函数的第一个参数,必须是一个数组,指明该模块的依赖性。

define(['myLib'], function(myLib){
&emsp;function foo(){
&emsp;&emsp;myLib.doSomething();
&emsp;}
&emsp;return {
&emsp;&emsp;foo : foo
&emsp;};
});
复制代码

require() 函数加载上面这个模块的时候,就会先加载myLib.js文件。

加载非规范的模块

理论上,require.js 加载的模块,必须是按照 AMD 规范、用 define()函数定义的模块。可是实际上,虽然已经有一部分流行的函数库(好比jQuery)符合 AMD规范,更多的库并不符合。那么,require.js 是否可以加载非规范的模块呢? 回答是能够的。 这样的模块在用 require() 加载以前,要先用 require.config()方法,定义它们的一些特征。 举例来讲,underscorebackbone 这两个库,都没有采用 AMD规范编写。若是要加载它们的话,必须先定义它们的特征。

require.config({
&emsp;shim: {
&emsp;&emsp;'underscore': {
&emsp;&emsp;&emsp;exports: '_'
&emsp;&emsp;},
&emsp;&emsp;'backbone': {
&emsp;&emsp;&emsp;deps: ['underscore', 'jquery'],
&emsp;&emsp;&emsp;exports: 'Backbone'
&emsp;&emsp;}
&emsp;}
});
复制代码

require.config() 接受一个配置对象,这个对象除了有前面说过的paths 属性以外,还有一个 shim属性,专门用来配置不兼容的模块。具体来讲,每一个模块要定义: (1)exports 值(输出的变量名),代表这个模块外部调用时的名称; (2)deps 数组,代表该模块的依赖性。 好比,jQuery 的插件能够这样定义:

shim: {&emsp;
    'jquery.scroll': {&emsp;&emsp;
        deps: ['jquery'],&emsp;
        exports: 'jQuery.fn.scroll'
    }
}
复制代码

CMD 规范

CMDCommon Module definition的缩写,即通用模块定义。CMD规范是国内发展出来的,就像AMD有个requireJS,CMD有个浏览器的实现SeaJS,SeaJS要解决的问题和requireJS同样,只不过在模块定义方式和模块加载(能够说运行、解析)时机上有所不一样

CMD规范的写法

  • 全局函数define,用来定义模块。
  • 参数 factory 能够是一个函数,也能够为对象或者字符串。
  • 当 factory 为对象、字符串时,表示模块的接口就是该对象、字符串。

CMD中,一个模块就是一个文件,格式为:

define( factory );
复制代码
  1. 定义JSON数据模块:
define({ "foo": "bar" });
复制代码

2.经过字符串定义模板模块:

define('this is `data`.');
复制代码
  1. factory 为函数的时候,表示模块的构造方法,执行构造方法即可以获得模块向外提供的接口。

    • require 是一个方法,接受模块标识做为惟一参数,用来获取其余模块提供的接口:require(id)


      define(function( require, exports ){ 
          var a = require('./a');
          a.doSomething(); 
      });
      复制代码

      require是同步往下执行的,须要的异步加载模块可使用 require.async 来进行加载:

      define( function(require, exports, module) { 
          require.async('.a', function(a){ 
              a.doSomething(); 
          }); 
      });
      复制代码

      require.resolve( id )可使用模块内部的路径机制来返回模块路径,不会加载模块。


    • exports 是一个对象,用来向外提供模块接口

    • module 是一个对象,上面存储了与当前模块相关联的一些属性和方法

    define( function(require, exports, module) { 
     // 模块代码 
    });
    复制代码

示例:

// math.js

define(function(require,exports,module) {
  exports.add = function() {
    var sum = 0,i = 0,args = arguments,l = args.length;
    while (i < l) {
      sum += args[i++];
    }
    return sum;
  };
});
复制代码
// increment.js

define(function(require,exports,module) {
  var add = require('math').add;
  exports.increment = function(val) {
    return add(val,1);
  };
});
复制代码
// program.js

define(function(require ,exports,module) {
  var inc = require('increment').increment;
  var a = 1;
  inc(a); // 2

  module.id == "program";
});
复制代码

AMD规范和CMD规范的比较

  1. 对于依赖的模块,AMD 是提早执行,CMD是延迟执行。CMD 推崇 as lazy as possible
  2. CMD 推崇依赖就近,AMD 推崇依赖前置。
// CMD
define(function(require, exports, module) { 
    var a = require('./a') 
    a.doSomething() 
    // 此处略去 100 行 
    var b = require('./b') // 依赖能够就近书写 
    b.doSomething() 
    // ... 
});

// AMD 默认推荐的是
define(['./a', './b'], function(a, b) { // 依赖必须一开始就写好 
    a.doSomething() 
    // 此处略去 100 行 
    b.doSomething() 
    // ... 
})
复制代码
  1. 二者的使用加载机制不一样,也就致使了AMD(requirejs)模块会提早执行**,用户体验好,**而CMD(seajs)性能好,由于只有在须要时候才执行。

ES6模块规范

ES6的语言规格中引入了模块化功能,也就很好的取代了以前的commonjsAMD规范,成为了浏览器和服务器的通用的模块解决方案,在现今(vuejs,ReactJS)等框架大行其道中,都引入了ES6中的模块化(Module)机制。

Es6中模块导出的基本语法

模块的导出,export关键字用于暴露数据,暴露给其余模块

使用方式是,能够将export放在任何变量,函数或类声明的前面,从而将他们从模块导出,而import用于引入数据,例如以下所示:

将下面这些js存储到exportExample.js中,分别导出的是数据,函数,类:

exportExample.js

// 1. 导出数据,变量前面加上export关键字
export var  name = "随笔川迹"; // 导出暴露name变量
export let  weChatPublic = "itclanCoder"; // 暴露weChatPublic
export const time = 2018; // 暴露time


// 2. 导出函数,函数前面加上export关键字
export function sum(num1,num2){
      return num1+num2;
 }
/* * * 以上等价于 * function sum(num1,num2){ * return num1+num2; * } * export sum; * 也能够这样:在定义它时没有立刻导出它,因为没必要老是导出声明,能够导出引用,所以下面这段代码也是能够运行的 */

// 3. 导出类,类前面加上export关键字
export class People{
      constructor(name,age){
         this.name = name;
         this.age = age;
      }
      info(){
         return `${this.name}${this.age}岁了`;
      }
 }
复制代码

注意:一个模块就是一个独立的文件,该文件内部的全部变量,外部没法获取,一样,任何未显示导出的变量,函数或类都是模块私有的,若没有用export对外暴露,是没法从模块外部访问的 例如:

function countResult(num1,num2){
       return num1-num2;
 }
// 没有经过export关键字导出,在外部是没法访问该模块的变量或者函数的
复制代码

对应在另外一个模块中经过import导入以下所示,模块命名为importExample.js

import { name, weChatPublic,time,sum,People} from "../modelTest1/exportExampleEs5.js"

var people = new People("小美",18); // 实例化perople对象
console.log(name);
console.log(weChatPublic);
console.log(time);
console.log(sum(1,2));
console.log(people.info());
复制代码

注意:在上面的示例中,除了export关键字外,每个声明与脚本中的如出一辙,由于导出的函数和类声明须要有一个名称,因此代码中的每个函数或类也确实有这个名称,除非用default关键字,不然不能用这个语法导出匿名函数或类。

Es6中模块导入的基本语法

若是想从一个文件(模块)访问另外一个文件(模块)的功能,则须要经过import关键字在另外一个模块中引入数据,import语句的两个部分组成分别是**:要导入的标识符标识符应当从那个模块导入,**另外,导入的标识符的顺序能够是任意位置,可是导入的标识符(也就是大括号里面的变量)与export暴露出的变量名应该是一致的。具体的写法以下:

import {identifer1,indentifer2}  from "./example.js"  // import {标识符1,标识符2} from "本地
复制代码

1. 导入单个绑定

// 只导入一个
 import {sum} from "./example.js"
  console.log(sum(1,2));  // 3
  sum = 1; // 抛出一个错误,是不能对导入的绑定变量对象进行改写操做的
复制代码

2. 导入多个绑定

若是想从示例模块中导入多个绑定,与单个绑定类似,多个绑定值之间用逗号隔开便可:

// 导入多个
import {sum,multiply,time} from "./exportExample.js"
console.log(sum(1,2)); // 3
console.log(multiply(1,2)); // 3
console.log(time);  // 2018
复制代码

在这段代码中,从exportExample.js模块导入3个绑定,sum,multiplytime以后使用它们,就像使用本地定义的同样 等价于下面这个: **无论在import语句中把一个模块写了多少次,该模块将只执行一次,导入模块的代码执行后,实例化过的模块被保存在内存中,**只要另外一个import语句使用它就能够重复使用它.

import {sum} from "./exportExample.js"
import {multiply} from "./exportExample.js"
import {time} from "./exportExample.js
复制代码

3. Es6中导入整个模块

特殊状况下,能够导入整个模块做为一个单一的对象,而后全部的导出均可以做为对象的属性使用,例如:

// 导入一整个模块
import * as example from "./exportExample.js"
console.log(example.sum(1,example.time));
consoole.log(example.multiply(1,2));// multiply与sum函数功能同样
复制代码

在上面这段代码中,从本地模块的exportExample.js中导出的全部绑定被加载到一个被称做为example的对象中,指定的导出sum()函数,multiply()函数和time以后做为example的属性被访问,这种导入格式被称为命名空间导入,由于exportExample.js文件中不存在example对象,因此它被做为exportExample.js中全部导出成员的命名空间对象而被建立

Es6中如何给导入导出时标识符重命名

从一个模块导入变量,函数或者类时,咱们可能不但愿使用他们的原始名称,就是导入导出时模块内的标识符(变量名,函数,或者类)能够不用一一对应,保持一致**,**能够在导出和导入过程当中改变导出变量对象的名称

使用方式①: 使用as关键字来指定变量,函数,或者类在模块外应该被称为何名称,例如以下一函数:

function sum(num1,num2){
     return num1+num2;
}
export {sum as add} // as后面是从新指定的函数名
复制代码

如上代码,函数sum是本地名称,add是导出时使用的名称,换句话说,当另外一个模块要导入这个函数时,必须使用add这个名称:

若在importExample.js一模块中,则导入的变量对象应是add而不是sum,是由它导出时变量对象决定的

import {add} from "./exportExample.js"
复制代码

使用方式②: 使用as关键字来指定变量,函数,或者类在主模块内应该被称为何名称,例如以下一函数:

// exportExample.js
export function sum(num1,num2){
     return num1+num2;
}
复制代码
// importExample.js
import {sum as add} from "./exportExample.js"
console.log(sum(1,2)); // 3
复制代码

如上代码导入add函数时使用了一个导入名称来重命名sum函数,注意这种写法与前面导出export时的区别,使用import方式时,从新命名的标识符在前面,as后面是本地名称,可是这种方式,即便导入时改变函数的本地名称,即便模块导入了add函数,在当前模块中也没有add()标识符,如上对add的类型检测就是很好的验证.

ES6匿名方式的导入和导出

若是在不给导出的标识符(变量,函数,类)呢,那么能够经过导出default关键字指定单个变量,函数或者类, 在import的时候, 名字随便写, 由于每个模块的默认接口就一个。

//a.js 
let sex = "boy"; 
export default sex //(sex不能加大括号) 
//本来直接export sex外部是没法识别的,加上default就能够了.可是一个文件内最多只能有一个export default。 其实此处至关于为sex变量值"boy"起了一个系统默认的变量名default,天然default只能有一个值,因此一个文件内不能有多个export default。
复制代码
// b.js 
//本质上,a.js文件的export default输出一个叫作default的变量,而后系统容许你为它取任意名字。因此能够为import的模块起任何变量名,且不须要用大括号包含 
import any from "./a.js" 
import any12 from "./a.js" 
console.log(any,any12) // boy,boy
复制代码

参考连接

javascript模块化编程

前端模块化:CommonJS,AMD,CMD,ES6

深刻了解CommonJS的模块实现原理

面试官说:说一说CommonJS的实现原理

详解Node全局变量global模块

JavaScript AMD 与 CMD 规范

AMD,CMD 规范详解

30分钟学会前端模块化开发

分析 Babel 转换 ES6 module 的原理

相关文章
相关标签/搜索