目录
背景介绍
在Web发展历程中,浏览器涌现了丰富多彩的标准API供JavaScript调用,这些过程发生在前端,后端JavaScript的规范却远远落后。
主要存在以下缺陷:
- 没有模块系统
- 标准库较少
- 没有标准接口
- 缺乏报管理系统
CommonJS规范提出,主要弥补当前JavaScript没有标准的缺陷,以达到像Python、Ruby和Java、具备开发大型应用的基础能力,而不是停留在小脚本程序的阶段。他们期望那些用CommonJS API写出的应用可以具备跨宿主环境执行的能力,这样不仅可以利用Javacript开发客户端应用,还可以编写以下应用
- 服务端JavaScript应用程序
- 命令行工具
- 桌面图形界面应用程序
- 混合应用
模块规范
主要分为模块引用、模块定义和模块标识三个部分
# 模块引用
require方法,引入一个模块的API到当前上下文中
ex: let fs = require('fs');
# 模块定义
exports对象用于导出当前模块的方法或者变量, 并且是唯一导出的出口。在模块中存在一个module对象,代表模块本身,而exports是module的属性
# 模块标识
模块标识其实就是传给require方法的参数,它必须是符合小驼峰命名,或者以.、..开头的相对路径或者绝对路径。
PS:每个模块具有独立的空间,他们互不干扰,在引用时也显得干净利落。命名空间方案与之相比相形见绌
模块实现
实现的过程主要分为以下三部分:
1) 路径分析
2) 文件定位
3) 模块编译
Node在实现规范的同时,也增加了少许自身需要的特性。
# 写在前面
在Node中,模块分Node提供的核心模块与用户编写的文件模块两种。同浏览器一样,Node也存在缓存机制,只不过前者缓存的是文件,后者缓存的是引用过的模块(编译和执行之后的对象),两者都是为了减少二次引用时的开销。而核心模块的优点不仅仅在于它原生被Node自带(fs、http、path),而且在缓存机制上也存在优先的待遇。
// 注意:二次引入模块不需要路径分析、文件定位以及编译执行的过程
# 路径分析
A 核心模块 ex:http、fs
B .或..开始的相对路径文件模块
C 以/开始的绝对路径文件模块
D 非路径形式的文件模块,如自定义的connect模块
耗时排行:D > B | C > A
自定义模块之所以最慢,是因为存在一个路径分析的性能消耗,如果require('xxx')的模块不再当前模块的node_modules里面,会向上一层(父目录)下的node_modules目录查找,以此类推直到根下面的/node_modules找不到的话即报错返回
# 文件定位
主要包括扩展名的分析、目录和包的处理。CommonJS允许在标识符不包含文件扩展名,这种情况下Node会按.js、.json、.node的次序补足扩展名,依次尝试。在分析标识符的过程中,requrie可能得到一个目录,那么会将目录当成一个包来处理
包的处理方式:
1. 是否存在package.json文件,存在的话获取里面main字段的值作为文件定位的入口路径
2. 不存在package.json,会默认将index当作默认文件名,然后依次查找index.json,index.node
# 模块编译
在Node中,每个文件模块都是一个对象。对不同文件(.js、.json、.node)的对象,处理方式也有所不同。
每一个编译成功的模块都会将其文件路径作为索引缓存在Module._cache对象上,以提高二次引入的性能。
模块的定义
function Module(id, parent) {
this.id = id;
this.exports = {};
this.parent = parent;
if(parant && parent.children) {
parent.children.push(this);
}
this.filename = null;
this.loaded = false;
this.children = [];
}
Q:模块定义里面没有__dirname、__filename、exports、module、require这三个变量?
A:在编译过程中,Node对JavaScript文件内容进行了头尾包装,在资源函数里面传入了(exports, require, module, __filename, __dirname)这五大参数
Q:既然存在module.exports,那么exports存在的意义何在?
A: exports与module.exports的关系就像前端JavaScript的export
有点不同的是,在前端导出多个属性可以通过下面这种方式导出多个属性
export = {
[key]: value
}
后端JavaScript则是
exports[key]=value;
exports[key]=value;
而导出一个对象则是
module.exports = {
[key]: value
}
最后在这里要注意的是不能直接给exports赋值,因为在模块编译里面我们已经知道,exports对象是作为一个形参传进模块函数的,如果直接给exports赋值,那么require出来的对象虽然改变了引用,但是没有改变作用域。
核心模块
# 转存为C/C++代码
Node采用了V8附带的js2c.py工具,将所有内置的JavaScript代码转换成C++里的数组,生成node_native.h头文件。
在启动Node进程时,JavaScript代码直接加载进内存中,在加载的过程中,JavaScript核心模块经历标识符分析后直接定位到内存中,比普通的文件模块从磁盘中一处一处查找要快很多
# C/C++核心模块的编译过程
在核心模块中,有些模块全部由C/C++编写,有些模块则由C/C++完成核心部分,其它部分则由JavaScript实现包装或向外导出,以满足性能需求。后面这种C++模块完成核心,JavaScript实现封装的模式时Node能够提高性能的常见方式。通常,脚本语言的开发速度优于静态语言,但是其性能则弱于静态语言。而Node的这种符合模式可以在开发速度和性能之间找到平衡点。我们将那些由纯C/C++编写的部分统一称为内建模块,因为他们通常不被用户直接调用。Node的buffer、crypto、eval、fs、os等模块都是部分通过C/C++编写的。
# 内建模块的导出
文件模块可能会依赖核心模块,核心模块可能依赖内建模块。不建议直接调用内建模块。如需调用,直接调用核心模块即可,因为核心模块中基本都封装了内建模块。Node在启动时,会生成一个全局变量process,并提供Binding()方法来协助加载内建模块。
require('os') > NativeModule.require('os') > process.binding('os') > get_builtin_module('node_os') > NODE_MODULE(node_os, reg_func)
# C/C++扩展模块的加载
require('hello.node') > process.dlopen('hello.node', exports) > libuv(uv_dlopen/uv_dlsym) >
*nix > dlopen/dlsym
windows > LoadLibraryExW/GetProcAddress
模块调用栈
C/C++内建模块 > JavaScript核心模块 > JavaScript文件模块
C/C++扩展模块 >
包与NPM
Node对模块规范的实现,一定程度上解决了变量依赖、依赖关系等代码组织性问题。包的出现,则是在模块的基础上进一步组织JavaScript代码。CommonJS的包规范的定义其实也很简单,它由包结构和包描述两个部分组成,前者用于组织包中的各种文件,后者则用于描述包的相关信息,以供外部独区分析。
# 安装依赖包
可以通过本地安装,也可以通过--registry=安装
# 包下面的bin目录的作用
在npm或者yarn包管理器的全局包下面创建一个xxx.cmd,注意,环境变量一定要配上,不然执行不了哈~~,而且有一个比较奇葩的点就是bin下面的命令不能使用test,真实go die
# 规范
前端JavaScript有ECMAScript作为规范,后端JavaScript有CommonJS,那么出现了一个问题,前后端写的包不能很好的互相兼容,然后就出现了AMD和CMD规范,不过自从ES6出了包的规范后,这两种规范的市场份额渐渐销声匿迹
本文介绍了Node.js的模块系统,包括背景介绍、模块规范、模块实现、核心模块、模块调用栈以及包和NPM的使用。CommonJS规范旨在提升JavaScript的大型应用开发能力,Node.js在实现规范时增加了自身特性。模块调用栈和包管理通过NPM进一步优化了代码组织和依赖管理。

被折叠的 条评论
为什么被折叠?



