Commonjs规范及Node模块实现

前面的话

  Node在实现中并非完全按照CommonJS规范实现,而是对模块规范进行了一定的取舍,同时也增加了少许自身需要的特性。本文将详细介绍NodeJS的模块实现

 

引入

  nodejs是区别于javascript的,在javascript中的顶层对象是window,而在node中的顶层对象是global

  [注意]实际上,javascript也存在global对象,只是其并不对外访问,而使用window对象指向global对象而已

  在javascript中,通过var a = 100;是可以通过window.a来得到100的

  但在nodejs中,是不能通过global.a来访问,得到的是undefined

  这是因为var a = 100;这个语句中的变量a,只是模块范围内的变量a,而不是global对象下的a

  在nodejs中,一个文件就是一个模块,每个模块都有自己的作用域。使用var来声明的一个变量,它并不是全局的,而是属于当前模块下

  如果要在全局作用域下声明变量,则如下所示

 

概述

  Node中模块分为两类:一类是Node提供的模块,称为核心模块;另一类是用户编写的模块,称为文件模块

  核心模块部分在Node源代码的编译过程中,编译进了二进制执行文件。在Node进程启动时,部分核心模块就被直接加载进内存中,所以这部分核心模块引入时,文件定位和编译执行这两个步骤可以省略掉,并且在路径分析中优先判断,所以它的加载速度是最快的

  文件模块则是在运行时动态加载,需要完整的路径分析、文件定位、编译执行过程,速度比核心模块慢

  接下来,我们展开详细的模块加载过程

 

模块加载

  在javascript中,加载模块使用script标签即可,而在nodejs中,如何在一个模块中,加载另一个模块呢?

  使用require()方法来引入

【缓存加载】

  再展开介绍require()方法的标识符分析之前,需要知道,与前端浏览器会缓存静态脚本文件以提高性能一样,Node对引入过的模块都会进行缓存,以减少二次引入时的开销。不同的地方在于,浏览器仅仅缓存文件,而Node缓存的是编译和执行之后的对象

  不论是核心模块还是文件模块,require()方法对相同模块的二次加载都一律采用缓存优先的方式,这是第一优先级的。不同之处在于核心模块的缓存检查先于文件模块的缓存检查

【标识符分析】

  require()方法接受一个标识符作为参数。在Node实现中,正是基于这样一个标识符进行模块查找的。模块标识符在Node中主要分为以下几类:[1]核心模块,如http、fs、path等;[2].或..开始的相对路径文件模块;[3]以/开始的绝对路径文件模块;[4]非路径形式的文件模块,如自定义的connect模块

  根据参数的不同格式,require命令去不同路径寻找模块文件

  1、如果参数字符串以“/”开头,则表示加载的是一个位于绝对路径的模块文件。比如,require('/home/marco/foo.js')将加载/home/marco/foo.js

  2、如果参数字符串以“./”开头,则表示加载的是一个位于相对路径(跟当前执行脚本的位置相比)的模块文件。比如,require('./circle')将加载当前脚本同一目录的circle.js

  3、如果参数字符串不以“./“或”/“开头,则表示加载的是一个默认提供的核心模块(位于Node的系统安装目录中),或者一个位于各级node_modules目录的已安装模块(全局安装或局部安装)

  [注意]如果是当前路径下的文件模块,一定要以./开头,否则nodejs会试图去加载核心模块,或node_modules内的模块 

//a.js
console.log('aaa');//b.js
require('./a');//'aaa'
require('a');//报错

【文件扩展名分析】

  require()在分析标识符的过程中,会出现标识符中不包含文件扩展名的情况。CommonJS模块规范也允许在标识符中不包含文件扩展名,这种情况下,Node会先查找是否存在没有后缀的该文件,如果没有,再按.js、.json、.node的次序补足扩展名,依次尝试

  在尝试的过程中,需要调用fs模块同步阻塞式地判断文件是否存在。因为Node是单线程的,所以这里是一个会引起性能问题的地方。小诀窍是:如果是.node和.json文件,在传递给require()的标识符中带上扩展名,会加快一点速度。另一个诀窍是:同步配合缓存,可以大幅度缓解Node单线程中阻塞式调用的缺陷

【目录分析和包】

  在分析标识符的过程中,require()通过分析文件扩展名之后,可能没有查找到对应文件,但却得到一个目录,这在引入自定义模块和逐个模块路径进行查找时经常会出现,此时Node会将目录当做一个包来处理

  在这个过程中,Node对CommonJS包规范进行了一定程度的支持。首先,Node在当前目录下查找package.json(CommonJS包规范定义的包描述文件),通过JSON.parse()解析出包描述对象,从中取出main属性指定的文件名进行定位。如果文件名缺少扩展名,将会进入扩展名分析的步骤

  而如果main属性指定的文件名错误,或者压根没有package.json文件,Node会将index当做默认文件名,然后依次查找index.js、index.json、index.node

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

 

