Javascript:什么是Module?Module的规范以及Module应用

509 阅读10分钟

前言

在其他编程语言中,我们可以通过划分模块,来组织庞大复杂的项目,而JS一开始并没有模块的概念,因为一开始JS的脚本就很简单。

后来随着JS的发展,前端要开发的项目越来越复杂,也越来越大型,因此需要更好的代码组织方式,于是JS也引入了模块功能,比如AMD,UMD,CommonJS等等不同的模块加载方式。

而在ES6规范中,也引入了官方的模块加载方式,也就是我们下面要讨论的知识。

什么是模块(Module )

在JS中,一个文件或者脚本就是一个模块,模块可以声明哪些变量或函数供外部使用,也可以引入其他模块的变量与函数为自己所用,在模块中,使用export标记了可以从当前模块外部访问的变量和函数,使用import从其他模块导入所需要的功能,如: hello.js

//声明当前模块可以被其他模块使用的变量
export function sayHello() {
   alert('Hello');
}

main.js

//导入其他模块的变量
import {sayHello} from './hello.js';
alert(sayHello);
sayHello(); 

在浏览器中使用模块

在支持ES6规范的浏览器中,可以使用用<script type="module"></script>标签可以声明模块或者导入其他模块,如:

<!doctype html> 
<script type="module"> 
   import {sayHi} from './say.js';
   document.body.innerHTML = sayHi('John'); 
</script>

提示:目前前端项目开发中,很少使用上面这种在浏览器中使用模块的方式(不排除有少数),我们一般是创建一个工程项目,再通过webpack这类的构建工具解析各个模块的导出与依赖,最终打包上线的。

模块的特性

现代模式

模块的代码始终默认使用现代模式use strict,例如对一个未声明的变量赋值或者重复声明变量都将产生错误。

<script type="module">
   test = 5; // 未声明就使用,报错
   let a = 1;
   let a = 2;//重复声明,报错
</script>

上面的示例中,如果把type=module去掉,变成非模块代码,则可以正常运行。

模块代码解析

如果同一个模块被导入多次,那么它的代码只在第一次导入时便解析运行,比如下面的示例中,只会弹窗一次:

hello.js

alert(1);

index.html

<script type="module" src="./hello.js"></script>
<script type="module" src="./hello.js"></script>
this的值

在JS模块中,this关键字的值undefined,非模块脚本的顶级 this 是全局对象,比如浏览器的window。 非模块代码

<script>
   alert(this)
</script>

输出Object 模块代码

<script type="module">
   alert(this)//undefined
</script>

输出undefined

export

export命令用于模块内部导出可供外部使用的变量,导出的方式有:

在声明前导出

可以在变量或函数声明加上export命令,便可以直接导出该变量或函数,如:

export function sayHello(){
   return "Hello";
}
export let a = 1;

导出与声明分开

也可以在声明之后,再使用export命令进行导出,不过这时候,导出的变量需要用花括号{}包括起来,如:

//声明
function sayHello(){
   return "Hello";
}
//导出
export {sayHello};//正确
//export sayHello;错误

也可以同时导出多个变量,如:

function sayHello(){
   return "Hello";
}
const username='test';
export {username,sayHello}

默认导出

如果直接使用export导出,则使用者必须知道模块导出的变量名称,否则就无法加载,因为使用者需要先了解你的模块代码,但如果想让使用者直接使用而不用了解你的代码,则可以使用export default来指定模块的默认输出。 hello.js

export default function(){
   return "hello"
}

导出时给个别名

在导出的时候,可以使用关键字as给导出的变量重新命名,如:

function sayHello(){
   return "Hello";
}
export {sayHello as sayHi}

import

import命令用于在当前模块中导出其他模块的可用变量与函数。

整体导入

如果要导入一个模块的全部变量与函数,可以使用*导出全部,但这种不按需导入的方式,可以会导入很多你不需要的东西,因此一般不推荐使用:

import * from './hello.js'

另外,可以使用as关键字给整体导出起一个别名,如:

import * as say from './hello.js';
say.hello();

按需导入

如果我们只需要导入其他模块的部分功能,则可以使用花括号{}来指定要导出的变量与函数,如:

import {sayHi,sayHello} from './hello.js';

导入时给个别名

在导入的时候,也可以使用关键字as给导入的变量与函数起个别名,如:

import {sayHi as hi, sayBye as bye} from './hello.js';

Module的应用

主流模块规范

目前主流模块规范有:

