前言

在项目开发过程中,引入模块有时使用require(),有时使用import…from..,导出模块有时用export或export default,一直会这么使用,但是没有太在意区别,以及它们分别遵循的是哪个模块规范。接下来就直接介绍几种模块的使用。

具体内容

一、模块概念介绍

模块通常是指编程语言所提供的代码组织机制,利用此机制可将程序拆解为独立且通用的代码单元;

所谓模块化主要是解决代码分割、作用域隔离、模块之间的依赖管理以及发布到生产环境的自动化打包与处理等多方面;

模块规范主要是进行模块加载,一个单独的文件也算是一个模块,一个模块就是一个单独的作用域,不会污染全局作用域;

一个模块就是一个对其他模块暴露自己的属性或者方法的文件。

二、模块的优点

(1)可维护性。 因为模块是独立的,一个设计良好的模块会让外面的代码对自己的依赖越少越好,这样自己就可以独立去更新和改进。

(2)命名空间。 在 JavaScript 里面,如果一个变量在最顶级的函数之外声明,它就直接变成全局可用。因此,常常不小心出现命名冲突的情况。使用模块化开发来封装变量,可以避免污染全局环境。

(3)重用代码。 有时从之前写过的项目中拷贝代码到新的项目,这没有问题,但是更好的方法是,通过模块引用的方式,来避免重复的代码库。

三、ES6CommonJSAMD、CMD、UMD模块介绍

(1)ES6模块(Module)

ES6的模块自动采用严格模式,不管是否在模块头部加上”use strict”;

严格模式主要有以下这些限制:

变量必须声明后再使用
函数的参数不能有同名属性,否则报错
不能使用with语句
不能对只读属性赋值,否则报错
不能使用前缀0表示八进制数,否则报错
不能删除不可删除的属性,否则报错
不能删除变量delete prop,会报错,只能删除属性delete global[prop]
eval不会在它的外层作用域引入变量
eval和arguments不能被重新赋值
arguments不会自动反映函数参数的变化
不能使用arguments.callee
不能使用arguments.caller
禁止this指向全局对象
不能使用fn.caller和fn.arguments获取函数调用的堆栈
增加了保留字(比如protected、static和interface)

•导出模块   export

作为一个模块,可以选择性地给其他模块暴露自己的属性和方法,供其他模块使用

1 //profile.js
2 export var firstName = "wang";
3 export var lastName = "qin";
4 export var born = "1996";
5 //等价于
6 var firstName = "wang";
7 var lastName = "qin";
8 var born = "1996";
9 export {firstName,lastName,born}

1、通常情况下,export输出的变量就是本来的名字,但是可以使用as关键字对输出的变量进行重命名

1 function v1(){}
2 function v2(){}
3 
4 export{
5     v1 as stream1,
6     v2 as stream2,
7     v2 as streamLatestVersion
8 };
9 //使用as关键字,重命名函数v1和v2的对外接口;重命名之后,v2可以用不同的名字输出2次

2、 需要特别注意的是,export命令规定的是对外的接口,必须与模块内部的变量建立一 一对应关系

 1 //注意:以下2种写法报错是因为没有提供对外的接口;第1种写法直接输出1;
 2 //第2种写法通过变量m,还是直接输出1,1只是一个值,不是对外接口;
 3 
 4 //报错
 5 export 1;     
 6 
 7 //报错
 8 var m = 1;
 9 export m;
10 
11 //以下3种写法都是正确的,规定了对外的接口m;其他脚本通过访问这个接口,可以取到1;
12 //实质就是在接口名与模块内部变量之间建立了一一对应关系;
13 //方法一
14 export var m = 1;
15 //方法二
16 var m = 1;
17 export {m};
18 //方法三
19 var n = 1;
20 export {n as m};

3、export命令和import命令可以出现在模块的任何位置,只要处于模块顶层就可以。如果处于块级作用域内,就会报错

