commonjs探秘

89 阅读11分钟

前言

最近我在梳理nodejs的模块机制,以供日后查阅。以下代码实例基于node v14.16.0,并且实例主要采用javascript进行书写,示例文件目录如下

commonjs

├── index.js

├── person.js

├── propagation.js

├── require

│ ├── index.js

│ ├── main.jsx

│ └── package.json

├── require.json

├── require.jsx

├── require.node

└── synchronization.js

下面将会有几个问题,可以自己先想一想,如果你对这些问题都能回答上来,那么你可以选择不需要往下看

  1. cjs模块化定义分为几个部分
  2. 模块导出机制是怎么样的(module.exports和exports有什么区别)
  3. cjs模块查找机制是怎么样的
  4. cjs模块引用是同步的还是异步的
  5. cjs是如何解决模块之间相互引用的
  6. require函数做了什么
  7. 模块中并没有申明module变量,为什么可以输出一个对象
  8. 自己能否实现一个类似cjs的模块

cjs模块定义

cjs定义模块主要分为以下3个部分

模块定义(导出)

必须使用module.exports或exports定义模块

const person1 = {
  name: 'zgl1',
  age: 27,
  getName() {
    return this.name;
  },
  setName(name) {
    this.name = name;
  },
};

const person2 = {
  name: 'zgl2',
  age: 27,
  getName() {
    return this.name;
  },
  setName(name) {
    this.name = name;
  },
};

// 采用module.exports的方式进行模块导出
module.exports = {
  person1
}

// 采用exports的方式进行模块导出
exports.person2 = person2;

模块引用(导入)

必须使用require函数引入模块

const person = require('./person');

console.log(person.person2, 'person2');

模块标识

本质上就是传给require函数的参数,他可以是具体的文件路径(相对路径或绝对路径),也可以是字符串,也可以是省略后缀的文件名

// 字符串
const react = require('react');
// 相对路径
const person = require('./person');
// 绝对路径
const methods = require('src/shard/index.js');
// 省略后缀的文件名
const components = require('src/components/index') 

模块导出机制

module

让那个需要导出的模块成为整个模块进行导出

const person = {
  name: 'zgl1',
  age: 27,
  getName() {
    return this.name;
  },
  setName(name) {
    this.name = name;
  },
};

const person2 = {
  name: 'zgl2',
  age: 27,
  getName() {
    return this.name;
  },
  setName(name) {
    this.name = name;
  },
};

module.exports = {
  person
}

exports

  • 让需要导出的模块成为模块的某个属性进行导出
const person = {
  name: 'zgl1',
  age: 27,
  getName() {
    return this.name;
  },
  setName(name) {
    this.name = name;
  },
};

const person2 = {
  name: 'zgl2',
  age: 27,
  getName() {
    return this.name;
  },
  setName(name) {
    this.name = name;
  },
};

exports.person = person

由此可见,module.expprts和exports刚开始引用的是同一个对象

module.exports和exports的关系

在不修改module.exports的引用时,module.exports和exports指向了用一个引用对象,如果修改了module.exports,那么他们之间的引用关系就会断开

  • 没有修改module.exports的引用
console.log(module.exports === exports); // true
  • 给exports增加了一个属性
const person2 = {
  name: 'zgl2',
  age: 27,
  getName() {
    return this.name;
  },
  setName(name) {
    this.name = name;
  },
};


exports.person2 = person2;
console.log(module.exports === exports); // true
  • 修改module.exports的引用
console.log(module.exports === exports);

const person1 = {
  name: 'zgl1',
  age: 27,
  getName() {
    return this.name;
  },
  setName(name) {
    this.name = name;
  },
};

module.exports = {
  person
};
console.log(exports, 'exprots'); // {}
console.log(module.exports, 'module.exports'); 
/* 
{
  name: 'zgl1',
  age: 27,
  getName: [Function: getName],
  setName: [Function: setName]
}
*/

console.log(module.exports === exports); // false

模块导入机制

