热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

Node中模块实现过程的详细介绍(附示例)

​本篇文章给大家带来的内容是关于Node中模块实现过程的详细介绍(附示例),有一定的参考价值,有需要的朋友可以参考一下,希望对你有所帮助。
本篇文章给大家带来的内容是关于Node中模块实现过程的详细介绍(附示例),有一定的参考价值,有需要的朋友可以参考一下,希望对你有所帮助。

CommonJS 定义了 module、exports 和 require 模块规范,Node.js 为了实现这个简单的标准,从底层 C/C++ 内建模块到 Javascript 核心模块,从路径分析、文件定位到编译执行,经历了一系列复杂的过程。简单的了解 Node 模块的原理,有利于我们重新认识基于 Node 搭建的框架。

一、CommonJS 模块规范

CommonJS 规范或标准简单来说是一种理论,它期望 Javascript 可以具备跨宿主环境执行的能力,不仅可以开发客户端应用,还可以开发服务端应用、命令行工具、桌面图形界面应用等。

CommonJS 规范对模块的定义分为三个部分:

模块定义

在模块中存在module对象代表模块本身,模块上下文提供exports属性 ,将方法挂载在exports对象上即可以定义导出方式,例如:

    // math.js
   exports.add = function(){ //...}

模块引用

module提供require()方法引入外部模块的 API 到当前的上下文中:

   var math = require('math')
模块标识

模块标识实际就是传递给require()方法中的参数,可以是按小驼峰(camelCase)命名的字符串,也可以是文件路径。

Node.js 借鉴了 CommonJS 规范的设计,特别是 CommonJS 的 Modules 规范,实现了一套模块系统,同时 NPM 实现了 CommonJS 的 Packages 规范,模块和包组成了 Node 应用开发的基础。

二、Node 模块加载原理

上述模块规范看起来十分简单,只有module、exports和require,但 Node 是如何实现的呢?

需要经历路径分析(模块的完整路径)、文件定位(文件扩展名或目录)、编译执行三个步骤。

2.1 路径分析

回顾require()接收 模块标识 作为参数来引入模块,Node 就是基于这个标识符进行路径分析。不同的标识符采用的分析方式是不同的,主要分为一下几类:

Node 提供的核心模块,如 http、fs、path

核心模块在 Node 源码编译时存为二进制执行文件,在 Node 启动时直接加载到内存中,路径分析中优先判断,所以加载速度很快,而且也不用后续的文件定位和编译执行。

如果想加载与核心模块同名的自定义模块,如自定义 http 模块,那必须选用不同标志符或改用路径方式。

路径形式的文件模块,.、..相对路径模块和/绝对路径模块

以.、..或/开始的标识符都会当成文件模块处理,Node 会将require()中的路径转为真实路径作为索引,然后编译执行。

由于文件模块明确了文件位置,所以缩短了路径分析时间,加载速度仅慢与核心模块。

自定义模块,即非路径形式的文件模块

即不是核心模块,也不是路径形式的文件模块,自定义文件是特殊的文件模块,在路径查找时 Node 会逐级查找该模块路径中的路径。
模块路径查找策略示例如下:

// paths.js
console.log(module.paths)

// Terminal
$ node paths.js
[ '/Users/tong/WebstormProjects/testNode/node_modules',
'/Users/tong/WebstormProjects/node_modules',
'/Users/tong/node_modules',
'/Users/node_modules',
'/node_modules' ]

从上述示例输出的模块路径数组可以看出,模块的查找时沿当前路径向上逐级查找node_modules目录,直到目标路径为止,类似 JS 原型链或作用域链。路径越深速度越慢,所以自定义模块加载速度最慢。

缓存优先机制:Node 会对引入过的模块进行缓存以提高性能,不同于浏览器缓存的是文件,Node 缓存的是编译和执行后的对象,所以require()对相同模块的二次加载采用缓存优先的方式。这个缓存优先是第一优先级的,比核心模块的优先级要高!

2.2 文件定位

模块路径分析完成后是文件定位,主要包括文件扩展名的分析、目录和包的处理。为了表达的更清晰,将文件定位分为四个步骤:

step1: 补充扩展名

通常require()中的标识符是不包含文件扩展名的,这种情况下,Node会按照 .js、.json、.node 的顺序尝试补充扩展名。

在尝试补充扩展名时,需要调用 fs 模块同步阻塞式判断文件是否存在,所以这里提升性能的小技巧,就是 .json 和 .node 文件传递给require()时带上扩展名会加快一些速度。

step2: 目录处理查找 pakage.json

如果补充扩展名后没有找到对应文件,但是得到了一个目录,此时 Node会将目录当做一个包处理。依据 CommonJS 包规范的实现,Node 会在目录下查找pakage.json(包描述文件),通过JSON.parse()解析成包描述对象,从中取main属性指定的文件名定位。

step3: 继续默认查找 index 文件

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

step4: 进入下一个模块路径

在上述目录分析过程中没有成功定位时,自定义模块按路径查找策略进入上一层node_modules目录,当整个模块路径数组遍历完毕后没有定位到文件,则会抛出查找失败异常。

缓存加载的优化策略使得二次引入不需要路径分析、文件定位、编译执行这些过程,而且核心模块也不需要文件定位的过程,这大大提高了再次加载模块时的效率

2.3 编译执行

Node 中每个模块都是一个对象,在具体定位到文件后,Node 会新建该模块对象,然后根据路径载入并编译。不同的文件扩展名载入方法为:

.js 文件: 通过 fs 模块同步读取后编译执行.json 文件: 通过 fs 模块同步读取后,用JSON.parse()解析并返回结果.node 文件: 这是用 C/C++ 写的扩展文件,通过process.dlopen()方法加载最后编译生成的其他扩展名: 都被当做 js 文件载入

载入成功后 Node 会调用具体的编译方式将文件执行后返回给调用者。对于 .json 文件的编译最简单,JSON.parse()解析得到对象后直接赋值给模块对象的exports,而 .node 文件是C/C++编译生成的,Node 直接调用process.dlopen()载入执行就可以,下面重点介绍 .js 文件的编译:

在 CommonJS 模块规范中有module、exports 和 require 这3个变量,在 Node API 文档中每个模块还有 __filename、__dirname这两个变量,但是在模块中没有定义这些变量,那它们是怎么产生的呢?

事实上在编译过程中,Node 对每个 JS 文件都被进行了封装,例如一个 JS 文件会被封装成如下:

(function (exports, require, module, __filename, __dirname) {
    var math = require('math')
    export.add = function(){ //... }
})

首先每个模块文件之间都进行了作用域隔离,通过vm原生模块的runInThisContext()方法(类似 eval)返回一个具体的 function 对象,最后将当前模块对象的exports属性、require()方法、模块对象本身module、文件定位时得到的完整路径__filename和文件目录__dirname作为参数传递给这个 function 执行。模块的exports属性上的任何方法和属性都可以被外部调用,其余的则不可被调用。

至此,module、exports 和 require的流程就介绍完了。

曾经困惑过,每个模块都可以使用exports的情况下,为什么还必须用module.exports。

这是因为exports在编译过程中时通过形参传入的,直接给exports形参赋值只改变形参的引用,不能改变作用域外的值,例如:

let change = function (exports) {
  exports = 100
  console.log(exports)
}

var exports = 2
change(exports) // 100
console.log(exports) // 2

所以直接赋值给module.exports对象就不会改变形参的引用了。

编译成功的模块会将文件路径作为索引缓存在 Module._cache 对象上,路径分析时优先查找缓存,提高二次引入的性能。

三、Node 核心模块

总结来说 Node 模块分为Node提供的核心模块和用户编写的文件模块。文件模块是在运行时动态加载,包括了上述完整的路径分析、文件定位、编译执行这些过程,核心模块在Node源码编译成可执行文件时存为二进制文件,直接加载在内存中,所以不用文件定位和编译执行。

核心模块分为 C/C++ 编写的和 Javascript 编写的两部分,在编译所有 C/C++ 文件之前,编译程序需要将所有的 Javascript 核心模块编译为 C/C++ 可执行代码,编译成功的则放在 NativeModule._cache对象上,显然和文件模块 Module._cache的缓存位置不同。

在核心模块中,有些模块由纯 C/C++ 编写的内建模块,主要提供 API 给 Javascript 核心模块,通常不能被用户直接调用,而有些模块由 C/C++ 完成核心部分,而 Javascript 实现封装和向外导出,如 buffer、fs、os 等。

所以在Node的模块类型中存在依赖层级关系:内建模块(C/C++)—> 核心模块(Javascript)—> 文件模块。

使用require()十分的方便,但从 Javascript 到 C/C++ 的过程十分复杂,总结来说需要经历 C/C++ 层面内建模块的定义、(Javascript)核心模块的定义和引入以及(Javascript)文件模块的引入。

四、前端模块规范

对比前后端的 Javascript,浏览器端的 Javascript 需要经历从同一个服务器端分发到多个客户端执行,通过网络加载代码,瓶颈在于宽带;而服务器端 Javascript 相同代码需要多次执行,通过磁盘加载,瓶颈在于 CPU 和内存,所以前后端的 Javascript 在 Http 两端的职责完全不用。

Node 模块的引入几乎是同步的,而前端模块如果同步引入,那脚本加载需要太长的时间,所以 CommonJS 为后端 Javascript 制定的规范不适合前端。而后出现 AMD 和 CMD 用于前端应用场景。

4.1 AMD 规范

AMD 即异步模块定义(Asynchronous Module Definition),模块定义为:

define(id?, dependencies?, factory);

AMD 模块需要用define明确定义一个模块,其中模块id与依赖dependencies是可选的,factory的内容就是实际代码的内容。例如指定一些依赖到模块中:

define(['dep1', 'dep2'], function(){
    // module code
});

require.js 实现 AMD 规范的模块化,感兴趣的可以查看 require.js 的文档。

4.2 CMD 规范

CMD 模块的定义更加简单:

 define(factory);

定义的模块同 Node 模块一样是隐式包装,在依赖部分支持动态引入,例如:

 define(function(require, exports, module){
     // module code
 });

requireexportsmodule通过形参传递给模块,需要依赖模块时直接使用require()引入。

sea.js 实现 AMD 规范的模块化,感兴趣的可以查看 sea.js 的文档。

以上就是Node中模块实现过程的详细介绍(附示例)的详细内容,更多请关注 第一PHP社区 其它相关文章!


推荐阅读
  • Node.js学习笔记(一)package.json及cnpm
    本文介绍了Node.js中包的概念,以及如何使用包来统一管理具有相互依赖关系的模块。同时还介绍了NPM(Node Package Manager)的基本介绍和使用方法,以及如何通过NPM下载第三方模块。 ... [详细]
  • 本文介绍了在开发Android新闻App时,搭建本地服务器的步骤。通过使用XAMPP软件,可以一键式搭建起开发环境,包括Apache、MySQL、PHP、PERL。在本地服务器上新建数据库和表,并设置相应的属性。最后,给出了创建new表的SQL语句。这个教程适合初学者参考。 ... [详细]
  • 本文讨论了在使用PHP cURL发送POST请求时,请求体在node.js中没有定义的问题。作者尝试了多种解决方案,但仍然无法解决该问题。同时提供了当前PHP代码示例。 ... [详细]
  • 微信民众号商城/小顺序商城开源项目介绍及使用教程
    本文介绍了一个基于WeiPHP5.0开发的微信民众号商城/小顺序商城的开源项目,包括前端和后端的目录结构,以及所使用的技术栈。同时提供了项目的运行和打包方法,并分享了一些调试和开发经验。最后还附上了在线预览和GitHub商城源码的链接,以及加入前端交流QQ群的方式。 ... [详细]
  • 必须先赞下国人npm库作品:node-images(https:github.comzhangyuanweinode-images),封装了跨平台的C++逻辑,形成nodejsAP ... [详细]
  • 前言:原本纠结于Web 模板,选了Handlebars。后来发现页面都是弱逻辑的,不支持复杂逻辑表达式。几乎要放弃之际,想起了Javascript中eval函数。虽然eval函 ... [详细]
  • JavaScript和Python是用于构建各种应用程序的两种有影响力的编程语言。尽管JavaScript多年来一直是占主导地位的编程语言,但Python的迅猛发展有 ... [详细]
  • launch.json配置php,launchjson有什么用
    本文目录一览:1、VSCodelaunch.json配置详解 ... [详细]
  • ①安装node.js②按照下面命令行执行node安装参照网址:https:www.cnblogs.compearl07p6247389.htmlwebpack讲解安 ... [详细]
  • 用JavaScript实现的太空人手表
    用JavaScript实现的太空人手表-JS写的太空人手表,没有用canvas、svg。主要用几个大的函数来动态显示时间、天气这些。天气的获取用到了AJAX请求。代码中有详细的注释 ... [详细]
  • 阿,里,云,物,联网,net,core,客户端,czgl,aliiotclient, ... [详细]
  • 本文介绍了使用PHP实现断点续传乱序合并文件的方法和源码。由于网络原因,文件需要分割成多个部分发送,因此无法按顺序接收。文章中提供了merge2.php的源码,通过使用shuffle函数打乱文件读取顺序,实现了乱序合并文件的功能。同时,还介绍了filesize、glob、unlink、fopen等相关函数的使用。阅读本文可以了解如何使用PHP实现断点续传乱序合并文件的具体步骤。 ... [详细]
  • 本文详细介绍了如何创建和使用VUE uni-app开发环境,包括通过HBuilderX可视化界面和通过vue-cli命令执行的方法。文章内容简单清晰,易于学习与理解。通过学习本文,读者可以深入了解VUE uni-app开发环境,并通过实践验证掌握具体的使用情况。编程笔记将为读者推送更多相关知识点的文章,欢迎关注! ... [详细]
  • 本文介绍了JavaScript进化到TypeScript的历史和背景,解释了TypeScript相对于JavaScript的优势和特点。作者分享了自己对TypeScript的观察和认识,并提到了在项目开发中使用TypeScript的好处。最后,作者表示对TypeScript进行尝试和探索的态度。 ... [详细]
  • .babelrc是用来设置转码规则和插件的,这种文件在window上无法直接创建,也无法在HBuilder中创建,甚至无法查看,但可以在sublimetext中创建、查看并编辑。当 ... [详细]
author-avatar
乐在TV
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有