访问变量

  如何在一个模块中访问另外一个模块中定义的变量呢? 

【global】

  最容易想到的方法,把一个模块定义的变量复制到全局环境global中,然后另一个模块访问全局环境即可

//a.js
var a = 100;
global.a = a;//b.js
require('./a');
console.log(global.a);//100

  这种方法虽然简单,但由于会污染全局环境,不推荐使用

【module】

  而常用的方法是使用nodejs提供的模块对象Module,该对象保存了当前模块相关的一些信息

function Module(id, parent) {this.id = id;this.exports = {};this.parent = parent;if (parent && parent.children) {parent.children.push(this);}this.filename = null;this.loaded = false;this.children = [];
}
module.id 模块的识别符,通常是带有绝对路径的模块文件名。
module.filename 模块的文件名,带有绝对路径。
module.loaded 返回一个布尔值,表示模块是否已经完成加载。
module.parent 返回一个对象,表示调用该模块的模块。
module.children 返回一个数组,表示该模块要用到的其他模块。
module.exports 表示模块对外输出的值。

【exports】

  module.exports属性表示当前模块对外输出的接口,其他文件加载该模块,实际上就是读取module.exports变量

//a.js
var a = 100;
module.exports.a = a;//b.js
var result = require('./a');
console.log(result);//'{ a: 100 }'

  为了方便,Node为每个模块提供一个exports变量,指向module.exports。造成的结果是,在对外输出模块接口时,可以向exports对象添加方法

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

  [注意]不能直接将exports变量指向一个值,因为这样等于切断了exportsmodule.exports的联系

 

模块编译

  编译和执行是模块实现的最后一个阶段。定位到具体的文件后,Node会新建一个模块对象,然后根据路径载入并编译。对于不同的文件扩展名,其载入方法也有所不同,具体如下所示

  js文件——通过fs模块同步读取文件后编译执行

  node文件——这是用C/C++编写的扩展文件,通过dlopen()方法加载最后编译生成的文件

  json文件——通过fs模块同步读取文件后,用JSON.parse()解析返回结果

  其余扩展名文件——它们都被当做.js文件载入

  每一个编译成功的模块都会将其文件路径作为索引缓存在Module._cache对象上,以提高二次引入的性能

  根据不同的文件扩展名,Node会调用不同的读取方式,如.json文件的调用如下:

// Native extension for .json
Module._extensions['.json'] = function(module, filename) {var content = NativeModule.require('fs').readFileSync(filename, 'utf8'); try {module.exports = JSON.parse(stripBOM(content));} catch (err) {err.message = filename + ': ' + err.message;throw err;}
};

  其中,Module._extensions会被赋值给require()的extensions属性,所以通过在代码中访问require.extensions可以知道系统中已有的扩展加载方式。编写如下代码测试一下:

console.log(require.extensions);

  得到的执行结果如下:

{ '.js': [Function], '.json': [Function], '.node': [Function] }

  在确定文件的扩展名之后,Node将调用具体的编译方式来将文件执行后返回给调用者

【JavaScript模块的编译】

  回到CommonJS模块规范,我们知道每个模块文件中存在着require、exports、module这3个变量,但是它们在模块文件中并没有定义,那么从何而来呢?甚至在Node的API文档中,我们知道每个模块中还有filename、dirname这两个变量的存在,它们又是从何而来的呢?如果我们把直接定义模块的过程放诸在浏览器端,会存在污染全局变量的情况

  事实上,在编译的过程中,Node对获取的JavaScript文件内容进行了头尾包装。在头部添加了(function(exports, require, module, filename, dirname) {\n,在尾部添加了\n});

  一个正常的JavaScript文件会被包装成如下的样子

(function (exports, require, module,  filename,  dirname) {var math = require('math');exports.area = function (radius) {return Math.PI * radius * radius;};
});

  这样每个模块文件之间都进行了作用域隔离。包装之后的代码会通过vm原生模块的runInThisContext()方法执行(类似eval,只是具有明确上下文,不污染全局),返回一个具体的function对象。最后,将当前模块对象的exports属性、require()方法、module(模块对象自身),以及在文件定位中得到的完整文件路径和文件目录作为参数传递给这个function()执行

  这就是这些变量并没有定义在每个模块文件中却存在的原因。在执行之后,模块的exports属性被返回给了调用方。exports属性上的任何方法和属性都可以被外部调用到,但是模块中的其余变量或属性则不可直接被调用

  至此,require、exports、module的流程已经完整,这就是Node对CommonJS模块规范的实现

