NodeJS模块原理分析

1.nodejs模块简介

  • 在CommonJS规范中一个文件就是一个模块。
  • 在CommonJS规范中通过exports暴露数据。
  • 在CommonJS规范中通过require()导入模块。

2.怎么执行读取的文件

我们都知道通过fs模块可以读取文件,但是读取到的数据要么是二进制,要么是字符串。无论是二进制还是字符串都无法直接执行。但是我们知道如果是字符串,在JS中是有办法让它执行的:eval或者new Function;

2.1 通过eval执行代码

缺点:存在依赖关系,字符串可以访问外界数据,不安全。

let str = 'console.log("hello")'
eval(str) // hello
// 存在依赖关系, 字符串可以访问外界数据,不安全
let name = "lgg";
let str1 = "console.log(name);";
eval(str1); // lgg

2.2 通过new Function执行代码

缺点:存在依赖关系,依然可以访问全局数据,不安全。

let str = "console.log('aaaa');";
let fn = new Function(str);
console.log(fn);
/*打印: anonymous() {
    console.log('aaaa');
  }
*/
fn(); // aaaa
// 存在依赖关系, 字符串可以访问外界数据,不安全
let name = "lgg";
let str = "console.log(name);";
let fn = new Function(str);

2.3 通过NodeJS的vm虚拟机执行代码

2.3.1 runInThisContext

提供了一个安全的环境给我们执行字符串中的代码。runInThisContext提供的环境不能访问本地的变量,但是可以访问全局的变量(也就是global上的变量)。

const vm = require('vm')
let str = "console.log('lgg')"
vm.runInThisContext(str) // lgg

let name1 = 'lgg'
let str1 = "console.log(name1)"
vm.runInThisContext(str1) // name is not defined

global.name2 = 'lgg'
let str2 = "console.log(name2)"
vm.runInThisContext(str2) // lgg

2.3.2 runInNewContext

提供了一个安全的环境给我们执行字符串中的代码。提供的环境不能访问本地的变量,也不能访问全局的变量(也就是global上的变量)。

let name1 = "lgg"
let str1 = "console.log(name1)"
vm.runInNewContext(str1) // name1 is not defined

global.name2 = "lgg"
let str2 = "console.log(name2)"
vm.runInNewContext(str2) // name2 is not defined

3.Node模块原理分析

既然一个文件就是一个模块,既然想要使用模块必须先通过require()导入模块。所以可以推断出require()的作用其实就是读取文件。所以想要了解Node是如何实现模块的,必须先了解如何执行读取到的代码。

3.1 Node模块加载流程分析

(1)内部实现了一个require方法。

function require(path) {
    
    
   return self.require(path);
}

(2)通过Module对象的静态__load方法加载模块文件。

Module.prototype.require = function(path) {
    
    
  return Module._load(path, this, /* isMain */ false);
};

(3)通过Module对象的静态_resolveFilename方法, 得到绝对路径并添加后缀名。

var filename = Module._resolveFilename(request, parent, isMain);

(4)根据路径判断是否有缓存, 如果没有就创建一个新的Module模块对象并缓存起来。

  var cachedModule = Module._cache[filename];
  if (cachedModule) {
    
    
    return cachedModule.exports;
  }
  var module = new Module(filename, parent);
  Module._cache[filename] = module;
  function Module(id, parent) {
    
    
    this.id = id;
    this.exports = {
    
    };
  }

(5)利用tryModuleLoad方法加载模块tryModuleLoad(module, filename)。

  • 取出模块后缀
     var extension = path.extname(filename);
    
  • 根据不同后缀查找不同方法并执行对应的方法, 加载模块
     Module._extensions[extension](this, filename);
    
  • 如果是JSON就转换成对象
    module.exports = JSON.parse(internalModule.stripBOM(content));
    
  • 如果是JS就包裹一个函数
    var wrapper = Module.wrap(content);
    NativeModule.wrap = function(script) {
          
          
       return NativeModule.wrapper[0] + script + NativeModule.wrapper[1];
    };
    NativeModule.wrapper = [
    '(function (exports, require, module, __filename, __dirname) { ',
    ‘\n});];
    
  • 执行包裹函数之后的代码, 拿到执行结果(String – Function)
    var compiledWrapper = vm.runInThisContext(wrapper);
    
  • 利用call执行fn函数, 修改module.exports的值
    var args = [this.exports, require, module, filename, dirname];
    var result = compiledWrapper.call(this.exports, args);
    
  • 返回module.exports
    return module.exports;
    

3.2 自己实现一个require方法

const path = require('path')
const fs = require('fs')
const vm = require('vm')

class NJModule {
    
    
  constructor(id) {
    
    
    this.id = id; // 保存当前模块的绝对路径
    this.exports = {
    
    }  
  }
}
NJModule._cache = {
    
    }
