前记
概述
几种之前的模块加载方案:CommonJS(服务器)和AMD(浏览器)。
ES6的模块加载规范可以完全取代上两者,成为浏览器和服务器通用的模块解决方案。
知己知彼,方能百战不殆。先来看看之前的两种加载方式。
1.CommonJs
概述
每个文件就是一个模块,有自己的作用域。在一个文件里面定义的变量、函数、类,都是私有的,对其他文件不可见。
1 | //test.js 这个文件就是一个module |
在模块内部的话,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 | //testA/index.js |
可见,在B中只加载A的require(B)的之前的部分得证。
require.main属性:
用来判断模块是直接执行还是调用执行。
1 | var result = require.main === moduleName ? '直接执行' : '调用执行'; |
模块的加载机制
输入的是被输出的值的拷贝。
模块中的值一旦被导出的话,就不受模块内部变量值的变化而变化了。
require的内部处理流程:
require是一个指向当前模块的module.require命令。
module.require调用Module._load。
1 | Module._load = function(request, parent, isMain) { |
可见,module.load方法用于加载模块,module.compile方法用来解析模块,也就是执行指定模块的脚本。
1 | Module.prototype._compile = function(content, filename) { |
require函数准备ok后,整个所有加载的脚本内容就被放在了一个新的函数之中了,这样避免污染全局变量。
1 | (function (exports, require, module, __filename, __dirname) { |
详见下文:
The Node.js Way - How require()
Actually Works
下图是项目中经过webpack打包后的某个文件的部分。
2.AMD规范(异步模块定义)
简单的了解的一下。
这个规范主要是用在浏览器。
也采用require()语句加载模块,但是要求两个参数。
1 | require([module], callbacl); |
这个规范,最常用的是require.js
库。
模块定义:define
模块必须采用特定的 define() 函数来定义。
1 | //moduleName = test test.js |
加载:1
2
3reuqire(['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 | /*CommonJS*/ |
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 | export var a = 1; |
as
这是一个关键词,可以指定导出变量的别名。
export命令规定的是对外的接口,必须与模块内部的变量建立一一对应关系
1 | export 1; //非法。 |
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
2D();
import {D as A , B , C} from 'xxx/someModule';
import是静态加载,所以在有关加载时,需要运行的写法都是不合法的。比如:1
2
3import {'a' + 'b'} from 'someModule'; // error
if( a > 1) import {'ab'} from 'someModule'; //error
var some = 'someModule'; import {'ab'} from some; //error
以上说的是从模块中导出接口变量,如果本身模块就是一个函数,则该如何调用呢?直接import + moduleName
。
1 | //只会之行一次的。 |
CommonJS和ES6-moule是可以写在一个模块中的,但是需要注意import的提升。
模块的整体加载
某个模块有很多接口,我导入的时候要一个一个的写在大括号里,实在是太麻烦了,所以有整体加载的方式,即*
,
注!!!!!!这种方法会忽略default的接口。
1 | import * as name from 'someModule'; |
export default命令
为模块指定默认输出。
导入默认的变量是不需要大括号的,否则是需要的。
1 | export default function(){console.log('test-1111');} |
然后在其他模块中的时候,可以指定任意名字来关联这个默认的输出。1
2
3import xiawan from 'module';
xiawan();
//输出:test-1111
export default就好像是约定了一个叫default的变量,所以后面不能再接变量声明语句。
export 和 import 的复合写法
在一个模块内,先导入,然后再导出用一个模块。可以这样:1
2import {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 | export * from 'someModule'; |
跨模块变量
1 | export const A = 1; |
import() 一个提案
前面提到es6-module是静态加载的,如果放在运行的代码中的话会报错。
import()用来完成动态加载。
关于import():
- 返回一个promise
- 可以用在任何地方。
- 与所加载的模块咩有静态链接关系。
- 类似require,但是是异步加载。
适用场景:
- 按需加载
- 条件加载
- 动态的模块路径