规范名称运行环境实现加载方式
AMD(异步模块定义)客户端require.js异步
CMD(通用模块定义)客户端sea.js异步
CommonJS服务端NodeJS同步(动态加载)
es6客户端es6静态加载

AMD和CMD(ES5)

AMD 和 CMD 加载多个文件时都是异步加载

区别:

  1. AMD推崇依赖前置,在定义模块的时候就要声明其依赖的模块 |  AMD (中文版) · amdjs/amdjs-api Wiki · GitHub
  2. CMD推崇就近依赖,只有在用到某个模块的时候再去require | CMD 模块定义规范 · Issue #242 · seajs/seajs · GitHub

AMD

required.js 是 AMD 的实现 使用:define(id?, dependencies?, factory);;

// 定义模块 myModule.js
define(['dependency'], function(){
   var name = 'Byron';
   function printName(){
       console.log(name);
   }
   return {
       printName: printName
   };
});
// 加载模块
require(['myModule'], function (my){
 my.printName();
});

CMD

sea.js 是 CMD 的实现 CMD 中一个模块就是一个文件 使用:define(id?, dependencies?, factory);

// 定义模块  myModule.js
define(function(require, exports, module) {
 var $ = require('jquery.js')
 $('div').addClass('active');
});
// 加载模块
seajs.use(['myModule.js'], function(my){
});

CommonJS(ES5)

CommonJS 模块就是对象,输入时必须查找对象属性。

  • CommonJS 的加载称为“运行时加载”或者动态加载
  • 一个文件就是一个模块,每个模块都是单独的作用域

*特点:

  • 所有代码都运行在模块作用域,不会污染全局作用域。
  • 模块可以多次加载,但是只在第一次加载时运行一次,然后运行结果就被缓存了,以后再加载,就直接读取缓存结果。要想让模块再次运行,必须清除缓存。
  • 模块加载的顺序,按照其在代码中出现的顺序。

module.exports

每个模块内部,都有一个module对象,代表当前模块。它有以下属性。

  • module.id 模块的识别符,通常是带有绝对路径的模块文件名。
  • module.filename 模块的文件名,带有绝对路径。
  • module.loaded 返回一个布尔值,表示模块是否已经完成加载。
  • module.parent 返回一个对象,表示调用该模块的模块。
  • module.children 返回一个数组,表示该模块要用到的其他模块。
  • module.exports 表示模块对外输出的值
module.exports = 123

exports

为了方便,Node为每个模块提供一个 exports 变量,指向 module.exports

注意:

exports 变量直接指向一个单一值。单一值只能使用 module.exports 输出

// 相当于每个模块顶部都有这个声明
var exports = module.exports;
// 可以给导出的对象添加属性
exports.area = function (r) {
 return Math.PI * r * r;
};
// 不可以导出一个单一值
exports = 123; // 无效

require

require 命令用于加载模块文件,返回该模块的 exports 对象

commonJS规范

Module(ES6)

ES6 模块不是对象,而是通过 export 命令显式指定输出的代码,再通过 import 命令输入。

  • ES6 的模块是“编译时加载”或者静态加载
  • ES6 使用基于文件的模块,也就是说一个文件一个模块
  • ES6 模块是单例。也就是说,模块只有一个实例,其中维护了它的状态。每次向其他模块导入这个模块的时候,得到的是对单个中心实例的引用。
  • ES6 模块的API是静态的。导出后的API是只读状态
  • 模块的公开 API 中暴露的属性和方法并不仅仅是普通的值或引用的赋值。它们是到内部模块定义中的标识符的实际绑定(几乎类似于指针)。
  • ES6 的模块自动采用严格模式

优点:

  1. 编译时加载方法,效率高
  2. 不再需要对象作为命名空间了

缺点:

  1. es6模块不是对象,所以无法引用模块本身

export

通过 export 命令规定模块的对外接口。有两种模式:

  • 命名导出(可以多个):export **
  • 默认导出(只有一个):export default
// 导出单个
export let name1 = 1;
// 导出多个
export { name1, name2, ...};
// 重命名导出
export {
 name1 as master
}
// 解构导出并重命名
export const { name1, name2: bar } = o;
// 默认导出
export default expression;
export { name1 as default, … };
// 聚合模块 - 输入后立马输出
export * from …; // does not set the default export
export * as name1 from …;
export { name1, name2, …, nameN } from …;
export { import1 as name1, import2 as name2, …, nameN } from …;
export { default } from …;

import

通过 import 命令加载模块

  • import 是静态的,在编译时就加载
  • import 命令会提升到顶部

