浅谈Node中module的实现原理

曾几什么时候,Javascript还没那么牛逼,几乎全部人都以为它是用来作网页特效的脚本而已。彼时仓促建立出来的javascript的自身缺点被各类吐槽。随着web的发展,Javascript现在是媳妇熬成婆,应用愈来愈普遍。javascript

虽然Javascript自身很努力,可是仍是缺少一项重要功能,那就是模块。毕竟Python有require,PHP有include和require。js经过<script>标签引入的方式虽然说也没问题,可是缺少组织和约束,也很难达到安全和易用。因此CommonJS规范的提出简直就是革命性的。java

如今咱们就来讲说在Node中的CommonJS模块的规范和实现。node

CommonJS的模块规范

CommonJS对模块定义分为三部分:模块定义、模块引用和模块标识。web

  1. 模块定义:

首先建立一个a.js的文件,在里面写上:json

module.exports = 'hello world';
复制代码

在Node中,一个文件就是一个模块,module.exports对象能够导出当前模块的方法或者变量。以上的代码就是将字符串hello world导出,上下文就提供了require()方法来引入外部模块。数组

  1. 模块引用:

建立一个b.js的文件,在里面写上:浏览器

let b = require('./b.js'); //.js能够不写,这咱们后面会讲到
console.log(b); // hello world
复制代码

3)模块标识: 模块标识其实就是require()方法里的参数,它能够以 . 和 .. 开头的相对路径,也能够是绝对路径。能够没有文件名后缀.js,后面会讲到为什么能够没有后缀。缓存


Node的模块实现

上面的代码就是最简单的模块的使用。那么在咱们这几行简单的代码背后,在实现过程当中到底是什么样的过程呢?咱们一点点来分析。安全

Node在引入模块经历3个步骤:路径分析、文件定位和编译执行bash

在Node中模块有两类:一类是Node提供的核心模块,另外一类是用户本身编写的文件模块。

部分的核心模块直接加载在内存中,因此引入这部分模块时,文件定义和编译执行均可以省略,在路径分析中优先判断,加载速度也是最快的。

另外须要知晓的是,Node引入的模块都会进行缓存,以减小二次引用时的开销。它缓存的是编译和执行后的对象,而不是和浏览器同样,缓存的是文件。


  1. 路径分析 上面已经说了,require()方法里参数叫模块标识,路径分析其实就是基于标识符来查找的。模块标识符在Node中分为如下几类:

    • 核心模块,好比http、fs、path等。
    • . 或 .. 开始的相对路径文件模块。
    • 以/开始的绝对路径文件模块。
    • 非路径形式的文件模块,如自定义的connect模块。
  • 核心模块的优先级仅次于缓存加载,因为在Node源码编译中已经被编译为二进制代码,因此加载过程是最快的。

  • 相对路径文件模块在分析路径时,require()方法会将路径转为真实路径,并以绝对路径最为索引,将编译执行后的结果放入缓存,以使二次引用时加载更快。

  • 自定义模块是特殊的文件模块,多是文件或者包的形式,也是查找最慢的一种模块。

Node在定位文件模块的具体文件时制定的查找策略能够表现为一个路径数组。

在js文件中console.log(module.paths);
放到任意目录中执行;
就会获得相似如下的数组:

[ '/Users/lq/Desktop/node_modules', //当前文件目录下的node_modules目录
  '/Users/lq/node_modules',//父目录下的node_modules目录
  '/Users/node_modules',//父目录的父目录下的node_modules目录
  '/node_modules' ]//沿路径向上逐级递归,直到找到根目录下的node_modules目录
复制代码

当前文件的路径越深,模块查找就越耗时,这是自定义模块加载慢的缘由。

  1. 文件定位

require()在分析标识符的时候,可能会出现没有传递文件拓展名的状况。CommonJS模块规范是容许这种状况出现的,不过Node会按照.js、.json、.node的顺序补足拓展名。依次调用fs模块同步阻塞式地判断文件是否存在。

