在聊模块之前,我们先聊聊JS模块化的不足
在实应用中,JavaScript的表现能力取决于宿主环境中的API支持程程。在Web 1.0时,只有对DOM、BOM等基本的支持。随着Web 2.0的推进 ,HTML5崭露头角,它将Web网页带进Web应用的时代,在浏览器中出现了更多、更强大的API供JavaScript调用。但是这些过程发生在前端,后端JavaScript 的规范却远远落后。对于JavaScript自身而言,它的规范是薄弱的,还有以下缺陷。
- JS没有模块系统,不支持封闭的作用域和依赖管理
- 没有标准库,没有文件系统和IO流API
- 没有标准接口
- 没有包管理系统
因此,社区也为JavaScript定了相应的规范,其中CommonJS的是最为重要的里程程。
CommonJS规范涵盖了模块、二进制、Buffer、字符集编码、I/O流、单元测试、文件系统、进程环境、包管理等等。Node能以一中比较成熟的姿态出现,不开CommonJS规范的影响。 下图是Node与浏览器以及w3c组织、CommonJS组织、ECMAScript之间的关系。

NodeCommonJS借鉴CommonJSd的的Modules规范实现了一套非常易用的模块系统,NPM对Packages规范的完好支持使得Node应用在开发过程中事半功倍。在本文中,就以Node的模块和包的实现展开说明。
CommonJS的规范
CommonJS对模块的定义十分简单,主要分为模块引用、模块定义、模块标识3部分。
在node.js 里,模块划分所有的功能,每个JS都是一个模块 实现require方法,NPM实现了模块的自动加载和安装依赖
(function(exports,require,module,__filename,__dirname){
exports = module.exports={}
exports.name = 'zfpx';
exports = {name:'zfpx'};
return module.exports;
})
//往下会实现一个简单的require方法
模块分类
- 原生模块
http
path
fs
util
events
编译成二进制,加载速度最快,原来模块通过名称来加载 - 文件模块 在硬盘的某个位置,加载速度非常慢,文件模块通过名称或路径来加载 文件模块的后缀有三种
- 后缀名为.js的JavaScript脚本文件,需要先读入内存再运行
- 后缀名为.json的JSON文件,fs 读入内存 转化成JSON对象
- 后缀名为.node的经过编译后的二进制C/C++扩展模块文件,可以直接使用
一般自己写的通过路径来加载,别人写的通过名称去当前目录或全局的node_modules下面去找
- 第三方模块
- 如果require函数只指定名称则视为从node_modules下面加载文件,这样的话你可以移动模块而不需要修改引用的模块路径
- 第三方模块的查询路径包括module.paths和全局目录
全局目录
window
如果在环境变量中设置了NODE_PATH
变量,并将变量设置为一个有效的磁盘目录,require
在本地找不到此模块时向在此目录下找这个模块。 UNIX操作系统中会从 $HOME/.node_modules
$HOME/.node_libraries
目录下寻找
模块的加载策略
Node.js模块分为两类(第三方模块这里暂时不提),一类是核心模块(即原生模块),一类是文件模块(我们自己写的)。原生模块加载速度最快,而文件模块是动态加载的,加载速度比原生模块慢。但Node对两类模块都会进行缓存,所以在第二次调用require的时候,是不会重复调用的。 在文件模块中,又分3类:.js .json .node

从文件模块缓存中加载 尽快原生模块与文件模块的优先级不同,但是都不会优先于从文件模块的缓存中加载已经存在的模块。
从原生模块加载 原生模块的优先级仅次于文件模块缓存的优先级。require方法在解析文件名之后,会先检查模块是否在原生模块列表中。举个例子,怡http为例,即使在目录下存在http.js http.json http.node文件,但require("http")不会先从这些文件中加载,而是优先从原生模块中加载。 从文件加载 当文件模块缓存中不存在,并且不是原生模块的时候,Node.js会解析require传入的参数,病加载实际的文件。
整个文件模块查找流程

