模块机制

CommonJS 模块规范

模块引用

require() 方法,引入一个模块的 API 到当前上下文中

const math = require('math')

模块定义

exports 对象用于导出当前模块的方法或者变量

module 对象代表模块自身,而 exports 是 module 的属性

// math.js
exports.add = function() {
  let sum = 0,
      i = 0,
      args = arguments,
      l = args.length
  while(i < l) {
    sum += args[i++]
  }
  return sum
}

模块标识

传递给 require() 方法的参数,它必须是符合小驼峰命名的字符串,或相对路径/绝对路径

Node 的模块实现

在 Node 中引入模块, 需要经历如下3个步骤

  1. 路径分析

  2. 文件定位

  3. 编译执行

在 Node 中, 模块分为两类:一类是 Node 提供的模块, 称为核心模块;另一类是用户编写的模块,称为文件模块

  • 核心模块部分在 Node 源代码的编译过程中,编译进了二进制执行文件。在 Node 进程启动时,部分核心模块就被直接加载进内存中,所以这部分核心模块引入时,文件定位和编译执行这两个步骤可以省略掉,并且路径分析中优先判断,所以它的加载速度是最快的

  • 文件模块则是在运行时动态加载,需要完整的路径分析、文件定位、编译执行过程,速度比核心模块慢

优先从缓存加载

Node 对引入过的模块都会进行缓存,以减少二次引入时的开销。与浏览器仅仅缓存文件不同,它缓存的是编译和执行之后的对象

路径分析和文件定位

  1. 模块标识符分析
  • 核心模块:优先级仅次于缓存加载

  • 路径形式的文件模块:将路径转为真实路径,并以真实路径作为索引,将编译执行后的结果存放到缓存中,以使二次加载时更快,其加载速度慢于核心模块

  • 自定义模块:非核心模块,也不是路径形式的标识符。它是一种特殊的文件模块,可能是一个文件或者包的形式。这类模块的查找是最费时的,也是所有方式中最慢的一种

  1. 文件定位
  • 文件扩展名分析:CommonJS 模块规范允许在标识符中不包括文件扩展名,这种情况下,Node 会按 .js、.json、.node 的次序补足扩展名,依次尝试

    在尝试的过程中,需要调用 fs 模块同步阻塞式地判断文件是否存在。因为 Node 是单线程的,所以这里是一个会引起性能问题的地方。 小诀窍是:如果是 .node 和 .json 文件,在传递给 require() 的标识符中带上扩展名,会加快一点速度。另一个诀窍是:同步配合缓存,可以大幅度缓解 Node 单线程中阻塞式调用的缺陷

  • 目录分析和包

    Node 在当前目录下查找 package.json,通过 JSON.parse() 解析出包描述对象,从中取出 main 属性指定的文件名进行定位。如果文件名缺失扩展名,将会进入扩展名分析的步骤。

    而如果 main 属性指定的文件名错误,或者压根没有 package.json 文件,Node 会将 index 当做默认文件名,然后依次查找 index.js、index.json、index.node

    如果在目录分析的过程中没有定位成功任何文件,则自定义模块进入下一个模块路径进行查找。如果模块路径数组都被遍历完毕,依然没有查找到目录文件,则会抛出查找失败的异常。

模块编译

  1. JavaScript 模块的编译

    在编译的过程中,Node 对获取的 JavaScript 文件内容进行了头尾包装。在头部添加了 (function(exports, require, module, ____filename, ____dirname)) {\n 在尾部添加了 \n}

    为何存在 exports 情况下,还存在 module.exports,其原因在于,exports 对象是通过形参的方式传入的,直接赋值形参会改变形参的引用,如果要达到 require 引入一个类的效果,请赋值给 module.exports 对象。这个迂回的方案不改变形参的引用。

  2. C/C++ 模块的编译

    Node 调用 process.dlopen() 方法进行加载和执行

  3. JSON 文件的编译

    Node 利用 fs 模块同步读取 JSON 文件的内容之后,调用 JSON.parse() 方法得到对象,然后将它赋给模块对象的 exports,以供外部调用。JSON 文件在用做项目的配置文件时比较有用。如果你定义了一个 JSON 文件作为配置,那就不必调用 fs 模块去异步读取和解析,直接调用 require() 引入即可。此外,你还可以享受到模块缓存的遍历,并且二次引入时页没有性能影响。

    核心模块

    核心模块其实分为 C/C++ 编写和 JavaScript 编写的两部分,其中 C/C++ 文件存放在 Node 项目的 src 目录下

    JavaScript 核心模块的编译过程

    1. 转存为 C/C++ 代码

    2. 编译 JavaScript 核心模块:也经历头尾包装的过程,然后才执行和道出了 exports 对象。不同于文件模块是,获取源代码的方式(核心模块是从内存中加载的)以及缓存执行结果的位置

    C/C++ 核心模块的编译过程

    在核心模块中,有些模块全部由 C/C++ 编写,有些模块则由 C/C++ 完成核心部分,其他部分则由 JavaScript 实现包装或向外导出,以满足性能需求。

    1. 内建模块的组织形式

    2. 内建模块的导出

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

