CommonJS是一个模块化的规范,Nodejs的模块系统,就是参照CommonJS规范实现的。每一个文件就是一个模块 ,每一个模块都要本身的做用域。node
首先咱们先了解一下模块化加载:git
.js
或 .json
的文件.js
文件,就把内容加一个闭包每一个模块内部,都有一个 module
对象,表明当前模块。github
查看 console.log(module)
module都有哪些属性:json
module.id
模块的识别符,一般是带有绝对路径的模块文件名module.filename
模块的文件名,带有绝对路径module.loaded
返回一个布尔值,表示模块是否加载module.parent
返回一个对象,表示调用该模块的模块module.children
返回一个数组,表示该模块要用到的其余模块module.exports
表示模块对外输出的值,默认值为空对象function Module(id) {
// 下面就简单定义两个经常使用的
this.id = id
this.exports = {}
}
复制代码
// 咱们本身写的文件模块 要写路径 ./ 或者../
let hello = require('./1.NodeJS')
console.log(hello) // first NodeJS
复制代码
//操做文件的模块
let fs = require('fs')
// 处理路径的模块
let path = require('path')
// 虚拟机模块,沙箱运行,防止变量污染
let vm = require('vm')
复制代码
Module._resolveFilename
方法实现的功能是判断用户引入的模块是否包含后缀名,若是没有后缀名会根据 Module._extensions
的键的顺序查找文件,直到找到后缀名对应的文件的绝对路径,优先查找 .js
数组
// 将引入文件处理为绝对路径
Module._resolveFilename = function (p) {
// 以js或者json结尾的
if ((/\.js$|\.json$/).test(p)) {
// __dirname当前文件所在的文件夹的绝对路径
// path.resolve方法就是帮咱们解析出一个绝对路径出来
return path.resolve(__dirname, p);
} else {
// 没有后后缀 自动拼后缀
// Module._extensions 处理不一样后缀的模块
let exts = Object.keys(Module._extensions);
let realPath; // 存放真实存在文件的绝对路径
for (let i = 0; i < exts.length; i++) {
// 依次匹配对应扩展名的绝对路径
let temp = path.resolve(__dirname, p + exts[i])
try {
// 经过fs的accessSync方法对路径进行查找,找不到对应文件直接报错
fs.accessSync(temp)
realPath = temp
break
} catch (e) {
}
}
if (!realPath) {
throw new Error('module not exists');
}
// 将存在绝对路径返回
return realPath
}
}
复制代码
Module._extensions
处理对应模块扩展名。这里咱们只提 .js
.json
,对应模块的处理功能咱们在后面来实现缓存
Module._extensions = {
"js": function() {},
"json": function() {}
}
复制代码
当用户重复加载一个已经加载过的模块,咱们只有第一次是加载,而后放入缓存中,后面在加载时,直接返回缓存中便可bash
Module._cacheModule = { }
存放模块缓存闭包
// 获取内存中的结果
let cache = Module_cacheModule[filename]
// 判断是否存在
if (cache) {
// 若是存在直接将 exports对象返回
return cache.exports
}
// 若是不存在内存中,就建立模块,而后加入到内存中
let module = new Module(filename)
Module._cacheModule[filename] = module
复制代码
根据前面咱们说到的模块化,对于已经存放在内存中的咱们直接返回就能够了,对于新添加的模块,咱们该读取文件了, 根据传入的模块,尝试加载模块方法app
// 根据传入的模块,尝试加载模块方法
function tryModuleLoad(module) {
// 前面咱们已经提到 module.id 为模块的识别符,一般是带有绝对路径的模块文件名
// path.extname 获取文件的扩展名
/* let ext = path.extname(module.id);
// 若是扩展名是js 调用js处理器 若是是json 调用json处理器
Module._extensions[ext](module); // exports 上就有了数组 */
let ext = path.extname(module.id);//扩展名
// 若是扩展名是js 调用js处理器 若是是json 调用json处理器
Module._extensions[ext](module); // exports 上就有了数组
}
复制代码
Module._extensions
处理对应后缀名模块。这里咱们只提 .js
.json
模块化
// 处理对应后缀名模块
Module._extensions = {
".js": function (module) {
// 对于js文件,读取内容
let content = fs.readFileSync(module.id, 'utf8')
// 给内容添加闭包, 后面实现
let funcStr = Module.wrap(content)
// vm沙箱运行, node内置模块,前面咱们已经引入, 将咱们js函数执行,将this指向 module.exports
vm.runInThisContext(funcStr).call(module.exports, module.exports, req, module)
},
".json": function (module) {
// 对于json文件的处理就相对简单了,将读取出来的字符串转换未JSON对象就能够了
module.exports = JSON.parse(fs.readFileSync(module.id, 'utf8'))
}
}
复制代码
上面咱们使用了 Module.wrap
方法,是帮助咱们添加一个闭包,简单说就是咱们在外面包了一个函数的前半段和后半段
// 存放闭包字符串
Module.wrapper = [
"(function (exports, require, module, __filename, __dirname) {",
"})"
]
复制代码
// 将咱们读到js的内容传入,组合成闭包字符串
Module.wrap = function (script) {
return Module.wrapper[0] + script + Module.wrapper[1];
}
复制代码
要运行,咱们来看一下完整的模块加载代码
// 模块加载
Module._load = function (f) {
// 相对路径,可能这个文件没有后缀,尝试加后缀
let fileName = Module._resolveFilename(f); // 获取到绝对路径
// 判断缓存中是否有该模块
if (Module._cacheModule[fileName]) {
return Module._cacheModule[fileName].exports
}
let module = new Module(fileName); // 没有就建立模块
Module._cacheModule[fileName] = module // 并将建立的模块添加到缓存
// 加载模块
tryModuleLoad(module)
return module.exports
}
复制代码
到这里,一个简单的module就实现了,让咱们测试一下吧
// 测试代码
function req(p) {
return Module._load(p); // 加载模块
}
// 第一次没有缓存,建立module,并添加到缓存中
let str = req('./1.NodeJS');
// 第二次就是返回的缓存中的
let str1 = req('./1.NodeJS.js');
console.log(str) // first NodeJS
console.log(str1) // first NodeJS
复制代码
重要:为了方便你们了解、查看、调试代码,完整的源码参见gitHub
本篇文章是基于CommonJS规范,实现了一个简单的NodeJS模块化,主要目的在于理解 NodeJS 模块化的实现思路,但愿对你们了解模块化起到必定的做用。
未来的你,必定会感谢如今拼命努力的本身!