浅析node.js的模块加载机制


Posted in Javascript onMay 25, 2018

在node.js中,模块使用CommonJS规范,一个文件是一个模块

node.js中的模块可分为三类

  1. 内部模块 - node.js提供的模块如 fs,http,path等
  2. 自定模块 - 我们自己写的模块
  3. 第三方模块 - 通过npm安装的模块

node.js提供了大量的模块供我们使用,比如 想解析一个文件的路径,可以使用path模块下的相应方法实现:

const path = require('path');
//返回目标文件的绝对路径
console.log(path.resolve('./1.txt'));

运行结果:

/Users/cuiyue/workspace/test/1.txt

使用require引入相应的模块,即可使用。

__dirname和__filename

node.js的每个模块都有这两个参数,它们都是一个绝对路径的地址,区别是__filename存放了从根目录到当前文件名的路径,__dirname只存放从根目录到模块的所在目录:

console.log(__dirname);
console.log(__filename);

运行结果:

/Users/cuiyue/workspace/test
/Users/cuiyue/workspace/test/module.js

vm模块

vm模块是node.js提供在V8虚拟机中编译和运行的工具,node.js中的模块内部实现就是通过此模块完成。

说说vm的基本用法。

在js环境中有一个eval函数,它可以运行js的代码字符串,比如:

eval('console.log("Hello javascript.")'); //输出Hello javascript.

可以看到,eval函数的参数是一段字符串,它可以运行字符串形式的js代码,但它可以使用上下文环境中的变量:

var num=100;
eval('console.log(num)'); //输出100

以上是可以正确访问num的值。

vm模块提供了方法创建一个安全的沙箱,在指定的上下文环境中运行代码,不受外界干扰。

const vm = require('vm');
var num = 100;
vm.runInThisContext('console.log(num)');

运行结果:

console.log(num)
            ^
ReferenceError: num is not defined

可以看到代码报错了,说明在vm创建了指定的上下文环境中,拿不到外界的参量。

CommonJS规范

在以前,由于javascript的历史原因导致它的模块机制很差,由于这些缺点使得javascript不太善于开发大型应用,于是提出了CommonJS规范以弥补javascript的不足。

CommonJS规范主要分为三块内容:模块导入导出、模块定义、模块标识。

模块导入导出

CommonJS中使用require()函数进行模块的引入。

const mymodule = require('mymodule');

使用exports导出模块

module.exports = {
  name: 'Tom'
};

引用的名称可以不带路径,若不带路径表示引入的是node提供的模块或是npm安装的第三方模块(node_modules)

模块定义

module对象:在每一个模块中,module对象代表该模块自身。

export属性:module对象的一个属性,它向外提供接口。

模块标识

模块标识指的是传递给require方法的参数,必须是符合小驼峰命名的字符串,或者以 .、..、开头的相对路径,或者绝对路径。

node中模块解析流程

  1. 首先接收参数,把传入的模块名称解析成绝对路径
  2. 若没有后缀名称,依次拼接.js .json .node尝试加载,仍到不到模块则报错
  3. 取得正确的路径后判断缓存中是否存在此模块,若有则取出
  4. 若缓存中不存在则加载此文件,在外包裹一层闭包并执行它

以上为大致流程,下面尝试着写一下模块。

代码的基本结构:

/**
 * Module类,用于处理模块加载
 */
function Module() {}

//模块的缓存
Module._cacheModule = {};

//不同扩展名的加载策略
Module._extensions = {};

//根据moduleId解析绝对路径,
Module._resolveFileName = function(moduleId) {};

//入口函数
function req(moduleId) {}

附上全部代码:

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

/**
 * Module类,用于处理模块加载
 */
function Module(file) {
 this.id = file; //当前模块的id,它使用完整的绝对路径标识,因此是唯一的
 this.exports = {}; //导出
 this.loaded = false; //模块是否已加载完毕
}

//模块的缓存
Module._cacheModule = {};

Module._wrapper = ['(function(exports,require,module,__dirname,__filename){', '});'];

//不同扩展名的加载策略
Module._extensions = {
 '.js': function(currentModule) {
  let js = fs.readFileSync(currentModule.id, 'utf8'); //读取出js文件内容
  let fn = Module._wrapper[0] + js + Module._wrapper[1];
  vm.runInThisContext(fn).call(
   currentModule.exports,
   currentModule.exports,
   req,
   currentModule,
   path.dirname(currentModule.id),
   currentModule.id);
  return currentModule.exports;
 },
 '.json': function(currentModule) {
  let json = fs.readFileSync(currentModule.id, 'utf8');
  return JSON.parse(json); //转换为JSON对象返回
 },
 '.node': ''
};

//加载模块(实例方法)
Module.prototype.load = function(file) {
 let extname = path.extname(file); //获取后缀名
 return Module._extensions[extname](this);
};

