模块的导来导去

目录
  1. 前记
    1. 概述
      1. 1.CommonJs
        1. 概述
        2. module对象
        3. AMD规范和CommonJS规范的兼容性
        4. require命令
        5. 模块的加载机制
      2. 2.AMD规范(异步模块定义)
      3. 3.CMD规范
  2. Module
    1. 严格模式
    2. export 命令
    3. import 命令
    4. 模块的整体加载
    5. export default命令
    6. export 和 import 的复合写法
    7. 模块的继承
    8. 跨模块变量
    9. import() 一个提案

前记

概述

几种之前的模块加载方案:CommonJS(服务器)和AMD(浏览器)。
ES6的模块加载规范可以完全取代上两者,成为浏览器和服务器通用的模块解决方案
知己知彼,方能百战不殆。先来看看之前的两种加载方式。

1.CommonJs

CommonJs规范

概述

每个文件就是一个模块,有自己的作用域。在一个文件里面定义的变量、函数、类,都是私有的,对其他文件不可见。

1
2
3
//test.js 这个文件就是一个module
var myName = 'hzhuang';
var mySex = 'boy';

在模块内部的话,module这个变量是存在的,表示当前的模块。想将myNamemySex这两个变量共享到其它模块的话,则除了设置window对象上的属性外(不推荐),可以将它们用module.exports(对外接口)方法输出出去。然后在其它模块中`require(filePath)`引入到其它的模块中。

module对象

模块内部都有一个module对象,代表当前的模块。

exports变量
内置的一个对象,等价

1
var exports = module.exports;

三点:

  • module.exports初始值是一个空对象{}。
  • exports是指向module.exports的引用。
  • require()返回的是module.exports而不是exports。

可以在这个对象中增加属性或者方法,但是不可以直接赋值。
注:如果在文件中对module.exports操作了的话,exports对象失效,所以如果模块输出一个函数,应定义在module.exports上,而不是在exports对象上。

关于module.exports与exports的差别,知道以上三点就可以了。

AMD规范和CommonJS规范的兼容性

CommonJS规范 ------- 同步加载。
AMD规范 ---------- 非同步加载。

require命令

CommonJS的内置命令,用于加载模块文件,读入并执行一个js文件。

文件加载规则:
后缀名默认是.js

规则 说明
/ 绝对路径查找模块
./ 相对路径查找模块
非以上两种 加载默认提供的核心模块(node自带/安装的模块)
非前两种的路径 则先按加载默认提供的核心模块加载到模块,然后根据路径查找到指定的模块
未查找到的情况下 添加.js,.json,.node文件后缀后继续查找。

目录加载规则:
可以解释为什么在平时的项目中,可以省略index.js的写法。

require发现参数字符串指向一个目录以后,会自动查看该目录的package.json文件,然后加载main字段指定的入口文件。如果package.json文件没有main字段,或者根本就没有package.json文件,则会加载该目录下的index.js文件或index.node文件。

也就是,可以在文件夹中增加package.json的main字段中指定入口文件。我们的项目中用的是直接取名index.js(x)的方式取的默认。

模块的缓存:
第一次加载模块时,会将这个加载的模块缓存起来。

所有的缓存保存在require.cache的对象中。想删除的话可以使用delete的方法删除对象中的某个属性。

模块的循环加载:
A加载B,B加载A的话,现有的理解是,在B中只加载A的require(B)的之前的部分。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//testA/index.js
console.log('a.js', require('../testB').x);
exports.x = 'A1';
exports.x = 'A2';

//testB/index.js
exports.x = 'B1';
console.log('b.js', require('../testA').x);
exports.x = 'B2';

//main.js
console.log('main.js ', require('./testA').x);

//结果
b.js undefined
a.js B2
main.js A2

可见,在B中只加载A的require(B)的之前的部分得证。

require.main属性:
用来判断模块是直接执行还是调用执行。