【C/C++模块的编译】

  Node调用process.dlopen()方法进行加载和执行。在Node的架构下,dlopen()方法在Windows和*nix平台下分别有不同的实现,通过libuv兼容层进行了封装

  实际上,.node的模块文件并不需要编译,因为它是编写C/C++模块之后编译生成的,所以这里只有加载和执行的过程。在执行的过程中,模块的exports对象与.node模块产生联系,然后返回给调用者

  C/C++模块给Node使用者带来的优势主要是执行效率方面的,劣势则是C/C++模块的编写门槛比JavaScript高

【JSON文件的编译】

  .json文件的编译是3种编译方式中最简单的。Node利用fs模块同步读取JSON文件的内容之后,调用JSON.parse()方法得到对象,然后将它赋给模块对象的exports,以供外部调用

  JSON文件在用作项目的配置文件时比较有用。如果你定义了一个JSON文件作为配置,那就不必调用fs模块去异步读取和解析,直接调用require()引入即可。此外,你还可以享受到模块缓存的便利,并且二次引入时也没有性能影响

 

CommonJS

  在介绍完Node的模块实现之后,回过头来再学习下CommonJS规范,相对容易理解

  CommonJS规范的提出,主要是为了弥补当前javascript没有标准的缺陷,使其具备开发大型应用的基础能力,而不是停留在小脚本程序的阶段

  CommonJS对模块的定义十分简单,主要分为模块引用、模块定义和模块标识3个部分

【模块引用】

var math = require('math');

  在CommonJS规范中,存在require()方法,这个方法接受模块标识,以此引入一个模块的API到当前上下文中

【模块定义】

  在模块中,上下文提供require()方法来引入外部模块。对应引入的功能,上下文提供了exports对象用于导出当前模块的方法或者变量,并且它是唯一导出的出口。在模块中,还存在一个module对象,它代表模块自身,而exports是module的属性。在Node中,一个文件就是一个模块,将方法挂载在exports对象上作为属性即可定义导出的方式:

// math.js
exports.add = function () {var sum = 0, i = 0,args = arguments, l = args.length;while (i < l) {sum += args[i++];}return sum;
};

  在另一个文件中,我们通过require()方法引入模块后,就能调用定义的属性或方法了

// program.js
var math = require('math');
exports.increment = function (val) {return math.add(val, 1);
};

【模块标识】

  模块标识其实就是传递给require()方法的参数,它必须是符合小驼峰命名的字符串,或者以.、..开头的相对路径,或者绝对路径。它可以没有文件名后缀.js

  模块的定义十分简单,接口也十分简洁。它的意义在于将类聚的方法和变量等限定在私有的作用域中,同时支持引入和导出功能以顺畅地连接上下游依赖。每个模块具有独立的空间,它们互不干扰,在引用时也显得干净利落

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/541389.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

thinkphp3 php jwt,ThinkPHP5 使用 JWT 进行加密