- 从module path数组中取出第一个目录作为查找基准。
- 从目录中查找该文件,如果存在,就结束查找。如果不存在,就进行下一条查找。
- 通过添加.js .json .node后缀查找,如果存在文件就结束查找。如果不存在,则进行下一条。
- 将require的参数作为一个包进行查找,读取目录下的package.json文件,取得main(入口文件)指定的文件。
- 如果有这个目录 但是没有package.json 就会去 当前目录下查找index.js index.json ,即重复第3条步骤查找
- 如果仍没找到,则取出module path数组中的下一个目录作为基准查找,循环1-5条步骤
- 如果继续失败,循环1-6个步骤,直到module path中的最后一个值。
- 如果仍没找到就会跑出异常。
整个查找过程类似原型链的查找和作用域的查找,但node对路径查找实现了缓存机制,所以不会很耗性能。
接下来,将会实现一个简单的require的方法,在此之前,先简单了解一下需要用到的方法:
//引入fs模块
let fs = require('fs');
// fs里面有一个新增 判断文件是否存在
fs.accessSync('./5.module/1.txt'); // 文件找到了就不会发生任何异常
let path = require('path');// 解决路径问题
console.log(path.resolve(__dirname,'a')); // 解析绝对路径
// resolve方法你可以给他一个文件名,他会按照当前运行的路径 给你拼出一个绝对路径
// __dirname 当前文件所在的文件的路径 他和cwd有区别
console.log(path.join(__dirname,'a')); // join就是拼路径用的 可以传递多个参数
// 获取基本路径
console.log(path.basename('a.js','.js')); // 经常用来 获取除了后缀的名字
console.log(path.extname('a.min.js')); // 获取文件的后缀名(最后一个.的内容)
console.log(path.posix.delimiter); // window下是分号 maclinux 是:
console.log(path.sep); // window \ linux /
// vm 虚拟机模块 runinThisContext
let vm = require('vm');//非常像eval,eval可以把字符串当成js文件执行,但它是依赖于环境的
var a = 1;
eval('console.log(a)');//运行这行代码的时候,是依赖于var a = 1这个环境的,这个方案会污染eval的执行结果
var b = 2;
vm.runInThisContext('console.log(b)');//runInThisContext会制造一个干净的环境。让代码跑在干净的环境里,干净的环境会隔离上面写的代码,输出:b is not defined
输出的结果:
c:\Users\19624\Desktop\201802\5.module\a
c:\Users\19624\Desktop\201802\5.module\a
a
.js
:
\
1
/*
可以在node环境中运行试试看
*/
require的简单实现:
let fs = require('fs');
let path = require('path');
let vm = require('vm');
// 自己写一个模块加载require
// require 出来的是一个模块
function Module(filename){//构造函数,每个模块都应该有个绝对路径
this.filename = filename;
this.exports = {};
}
// 如果没有后缀的话,希望加js,json还有node的扩展名,扩展名存到构造函数上
Module._extentions = ['.js','.json','.node'];//如果没有后缀,希望添加上查找
// 缓存的地址
Module._cathe = {};//读到一个文件,就往里放一个, key是它的绝对路径,值是它的内容
// 解析绝对路径
Module._resolvePathname = function(filename){
let p = path.resolve(__dirname,filename);//以当前的文件夹路径和filename解析
console.log(path.extname(p))
if(!path.extname(p)){//判断文件是否有后缀名,如果有,则直接返回p,没有则做进一步处理
//没有的话,循环Module._extentions,一个个加后缀
for(var i = 0;i<Module._extentions.length;i++){
let newPath = p + Module._extentions[i];
//console.log(newPath);
// 判断路径存不存,如果不存在,会报异常,为了不报错,使用try catch
try {//如果访问的文件不存在,就会发生异常
fs.accessSync(newPath);
// 没报错,就返回newPath
return newPath
} catch (e) {}
}
}
return p;//解析出来的就是一个绝对路径
}
// 加载模块
Module.wrapper = [
"(function(exports,require,module,__dirname,__filename){","\n})"
];
Module.wrap = function(script){
return Module.wrapper[0] + script + Module.wrapper[1];//拼成一个函数
}
Module._extentions['js'] = function(module){//Module._extentions加了一个属性,属性等于函数
// 如果是js,就同步的读取出来,然后去exports的值
// module是个对象,对象里有 filename exports
let script = fs.readFileSync(module.filename);
// 执行的时候,会套个闭包环境
// (function(exports,require,module,__dirname,filename){})
let fnStr = Module.wrap(script);//拼成一个函数
vm.runInThisContext(fnStr).call(module.exports,module,exports,req,module)
};
Module._extentions['json'] = function(module){
let script = fs.readFileSync(module.filename);
// 如果是json直接拿到内容 json.parse即可
module.exports = JSON.parse(script);
}
Module.prototype.load = function(filename){//加了prototype是实例调用的,不加是构造函数调用的
// 模块可能是json,也可能是js
let ext = path.extname(filename).slice(1);//.js .json,slice方法删除.
// js用js的方式加载,json用json的方式加载
Module._extentions[ext](this);//原型中的this,指的是当前Module的实例,看上两行代码Module._extentions['js'],这里的this传给module,module代表当前加载的模块
}
function req(filename){//filename是文件名 文件名可能没有后缀
// 我们需要弄出一个绝对路径来,缓存是根据绝对路径来的
// 先获取到绝对路径
filename = Module._resolvePathname(filename);
console.log(filename);
// 先看这个路径在缓存中有没有,如果有则直接返回
let catchModule = Module._cathe[filename];
if(catchModule){
// 如果是true,表示缓存过,就会直接返回缓存的exports属性
return catchModule.exports
}
// 没缓存,加载模块--创建实力
let module = new Module(filename);//创建模块
// 将模块的内容加载出来
module.load(filename);//load是原型链上的方法,往上看~
Module._cache[filename] = module;
module.loaded = true; // 表示当前模块是否加载完
return module.exports;
}
let result = require('./school');
result = req('./school');
console.log(result);
此时school的文件是:
// 导出文件 才能给其他文件使用
console.log('加载');
module.exports = 'leo'
输出的结果为:
加载
加载
zfpx