模块导入机制比较复杂,接下来我们一点一点的去揭开他的神秘面纱,由于这一块内容比较复杂,我们先来简单的看一下脑图

模块真正导入的是module.exports

我们分别采用module.exports和exports分别导出一个对象,看看最后require引用的对象是谁

const person1 = {
  name: 'zgl1',
  age: 27,
  getName() {
    return this.name;
  },
  setName(name) {
    this.name = name;
  },
};

const person2 = {
  name: 'zgl2',
  age: 27,
  getName() {
    return this.name;
  },
  setName(name) {
    this.name = name;
  },
};

module.exports = {
  person1
}
exports.person2 = person2;
const person = require('./person');

console.log(person.person2, 'person2'); 
// undefined person2
console.log(person.person1, 'person1');
/* 
{
  name: 'zgl1',
  age: 27,
  getName: [Function: getName],
  setName: [Function: setName]
} person1 
*/

// 实际导出的是module.exports

模块导入是值传递(浅复制)

  • 对于基本数据类型,模块导入复制的是数值
let number1 = 1;

module.exports = number1;

setTimeout(() => {
  console.log(number1,'我是暴露出去的number1') // 1
},2000)
let number1 = require('./propagation');

number1 = 2;

setTimeout(() => {
  console.log(number1, '我是引用被修改后的number1')// 2
},1000)
  • 对于引用数据类型,模块导入复制的是引用地址
const obj = {
  number1: 1
}

module.exports = obj;

setTimeout(() => {
  console.log(obj, '我是暴露出去的obj') // {number1: 2}
},1000)
const obj = require('./propagation');

obj.number1 = 2;

setTimeout(() => {
  console.log(obj, '我是引用的obj') // {number1: 2}
},2000)

require函数的查询机制

模块路径是文件名
  • 模块路径未省略后缀名

如果当前require引用的模块标识是完整的,那么就会按照当前标识符进行查找

module.exports = {
  extname: 'js'
}

模块路径省略后缀名

  • 优先查找是否存在模块名.js文件
module.exports = {
  extname: 'js'
}

console.log('js')
{
  "extname": "json"
}
module.exports = {
  extname: 'node'
} // 该文件为二进制文件,会报错,但是不会报查找不到报错
const extnameObj = require('./require');
console.log(extnameObj, '444');
// { extname: 'js' }
  • 如果未找到模块名.js,其次查找模块名.json
module.exports = {
  extname: 'js'
}

console.log('js')
{
  "extname": "json"
}
module.exports = {
  extname: 'node'
} // 该文件为二进制文件,会报错,但是不会报查找不到报错
const extnameObj = require('./require');
console.log(extnameObj, '444');
// { extname: 'json' }
  • 如果未找到模块名.json,其次查找模块名.node
module.exports = {
  extname: 'js'
}

console.log('js')
{
  "extname": "json"
}
module.exports = {
  extname: 'node'
} // 该文件为二进制文件,会报错,但是不会报查找不到报错
const extnameObj = require('./require');
console.log(extnameObj, '444');
// return process.dlopen(module, path.toNamespacedPath(filename));
  • 如果未查找到模块名.node,则报错
module.exports = {
  extname: 'js'
}

console.log('js')
{
  "extname": "json"
}
module.exports = {
  extname: 'node'
} // 该文件为二进制文件,会报错,但是不会报查找不到报错
const extnameObj = require('./require');
console.log(extnameObj, '444');
// Error: Cannot find module './require'
模块路径是文件夹名
  • 先判断该模块路径是否存在,如果不存在报错
  • 如果模块路径存在,查找当前文件夹中是否存在package.json
    • 如果存在package.json,判断package.json是否有main字段
module.exports = {
    extname: '我是require文件中的index.js'
}
{
  "main": "main.js"
}
module.exports = {
  extname: '我是通过package中指定的路径'
}
const extname = require('./require');
console.log(extname, 'extname'); // { extname: '我是package中的main.js' }
      • 如果不存在main字段则依次查找当前目录是否存在index.js,index.json,index.node
