NodeJs 深入浅出之旅:模块🌀
NodeJs
虽然曾经在以前写过 nodeJs 的一篇文章,不过系统性的开始学习还是从现在开始吧。(不过我也同时在学TypeScript
,所以NodeJs
不能保证不间断学习)
以前学过些:《Node.js学习(一)——简介》
Node.js 是一个基于 chrome V8 引擎的 JavaScript 运行环境 node.js 不是语言,不是服务器,不是数据库。
本次我的 node 学习,直接从node
的express
框架开始来代替http模块
,express
学习可参考:http://javascript.ruanyifeng.com/nodejs/express.html
express
是 Node 一个很经典的框架,也是当今全球最流行的
比如下面的例子:入口 app.js
Node 保持了 JavaScript 中的单线程特点
单线程最大的好处在于不用处处在意状态的同步问题
坏处:
无法利用多核 CPU
错误会引起整个应用退出,应用的健壮性值得考验
大量计算占用 CPU 导致无法继续调用异步 I/O
Node 解决单线程计算量大问题
与Web Workers
类似,node 中有child_process
子进程
Web Workers 的优点和限制以前也写过:https://blog.csdn.net/qq_36171287/article/details/117172844
child_process
子进程的出现,意味着 Node 考研应对单线程在健壮性和无法利用多核 CPU 方面的问题。通过将计算分发到各个子进程,考研将大量计算分解掉,然后再通过进程之间的事件消息来传递结果。
模块机制
JavaScript 先天缺少一项功能:模块。
python 有 import 机制
Ruby 有 require
PHP 有 include 和 require
而 JavaScript 通过<script>
标签引入代码的凡是显得杂乱无章,所以社区也为 JavaScript 制定了相应的规范,其中 CommonJS 规范就是最重要的里程碑,现在还有 es6 提出的 Module
参考文章:http://javascript.ruanyifeng.com/nodejs/module.html
Node 模块规范
Node 采用 CommonJS 规范,CommonJS 对模块的定义十分简单,主要分为模块引用、模块定义和模块标识 3 个部分。
模块引用模块引用示例(可以不引入扩展名):
模块定义在模块中,存在一个 module 对象,代表模块本身。exports 是 module 的属性,exports 对象用于导出当前模块的方法或者遍历,并且是唯一导出的出口。在 node 中,一个文件就是一个模块,将方法挂载在 exports 对象上作为属性就算定义导出的方式
模块标识模块标识其实就算传递给 require()方法的参数,必须是符合小驼峰命名的字符串,或者以.、..开头的相对路径或者绝对路径。 可以没有文件后缀名模块的定义十分简单,接口也十分简洁。意义在于将类聚的方法和变量等限定在私有的作用域中,同事支持引入和导出功能以顺畅地连接上下游依赖。
Node 中模块的分类
核心模块(已经封装好的内置模块)
自定义模块
第三方的模块(npm 下载来的模块)
Node 模块实现
node 中引入模块,需要经历如下三个步骤:
路径分析
文件定位
编译执行
核心模块在 node 编译过程中编译进了二进制执行文件,在 node 进程启动时,部分核心模块被直接加载进内存中,所以这部分核心模块引入时文件定位和编译执行可以忽略,并且在路径分析中有限判断。所以核心模块加载速度最快
优先从缓存加载
与前端浏览器会缓存静态脚本文件以提高性能一样,Node 对引入过的模块都会进行缓存,以减少二次引入时的开销。
不同的地方在于,浏览器仅仅缓存文件,而 Node 缓存的是编译和执行之后的对象。
不论是核心模块还是文件模块,require()方法对相同模块的二次加载都一律采用缓存优先的方式
,这是第一优先级的。不同之处在于核心模块的缓存检查先于文件模块的缓存检查。
文件扩展名分析
require()在分析标识符的过程中,会出现标识符中不包含文件扩展名的情况。 CommonJS 模块规范也允许在标识符中不包含文件扩展名,这种情况下,Node 会按.js、.node、.json 的次序补足扩展名后尝试。
在调用过程中,需要 fs 文件读取模块同步阻塞式地判断文件是否存在,因为 node 是单线程的。
小技巧:
如果是 node 和 json 文件,在传递时将扩展名加上,会提升速度。
同步配合缓存,可以大幅度缓解 node 单线程中阻塞式调用的缺陷
模块编译
在 Node 中,每个文件模块都是一个对象
文件模块定义如下:
对于不同的文件扩展名,node 的载入方法也不同:
.js 文件。 通过 fs 模块同步读取文件后编译执行
.node 文件。 这是用 C/C++编写的扩展文件,通过 dlopen()方法加载最后编译生成的文件
.json 文件。 通过 fs 模块同步读取文件后,用 JSON.parse()解析返回结果
其余扩展名文件。 都会被当作.js 文件载入
每个编译成功的模块,都会将文件路径作为索引缓存在 Module._cache 对象上,以提升二次引入的性能。可以使用console.log(require.cache)
打印查看
JavaScript 模块的编译
在 node 的 API 文档中,知道每个模块中还有_filename
和_dirname
这两个变量。 如果把直接定义模块的过程放在浏览器端,会存在污染全局变量的情况。
所以,node 对获取的 JavaScript 文件内容进行了头尾包装。
一个正常的 JavaScript 文件会被包装成如下:
这样,每个模块文件之间都进行了作用域隔离。 包装后的代码会通过 vm 原生模块的 runInThisContext()方法执行,然后返回一个 function 对象。
exports 属性上的任何方法和属性都可以被外部调用到,但是模块中其余变量或属性则不可直接被调用。
vm 原生模块:vm 是 nodejs 的一个核心模块,使用 vm 模块可以在 v8 虚拟机上下文中编译运行代码。JavaScript 代码可以被立即编译运行,也可以编译保存稍后运行。利用 vm 模块,我们可以使得 nodejs 动态执行代码,服务的扩展性,动态性更好
exports 不能直接赋值,原因是由于 exports 对象是通过形参的方式传入的,直接赋值会改变形参的引用
如果需要直接赋值,那么就给 module.exports 这个对象
C/C++模块的编译
.node 模块文件不需要编译,因为是编写 C/C++模块之后编译生成的,所以只有加载和执行过程。 C/C++模块通过预先编译成.node 文件,然后调用 process.dlopen()方法加载执行。
在执行过程中,模块的 exports 对象与.node 模块产生联系,返回给调用者。
Windows 下编译和执行过程:
C/C++模块
优势主要是执行效率
劣势是 C/C++模块编写门槛高
JSON 文件的编译
.json 文件的编译是三种编译方式中最简单的。 利用 fs 文件读取模块同步读取 json 文件的内容后,调用 JSON.parse()方法得到对象,然后赋值给模块对象的 exports。
模块依赖
文件模块可能会依赖于核心模块,核心模块可能会依赖于内建模块
文件模块不推荐直接调用内建模块
版权声明: 本文为 InfoQ 作者【空城机】的原创文章。
原文链接:【http://xie.infoq.cn/article/dc7677532195a55d2cb786f8e】。文章转载请联系作者。
评论