浅析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 相关文章推荐
JavaScript高级程序设计(第3版)学习笔记4 js运算符和操作符
Oct 11 Javascript
瀑布流布局并自动加载实现代码
Mar 12 Javascript
js多级树形弹出一个小窗口层(非常好用)实例代码
Mar 19 Javascript
jQuery使用before()和after()在元素前后添加内容的方法
Mar 26 Javascript
javascript:void(0)是什么意思及href=#与href=javascriptvoid(0)的区别
Nov 13 Javascript
基于jQuery实现的无刷新表格分页实例
Feb 17 Javascript
js实现炫酷的左右轮播图
Jan 18 Javascript
完美实现js焦点轮播效果(一)
Mar 07 Javascript
基于HTML5+JS实现本地图片裁剪并上传功能
Mar 24 Javascript
详解使用vue-admin-template的优化历程
May 20 Javascript
Angular 2使用路由自定义弹出组件toast操作示例
May 10 Javascript
vue实现节点增删改功能
Sep 26 Javascript
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 文章中的远程图片采集到本地的代码
2009/07/30 PHP
浅谈php+phpStorm+xdebug配置方法
2015/09/17 PHP
ubutu 16.04环境下,PHP与mysql数据库,网页登录验证实例讲解
2017/07/20 PHP
thinkPHP5.1框架中Request类四种调用方式示例
2019/08/03 PHP
jquery下利用jsonp跨域访问实现方法
2010/07/29 Javascript
window.event快达到全浏览器支持了,以后使用就方便了
2011/11/30 Javascript
一些老手都不一定知道的JavaScript技巧
2014/05/06 Javascript
$(document).ready(function() {})不执行初始化脚本
2014/06/19 Javascript
JQuery实现表格动态增加行并对新行添加事件
2014/07/30 Javascript
javascript控制台详解
2015/06/25 Javascript
易操作的jQuery表单提示插件
2015/12/01 Javascript
javascript制作照片墙及制作过程中出现的问题
2016/04/04 Javascript
Angular2开发——组件规划篇
2017/03/28 Javascript
微信小程序中显示html格式内容的方法
2017/04/25 Javascript
详解vue-cli脚手架中webpack配置方法
2018/08/22 Javascript
iview form清除校验状态的实现
2019/09/19 Javascript
CentOS 6.X系统下升级Python2.6到Python2.7 的方法
2016/10/12 Python
python使用matplotlib绘制折线图教程
2017/02/08 Python
在python里从协程返回一个值的示例
2019/02/19 Python
python matplotlib库直方图绘制详解
2019/08/10 Python
使用python自动追踪你的快递(物流推送邮箱)
2020/03/17 Python
python输入一个水仙花数(三位数) 输出百位十位个位实例
2020/05/03 Python
Python中logger日志模块详解
2020/08/04 Python
Sunglasses Shop丹麦:欧洲第一的太阳镜在线销售网站
2017/10/22 全球购物
全球工业:Global Industrial
2020/02/01 全球购物
几道PHP面试题
2013/04/14 面试题
linux面试题参考答案(6)
2016/06/23 面试题
出口公司经理求职简历中的自我评价
2013/10/13 职场文书
千元咖啡店的创业计划书范文
2013/12/29 职场文书
宣传普通话标语
2014/06/27 职场文书
关于随地扔垃圾的检讨书
2014/09/30 职场文书
2014年社区工作总结
2014/11/18 职场文书
安全教育的主题班会
2015/08/13 职场文书
研究生毕业登记表的自我鉴定范文
2019/07/15 职场文书
Redis集群新增、删除节点以及动态增加内存的方法
2021/09/04 Redis
Java 超详细讲解设计模式之中的抽象工厂模式
2022/03/25 Java/Android