module.exports = {
    extname: '我是require文件中的index.js'
}
{
  "main1": "main.js"
}
module.exports = {
  extname: '我是通过package中指定的路径'
}
const extname = require('./require');
console.log(extname, 'extname'); // { extname: '我是require文件中的index.js' }
    • 如果不存在package.json, 依次查找当前文件是否存在index.js,index.json,index.node
module.exports = {
    extname: '我是require文件中的index.js'
}
{
  "main1": "main.js"
}
module.exports = {
  extname: '我是通过package中指定的路径'
}
const extname = require('./require');
console.log(extname, 'extname'); // { extname: '我是require文件中的index.js' }
模块路径是字符串(node_modules查找)
  • 判读模块路径是否为内置模块
module.exports = {
  fs: '自定义的fs'
}
const fs = require('fs');
console.log(fs, '6666'); // node内置的fs模块
  • 如果模块路径是内置模块,则直接加载
  • 如果文件不是内置模块,那么查找和当前引用所在的文件下是否存在node_modules
  • 如果存在则去node_modules中按照模块路径是文件夹的规则进行查找
module.exports = {
  fs: '自定义的fs'
}
const fs = require('fs1');
console.log(fs, '6666'); // { fs: '自定义的fs' }
  • 如果不存在,则返回上一级目录继续查找,一直查找不到则一直返回,直到返回根node_modules
module.exports = {
  fs: '自定义的fs'
}
module.exports = {
  fs: '我是common下面的node_modules'
}
const fs = require('fs1');
console.log(fs, '6666'); // { fs: '我是common下面的node_modules' }

讲了这么模块查找机制总结一下

模块的导入机制

  • 核心模块,如http、fs、path等
  • 相对路径文件模块,以.或者..开头的
  • 绝对路径文件模块,以/开头
  • 非路径模块形式的文件模块,比如自定义模块

针对不同的标识符,Node会区别对待

  • 文件扩展名分析 如果require()传递的标识是不包含文件的扩展名,那么Node会按.js、.json、.node的次序不全扩展名,而后依次尝试定位文件。
  • 目录分析和包 在分析标识符的过程中,可能没有找到对应的文件,但却得到一个目录,此时Node会将目录当做一个包来处理

那么,Node首先在当前目录查找package.json,通过JSON.parse()解析出包的描述对象,从中取出main属性指定的文件名进行定位,如果文件缺少扩展名,将会进入到扩展名分析的步骤。而如果main属性指定的文件名错误或压根没有package.json文件,Node就会把index当做默认文件名,然后依次查找index.js、index.json、index.node

如果在目录分析过程中没有成功定位,则自定义模块进入下一个模块路径进行查找(node_modules查找规则)。如果模块路径数组都遍历完依然没找到目标文件,则抛出查找失败的异常

模块加载机制

内置模块是由Node.js官方提供,内置模块的加载优先级是最高的。

自定义模块的加载机制

使用require()加载自定义模块时,必须指定以./或../开头的文件路径,如果没有指定,就会按内置模块或第三方模块进行加载;

如果缺省文件后缀时,先会按照具体的文件路径加载,然后会自动补齐js后缀进行加载,如果还是没找到,就补齐json后缀进行加载,还是没找到,就会补上node后缀进行查找,如果还是没查到,报错。

第三方模块的加载机制

首先一般会先尝试在node_modules文件夹进行加载,如果没有,就会翻到上一层目录中去查找,如果还是都没有找到,就报错

以目录作为模块

首先在package.json中查找main属性,以main属性指向的入口作为require入口,如果没有main属性,就会以index.js进行加载。

模块缓存机制

Node.js 模块不会被重复加载,这是因为 Node.js 通过文件名缓存所有加载过的文件模块,所以以后再访问到时就不会重新加载了。Node.js 是根据实际文件名缓存的, 而不是 require() 提供的参数缓存的, 也就是说即使你分别通过require('react') 和 require('./node_modules/react') 加载两次,也不会重复加载,因为尽管两次参数不同,解析到的文件却是同一个。正是这一机制的存在很好的解决了模块相互之间引用导致的模块依赖问题