//根据moduleId解析绝对路径,
Module._resolveFileName = function(moduleId) {
 let p = path.resolve(moduleId);

 if (!path.extname(moduleId)) { //传入的模块没有后缀
  let arr = Object.keys(Module._extensions);

  //循环读取不同扩展名的文件
  for (var 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;
 }
};

function req(moduleId) {
 let file = Module._resolveFileName(moduleId);

 if (Module._cacheModule[file]) { //若缓存中存在此模块
  return Module._cacheModule[file];
 } else {
  let module = new Module(file);
  module.exports = module.load(file);
  return module.exports;
 }
}

console.log(req('./a.js')());

a.js的文件内容:

module.exports = function() {
 console.log('This message from a.js');
 console.log(__dirname);
 console.log(__filename);
}

最终运行结果:

This message from a.js
/Users/cuiyue/workspace/test
/Users/cuiyue/workspace/test/a.js

重要代码说明

_resolveFileName

_resolveFileName方法的主要作用是把传入的模块解析成绝对路径,这样才可以进行下一步,根据完整的路径加载模块。

因此要进行判断,如果传入的模块不存在,则要报错;如果传入的模块已经有扩展名了,就不要拼接了;若没有扩展名,依次以.js .json .node的顺序拼接成完成的模块进行加载。

_extensions

此对象中封装了加载不同类型模块的处理方法,其中若是.json类型则使用fs读取文件直接转换成JSON对象并返回。

若是.js文件则读取后,拼接闭包,将exports,require,module,__dirname,__filename五大参数拼接好,使用vm模块的沙箱机制运行,得到的结果放入module.exports返回。

总结

以上就是node.js的模块加载的简单逻辑,实际上node.js的源码远远比上面的代码复杂,光是处理模块路径、判断合法等操作就写了N行。而且我这里没有写缓存以及其它的复杂逻辑,但核心差不多就是这些,核心的核心就是用fs.readFileSync读取js文件,把内容拼接到一个大大的闭包中,这也解释了为什么我们自己写的所有node模块中都会有require方法,exports导出,以及__dirname和__filename参数。

了解了node.js的模块加载逻辑,在以后写node.js就更可避免一些误解,写出精细的代码。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
在textarea文本域中显示HTML代码的方法
Mar 06 Javascript
深入分析原生JavaScript事件
Dec 29 Javascript
如何在JS中实现相互转换XML和JSON
Jul 19 Javascript
js拖拽功能实现代码解析
Nov 28 Javascript
详解JavaScript的内置对象
Dec 07 Javascript
Vue.js移动端左滑删除组件的实现代码
Sep 08 Javascript
JavaScript requestAnimationFrame动画详解
Sep 14 Javascript
JS实现仿微信支付弹窗功能
Jun 25 Javascript
基于Express框架使用POST传递Form数据
Aug 10 Javascript
JS绘图Flot应用图形绘制异常解决方案
Oct 16 Javascript
低门槛开发iOS、Android、小程序应用的前端框架详解
Oct 16 Javascript
Vue组件化(ref,props, mixin,.插件)详解
May 15 Vue.js
webpack4的迁移的使用方法
May 25 #Javascript
最后说说Vue2 SSR 的 Cookies 问题
May 25 #Javascript
详解webpack4多入口、多页面项目构建案例
May 25 #Javascript
js中的 || 与 &amp;&amp; 运算符详解
May 24 #Javascript
vue axios整合使用全攻略
May 24 #Javascript
vue路由拦截及页面跳转的设置方法
May 24 #Javascript
使用Vue自定义指令实现Select组件
May 24 #Javascript
You might like
PHP 应用程序的安全 -- 不能违反的四条安全规则
2006/11/26 PHP
通过PHP设置BugFree获取邮箱通知
2019/04/25 PHP
使用laravel的Eloquent模型如何获取数据库的指定列
2019/10/17 PHP
Extjs中的GridPanel隐藏列会显示在menuDisabled中解决方法
2013/01/27 Javascript
js事件冒泡实例分享(已测试)
2013/04/23 Javascript
JavaScript AJAX之惰性载入函数
2014/08/27 Javascript
老司机带你解读jQuery插件开发流程
2016/05/16 Javascript
js遮罩效果制作弹出注册界面效果
2017/01/25 Javascript
简单好用的nodejs 爬虫框架分享
2017/03/26 NodeJs
jQuery实现文章图片弹出放大效果
2017/04/06 jQuery
angular2中Http请求原理与用法详解
2018/01/11 Javascript
es6 for循环中let和var区别详解
2020/01/12 Javascript
Ant Design moment对象和字符串之间的相互转化教程
2020/10/27 Javascript
[02:40]DOTA2英雄基础教程 炼金术士
2013/12/23 DOTA
[07:54]DOTA2-DPC中国联赛 正赛 iG vs VG 选手采访
2021/03/11 DOTA
python益智游戏计算汉诺塔问题示例
2014/03/05 Python
Python基础教程之内置函数locals()和globals()用法分析
2018/03/16 Python
使用浏览器访问python写的服务器程序
2019/10/10 Python
python Django框架实现web端分页呈现数据
2019/10/31 Python
Python 实现自动获取种子磁力链接方式
2020/01/16 Python
Python计算指定日期是今年的第几天(三种方法)
2020/03/26 Python
Win10下配置tensorflow-gpu的详细教程(无VS2015/2017)
2020/07/14 Python
python定义类的简单用法
2020/07/24 Python
Python3+RIDE+RobotFramework自动化测试框架搭建过程详解
2020/09/23 Python
Python为何不支持switch语句原理详解
2020/10/21 Python
护士思想汇报
2014/01/12 职场文书
策划创业计划书
2014/02/06 职场文书
培训协议书范本
2014/04/22 职场文书
物理课外活动总结
2014/08/27 职场文书
优秀共产党员推荐材料
2014/12/18 职场文书
小学教师工作总结2015
2015/04/07 职场文书
2015驻村干部工作总结
2015/04/07 职场文书
田径运动会通讯稿
2015/07/18 职场文书
Spring Boot 启动、停止、重启、状态脚本
2021/06/26 Java/Android
《现实主义勇者的王国再建记》第三弹OST全曲试听片段公开
2022/04/04 日漫
OpenFeign实现远程调用
2022/08/14 Java/Android