1 function foo(){
2     export default 'bar'     //语法错误
3 }
4 foo();

•默认导出模块   export default

每个模块支持我们导出一个没有名字的变量,使用关键语句export default来实现

1 //module.js
2 export default function(){   //使用export default关键字对外导出一个匿名函数
3     console.log('I am default Fn');
4 }
5 
6 //导入上面模块时,可以为该匿名函数取任意名字
7 import defaultFn from './module.js'
8 defaultFn();      //输出"I am default Fn"

1、默认输出和正常输出的比较

1 export default function diff(){}     //默认导出
2 import diff from 'diff';      //导入
3 
4 
5 export function diff(){}    //正常导出
6 import {diff} from 'diff';   //导入
7 
8 //注意:使用export default默认导出时,对应的import语句不需要使用大括号;
9 //而使用export正常导出时,对应的import语句需要使用大括号;

注意:export default命令用于指定模块的默认输出。显然,一个模块只能有一个默认输出,因此export default命令只能使用一次。所以,import命令后面才不用加大括号,因为只可能对应一个方法。

2、export default本质是将该命令后面的值,赋给default变量以后再默认,所以直接将一个值写在export default之后

1 //正确;指定对外接口为default
2 export default 66;   
3 
4 //报错;因为没有指定对外的接口
5 export 66;    

3、在一条import语句中,同时导入默认方法和其他变量,可以写成下面这样

1 export default function(){}     //默认导出
2 export function each(obj,iterator,context){}    //正常导出
3 
4 //同时导入默认方法和其他变量
5 import _,{each} from 'loadsh'

•导入模块   import

作为一个模块,可以根据需要,引入其他模块的提供的属性或者方法,供自己模块使用

1、 import命令接受一对大括号,里面指定要从其他模块导入的变量名。大括号里面的变量名,必须与被导入模块(profile.js)对外接口的名称相同。如果想为输入的变量重新取一个名字,import命令要使用as关键字,将输入的变量重命名

 1 import {lastName as realName} from ‘./profile.js’  

2、import后面的from指定模块文件的位置,可以是相对路径,也可以是绝对路径,.js路径可以省略。如果只是模块名,不带有路径,那么必须有配置文件,告诉 JavaScript 引擎该模块的位置

3、import命令具有提升效果,会提升到整个模块的头部,首先执行

1 foo();
2 import { foo } from 'my_module';
3 //先调用foo()不会报错,原因是import的执行早于foo()的调用;
4 //本质是因为import命令是在编译阶段执行,在代码运行之前;

4、由于import是静态执行,所以不能使用表达式和变量,这些只有在运行时才能得到结果的语法结构

 1 //报错
 2 import { 'f' + 'oo' } from 'my_module';
 3  
 4 //报错
 5 let module = 'my_module';
 6 import { foo } from module;
 7  
 8 //报错
 9 if (x === 1) {
10   import { foo } from 'module1';
11 } else {
12   import { foo } from 'module2';
13 }

5、import语句会执行所加载的模块

 1 import ‘loadsh’ //仅仅执行loadsh模块,但不输入任何值 

(2)CommonJS

CommonJS 最开始是 Mozilla 的工程师于 2009 年开始的一个项目,它的目的是让浏览器之外的 JavaScript (比如服务器端或者桌面端)能够通过模块化的方式来开发和协作。

在 CommonJS 规范中,每个 JavaScript 文件就是一个独立的模块上下文(module context),在这个上下文中默认创建的属性都是私有的。也就是说,在一个文件定义的变量(还包括函数和类),都是私有的,对其他文件是不可见的。

 需要注意的是,CommonJS 规范的主要适用场景是服务器端编程,采用同步加载模块的策略。

如果某个文件依赖3个模块,代码会一个一个依次加载它们。

