NODE基础总结(2) —— Module

315 阅读6分钟

Module发展历程

一开始浏览器是不支持模块的概念的,所以就提出了命名空间的方法,但是这种方式存在着一个很大的问题就是很难保证名字永远不会重复。另外,现在也很多人使用闭包的方式来实现,但是由于闭包会使得函数中的变量都被保存在内存中,所以使用不当的话就会造成性能问题。

在这之后陆续出现了实现CMD规范的Seajs和AMD规范的RequireJS,如果有兴趣的可以去了解一下,但是目前来说它们都已经被deprecate了。

在Node出现之后,它实现了CommonJS的规范,CommonJS的规范采用的是同步的加载方式(也称动态加载),但是这套规范在浏览器中并不能使用

之后就出现了umd规范,其实它就是统一了上述3种规范

在es6出现之后,又衍生出了esmodule规范,在node的新版本中已经开始尝试去支持使用这套规范了,在Node中就叫mjs

CommonJS简介

CommonJS背后其实使用的就是闭包的原理来保证封闭作用域与封装功能,同时它也解决了模块间依赖的问题,在CommonJS规范中规定了一个文件就是一个模块。

模块常见有两种形式,第一种就是系统模块,就是Node中提供的如fs,http等,还有一种就是文件模块,其实也就是我们自己实现的模块

文件模块的用法上其实就是在我们实现的模块文件中加入module.exports

// school.js
module.exports = 'test'

// use school.js
let school = require('./school');
console.log(school)

CommonJS原理

接下来我们就自己来实现一个简单版本的模块加载来理解CommonJS背后的一些主要原理

首先我们先定义好一个基本的框架和测试代码,这里filename是文件名,而且要注意文件名可能没有后缀,在Node使用模块时如果是js或者json或者node结尾的文件可以省略后缀

function req(filename) {
}

let result = req('./school');
console.log(result);

在调用模块时,为了节约性能,node会实现缓存的机制,将module.exports后面的结果进行缓存,require时如果有直接把缓存返回回去,从而避免重复读取调用问题,那么在不同的路径下可能存在同样的文件名,所以我们缓存需要根据绝对路径来存储,我们可以先定义一个Module的构造函数来创建一个模块的实例,然后它会存放着一个exports属性用来存模块中要export的数据

function Module(filename) { // 构造函数
    this.filename = filename;
    this.exports = {};
}
Module._cache = {}; //缓存

同时我们也需要定义一个用来存放各种后缀名的数组,用于查找处理不同的文件引入

Module._extentions = ['.js','.json','.node'];

接下来我们再定义一个函数用来将filename解析为绝对路径

let path = require('path');
let fs = require('fs');

Module._resolvePathname = function(filename) {
    let p = path.resolve(__dirname, filename);
    if (!path.extname(p)) {
        for(var i = 0;i < Module._extentions.length;i++){
            let newPath = p + Module._extentions[i];
            try{ // 如果访问的文件不存在 就会发生异常
                fs.accessSync(newPath);
                return newPath
            }catch(e){
            }
        }
    }
    return p; //解析出来的就是一个绝对路径
}

接着我们需要判断返回的绝对路径在缓存中是否存在,如果存在就直接返回缓存的exports属性

function req(filename) { // filename是文件名 文件名可能没有后缀
    // 我们需要弄出一个绝对路径来,缓存是根据绝对路径来的
    filename = Module._resolvePathname(filename); 
    // 先看这个路径在缓存中有没有,如果有直接返回
    let cacheModule = Module._cache[filename];
    if (cacheModule) { // 缓存里有 直接把缓存中的exports属性进行返回
        return cacheModule.exports
    }
}

如果没有缓存的话,我们就需要开始加载模块了,首先先创建一个模块的实例

let module = new Module(filename);

接着我们需要定义一个load方法来加载这个模块,对于不同的文件类型,我们需要有不同的处理方法

let vm = require('vm');

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) { // {filename,exports={}}
    let script = fs.readFileSync(module.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) { //{filename:'c://xxxx',exports:'zfpx'}
    // 模块可能是json 也有可能是js
    let ext = path.extname(filename).slice(1); // .js   .json
    Module._extentions[ext](this);
}

这里对于json我们就是简单的将json文件的内容解析并返回给module的exports属性即可,而对于js我们做的其实是将文件内容拼接到我们的闭包函数字符串中然后通过vm.runInThisContext(fnStr).call的方式去调用这个闭包执行,我们会将module这个对象传入函数中,所以当文件内容执行module.exports之后其实就是将要导出的数据传给了module对象的exports属性。

最后我们将module存入缓存中,然后返回module的exports属性即可,下面就是整个完整的代码:

let path = require('path');
let fs = require('fs');
let vm = require('vm');

function Module(filename) { // 构造函数
    this.filename = filename;
    this.exports = {};
    this.loaded = true;
}

Module._extentions = ['.js','.json','.node']; // 如果没有后缀 希望添加上查找
Module._cache = {};
Module._resolvePathname = function(filename) {
    let p = path.resolve(__dirname, filename);
    if (!path.extname(p)) {
        for(var i = 0;i < Module._extentions.length;i++){
            let newPath = p + Module._extentions[i];
            try{ // 如果访问的文件不存在 就会发生异常
                fs.accessSync(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) { // {filename,exports={}}
    let script = fs.readFileSync(module.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) { //{filename:'c://xxxx',exports:'zfpx'}
    // 模块可能是json 也有可能是js
    let ext = path.extname(filename).slice(1); // .js   .json
    Module._extentions[ext](this);
}

function req(filename) { // filename是文件名 文件名可能没有后缀
    // 我们需要弄出一个绝对路径来,缓存是根据绝对路径来的
    filename = Module._resolvePathname(filename); 
    // 先看这个路径在缓存中有没有,如果有直接返回
    let cacheModule = Module._cache[filename];
    if (cacheModule) { // 缓存里有 直接把缓存中的exports属性进行返回
        return cacheModule.exports
    }
    // 没缓存 加载模块
    let module = new Module(filename);  // 创建模块 {filename:'绝对路径',exports:{}}
    module.load(filename); // 加载这个模块     {filename:'xxx',exports = 'zfpx'}
    Module._cache[filename] = module;
    module.loaded = true; // 表示当前模块是否加载完 
    return module.exports;
}
let result = req('./school');
console.log(result);

模块的加载策略就是按照以下这张图的逻辑执行

对于文件模块的查找规则可以参照下面这张图

这里要注意几个点

  1. 对于第三方模块的引入,不可以加./
  2. CommonJS会有循环引用的问题,所以在引入时需要注意
  3. 在使用exports时,从我们前面提到的实现解析中就可以看出exports本质上就是module.exports,所以两者并无区别,但是需要注意直接使用exports = 这样的写法是错误的,因为这样就将exports指向另一个地址,但是module对象里的exports并不会跟着改变