写点什么

NodeJs 深入浅出之旅:模块🌀

用户头像
空城机
关注
发布于: 3 小时前
NodeJs深入浅出之旅:模块🌀

NodeJs

虽然曾经在以前写过 nodeJs 的一篇文章,不过系统性的开始学习还是从现在开始吧。(不过我也同时在学TypeScript,所以NodeJs不能保证不间断学习)


以前学过些:《Node.js学习(一)——简介》


Node.js 是一个基于 chrome V8 引擎的 JavaScript 运行环境 node.js 不是语言,不是服务器,不是数据库。


本次我的 node 学习,直接从nodeexpress框架开始来代替http模块express学习可参考:http://javascript.ruanyifeng.com/nodejs/express.html


express是 Node 一个很经典的框架,也是当今全球最流行的


比如下面的例子:入口 app.js

const express = require('express');  // 直接用express代替http, 因为express底层是基于http模块
var app = express();
// 将app的监听都存放到另外一个router的index.js当中const routes = require('./routes')(app);
//服务器本地主机的数字app.listen('3001',function(){ console.log("启动了。。。, 运行 http:localhost:3001/AjaxTest/app.html ")})
复制代码


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 个部分。


  1. 模块引用模块引用示例(可以不引入扩展名):

    var math = require('math')
复制代码


  1. 模块定义在模块中,存在一个 module 对象,代表模块本身。exports 是 module 的属性,exports 对象用于导出当前模块的方法或者遍历,并且是唯一导出的出口。在 node 中,一个文件就是一个模块,将方法挂载在 exports 对象上作为属性就算定义导出的方式

    // math.js    exports.add = ()=>{        let sum = 0, i = 0, args = arguments, l = args.length;        while(i < l) {            sum += args[i++];        }        return sum;    }
复制代码


  1. 模块标识模块标识其实就算传递给 require()方法的参数,必须是符合小驼峰命名的字符串,或者以.、..开头的相对路径或者绝对路径。 可以没有文件后缀名模块的定义十分简单,接口也十分简洁。意义在于将类聚的方法和变量等限定在私有的作用域中,同事支持引入和导出功能以顺畅地连接上下游依赖。

Node 中模块的分类

  • 核心模块(已经封装好的内置模块)

  • 自定义模块

  • 第三方的模块(npm 下载来的模块)

Node 模块实现

node 中引入模块,需要经历如下三个步骤:

  1. 路径分析

  2. 文件定位

  3. 编译执行


核心模块在 node 编译过程中编译进了二进制执行文件,在 node 进程启动时,部分核心模块被直接加载进内存中,所以这部分核心模块引入时文件定位和编译执行可以忽略,并且在路径分析中有限判断。所以核心模块加载速度最快

优先从缓存加载

与前端浏览器会缓存静态脚本文件以提高性能一样,Node 对引入过的模块都会进行缓存,以减少二次引入时的开销。


不同的地方在于,浏览器仅仅缓存文件,而 Node 缓存的是编译和执行之后的对象。


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

文件扩展名分析

require()在分析标识符的过程中,会出现标识符中不包含文件扩展名的情况。 CommonJS 模块规范也允许在标识符中不包含文件扩展名,这种情况下,Node 会按.js、.node、.json 的次序补足扩展名后尝试。


在调用过程中,需要 fs 文件读取模块同步阻塞式地判断文件是否存在,因为 node 是单线程的。


小技巧:

  • 如果是 node 和 json 文件,在传递时将扩展名加上,会提升速度。

  • 同步配合缓存,可以大幅度缓解 node 单线程中阻塞式调用的缺陷



模块编译

在 Node 中,每个文件模块都是一个对象


文件模块定义如下:

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 = [];}
复制代码


对于不同的文件扩展名,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 文件会被包装成如下:

(function(exports, require, module, _filename, _dirname){    var math = require('math');    exports.area = function (radius) {        return Math.PI * radius * radius;    }})
复制代码


这样,每个模块文件之间都进行了作用域隔离。 包装后的代码会通过 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。



模块依赖

文件模块可能会依赖于核心模块,核心模块可能会依赖于内建模块


文件模块不推荐直接调用内建模块

发布于: 3 小时前阅读数: 4
用户头像

空城机

关注

曾经沧海难为水,只是当时已惘然 2021.03.22 加入

业余作者,在线水文 主要干前端的活,业余会学学python 欢迎各位关注,互相学习,互相进步

评论

发布
暂无评论
NodeJs深入浅出之旅:模块🌀