CommonJS模块化规范实现方案主要包含 require 与 module.exports( exports ) 这两个关键字,允许某个模块对外暴露部分接口并且由其他模块导入使用。

 1 //======sayHello.js=======
 2 function sayHello(){
 3     this.hello = function(){
 4         console.log('hello');
 5     };
 6     this.hi = function(){
 7         console.log('hi');
 8     }
 9 }
10 module.exports = sayHello;
11 
12 //=======main.js===========
13 var say = require('./sayHello.js');
14 var sayPlay = new say();
15 sayPlay.hello();     //hello

作为服务器端的解决方案,CommonJS需要一个兼容的脚本加载器(即NodeJS)作为前提条件,该脚本加载器必须支持名为require和module.exports的函数,它们可以将模块相互导入导出。

==================补充说明:Node.js=======================

1、Node是对CommonJS模块规范的实现。

2、Node.js模块分为两大类:一类是核心模块,一类是文件模块;

核心模块:就是Node.js标准API中提供的模块,如fs/http/path等,这些由Node.js官方提供的模块,编译成了二进制代码,可以直接通过require获取核心模块,例如require(‘fs’),核心模块拥有最高的加载优先级,如果有模块与核心模块命名冲突,Node.js总是会加载核心模块。

文件模块:是存储为单独的文件(或文件夹)的模块,可能是JavaScript代码、JSON或编译好的C/C++代码。在不显式指定文件模块扩展名的时候,Node.js会分别试图加上.js、.json、.node(编译好的C/C++代码)。

        Node.js会根据后缀名来决定3类文件模块的加载方法

        .js   通过fs模块同步读取js文件并编译执行。

        .node 通过C/C++进行编写的Addon。通过dlopen方法进行加载。

        .json 读取文件,调用JSON.parse解析加载。

 1 /* js后缀的编译过程:【Node.js在编译js文件的过程中实际完成的步骤有对js文件内容进行头尾包装*/
 2 //==========circle.js===========
 3 var PI = Math.PI;
 4 exports.area = function (r) {
 5     return PI * r * r;
 6 };
 7 exports.circumference = function (r) {
 8     return 2 * PI * r;
 9 };
10  
11 //============app.js============
12 var circle = require('./circle.js');
13 console.log( 'The area of a circle of radius 4 is ' + circle.area(4));
14  
15 
16 //==============app包装后=========
17 (function (exports, require, module, __filename, __dirname) {
18     var circle = require('./circle.js');
19     console.log('The area of a circle of radius 4 is ' + circle.area(4));
20 });
21  
22 //这段代码会通过原生模块vmrunInThisContext方法执行
23 //(类似eval,只是具有明确上下文,不污染全局),返回为一个具体的function对象。
24 //最后传入module对象的exports,require方法,module,文件名,目录名作为实参并执行

3、加载方式

按路径加载模块

如果require参数以” / “开头,那么就以绝对路径的方式查找模块名称,如果参数以” ./ “、” ../ “开头,那么则是以相对路径的方式来查找模块。

 查找node_modules目录加载模块

如果require参数不以” / “、” ./ “、” ../ “开头,而该模块又不是核心模块,那么就要通过查找node_modules加载模块了。使用npm工具获取的模块/包通常就是以这种方式加载的。

4、加载缓存

Node.js模块不会被重复加载,因为Node.js通过文件名缓存所有加载过的文件模块,之后再访问相同文件模块时就不会重新加载了。 

注意: Node.js是根据实际文件名缓存的,而不是require()提供的参数缓存的,也就是说即使分别通过require(‘express’)和require(‘./node_modules/express’)加载两次,也不会重复加载,因为尽管两次参数不同,解析到的文件却是同一个。