依赖层级关系.png

通常,不推荐文件模块直接调用内建模块。如需调用,直接调用核心模块即可,因为核心模块中基本都封装了内建模块。

核心模块的引入流程

os原生模块的引入流程.png

编写核心模块

编写头文件和编写 C/C++ 文件

C/C++ 扩展模块

JavaScript 的一个典型弱点就是位运算。JavaScript 的位运算参照 Java 的位运算实现,但是 Java 位运算是在 int 型数字的基础上进行的, 而 JavaScript 中只有 double 型的数据类型,在进行位运算的过程中,需要将 double 型转换为 int 型,然后再进行。所以,在 JavaScript 层面上作位运算的效率不高。

在应用中,会频繁出现效率低的操作(如上面提到的位运算),如果通过 JavaScript 来实现,CPU 资源将会耗费好多,这时编写 C/C++ 扩展模块来提升性能的机会来了。

扩展模块不同平台上的编译和加载过程.png

前提条件

  • GYP 项目生成工具

  • V8引擎 C++ 库

  • libuv 库

  • Node 内部库

  • 其他库

C/C++ 扩展模块的编写

普通的扩展模块与内建模块的区别在于无须将源代码编译进 Node,而是通过 dlopen() 方法动态加载。所以在编写普通模块时,无须将源代码写进 node 命名空间,也不需要提供头文件。

C/C++ 扩展模块的编译

写好 .gyp 项目文件,node-gyp 约定 .gyp 文件为 binding.gyp

C/C++扩展模块的加载

require() 方法通过间隙标识符、路径分析、文件定位,然后加载执行即可

require()引入.node文件的过程.png

C/C++ 扩展模块与 JavaScript 模块的区别在于加载之后不需要编译,直接执行之后就可以被外部调用了,其加载速度比 JavaScript 模块略快

使用 C/C++ 扩展模块的一个好处在于可以更灵活和动态地加载他们,保持 Node 模块自身简单性的同时,给予 Node 无线的可扩展性

模块调用栈

模块之间的调用关系.png

包与 NPM

CommonJS 的包规范的定义其实十分简单,它由包结构和包描述文件两个部分组成,前者用于组织包中的各种文件,后者则用于描述包的相关信息,以供外部读取分析

包结构

完全符合 CommonJS 规范的包目录应该包含如下这些文件

  • package.json:包描述文件

  • bin:用于存放可执行二进制文件的目录

  • lib:用于存放 JavaScript 代码的目录

  • doc:用于存放文档的目录

  • test:用于存放单元测试用例的代码

包描述文件与 NPM

包描述文件用于表达非代码相关的信息,它是一个 JSON 格式的文件——package.json, 位于包的根目录下,是包的重要组成部分

NPM 常用功能

  1. 查看帮助
$ npm help <command>
  1. 安装依赖包
# 全局模式安装
$ npm install <package> -g
# 从本地安装
$ npm install <tarball file>
$ npm install <tarball url>
$ npm install <folder>
# 从非官方源安装
$ npm install underscore --registry=http://registry.url
# 指定默认源
$ npm config set registry http://registry.url
  1. npm 钩子命令
"scripts": {
  "preinstall": "preinstall.js",
  "install": "install.js",
  "uninstall": "uninstall.js",
  "test": "test.js"
}

在以上字段中执行 npm install <package> 时,preinstall 指向的脚本将会被加载执行,然后 install 指向的脚本会被执行。在执行 npm uninstall <package> 时,uninstall 指向的脚本也许会做一些清理工作等。当在一个具体的包目录下执行 npm test 时,将会运行 test 指向的脚本

  1. 发布包

    • 编写模块

    • 初始化包描述文件

    • 注册包仓库账号

    • 上传包

    • 安装包

    • 管理包权限

    • 分析包

局域 NPM

搭建自己的 NPM 仓库

NPM 潜在问题

潜在问题在于,鉴于开发者水平不一,上面的包质量也良莠不齐。另一个问题则是,Node 代码可以运行在服务器端,需要考虑安全问题

前后端公用模块

模块的侧重点

客户端的瓶颈在于带宽,服务端的瓶颈在于 CPU 和内存等资源

鉴于网络的原因,CommonJS 为后端 JavaScript 指定的规范并不完全是个前端的应用场景

AMD 规范

CMD 规范

兼容多种模块规范

;(function (name, definition) {
 // 检测上下文环境是否为AMD或CMD
 var hasDefine = typeof define === 'function',
 // 检查上下文环境是否为Node
 hasExports = typeof module !== 'undefined' && module.exports;
 if (hasDefine) {
 // AMD环境或CMD环境
 define(definition);
 } else if (hasExports) {
 // 定义为普通Node模块
 module.exports = definition();
 } else {
 // 将模块的执行结果挂在window变量中,在浏览器中this指向window对象
 this[name] = definition();
 }
})('hello', function () {
 var hello = function () {};
 return hello;
}); 
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,937评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,503评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,712评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,668评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,677评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,601评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,975评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,637评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,881评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,621评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,710评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,387评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,971评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,947评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,189评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,805评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,449评论 2 342

推荐阅读更多精彩内容