NJModule.wrapper = ['(function (exports, require, module, __filename, __dirname) { ', '\n});'];
NJModule._extensions = {
    
    
  '.js': function(module) {
    
    
    // 1、读取js代码
    let script = fs.readFileSync(module.id)
    // 2、将JS代码包裹到函数中
    /*
    (function (exports, require, module, __filename, __dirname) {
    exports.名 = 值;
    });
    * */
    let strScript = NJModule.wrapper[0] + script  + NJModule.wrapper[1]
    // 3、将字符串转换成JS代码
    let jsScript = vm.runInThisContext(strScript)
    // 4、执行转换后的JS代码
    // var args = [this.exports, require, module, filename, dirname];
    // var result = compiledWrapper.call(this.exports, args);
    jsScript.call(module.exports, module.exports) // 在函数中使用的exports就是对象(即module.exports)的exports。因为是对象为引用关系。
  },
  '.json': function(module) {
    
    
      let json = fs.readFileSync(module.id);
      let obj = JSON.parse(json);
      module.exports = obj
  } 
}

function tryModuleLoad(module) {
    
    
  // 4.1取出模块后缀
  let extName = path.extname(module.id)
  NJModule._extensions[extName](module)
}

function njRequire(filePath) {
    
    
  // 1.将传入的相对路径转换成绝对路径
  let absPath = path.join(__dirname, filePath)
  // 2.尝试从缓存中获取当前的模块
  let cachedModule = NJModule._cache[absPath]
  if(cachedModule) {
    
    
    return cachedModule.exports;
  }
  // 3.如果没有缓冲就自己创建一个njModule对象,并缓存起来
  var module = new NJModule(absPath)
  NJModule._cache[absPath] = module
  // 4.利用tryModuleLoad方法加载模块
  tryModuleLoad(module);
  // 5.返回模块的exports
  return module.exports
}

下面来测试下:

//test.js
exports.a=12
exports.fn=()=>{
    
    
    console.log("111")
}
//person.js
{
    
    
  "name": "bob",
  "age": 12
}
//index.js
let test=njRequire("./test.js");
console.log(test)//{ a: 12, fn: [Function (anonymous)] }
test.fn();//111

let person=njRequire("./person.json");
console.log(person)//{ name: 'bob', age: 12 }

在这里插入图片描述

4.一些思考

4.1 NodeJS中为什么可以直接使用exports、require、module、__filename、__dirname?

因为所有的NodeJS文件在执行的时候都会被包裹到一个函数中,这些属性都被通过参数的形式传递过来了。

(function (exports, require, module, __filename, __dirname) {
    
    
exports.=;
});

4.2 NodeJS中为什么不能直接exports赋值,而可以给module.exports赋值?

(function (exports, require, module, __filename, __dirname) {
    
    
    exports = "lnj";
});
jsScript.call(module.exports, module.exports);
return module.exports;

//相当于
let exports = module.exports;
exports = "lnj";
return module.exports;

在这里插入图片描述
exports是形参,module.exports传递给它,两者指向同一个对象。如果直接给exports赋值(exports=‘aaa’)则相当于修改了它的指向,但最后却返回module.exports。

在这里插入图片描述

let moduleA={
    
    
    exportsA:{
    
    }
}
let exportsA=moduleA.exportsA;
// exportsA.aaa=222;
// console.log("moduleA",moduleA);//moduleA { exportsA: { aaa: 222 } }
// console.log("exportsA",exportsA);//exportsA { aaa: 222 }


exportsA=222;
exportsA.aaa=333;

console.log("moduleA",moduleA);//moduleA { exportsA: {} }

console.log("exportsA",exportsA);//exportsA 222

4.3 通过require导入包的时候应该使用var/let还是const?

导入包的目的是使用包而不是修改包,所以导入包时使用const接受。

4.4 require和import的区别

import和require都是被模块化所使用。在ES6当中,用export导出接口,用import引入模块。但是在node模块中,使用module.exports/exports导出接口,使用require引入模块。

4.4.1 出现的时间不同

  • require表示的是运行时加载/调用,所以理论上可以运用在代码的任何地方。
  • import表示的是编译时加载(效率更高),由于是编译时加载,所以import命令会提升到整个模块的头部。
  • import输入的变量是只读的,引用类型可以,其他模块也可以读到改后的值,但不建议修改。
  • import是静态执行,不能使用表达式和变量。
    // 报错
    import {
          
           'f' + 'oo' } from 'my_module';
    // 报错
    let module = 'my_module';
    import {
          
           foo } from module;
    
  • import是Singleton模式。
    import {
          
           foo } from 'my_module';
    import {
          
           bar } from 'my_module';
    // 等同于
    import {
          
           foo, bar } from 'my_module';
    // 虽然foo和bar在两个语句加载,但是他们对应的是同一个my_module实例
    

4.4.2 遵循的模块化规范不同

  • require是AMD规范引入方式。
  • import是ES6的一个语法标准,如果要兼容浏览器的话必须转化成ES5的语法。

4.4.3 本质

  • require是赋值过程。module.exports后面的内容是什么,require的结果就是什么,比如对象、数字、字符串、函数等,然后再把require的结果赋值给某个变量,它相当于module.exports的传送门。
  • import是结构过程,但是目前所有的引擎都还没有实现import,我们在node中使用babel支持ES6,也仅仅是将ES6转码为ES5再执行,import语法会被转码为require。
  • import虽然是es6中的语法,但就目前来说,所有的引擎都还没有实现import。
  • import语法实际上会被转码为require。这也是为什么在模块导出时使用module.exports,在引入模板时使用import仍然起效,因为本质上,import会被转码为require去执行。