1
var result = require.main === moduleName ? '直接执行' : '调用执行';

模块的加载机制

输入的是被输出的值的拷贝。

模块中的值一旦被导出的话,就不受模块内部变量值的变化而变化了。

require的内部处理流程:

require是一个指向当前模块的module.require命令。
module.require调用Module._load。

1
2
3
4
5
6
7
8
Module._load = function(request, parent, isMain) {
//步骤:
//1. 检查Module._cache
//2. 第一次加载,没缓存过, 则创建新的Module实例,并将之保存到缓存中
//3. 使用module.load()加载指定的模块文件,读取文件内容之后使用module.compile()执行文件代码。
//4. 如果加载/解析过程报错,就从缓存删除该模块。
//5. 返回该模块的 module.exports。
}

可见,module.load方法用于加载模块,module.compile方法用来解析模块,也就是执行指定模块的脚本。

1
2
3
4
5
6
Module.prototype._compile = function(content, filename) {
// 1. 生成一个require函数,指向module.require
// 2. 加载其他辅助方法到require
// 3. 将文件内容放到一个函数之中,该函数可调用 require
// 4. 执行该函数
};

require函数准备ok后,整个所有加载的脚本内容就被放在了一个新的函数之中了,这样避免污染全局变量。

1
2
3
(function (exports, require, module, __filename, __dirname) {
// 模块的代码。
})
;

详见下文:
The Node.js Way - How require() Actually Works

下图是项目中经过webpack打包后的某个文件的部分。

2.AMD规范(异步模块定义)

简单的了解的一下。
这个规范主要是用在浏览器。

也采用require()语句加载模块,但是要求两个参数。

1
2
3
require([module], callbacl);
//第一个参数是一个数组,表示需要加载的模块。
//第二个参数是加载成功之后的回调函数。

这个规范,最常用的是require.js库。

模块定义:define
模块必须采用特定的 define() 函数来定义。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//moduleName = test   test.js
//无依赖
define(function(){
var add = function(x,y){ return x + y;};

return {
add: add
}
})

//有依赖
define(['dependModule'], function(dependModule){
var add = function(x,y){ return x + y;};
//do something else
return {
add: add
}
})

加载:

1
2
3
reuqire(['test'], function(test){
//do something
})

3.CMD规范

代表人物seaJs
一个模块就是一个文件。define 是一个全局函数,用来定义模块。

define全局函数接受一个factory参数。

factory参数是对象/字符串:
表示模块的接口就是该对象/字符串

factory参数是函数时,接受三个参数:require、exports和module。

  • require 是一个方法,接受模块标识作为唯一参数,用来获取其他模块提供的接口:require(id)
  • exports 是一个对象,用来向外提供模块接口。
  • module 是一个对象,上面存储了与当前模块相关联的一些属性和方法。

好了,这几种规范有了一个初步的了解后,再回来继续学习导来导去。

ES6的模块的设计思想是尽量的静态化,使得编译的时候就能确定模块的依赖关系,以及输入和输出的变量。
相比而言,CommonJs只能是在运行时才能确定依赖关系。

先看看CommonJS和ES6-module的差别:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/*CommonJS*/

let {A, B, C} = require('Test');

//分解来看是:
let _Test = require('Test'); //Test模块先全部加载。
let A = _Test.A;
let B = _Test.B;
let C = _Test.C;

//可见需要先将Test整个模块先加载出来。**运行时加载**

/*ES6-module*/
import {A, B, C} from 'Test';

//可见,es6的模块,直接是从模块中取所需即可。**编译时加载/静态加载**

ES6-module这么酷,那就来系统的看看吧。

Module

严格模式

ES6-module默认采用严格模式。

严格模式限制:

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

好多不知道啊,这个需要一篇专文来扯扯什么是严格模式。
占个位!!!!!!!

export 命令

规定模块的对外接口。

一个文件就是一个模块。文件内部的变量只能用export导出去。

