NPM的作用: Node.js程序依赖包的发布、管理和安装。
CommonJS规范
require
require是一个函数,参数是模块标识符,返回值是所引用模块暴露给外部使用者的内容。
直白一点讲:
一个模块定义文件module1.js
module.exports={
a:1,
f: function(){
console.log('f');
}
}
一个引用模块module1.js的文件
let m1 = require('module1.js');
console.log(m1);
// {a: 1, f: ƒ}
模块的上下文
在每个模块中,都有如下一些事物:
- require函数
- exports对象,这个对象上挂载的东西会被暴露给外部。
- module对象,里面包含模块自身的一些信息,例如ID
![module对象实例]()
Note: exports 对象实际上指向 module.exports的引用,如果随意替换module.exports
的引用,则会导致二者的实际引用不相同。不了解这个问题的话,在世纪城使用中,可能会遇到一些难以预料的问题。
模块标识
模块标识是一个字符串,理论上应该是小驼峰式的命名方式,或者以“.” , “..”及“/”开头的路径。
如果模块标识是文件路径,理论上不需要在文件名后面加后缀“.js”。
未指定的约定
在符合CommonJS规范的情况下,有一些机制可以随意实现。之所以这么说,是因为实际上这些东西也是构建完整模块所必须的,但CommonJS并未严格定义这些东西,理论上只要能够完成既定目标就可以。
- 模块的存储方案,模块但内容可以存储在数据库、文件系统、工厂函数甚至一个链接库中;
- 模块加载器可以支持PATH环境变量用于加载时但寻址,也可以不支持,不作强制限制
Node.js的模块
Node程序通常就是由一个个的模块拼装起来的。这些模块可能的形式: js文件、json文件、C++模块的二进制文件.node
。
这些模块都是通过 require函数 引入。
Node中的模块查找
Node中的模块分为两类: 核心模块 和 文件模块。
核心模块存在于Node源码,属于Node自带模块。主要是一些基础性的模块,例如:fs
, http
等。它们查找非常简单,如果require中的模块标志符与这些核心模块的名字吻合,则直接返回这些核心模块export出的内容。
文件模块是需要通过查找文件来获得的模块。这些模块都是第三方开发者编写的程序,并不属于Node本身的一部分。它们是你在编写Node程序(基于Node.js运行的程序, Not Node.js self)时,引入的文件。
文件模块又可以分成 第三方模块和 项目模块。
如果传入require函数是一个文件路径,一般可以看作是项目模块。对于项目模块,如果路径中指定了具体文件,则直接引入。如果路径是一个目录,则首先查找是否存在package.json
文件,如果存在且在package.json中定义了main
属性,则根据main属性的值查找模块的入口文件,如果没有,则依次查找目录下的index.js
, index.json
以及index.node
。
如果传入require函数的是一个非路径的值,则可以看作 第三方模块。Node查找第三方模块也有一套规则存在。
- 当前文件(require函数所在的文件)的 node_modules目录;
- 当前文件父级目录下的 node_modules下,如果没有,则继续向上级目录,直至根目录下的node_modules;
找到对应的模块后,实际上也就等于获取到了模块的文件PATH,然后就会查找模块的入口文件(查找规则跟项目模块一样),获取模块export的内容。
模块缓存
Node会缓存require的模块,也就是说如果某个模块被加载过,当在有其他模块引用该模块的时候,Node会直接读取缓存中的内容返回。
Node的包机制
包描述文件package.json
主要内容之前有写过。
包目录结构
- package.json在根目录下
- 二进制文件放在bin目录下
- JavaScript源码放在礼拜目录下
- 文档放在doc目录下
- 单元测试文件放在test目录下
Node.js/ NPM下的包
NPM(Node.js Package Manager)。
在NPM中存在一个包信息描述文件————package.json。这个文件会记录这个包的其它依赖。而且它的第三方模块会被放到node_modules目录中,这就导致有些开发者会把这个目录也作为项目的一部分提交到git仓库,但我们实际并不推崇这么做。因为node_modules提供的文件是项目运行时所需,且通常情况下,它的内容会比较多。我们可以根据pacakge.json文件的描述,在运行项目之前安装这些依赖(一般通过npm命令npm install
)。
如果你对第三方模块的源码有所改动,推荐的做法是在项目初始化时或者其他比较合适的时机进行hack。
NPM下的package.json文件是基于CommonJS定义的包描述文件来实现的。但它有一些地方是与其不同的。这一点需要我们明确。
NPM
在NPM中,NPM2与NPM3是有区别的,NPM2的依赖安装路径是嵌套式的,适合Node.js进行工具、后端的开发。在NPM2中,不同的的包如果使用名称相同但版本不同的的第三方模块,那么这个模块的两个版本分别被放在各自引用者的node_modules目录中,互不干扰。
node_modules
---- bar
---- node_modules
---- foo@1.0.0
---- baz
---- node_modules
---- foo@2.0.0
而在NPM3中,依赖的安装路径变成了扁平式的。所有的第三方依赖包都被放倒根目录下的node_modules中。这种方式适合前端项目:需要优化到很小代码空间占用量、以及打包分析等,同时顺带解决了Windows中文件路径不允许过长的问题。
只有在遇到存在冲突的情况下,才会出现嵌套。
NPM3的思想:尽可能的扁平化你的依赖。
CNPM
CNPM是阿里巴巴集团的Node.js团队研发的一套更快的NPM工具。它比NPM快不仅仅是依靠使用国内的镜像,还有就是在安装过程中,将一些包缓存到node_modules/.npminstall 目录下,再以符号链接(Symbol Link)的形式将依赖目录连接到对应的路径。也就是说,在实际安装过程中,相同版本的包只有一份实体,类似于NPM3。
Chrome V8
Node.js使用Chrome V8作为Javascript的解释器。
V8是一个高效的JavaScript引擎,不同于其他JS引擎的地方:
- JIT编译
- 垃圾回收
- 内敛缓存
- 隐藏类
JIT编译
JIT编译,Just-In-Time编译,即时编译。编译输出结果是机器语言,而不是字节码。
垃圾回收
V8的垃圾回收器借鉴了Java VM的精确垃圾回收管理,其垃圾回收机制的效率是相当高的。
内联缓存(Inline Cache)
如果访问过a.b,那么当再次访问a.b时,不会再对哈希表进行一次寻址,因为V8已经缓存过这个属性的偏移量,不用再次计算寻址的偏移量。
隐藏类
TODO: 隐藏类的具体机制还有待进一步查询资料。
V8引擎比较高效的机制主要是 内联缓存和 隐藏类。
V8引擎遵循ECMA Script标准。Node.js会随着V8的更新升级自己的代码。
libuv
libuv 是一个专注于异步I/O的跨平台类库。最初是由Node的创始人Ryan Dahl为Node写的。只要是为Node开发,但是也可以支持其他项目。
libuv中一个重要的概念是 事件循环。
libuv的一些特性
- 基于epoll/kqueue/IOCP/event ports实现的全能事件循环;
- 异步TCP 和 UDP套接字
- 异步DNS解析
- 异步文件、文件系统操作
- 文件系统事件
- ANSI转义码控制的TTY
- 使用UNIX domain套接字或者命名管道实现的套接字共享IPC
- 子进程
- 线程池
- 信号(Signal)处理
- 高精度时钟
- 线程和同步元
PS:不同的平台有不同的异步机制(如epoll、IOCP等),libuv 基于此实现来跨平台的事件循环。
另外libuv接口简洁明了,灵活性好,便于使用。Node中事件循环直接使用libuv的事件循环。
Node中其他一些依赖
http-parser
一个C实现的HTTP消息解析器,能解析HTTP协议的请求数据和返回数据。
OpenSSL
安全套接字层协议库。
SSL(Secure Socket Layer),可以在网络上提供秘密性的传输。用于对HTTP协议进行加密。
zlib
zlib是一个提供数据压缩功能的库。
IDE
神的编辑器和编辑器之神
- VIM/NeoVIM
- Emacs
- Sublime Text
- Visual Studio Code
- Atom
- Visual Studio
node-gyp
node-gyp 是 Node.js下的C++扩展构建工具。基于GYP(General Your Projects)进行工作,GYP时Google的一套构建工具。
node-gyp进行工作需要一些依赖。
在UNIX系列平台下:
- python
- make
- C++编译工具包(GCC)
在macOS下:
- xcode
在 Windows中
- Visual Studio
也或者使用Visual C++ Build Tools(比较轻量级)。