实现模块加载器

module是什么

我们来输出看一下module 是什么东西

module.exports = {
  number: 1
}
console.log(module, 'module')
/*
Module {
  id: '.',
  path: '/Users/zhuguanglong/Desktop/前端工程化/模块化/commonjs/test',
  exports: { number: 1 },
  parent: null,
  filename: '/Users/zhuguanglong/Desktop/前端工程化/模块化/commonjs/test/index.js',
  loaded: false,
  children: [],
  paths: [
    '/Users/zhuguanglong/Desktop/前端工程化/模块化/commonjs/test/node_modules',
    '/Users/zhuguanglong/Desktop/前端工程化/模块化/commonjs/node_modules',
    '/Users/zhuguanglong/Desktop/前端工程化/模块化/node_modules',
    '/Users/zhuguanglong/Desktop/前端工程化/node_modules',
    '/Users/zhuguanglong/Desktop/node_modules',
    '/Users/zhuguanglong/node_modules',
    '/Users/node_modules',
    '/node_modules'
  ]
}
*/

我们发现module其实是一个对象,我们接下来分析一下module对象上的各属性

module对象上的属性

  • id 模块唯一标识(一般是当前文件所在的路径)
  • path 模块所在的文件夹的路径
  • exports 模块的导出对象
  • parent
  • filename 文件绝对路径
  • loaded 是否第一次加载
  • children 所被加载的其他模块
  • paths 当前文件假想的所在node_modules的路径

搞清楚了module对象上都有哪些属性以后我们就可以着手创建模块对象了

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

class Module {
  private id: string;
  private path: string;
  public exports: Record<string, any>;
  private parent: null | Array<any>;
  private filename: string;
  public loaded: boolean;
  public children: Module[];
  private paths: string[];

  constructor(filename: string) {
    this.path = path.resolve('.');
    this.paths = resolvePaths(path);
    this.loaded = false;
    this.filename = filename;
    this.id = filename;
    this.parent = null;
  }
}

const NODE_MODULES = '/node_modules';

/**
 * @param path { string } 文件路径;
 * @returns paths {string[]} 文件可能存在的路径;
 * @author zgl
 * @desprition 获取文件路径数组
 */
function resolvePaths(path: string): string[] {
  const newPaths: string[] = [];
  
  if (typeof path !== 'string') {
    return newPaths;
  }

  const paths = path.split('/');
  let filepath: string  = '';
  for (let i = 0; i < paths.length; i++) {
    if (i > 0) {
      filepath += '/' + paths[i];
    } 
      newPaths.push(`${filepath}${NODE_MODULES}`);   
  }

  return newPaths;
}

加载模块

function tryModuleLoad(module: Module) {
  const extension = extname(module.filename);
    extensions[extension](module);
}

将路径转化为绝对路径

不管模块标识是怎么样的,最后都会将其转化为绝对路径

const { resolve } = require('path');
function resolveFilename(path: stirng): string {
  const absoultPath = resolve(__dirname, path);
  return absoultPath;
}

获取文件后缀

由于针对以.js结尾的和.json结尾和.node结尾的逻辑不一样,因此我们需要获取文件后缀

function getExtision(path: string): stirng {
  const extision = path.extname(path);
  return extision;
}

处理不一样后缀的逻辑

  • .js: 将代码包裹在一个匿名自执行函数中
const wrapper = [
  '(function(exports, module, require, __dirname, __filename)){',
  '})'
];

const extensions = {
  '.js'(module) {
    const content = readFileSync(module.filename, 'utf-8');
    const fnStr = wrapper[0] + content + wrapper[1];
    const fn = runInThisContext(fnStr);
    const { exports, path, filename } = module;
    // 将this绑定给暴露出来的模块
    fn.call(exports, exports, module, myRequire, path, filename)
  },
}
  • .json: 将json进行序列化
