在聊模块以前,咱们先聊聊JS模块化的不足javascript
在实应用中,JavaScript的表现能力取决于宿主环境中的API支持程程。在Web 1.0时,只有对DOM、BOM等基本的支持。随着Web 2.0的推动 ,HTML5崭露头角,它将Web网页带进Web应用的时代,在浏览器中出现了更多、更强大的API供JavaScript调用。可是这些过程发生在前端,后端JavaScript 的规范却远远落后。对于JavaScript自身而言,它的规范是薄弱的,还有如下缺陷。前端
所以,社区也为JavaScript定了相应的规范,其中CommonJS的是最为重要的里程程。java
CommonJS规范涵盖了模块、二进制、Buffer、字符集编码、I/O流、单元测试、文件系统、进程环境、包管理等等。Node能以一中比较成熟的姿态出现,不开CommonJS规范的影响。 下图是Node与浏览器以及w3c组织、CommonJS组织、ECMAScript之间的关系。 node
NodeCommonJS借鉴CommonJSd的的Modules规范实现了一套很是易用的模块系统,NPM对Packages规范的无缺支持使得Node应用在开发过程当中事半功倍。在本文中,就以Node的模块和包的实现展开说明。linux
CommonJS对模块的定义十分简单,主要分为模块引用、模块定义、模块标识3部分。json
在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
编译成二进制,加载速度最快,原来模块经过名称来加载通常本身写的经过路径来加载,别人写的经过名称去当前目录或全局的node_modules下面去找数组
全局目录 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传入的参数,病加载实际的文件。
整个查找过程相似原型链的查找和做用域的查找,但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
复制代码