首页 > 设计 > WEB开发 > 正文

CommonJS模块规范与NodeJS的模块系统底层原理

2019-11-02 18:24:17
字体:
来源:转载
供稿:网友

原谅我标题党 其实也没有非常深入底层

在了解NodeJS模块之前 首先来科普一下什么是CommonJS

CommonJS规范

它为javaScript制定一套规范——希望Javascript能在任何地方运行 使其具备开发大型应用的能力

出发点便是为了弥补当时JavaScript语言自身的缺点:

无模块系统 现在ES6弥补了这个缺点没有包管理胸痛 导致js应用没有自加载和安装依赖能力无标准接口 没有定义过像Web服务器一类的标准统一接口标准库太少 仅有部分核心库,文件系统等常见需求没有标准API;H5推进了这个过程,但也只是浏览器端

CommonJS-API写出的应用可以跨宿主环境 这样JavaScript就不仅仅只是停留在客户端,他还可以开发:

服务器端JS应用命令行工具桌面图形界面应用程序混合应用(原生应用内嵌浏览器)

CommonJS涵盖一下内容

模块I/O流二进制缓冲区套接字进程环境文件系统单元测试字符集编码Web服务器网关接口包管理……

CommonJS模块

为什么要介绍CommonJS 因为Node简单易用的模块系统就是借鉴了CommonJS的Modules规范 CommonJS模块分为三部分:模块引用、模块定义、模块标识

模块导出

Node中,一个文件就是一个模块 模块中使用exports导出当前模块的变量或函数 下面我就建立一个tool.js的工具模块 并且通过exports导出

//tool.jsvar add = function add(a, b){ return a + b;}exPRots.add = add;

导出给外部的对象 就拥有一个add方法

不过要特别注意 如果直接给exports赋值为一个基本类型值不会成功 比如我们只是想单纯的导出一个数字(虽然不会这么用)

exports = 123;

是完全不能够导出的 至于为什么下面再说


我们一般不会直接通过exports这么用 在我们模块的上下文中,还有一个module对象,引用我们模块自身 而这个exports对象便是module上的属性 我们通常的做法就是通过 module.exprots 导出

//tool.jsvar tool = { add: function (a, b){ return a + b; }}module.exprots = tool;

模块引用

模块引用很简单 只需调用require()方法,接收一个模块标识字符串作为参数 如此引入一个模块到我们当前的环境中

var tool = require('./tool');

我们调用起来倒是轻松愉快 其实内部发生了日异月殊的变化(下面再说)

引入之后,我们就能调用内部的API了

tool.add(1, 2); //3

模块标识

模块标识就是我们传递给require()的那个字符串参数 这个字符串是符合小驼峰命名的字符串 或者是 . / .. 开头的相对路径,再或者绝对路径 如果要引入的模块后缀为 .js / .json / .node 可以省略

这种模块机制导入容易,导出也容易 把类聚的方法和变量限定在私有作用域内 模块之间空间独立、互不干扰,好处不言而喻 妈妈再也不用担心我们变量污染了

NodeJS模块原理

NodeJS在CommonJS模块规范基础上作出了改动 在了解NodeJS模块原理之前 先来了解一下NodeJS的模块缓存机制

模块缓存机制

NodeJS为了提高性能,我们引入模块后,它都会进行缓存 这和我们在浏览器端的很像 但是浏览器缓存的是文件 而Node缓存编译执行后的对象 我们可以做一个实验

//increase.jsvar a = 0;var increase = function(){ ++a;}//index.jsvar increase = require('./increase');console.log(increase());console.log(increase());var add = require('./tool');console.log(increase());console.log(increase());

实验的结果返回了 1 2 3 4 而不是 1 2 1 2 这就证明二次引用时实际引用了缓存的对象(编译执行后的模块)

所以当我们调用require( )方法时 Node会优先查看缓存(第一优先级),没有缓存再进行一系列过程 这一系列过程就是:

路径分析文件定位编译执行

了解这些过程前 我们还要知道模块分类

模块种类

模块大体上分两种,它们还可以细分

核心模块:Node提供的模块 JavaScript核心模块C/C++核心模块文件模块:用户编写的模块 本地模块:本地编写模块第三方模块:从第三方下载的模块

核心模块在Node源码编译过程中,编译进二进制执行文件 Node启动,部分核心模块被直接加载进内存 所以文件定位和编译执行阶段可省略,并且优先判断路径分析(加载最快)

文件模块运行时动态加载,速度稍慢

模块引入原理

路径分析

路径分析的优先级如下:

缓存加载核心模块加载文件模块加载

如果引入的是核心模块,就直接填写模块名字符串就可以了

var http = require('http');

如果引入的是文件模块,就会根据填入的路径来定位文件

var tool = require('./tool');

我们下载的第三方模块会存在于node_modules的文件夹 在分析它的时候 就会查找当前目录下的node_modules中有没有该文件 如果没找到,就会查找父级目录下有没有node_modules并查找 以此类推 引用第三方模块同样不必输入路径

var react = require('react');

文件定位

require()分析标识符的时候,可能会出现省略文件扩展名的情况 此时,Node会按照 .js / .json / .node 的顺序依次尝试 很显然这有一点儿性能问题,尝试也需要时间 所以我们最好给 .json.node 形式的文件添加扩展名

如果我们定位到的是一个文件夹 Node会把它当做一个包来处理 根据包内部的package.json文件的main属性继续定位入口文件

关于包的概念,这里不讲 可以暂时把它理解为拥有package.json配置文件的一个文件夹

模块编译

文件格式不同,载入方法也不同

.js文件:通过fs核心模块同步读取后编译执行.node文件:C/C++扩展文件,通过dlopen()加载最后编译生成的文件.json文件:通过fs核心模块同步读取后利用JSON.parse()解析其他:均当做.js文件处理

这里我只说一下JavaScript文件模块的编译 大家一定很奇怪一个问题 我们的文件中根本没有什么exports,没有什么require,它们从哪儿来的? 答案就在这里 就拿我们上面的模块为例

//increase.jsvar a = 0;var increase = function(){ ++a;}

在这个编译过程中,Node实际上对JS文件进行了包装 加上了“龙头凤尾”(致敬儿时玩的四驱车) 龙头:(function(exports, require, module, __filename, __dirname){/n 凤尾:/n}); 封装后的文件变成了这样

(function(exports, require, module, __filename, __dirname){ var a = 0; var increase = function(){ ++a; }});

这回我们就可以理解为什么直接给exports赋基本类型值不可以 因为exports实际上作为形参传入 赋值仅仅只是改变了形参


包装后的代码通过原生vm模块的runInThisContext()执行(类似eval) 返回一个函数 最后将当前模块的exports属性、require方法等等传入这个函数执行

==主页传送门==


发表评论 共有条评论
用户名: 密码:
验证码: 匿名发表