CommonJS模块规范与NodeJS的模块系统底层原理
Posted 刻刻帝丶
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了CommonJS模块规范与NodeJS的模块系统底层原理相关的知识,希望对你有一定的参考价值。
tip:有问题或者需要大厂内推的+我脉脉哦:丛培森 ٩( ‘ω’ )و
【本文源址:http://blog.csdn.net/q1056843325/article/details/54945733 转载请添加该地址】
原谅我标题党
其实也没有非常深入底层
在了解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.js
var add = function add(a, b)
return a + b;
exprots.add = add;
导出给外部的对象
就拥有一个add方法
不过要特别注意
如果直接给exports赋值为一个基本类型值不会成功
比如我们只是想单纯的导出一个数字(虽然不会这么用)
exports = 123;
是完全不能够导出的
至于为什么下面再说
我们一般不会直接通过exports这么用
在我们模块的上下文中,还有一个module对象,引用我们模块自身
而这个exports对象便是module上的属性
我们通常的做法就是通过 module.exprots
导出
//tool.js
var 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.js
var a = 0;
var increase = function()
++a;
//index.js
var 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.js
var 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方法等等传入这个函数执行
[==主页传送门==](http://blog.csdn.net/q1056843325)
以上是关于CommonJS模块规范与NodeJS的模块系统底层原理的主要内容,如果未能解决你的问题,请参考以下文章
ES6的export与Nodejs的module.exports