const extensions = {
  '.json'(module) {
    const content = readFileSync(module.filename, 'utf-8');
    const contentObj =  JSON.parse(content);
    module.exports = contentObj;
  },
}
  • .node: 利用process.dlopen进行执行
const extensions = {
  '.node'(module) {
    const { filename } = module;
    return dlopen(module, toNamespacedPath(filename));
  }
}

尝试添加后缀

如果文件是省略文件后缀的形式给文件名分别添加.js,.json, .node 判断文件是否存在

const { accessSync } = require('fs');

function findExt(absolutePath: sting): sting {
  const extNames = Object.keys(extensions);
  let index = 0;
  try {
    if(extNames.length === index) {
      // 如果没有找到文件名,那么就去找文件夹
    }
    accessSync(absolutePath + extNames[index]);
    return absolutePath + extNames[index];
  }catch(e) {
    index++;
    findExt(absolutePath + extNames[index]);
  }
}

路径是文件

如果当前路径为文件,那么先判断当前文件下是否存在package.json, 如果存在解析main字段所对应的文件路径,如果不存在package.json,那么尝试寻找index.js,index.json, index.node

if(extNames.length === index) {
      // 如果没有找到文件名,那么就去找文件夹
  findDirExt(absoultPath);
}
function findDirExt(absoultPath: string): string {
  try {
     accessSync(join(absolutePath, 'package.json'));
     const content = findExt(join(absolutePath, './package.json'));
      const contentObj = JSON.parse(content);
      if ('main' in contentObj) {
        absolutePath = findExt(resolve(absolutePath, contentObj[main]));
      } else {
        absolutePath = findExt(resolve(absolutePath, 'index'));
      }
  }
  } catch(e) {
    absolutePath = findExt(resolve(absolutePath, 'index'));
  }
}

路径是模块

内置模块
if (naviteModule.includes(absolutePath)) {
      // 内置模块对应的二机制文件
      absolutePath = '内置模块路径';
    }
自定义模块
const paths = resolvePaths(absolutePath);
      let i = paths.length - 1;
      while (i >= 0) {
        if (findExt(paths[i)) {
          absolutePath = findExt(paths[i]);
          break;
        } else {
          
        }
        const path = findDirExt(paths[i]);
        if (path) {
          absolutePath = path;
          break;
        }
        i--;
      }

完整的require函数逻辑

require函数主要是用来加载模块的

const CHAR_DOT = '.';
const CHAR_FORWARD_SLASH = '/'
function require(moduleId) {
  // 获取文件绝对路径
  let absolutePath = resolve(__dirname, moduleId);
  let oldAbsolutePath = absolutePath;
  const extension = extname(absolutePath);
  if (Module.getCacheByModuleId(absolutePath)) {
    return Module.getCacheByModuleId(absolutePath);
  }
  // 如果模块后缀不存在那么加判断加载模块
  if (!extension) {
    absolutePath = findExt(absolutePath);
    // 不是完整的路径
  }  else if (absolutePath.charCodeAt(0) !== CHAR_DOT ||
       absolutePath.charCodeAt(0) !== CHAR_FORWARD_SLASH) {
    // 内置模块
    if (naviteModule.includes(absolutePath)) {
      // 内置模块对应的二机制文件
      absolutePath = '内置模块路径';
    } else {
      // 第三方定义的模块
      const paths = resolvePaths(absolutePath);
      let i = paths.length - 1;
      while (i >= 0) {
        const path = require(paths[i]);
        if (path) {
          absolutePath = path;
          break;
        }
        i--;
      }
    }
  }
  // 加载模块
  // 模块加载完以后才创建一个模块
  const module = new Module(absolutePath);
  Module.setCacheByModuleId(absolutePath, module);
  module.loade = true;
  module.children.push(absolutePath);
  tryModuleLoad(module);
  // 返回module.exports
  return module.exports;
};