1
2
3
4
5
6
7
8
9
10
export var a = 1;
export var b = 2;

//等价
var a = 1;
var b = 2;

//导出函数
function fn(x,y){return x+y;};
export {a, b, fn};

as这是一个关键词,可以指定导出变量的别名。

export命令规定的是对外的接口,必须与模块内部的变量建立一一对应关系

1
2
3
4
export 1; //非法。

var m = 1;
export m; //也是非法的。

export导出的接口和模块内部的值是动态绑定的,通过接口可以取到模块内部实时的值。

export命令可以出现在模块的任何位置,只要处于模块顶层就可以

import 命令

通过import命令来加载模块。

1
import {A , B , C} from 'xxx/someModule';

以上,大括号内的ABC必须是someModule中导出的接口名称。
同样,如果想换一个名字,那就用as命令。

1
import {D as A , B , C} from 'xxx/someModule';

import导入的是read-only的,所以不能直接修改导入的变量,但是可以修改它的属性值。

from是指定模块文件的位置,可以是相对路径或者绝对路径,.js后缀名可以省略。也可以直接取模块名(配置好的)。

另,import会得到提升,提升到整个模块的前面,优先之行。所以,下面的写法是合理的。

1
2
D(); 
import {D as A , B , C} from 'xxx/someModule';

import是静态加载,所以在有关加载时,需要运行的写法都是不合法的。比如:

1
2
3
import {'a' + 'b'} from 'someModule'; // error
if( a > 1) import {'ab'} from 'someModule'; //error
var some = 'someModule'; import {'ab'} from some; //error

以上说的是从模块中导出接口变量,如果本身模块就是一个函数,则该如何调用呢?直接import + moduleName

1
2
3
4
//只会之行一次的。
import 'lodash'; //执行所加载的模块。
import 'lodash';
import 'lodash';

CommonJS和ES6-moule是可以写在一个模块中的,但是需要注意import的提升。

模块的整体加载

某个模块有很多接口,我导入的时候要一个一个的写在大括号里,实在是太麻烦了,所以有整体加载的方式,即*
注!!!!!!这种方法会忽略default的接口。

1
2
3
4
5
import * as name from 'someModule';
name.A;
name.B;
//...
name.Z;

export default命令

为模块指定默认输出。

导入默认的变量是不需要大括号的,否则是需要的。

1
export default function(){console.log('test-1111');}

然后在其他模块中的时候,可以指定任意名字来关联这个默认的输出。

1
2
3
import xiawan from 'module';
xiawan();
//输出:test-1111

export default就好像是约定了一个叫default的变量,所以后面不能再接变量声明语句。

export 和 import 的复合写法

在一个模块内,先导入,然后再导出用一个模块。可以这样:

1
2
import {A , B , C} from 'someModule';
export {A, B, C};

这种写法可以复合成一句:

1
export {A, B, C} from 'someModule';

注:

  • 当前模块并没有导入此模块,只是做了一个转发。
  • 在当前模块中是不可以使用导入模块的接口的。

可以实现模块接口的改名再整理输出

1
2
3
4
5
6
7
8
9
10
//接口改名
export {A as D} from 'someModule';
//整体输出
export * from 'someModule';
//默认接口
export {default} from 'someModule';
//接口改为默认接口
export {A as default} from 'someModule';
//默认接口改名
export {default as A} from 'someModule';

模块的继承

模块之间是可以继承的。

1
2
3
export * from 'someModule';
export var test = 1111;
export default function(){console.log('default');};

跨模块变量

1
export const A = 1;

import() 一个提案

前面提到es6-module是静态加载的,如果放在运行的代码中的话会报错。
import()用来完成动态加载。

关于import():

  • 返回一个promise
  • 可以用在任何地方。
  • 与所加载的模块咩有静态链接关系。
  • 类似require,但是是异步加载。

适用场景:

  • 按需加载
  • 条件加载
  • 动态的模块路径