模块机制

在聊模块以前,咱们先聊聊JS模块化的不足javascript

在实应用中,JavaScript的表现能力取决于宿主环境中的API支持程程。在Web 1.0时,只有对DOM、BOM等基本的支持。随着Web 2.0的推动 ,HTML5崭露头角,它将Web网页带进Web应用的时代,在浏览器中出现了更多、更强大的API供JavaScript调用。可是这些过程发生在前端,后端JavaScript 的规范却远远落后。对于JavaScript自身而言,它的规范是薄弱的,还有如下缺陷。前端

  • JS没有模块系统,不支持封闭的做用域和依赖管理
  • 没有标准库,没有文件系统和IO流API
  • 没有标准接口
  • 没有包管理系统

所以,社区也为JavaScript定了相应的规范,其中CommonJS的是最为重要的里程程。java

CommonJS规范涵盖了模块、二进制、Buffer、字符集编码、I/O流、单元测试、文件系统、进程环境、包管理等等。Node能以一中比较成熟的姿态出现,不开CommonJS规范的影响。 下图是Node与浏览器以及w3c组织、CommonJS组织、ECMAScript之间的关系。 node

关系图

NodeCommonJS借鉴CommonJSd的的Modules规范实现了一套很是易用的模块系统,NPM对Packages规范的无缺支持使得Node应用在开发过程当中事半功倍。在本文中,就以Node的模块和包的实现展开说明。linux

CommonJS的规范

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方法
复制代码

模块分类

  1. 原生模块 http path fs util events 编译成二进制,加载速度最快,原来模块经过名称来加载
  2. 文件模块 在硬盘的某个位置,加载速度很是慢,文件模块经过名称或路径来加载 文件模块的后缀有三种
  • 后缀名为.js的JavaScript脚本文件,须要先读入内存再运行
  • 后缀名为.json的JSON文件,fs 读入内存 转化成JSON对象
  • 后缀名为.node的通过编译后的二进制C/C++扩展模块文件,能够直接使用

通常本身写的经过路径来加载,别人写的经过名称去当前目录或全局的node_modules下面去找数组

  1. 第三方模块
  • 若是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

从文件模块缓存中加载 尽快原生模块与文件模块的优先级不一样,可是都不会优先于从文件模块的缓存中加载已经存在的模块。

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


整个文件模块查找流程

此处输入图片的描述
若是require绝对路径的文件,就不会去遍历每个node_modules目录,它的速度最快。其他流程以下:

  1. 从module path数组中取出第一个目录做为查找基准。
  2. 从目录中查找该文件,若是存在,就结束查找。若是不存在,就进行下一条查找。
  3. 经过添加.js .json .node后缀查找,若是存在文件就结束查找。若是不存在,则进行下一条。
  4. 将require的参数做为一个包进行查找,读取目录下的package.json文件,取得main(入口文件)指定的文件。
  5. 若是有这个目录 可是没有package.json 就会去 当前目录下查找index.js index.json ,即重复第3条步骤查找
  6. 若是仍没找到,则取出module path数组中的下一个目录做为基准查找,循环1-5条步骤
  7. 若是继续失败,循环1-6个步骤,直到module path中的最后一个值。
  8. 若是仍没找到就会跑出异常。

整个查找过程相似原型链的查找和做用域的查找,但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
复制代码
相关文章
相关标签/搜索