- 使用 Composer安装此扩展- 代码示例<?php /*** [InterCommon-接口公用]* Author RainCyan* DateTime 2019-08-12T16:38:080800*/namespace app\hladmin\controller;use think\Controller;use \Firebase\JWT\JWT;class InterCommonController extends Controller {private…

数据管理与商业智能_商业智能与数据科学

数据管理与商业智能In this heavily jargonized trade, the words typically overlap one another, leading to a scarcity of understanding or a state of confusion around these ideas. whereas big data vs analytics or computing vs machine learning vs cognitive inte…

JavaWeb网上图书商城完整项目--day02-14.登录功能的login页面处理

1、现在注册成功之后&#xff0c;我们来到登录页面&#xff0c;登录页面在于 在登录页面。我们也需要向注册页面一样对登录的用户名、密码 验证码等在jsp页面中进行校验&#xff0c;校验我们单独放置一个login.js文件中进行处理&#xff0c;然后login.jsp加载该js文件 我们来看…

php多线程是什么意思,多线程是什么意思

线程是操作系统能够进行运算调度的最小单位&#xff0c;它被包含在进程之中&#xff0c;是进程中的实际运作单位&#xff0c;而多线程就是指从软件或者硬件上实现多个线程并发执行的技术&#xff0c;具有多线程能力的计算机因有硬件支持而能够在同一时间执行多于一个线程&#…

c++一个类创建多个对象_C ++ | 创建一个类的多个对象

c一个类创建多个对象In the below program, we are creating a C program to create multiple objects of a class. 在下面的程序中&#xff0c;我们正在创建一个C 程序来创建一个类的多个对象 。 /* C program to create multiple objects of a class */#include <iostrea…

Activity中与ListActivity中使用listview区别

一.Activity中与ListActivity中使用listview区别&#xff08;本身没多大区别&#xff0c;只是ListActivity在listview的显示上做了一些优化&#xff09;Activity中使用Listview步骤&#xff1a;1.xml布局中,ListView标签id可以任意取值如&#xff1a;<ListView andro…

java相关是什么,什么是java

java基础常见面试题&#xff0c;这是一篇超长的随笔&#xff01;&#xff01;&#xff01;1. Java基础部分....................................................... 4 1、一个".java"源文件中是否可以包括多个类(不是内部类)&#xff1f;有什么限制&#xff1f;.. …

如何在Scala中将Double转换为String?

Double in Scala is a data type that stores numerical values that have decimals. It can store a 64-bit floating point number. Scala中的Double是一种数据类型&#xff0c;用于存储带有小数的数值。 它可以存储一个64位浮点数。 Example: 例&#xff1a; val decimal…

basic knowledge

Position 属性&#xff1a;规定元素的定位类型。即元素脱离文档流的布局&#xff0c;在页面的任意位置显示。 ①absolute &#xff1a;绝对定位&#xff1b;脱离文档流的布局&#xff0c;遗留下来的空间由后面的元素填充。定位的起始位置为最近的父元素(postion不为static)&…

avatar.php uid,phpcms函数库中获取会员头像方法get_memberavatar()有时无效问题

修复方法&#xff1a;首先我先给出无效情况的演示代码&#xff0c;如下&#xff1a;$userid intval($_GET[userid]);$userinfo $this->db->get_one(userid.$userid);$this->db->set_model(10); //原因便在这里$userdetail $this->db->get_one("useri…

ruby 集合 分组_将Ruby中两个集合的所有元素结合在一起

ruby 集合 分组In this program, we will see how we can combine the two sets? This is not a very difficult task. This can be easily done with the help of the operator. In many places of programming, you will find that operator is overloaded for various ty…

​Python中面向对象的编程

Python面向对象的编程1概述&#xff08;1&#xff09;面向对象编程面向对象的编程是利用“类”和“对象”来创建各种模型来实现对真实世界的描述&#xff0c;使用面向对象编程的原因一方面是因为它可以使程序的维护和扩展变得更简单&#xff0c;并且可以大大提高程序开发效率&a…

php中用for循环制作矩形,PHP中for循环语句的几种变型

PHP中for循环语句的几种变型2021-01-22 10:21:42406for语句可以说是PHP(同时也是多种语言)的循环控制部份最基本的一个语句了&#xff0c;for语句的执行规律和基础用法在这里就不多说&#xff0c;可以参见PHP手册for语句部分。PHP手册中对它的语法定义如下&#xff1a;for(expr…

c语言用命令行编译运行程序_使用C程序执行系统命令

c语言用命令行编译运行程序Sometimes, we may need to execute Linux/Windows DOS commands through our C program. (Note: the code given below is compiled and executed on Linux GCC compiler, so here we are testing Linux commands only). 有时&#xff0c;我们可能需…

python 熊猫,Python熊猫

我试图连续分组和计算相同的信息&#xff1a;#Functionsdef postal_saude ():global df, lista_solic#List of solicitantes in Postal Saudelist_sol [lista_solic["name1"], lista_solic["name2"]]#filter Postal Saude Solicitantesdf df[(df[Cliente…

Spring的两种任务调度Scheduled和Async

Spring提供了两种后台任务的方法,分别是: 调度任务&#xff0c;Schedule异步任务&#xff0c;Async当然&#xff0c;使用这两个是有条件的&#xff0c;需要在spring应用的上下文中声明<task:annotation-driven/>当然&#xff0c;如果我们是基于java配置的&#xff0c;需要…

建立单链表 单链表的插入_单链列表插入

建立单链表 单链表的插入All possible cases: 所有可能的情况&#xff1a; Inserting at beginning 开始插入 Inserting at the ending 在末尾插入 Inserting at given position 在给定位置插入 Algorithms: 算法&#xff1a; 1)开始插入 (1) Inserting at the beginning) In…

mysql学习笔记(1-安装简介)

mysql的安装方式&#xff1a;(1)通过系统提供的默认版本(rpm包&#xff0c;稳定版&#xff0c;该版本满足了使用的需求&#xff0c;建议使用&#xff0c;os vendor)(2)mysql官方提供官方提供的通用rpm安装包通用二进制格式的程序包(直接下载文件&#xff0c;解压到指定目录&…

存储器间接寻址方式_8086中的数据存储器寻址模式

存储器间接寻址方式In this type of addressing mode, first the offset address is calculated, then the memory address is calculated and then the operand form that memory location is fetched. There are following modes which lie under the Data Addressing Mode: …

oracle asm 删除diskgroup,ASM磁盘组删除DISK操作

没想到这么简单的操作&#xff0c;由于不熟悉还碰到了两个小问题。[oracledbserver1 ~]$ sqlplus / as sysdbaSQL*Plus: Release 11.2.0.2.0 Production on Tue Aug 9 10:08:062011Copyright (c) 1982, 2010, Oracle.All rights reserved.Connected to:Oracle Database 11g Ent…