模块与模块加载小册

708 阅读3分钟
  • 模块的分类
  • 模块的加载机制
  • 文件的查找机制(非原生模块查找)
    • 先当做一个文件路径名
    • 当做一个文件夹/包 路径名
    • 顺着node_modules的链往上查找
  • 模块加载的简单实现
  • 其它注意事项

模块的分类

原生模块(内置模块、核心模块)、文件模块、第三方模块

  • 原生模块:Node.js自带的模块
  • 文件模块:我们自己写的模块
  • 第三方模块:通过NPM下载的模块

模块的加载机制

下图是个大概示例

文件的查找机制(非原生模块查找)

查找文件时,它会进行如下几种尝试(按照从上到下的优先级)

先当做一个文件路径名

  • 尝试直接查找该文件,

  • 尝试添加扩展名后查找文件

当做一个文件夹/包 路径名

  • 尝试根据包查找文件 ->从包描述文件(package.json)获取文件名->尝试添加扩展名后查找文件

  • 尝试查找该目录下的index(.js/.node)

顺着node_modules的链往上查找

如果到现在为止还没找到,且加载的不是相对路径,像这样requrie('./a'),那么会继续当做一个包并且顺着module.paths的路径依次往上查找node_modules里面有没有这么一个包/文件夹

like this

// [ 'd:\\WEB\\A\\node-basic\\03module\\node_modules',
//   'd:\\WEB\\A\\node-basic\\node_modules',
//   'd:\\WEB\\A\\node_modules',
//   'd:\\WEB\\node_modules',
//   'd:\\node_modules' ]

模块加载的简单实现

关键字:绝对路径与resolvePathname后缀名与不同的加载方式module.exports

首先我们定义一个我们自己的require方法和一个Module构造函数函数

function Module(){}
function req(filename){}

这个构造函数下有一些属性

Module._cache = {} //用来存放已经加载的模块
Module._extentions = ['.js','.json','.node']; //当文件没有后缀名时候会从这里取

其中所存放的cache是长这样的

//cache
{
    "文件的绝对路径":"Module的实例"
    ...
}

再让我们看看rq方法,

其中要做的第一件事是解析传入的文件名,是否是有效的,并且确保它是绝对路径。

为什么要确保它是绝对路径呢?有些相对路径在某些编辑器环境中代表的真正路径并不是我们想要的。我们使用绝对路径来读取文件会更可靠。

拿到解析后的路径后我们会尝试用这个路径作为key去Module._cache中获取缓存。

如有缓存是存在的我们就返回这个缓存模块下的exports,如果不存在我们就加载这个模块

function req(){
    filename = Module._resolvePathname(filename);
    let cacheModule = Module._cache['filename'];
    if(cacheModule){
        return cacheModule.exports;
    }
    
    let module = new Module(filename)
}

So,加载加载,是干了什么事呢?

我们所说的加载就是Module的实例化,并在这个实例上的exports上挂载我们readFileSync的那个文件内部module.exports所导出的内容。

但需要注意的是,加载不同的模块时我们做的事其实是有所区别的,

Module.prototype.load = function(){
    let ext = path.extname(this.filename);
    Module._extentions[ext](this);
}

我们可以发现对于不同的文件类型我们将调用不同的方法去加载它。

  • 如果是一个json文件,我们加载它就是直接将读取到的内容挂载在module.exports上。
Module._extentions['json'] = function(module){
    let json = fs.readFileSync(module.filename,'utf8');
    json = JSON.parse(json);
    return module.exports = json;
}
  • 如果是一个js文件,我们加载它不仅要读取它的内容还要让这个js文件执行,并且是将它包起来作为一个闭包传入我们的module和req让它执行。
Module._extentions['js'] = function(module){
    let script = fs.readFileSync(module.filename,'utf8');
    script = Module._wrap(script);
    vm.runInThisContext(script).call(module.exports,module.exports,req,module);
}

注意: 在js的加载中,我们是将实例化后的module传入到闭包当中,在闭包中用module.exports导出模块的内容的。


加载完成后,我们需要缓存模块。

而缓存就是将module实例存储到Module._cahce的过程,每一个module都有对应的一个key,这个key就是这个模块的绝对路径。

function req(){
    ...
    Module._cache[filename] = module;
    ...
}

其它注意事项

  • 模块的加载是同步的!

源码

仓库地址: 点我~