Node模块
Node里面的模块系统遵循的是CommonJS规范。
CommonJS定义的模块分为:
1、模块标识(module)
2、模块定义(exports)
3、模块引用(require)
先解释 exports 和 module.exports
在一个node执行一个文件时,会给这个文件内生成一个 exports和module对象, 而module又有一个exports属性。他们之间的关系如下图,都指向一块{}内存区域。
exports = module.exports = {};
接下来上代码:
//utils.js let test = 'today'; console.log(module.exports); //能打印出结果为:{} console.log(exports); //能打印出结果为:{} exports.test = 'tomorrow'; //这里辛苦劳作帮 module.exports 的内容给改成 {test: 'tomorrow'} exports = '指向其他内存区'; //然后在这里把exports的指向指到其他地方 //test.js let _test = require('/utils'); console.log(_test) // 打印为 {test: 'tomorrow'}
1、由此可见,require导出的内容其实是module.exports的指向的内存块内容({test: 'tomorrow'}),并不是exports的内容('指向其他内存区')。
2、简而言之,区分他们之间的区别就是 exports 只是 module.exports的引用,exports是辅助module.exports操作内存中的数据用的,结果到最后真正被require出去的内容还是module.exports的。
为了避免糊涂,尽量都用 module.exports 导出,然后用require导入
ES中的模块导入和导出
先解释 export 和 export default
1、export与export default均可用于导出常量、函数、文件、模块等
2、在一个文件或模块中,export、import可以有多个,export default仅有一个
3、通过export方式导出,在导入时要加{ },export default则不需要
4、export能直接导出变量表达式,export default不行。
例如下面代码:
1、export (testExport.js)
'use strict'
//导出常量 export const a = '100'; //导出函数方法 export const foo = function(){ console.log('This is a function foo'); } //导出方法第二种 function bar(){ console.log('My name is bar'); } export { bar }; // 相应的导入的方式为 import { foo, bar } from './testExport'; //导出了 export 方法 foo(); // 直接执行foo方法 bar(); // 直接执行bar方法
2、export default (testExportDefault.js)
'use strict' //export default导出 const b = 100; export const foo = function(){ console.log('This is a function foo'); } export default b // 也可以只导出一个对象 // export default {b, foo}; //export defult const b = 100;// 这里不能写这种格式,是错误的,可以导出一个对象, export default {b}。
综合应用:
//index.js
'use strict' var express = require('express'); var router = express.Router(); import { foo, too } from './testExport'; //导出了 export 方法 import b from './testExportDefault'; //导出了 export default import * as testExportModule from './testExportDefault'; //as 集合成对象导出 import * as testExportDefaultModule from './testExportDefault'; //as 集合成对象导出 foo(); bar(); testExportModule.foo(); console.log(b); console.log(testExportDefaultModule.b); // undefined , 因为 as 导出是 把 零散的 export 聚集在一起作为一个对象,而export default 是导出为 default属性。 console.log(testExportDefaultModule.default); // 100 or {b: 100, foo: function}
module.exports = router;
补充一下模块化标准规范
1、模块化的最佳实践
node.js 环境中,遵循 CommonJS 规范
浏览器环境中,遵循 ES Modules 规范
1.1、ES Modules 基本特性
自动采用严格模式,忽略 'use strict'
每个 ESM 模块都是单的私有作用域
ESM 是通过 CORS 去请求外部 JS 模块的
ESM 的 script 标签会延迟执行脚本
ES Modules 注意事项
- export {} 这是一个固定的语法,不是 es6 中的对象简写
- import {} 这是一个固定的语法,不是 es6 中的对象解构
- 导出得到的是对值的引用,模块内部修改了值外部也会跟着改变
- 导入的成员是只读的成员
1.2、ES Modules 导出和导入
- export 注意有无 default 关键字
- 不能省略文件后缀名,不能省略 ./
- 执行某个模块,不需要提取模块中的成员 import './module.js'
- 动态导入模块,可以用全局函数 import()
1.3、ES Modules in Node.js 支持情况:
- 执行文件时,node --experimental-modules index.mjs
- ES Module 中可以导入 CommonJS 模块
- CommonJS 中不能导入 ES Module 模块
- CommonJS 始终只会导出一个默认成员
- 注意 import 不是解构导出对象
ES6 模块和 CommonJS 的差异点:
- CommonJS 可以在运行时使用变量进行 require, 例如 require(path.join('xxxx', 'xxx.js')),CommonJS 模块是运行时加载,而静态 import 语法(还有动态 import,返回 Promise)不行,因为 ES6模块是编译时输出接口, 模块会先解析所有模块再执行代码。
- require 会将完整的 exports 对象引入,CommonJS输出的是一个值的拷贝,import 可以只 import 部分必要的内容,ES6 模块输出的是值的引用,这也是为什么使用 Tree Shaking 时必须使用 ES6 模块 的写法。
- import 另一个模块没有 export 的变量,在代码执行前就会报错,而 CommonJS 是在模块运行时才报错。
- 因为CommonJS的require语法是同步的,所以就导致了CommonJS模块规范只适合用在服务端,而ES6模块无论是在浏览器端还是服务端都是可以使用的,但是在服务端中,还需要遵循一些特殊的规则才能使用 ;
- 因为两个模块加载机制的不同,所以在对待循环加载的时候,它们会有不同的表现。CommonJS遇到循环依赖的时候,只会输出已经执行的部分,后续的输出或者变化,是不会影响已经输出的变量。而ES6模块相反,使用import加载一个变量,变量不会被缓存,真正取值的时候就能取到最终的值;
- 关于模块顶层的this指向问题,在CommonJS顶层,this指向当前模块;而在ES6模块中,this指向undefined;
- 关于两个模块互相引用的问题,在ES6模块当中,是支持加载CommonJS模块的。但是反过来,CommonJS并不能requireES6模块,在NodeJS中,两种模块方案是分开处理的。
为什么平时开发可以混写?
面提到 ES6 模块和 CommonJS 模块有很大差异,不能直接混着写。这和开发中表现是不一样的,原因是开发中写的 ES6 模块最终都会被打包工具处理成 CommonJS 模块,以便兼容更多环境,同时也能和当前社区普通的 CommonJS 模块融合。
循环加载(circular dependency)
1、CommonJS的循环加载
想要搞清楚CommonJS的循环加载问题,首先我们要先大概了解下它的加载原理。CommonJS的一个模块,一般就是一个文件,使用reqiure第一次加载一个模块的时候,就会在内存中生成一个对象。
{
id: '...',
exports: { ... },
loaded: true,
...
}
CommonJS模块的特性就是加载时执行,当脚本被reqiure的时候,就会全部执行。一旦出现某个模块被"循环加载",就只输出已经执行的部分,还未执行的部分不会输出。
// a.js exports.done = false; // 首先输出一个done变量({done:false}),然后开始加载b.js,等待b.js执行完,才会继续执行后面的代码 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'); //此时a里面只有 {done:false} 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);
最后执行结果:
在 b.js 之中,a.done = false
b.js 执行完毕
在 a.js 之中,b.done = true
a.js 执行完毕
在 main.js 之中, a.done=true, b.done=true
2、ES6中的循环加载
ES6 模块是动态引用,如果使用import加载一个变量,变量不会被缓存,真正取值的时候就能取到最终的值
// even.js import { odd } from './odd' export var counter = 0; export function even(n) { counter++; return n === 0 || odd(n - 1); } // even.js里面的函数even有一个参数n,只要不等于 0,就会减去 1,传入加载的odd()。odd.js也会做类似操作。 // odd.js import { even } from './even'; export function odd(n) { return n !== 0 && even(n - 1); } // 参数n从 10 变为 0 的过程中,even()一共会执行 6 次,所以变量counter等于 6 > import * as m from './even.js'; > m.even(10); true > m.counter 6