使用:

  • 可导入整个模块内容:使用 * 指定一个对象,所有输出值都加载在这个对象上
  • 可导入单个接口:使用大括号包裹需引入值
  • 可导入多个接口:使用大括号包裹需引入值,多个值用逗号分隔
  • 可导入有别名的接口:使用 as 可以设置别名
  • 执行加载模块:直接 import 某个模块,会执行,但不会输入任何值
  • 导入默认值:不需要大括号,直接指定任意值。对应模块内的 export default
// 执行 lodash 模块,但不输入任何值
import 'lodash';
// 非默认导出需有大括号。可以使用 as 设置别名
import { firstName as fn, persion } from './profile.js';
// export default 默认导出不需要括号
import class from './class.js';
// 变量不允许改写
fn = 123; // Syntax Error : 'firstName' is read-only;
// 对象属性可以改写 - 不建议
persion.age = 18;

动态import

import() 可以实现动态加载

  • import() 可以像调用函数一样动态导入模块,并返回一个promise
  • import() 支持 await 关键字
  • import() 可以在commonJS 模块中运行

使用场景:

  • 不需要马上使用的模块(可用于异步加载,提高性能)
  • 需要根据场景判断才引入的模块
import('/modules/my-module.js')
.then((module) => {
 // Do something with the module.
});
async getModule() {
 if (true) {
   let module = await import('/modules/my-module.js');
 }
}

模块(module)循环引用/循环加载【问题解决】

"循环加载"(circular dependency)指的是,a脚本的执行依赖b脚本,而b脚本的执行又依赖a脚本。

// a.js
var b = require('b');
// b.js
var a = require('a');

通常,"循环加载"表示存在强耦合,如果处理不好,还可能导致递归加载,使得程序boom....无法执行。

如何解决?commonJS 和 Es6 的module 给出了不同的解决方案。

commonJS 模块

commonJS 是动态加载,执行一次就会缓存结果。如果出现某个模块被"循环加载",返回的是当前已经执行的部分的值,而不是代码全部执行后的值。所以,输入变量时需要非常小心。

a.js

exports.done = false;
var b = require('./b.js');
console.log('在 a.js 之中,b.done = %j', b.done);
exports.done = true;
console.log('a.js 执行完毕');

b.js

exports.done = false;
var a = require('./a.js');
console.log('在 b.js 之中,a.done = %j', a.done);
exports.done = true;
console.log('b.js 执行完毕');

main.js

var a = require('./a.js');
var b = require('./b.js');
console.log('在 main.js 之中, a.done=%j, b.done=%j', a.done, b.done);

执行main.js

node main.js
在 b.js 之中,a.done = false [解释1]
b.js 执行完毕
在 a.js 之中,b.done = true [解释2]
a.js 执行完毕
在 main.js 之中, a.done=true, b.done=true
  • 解释1:b中引用a输出的时候a.done时候,a只执行到了赋值done=false,还没到下面的赋值done=true,所以这里输出的是false,(适用已执行的部分值,非全部执行完毕后的值理论)
  • 解释2:

ES6 模块(module)

ES6 是静态加载,不会缓存结果。 它只是生成一个指向被加载模块的引用,每次都会根据引用动态去加载模块取值。

// a.mjs
import {bar} from './b';
console.log('a.mjs');
console.log(bar);
export let foo = 'foo';
// b.mjs
import {foo} from './a';
console.log('b.mjs');
console.log(foo);
export let bar = 'bar';

执行

node --experimental-modules a.mjs
b.mjs
ReferenceError: foo is not defined -> 原因: foo未定义。

解决:将foo写成函数,使它产生函数提升。函数表达式不行,因为它不能提升

// a.mjs
import {bar} from './b';
console.log('a.mjs');
console.log(bar());
function foo() { return 'foo' }
export {foo};
// b.mjs
import {foo} from './a';
console.log('b.mjs');
console.log(foo());
function bar() { return 'bar' }
export {bar};

执行

$ node --experimental-modules a.mjs
b.mjs
foo
a.mjs
bar

例子2:

// even.js
import { odd } from './odd'
export var counter = 0;
export function even(n) {
 counter++;
 return n === 0 || odd(n - 1);
}
// odd.js
import { even } from './even';
export function odd(n) {
 return n !== 0 && even(n - 1);
}

执行

$ babel-node
> import * as m from './even.js';
> m.even(10);
true
> m.counter
6
> m.even(20)
true
> m.counter
17

Module应用案例