在这个过程当中,Node对CommonJS模块规范进行了必定的支持。首先,Node会在当前目录下查找包描述文件package.json,经过JSON.parse()解析出包描述对象,取出main属性指定的文件名进行定位,若是缺乏拓展名,就进入上面说的拓展名分析步骤,按顺序补足拓展名再查找。若是main属性指定的文件名错误或者没有包描述文件package.json,Node会将index看成默认文件名,依次查找index.js、index.json、index.node。若是全部的路径数组都遍历了,仍是没有找到,就会抛出错误。

  1. 模块编译 在定位到具体的文件后,Node会新建一个模块对象,而后根据路径进行编译。根据不一样的拓展名操做不一样的方法。
  • 若是是.js文件,经过fs模块同步读取文件后编译执行
  • 若是是.json文件,经过fs模块同步读取文件后,用JSON.parse()解析返回结果
  • 若是是.node文件,经过dlopen()方法加载最后编译生成的文件。这是C/C++编写的拓展文件,本人在后面实现原理的过程当中予以忽略。

说了这么多,下面直接进入实现环节

先建立一个a.js文件,写入:

module.exports = 'hello world';
复制代码

再建立一个b.js,写入:

let b = require('./a.js');
console.log(b); //hello world
复制代码

打印的结果是hello wrold。这是Node自带的require方法。如今咱们来实现下咱们本身的require方法。

咱们直接在b.js里修改下:

//引入Node的核心模块
let fs = require('fs');
let path = require('path');
let vm = require('vm');

function Module(p) {
    this.id = p; //当前模块的标识,也就是绝对路径
    this.exports = {}; //每一个模块都有exports属性,添加一个
    this.loaded = false; //是否已经加载完
}
//对文件内容进行头尾包装
Module.wrapper = ['(function(exports,require,module){', '})']

//全部的加载策略
Module._extensions = {
    '.js': function (module) { //读取js文件,增长一个闭包
        let script = fs.readFileSync(module.id, 'utf8');
        let fn = Module.wrapper[0] + script + Module.wrapper[1];//包装在一个闭包里
        vm.runInThisContext(fn).call(module.exports, module.exports, myRequire, module);//经过runInThisContext()方法执行不污染全局
        return module.exports;

    },
    '.json': function (module) {
        return JSON.parse(fs.readFileSync(module.id, 'utf8')); //读取文件
    }
}

Module._cacheModule = {} //存放缓存

Module._resolveFileName = function (moduleId) { //根据传入的路径参数返回一个绝对路径的方法
    let p = path.resolve(moduleId);
    if (!path.extname(moduleId)) { //若是没有传文件后缀
        let arr = Object.keys(Module._extensions); //将对象的key转成数组
        for (let i = 0; i < arr.length; i++) { //循坏数组添加后缀
            let file = p + arr[i];
            try {
                fs.accessSync(file); //查看文件是否存在,存在的就返回
                return file;
            } catch (e) {
                console.log(e); //不存在报错
            }
        }
    } else {
        return p; //若是已经传递了文件后缀,直接返回绝对路径
    }
}

Module.prototype.load = function (filepath) { //模块加载的方法
    let ext = path.extname(filepath);
    let content = Module._extensions[ext](this);
    return content;
}

function myRequire(moduleId) { //自定义的myRequire方法
    let p = Module._resolveFileName(moduleId); //将传递进来的模块标示转成绝对路径
    if (Module._cacheModule[p]) { //若是模块已经存在
        return Module._cacheModule[p].exports; //直接返回编译和执行以后的对象
    }
    let module = new Module(p); //模块不存在,先建立一个新的模块对象
    let content = module.load(p); //模块加载后的内容
    Module._cacheModule[p] = module;
    module.exports = content;
    return module.exports;
}

let b = myRequire('./a.js');
console.log(b);
复制代码

这样就能够经过本身的myRequire()方法拿到a.js里的字符串hello world了。固然,module的源码不止这么多,有兴趣的能够本身查看。本文只是说明下module加载的原理。有写的不够严谨的地方,望谅解。若有错漏,可指出,定及时修改。

参考

部份内容根据《深刻浅出Node.js》一书整理

相关文章
相关标签/搜索