Node.js 中的模块在加载之后是以单例化运行,并且遵循值传递原则:如果是一个对象,就相当于这个对象的引用。

 5、模块载入过程(进一步理解

加载文件模块的工作,主要由原生模块module来实现和完成,该原生模块在启动时已经被加载,进程直接调用到runMain静态方法

 1 //例如运行: node app.js
 2 
 3 Module.runMain = function () {
 4     // Load the main module--the command line argument.
 5     Module._load(process.argv[1], null, true);
 6 };
 7  
 8 //_load静态方法在分析文件名之后执行
 9 var module = new Module(id, parent);
10  
11 //根据文件路径缓存当前模块对象,该模块实例对象则根据文件名加载。
12 module.load(filename);

6、require 函数

require 引入的对象主要是函数。当 Node 调用 require() 函数,并且传递一个文件路径给它的时候,Node 会经历如下几个步骤:

Resolving:找到文件的绝对路径;

Loading:判断文件类型;

Wrapping:打包,给文件赋予一个私有作用范围。这是使 require 和 module 模块在本地引用的一种方法;

Evaluating:VM 对加载的代码进行处理;

Caching:当再次需要用这个文件时,不需要重复一遍上面步骤。

7、require.extensions 来查看对三种文件的支持情况

  Node 对每种扩展名所使用的函数及其操作:

  对 .js 文件使用 module._compile;

  对 .json 文件使用 JSON.parse;

  对 .node 文件使用 process.dlopen。

8、文件查找策略

从文件模块缓存中加载

尽管原生模块与文件模块的优先级不同,但是优先级最高的是从文件模块的缓存中加载已经存在的模块。

从原生模块加载

原生模块的优先级仅次于文件模块缓存的优先级。require方法在解析文件名之后,优先检查模块是否在原生模块列表中。以http模块为例,尽管在目录下存在一个httphttp.jshttp.nodehttp.json文件,require(“http”)都不会从这些文件中加载,而是从原生

模块中加载。 原生模块也有一个缓存区,同样也是优先从缓存区加载。如果缓存区没有被加载过,则调用原生模块的加载方式进行加载和执行。

从文件加载

当文件模块缓存中不存在,而且不是原生模块的时候,Node.js会解析require方法传入的参数,并从文件系统中加载实际的文件,加载过程中的包装和编译细节在前面说过是调用load方法。 

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

9、模块循环依赖

 1 //创建两个文件,module1.js 和 module2.js,并且让它们相互引用
 2 //========module1.js==========
 3 exports.a = 1;
 4 require('./module2');
 5 exports.b = 2;
 6 exports.c = 3;
 7     
 8 //========module2.js=============
 9 const Module1 = require('./module1');
10 console.log('Module1 is partially loaded here', Module1);

在 module1 完全加载之前需要先加载 module2,而 module2 的加载又需要 module1。这种状态下,我们从 exports 对象中能得到的就是在发生循环依赖之前的这部分。上面代码中,只有 a 属性被引入,因为 b 和 c 都需要在引入 module2 之后才能加载进来。

Node 使这个问题简单化,在一个模块加载期间开始创建 exports 对象。如果它需要引入其他模块,并且有循环依赖,那么只能部分引入,也就是只能引入发生循环依赖之前所定义的这部分

(3)AMD
AMD 是 Asynchronous Module Definition 的简称,即“异步模块定义”,是从 CommonJS 讨论中诞生的。AMD 优先照顾浏览器的模块加载场景,使用了异步加载和回调的方式。

RequireJS 是一个前端的模块化管理的工具库,遵循AMD规范。

(4)CMD
CMD(Common Module Definition),在CMD中,一个模块就是一个文件。

Sea.js遵循CMD规范,与NodeJS般的书写模块代码。

(5)UMD

统一模块定义(UMD:Universal Module Definition )就是将 AMD 和 CommonJS 合在一起的一种尝试,常见的做法是将CommonJS 语法包裹在兼容 AMD 的代码中。

结束语

  在当前项目开发过程中,主要使用ES6模块化规范:即import和export/export default;有时也使用CommonJS模块化规范,Nodejs遵循该模块化规范:即require和exports。其他模块化规范目前只作为了解,后续项目开发中进行更深入学习和总结。