CommonJS规范
希望JavaScript可以在任何地方运行,以达到像Java、PHP、Ruby、Python具备开发大型应用的能力。
出发点:
没有模块系统
标准库较少
缺乏包管理系统
CommonJS模块规范
- 模块引用require
- 模块定义
一个文件就是一个模块
将方法挂载到exports对象上作为属性即可定义导出的方式
- 模块标识
必须是符合小驼峰命名的字符串
以.、..开头的相对路径
绝对路径
可以没有文件名后缀.js
CommonJS模块特点
1、所有代码都运行在模块作用域,不会污染全局作用域
2、模块可以多次加载,但是只会在第一次加载时运行一次,然后运行结果就缓存了,以后再加载,就直接读取缓存结果。
3、模块加载的顺序,按照其在代码中出现的顺序
Node.js模块介绍
1、Node程序由许多模块组成,每个模块就是一个文件。Node模块采用了CommonJS规范。
2、Node.js本身就是一个高度模块化的一个平台。
3、根据CommonJS规范,每一个模块都是一个单独的作用域。
4、CommonJS规定,每个文件对外的接口是module.exports对象,该对象所有属性和方法,都可以被其它文件导入。
模块系统
-
1、什么是模块
一个文件就是一个模块
-
2、模块作用域
在一个模块内变量、函数、对象都属于这个模块,对外是封闭的。
3、require
require("路径")
//路径现阶段使用相对路径
在Node.js中,require命令用于加载模块文件。
基本功能:
读取并执行一个JavaScript文件
然后返回该模块的exports对象
如果没有发现指定模块,会报错
① 是module.exports的一个别名、引用,exports能做的module.exports都可以完成。
② 为了方便,Node为每个模块提供一个exports变量,指向module.exports。
相当于在每个模块头部,有这样一行命令:
var exports = module.exports;
结果就是:在对外输出模块接口时,可以向exports对象添加方法
③ 注意:不能直接给exports赋值,因为这样等于切断了exports和module.exports的联系
module.exports 别名
module.exports=123;已经使用过一次了,exports就不生效了
module.exports.a=12;这样使用,exports是还可以使用的
1、module.exports属性表示当前模块对外输出的接口,其它文件加载该模块,实际上就是读取module.exports属性
2、点儿导出单个函数、对象或者值的时候非常有用,本质上就是少了一个.
3、最终想曝露出去对象、属性、方法就挂载到module.exports
补充: module对象
1、 是一个全局对象,它的作用是存储模块信息的。每一个js是一个模块,每一个模块里面都有一个module,module里面还存储了父子结构。直接给module.exports赋值,exports是无效,用属性的形式去赋值的时候二者都有效。
2、Node内部提供一个Module构造函数,所有模块都是Module的实例。
每个模块内部,都有一个module对象,代表当前模块。
module.id 带有绝对路径的模块文件名
module.filename 模块的文件名,带有绝对路径
module.loaded 表示模块是否已经完成加载
module.parent 返回一个对象,表示调用该模块的模块。
module.children 返回一个数组,表示该模块要用到的其他模块。
module.exports 模块对外输出的值
1 模块种类
在Node.js中,模块分为两类:
第一类,核心模块(原生模块),node自带,用名称直接可以加载。
- 核心模块
fs: file system与文件系统交互
http: 提供http服务器功能
os
path: 处理文件路径
querystring : 解析url查询字符串
url:解析url
util: 提供一系列实用小工具
...
核心模块的源码都在Node的lib子目录中。为了提高运行速度,它们安装的时候都会被编译成二进制文件。
第二类,文件模块,用路径加载,用户自己编写的模块。有一种特殊的文件模块—–>包,可以直接用名字。
包就是在模块的基础之上进一步组织JavaScript代码。包中必须要有一个包说明文件package.json。
规范的包目录结构:
package.json : 包描述文件,说明文件
Bin : 存放可执行二进制文件的目录
Lib : 存放JavaScript代码的目录
Doc : 存放文档的目录
Test : 存放单元测试用例的代码
其中package.json:
name
包的名称
description
包的简介
version
包的版本号
keywords
关键词数组,用于在npm中分类搜索
author
包的作者
main
配置包的入口,默认是模块根目录下的index.js
dependencies
包的依赖项,npm会通过该属性自动加载依赖包
scripts
指定了运行脚本命令的npmm命令行缩写,例如start
package.json全字段解析](http://blog.csdn.net/woxueliuyun/article/details/39294375)
一个例子:
//test_package/node_modules/myPackage/package.json
{
"name": "test",
"version": "0.0.1",
"description": "",
"main": "index.js",
"keywords": [
],
"author": "KingNigel",
"repository": {
"type": "git",
"url": "https://github.com/KingNigel/test.git"
},
"bugs": {
"url": "https://github.com/KingNigel/test/issues"
},
"license": "MIT",
"devDependencies": {
"test": "*"
},
"dependencies": {
"test": "*"
}
}
//test_package/node_modules/myPackage/index.js
exports.a=123;
//test_package/index.js
var foo = require('myPackage'); //文件夹名就是包名
console.log(foo); //{a:123}
2 优先从缓存加载
common.js规范----->加载后,再次加载时,去缓存中取module.exports
只在第一次加载的时候,从硬盘中读取js,执行代码,缓存整个module对象。第二次加载去缓存中取module.exports。
加载模块时将运行模块文件中的每一行代码,相同模块多次引用不会引起模块内代码多次执行。
//add.js
var add = function(a,b){
return a+b;
}
module.exports = add;
console.log('执行add.js');
//sub.js
var sub = function(a,b){
return a-b;
}
module.exports = sub;
console.log('执行sub.js');
//math.js
var add1 = require('./add.js');
var add2 = require('./add.js');
var add3 = require('./add.js');
var sub1 = require('./sub.js');
console.log(add1(3,5));
console.log(add3(1,5));
//执行结果:
执行add.js
执行sub.js
8
6
测试结果可知,add1和add3只加载一次;sub没有使用,也会加载执行
3 require参数解析及加载规则
核心模块是Node.js原生提供的,加载核心模块的时候,不需要传入路径,因为Node.js已经将核心模块的文件代码 编译到了二进制的可执行文件中了。 在加载的过程中,原生的核心模块的优先级是是最高的。
加载文件模块的时候,需要给定准确的路径。
require 加载包的时候: 直接写包名,先在包内的node_modules目录下查找,去父级目录下的node_modules目录下查找, 依次向上查找,直到根目录。
-
./ 或 ../ 开始的相对路径文件模块
- 在加载一个自己编写的模块的时候,最好使用__dirname 和 你要加载的模块的文件名拼接。
-
以 / 开始的绝对路径文件模块(不建议使用)
- 在Linux或者MAc的操作系统中,/表示系统的根路径
- 在Windows中,/表示当前文件模块所属的根磁盘路径
-
参数字符串不以“./“或”/”开始
- 表示加载核心模块,或者一个位于各级node_modules目录已安装的模块。
-
标识符中可以不包含扩展名
- Node会按照
.js、.node、.json
的次序补足扩展名,依次尝试
- Node.js会通过同步阻塞的方式看这个路径是否存在。依次尝试,直到找到为止, 如果找不到,报错。
路径.js 以后自己在加载js文件模块的时候,就省略掉.js后缀就可以了
路径.node 后缀为node的文件是c/c++写的一些扩展模块
路径.json 如果是加载json文件模块,最好加上后缀.json,能稍微的提高一点加载的速度
.json文件最终Node.js也是通过fs读文件的形式读取出来的,然后通过JSON.parse()转换成一个对象
最终获取到的是module.exports
-
参考田永强《深入浅出nodejs》
4 require加载机制
如果require绝对路径的文件,查找时不会去遍历每一个node_modules目录,其速度最快。其余流程如下:
1. 从module path数组中取出第一个目录作为查找基准。
2. 直接从目录中查找该文件,如果存在,则结束查找。如果不存在,则进行下一条查找。
3. 尝试添加.js、.json、.node后缀后查找,如果存在文件,则结束查找。如果不存在,则进行下一条。
4. 尝试将require的参数作为一个包来进行查找,读取目录下的package.json文件,取得main参数指定的文件。
5. 尝试查找该文件,如果存在,则结束查找。如果不存在,则进行第3条查找。
6. 如果继续失败,则取出module path数组中的下一个目录作为基准查找,循环第1至5个步骤。
7. 如果继续失败,循环第1至6个步骤,直到module path中的最后一个值。
8. 如果仍然失败,则抛出异常。
5 require的实现原理
把代码从文件中读出来,用匿名函数的方式头尾包装,返回modules.exports
对象,曝露出想要曝露出来的属性、方法、对象。
补充了exports是指向modules.exports
的一个指针,exports能做的,modules.exports都能做的。
6 仿写MyRequire.js
//MyRequire.js
function MyRequire(path) {
//定义一个构造函数Module
function Module() {
this.exports = {};
}
var fs = require('fs');
//同步读取文件,文件的内容source
var source = fs.readFileSync(path, 'utf-8');
//拼接代码变成一个函数的string
var package = '(function(exports,module){' + source + ' return module.exports;})';
var packObj = eval(package);
//调用构造函数创建Module
var module = new Module();
//var exports = module.exports 把module.exports当实参传入,exports当形参去接
var obj = packObj(module.exports, module);
return obj;
}
var foo = MyRequire('./hia.js');
console.log(foo);
console.log(foo.a);
foo.fun();
//hia.js
var a = 123;
var fun = function(){
console.log("我是fun函数");
}
console.log("helloworld");
exports.a=a;
exports.fun = fun;
参考文档:
package.json全字段解析:
(http://blog.csdn.net/woxueliuyun/article/details/39294375)
参考田永强《深入浅出nodejs》:(http://www.infoq.com/cn/articles/nodejs-module-mechanism/)