《深入浅出Node.js》学习笔记(二)

958 阅读12分钟

模块机制

Web1.0时代,JavaScript脚本语言的两个主要功能:

  1. 表单验证;
  2. 网页特效;

Web2.0时代,前端工程师利用JavaScript大大提升了网页的用户体验,经历了工具类库、组件库、前端框架、前端应用的变迁。

JavaScript的先天缺陷:模块

高级语言的模块化机制:

  1. Java-类文件;
  2. Python-import机制;
  3. Ruby-require;
  4. PHP-include和require;

1.CommonJS规范

commonJS的愿景:希望JavaScript能够在任何地方运行。

1.1CommonJS的出发点

JavaScript规范的缺陷:

  1. 没有模块系统;
  2. 标准库较少;
  3. 没有标准接口;
  4. 缺乏包管理系统;

CommonJS规范中,CommonJSAPI可以编写的应用:

  1. 服务端JavaScript应用程序;
  2. 命令行工具;
  3. 桌面图形界面应用程序;
  4. 混合应用;

1.2CommonJS的模块规范

  1. 模块引用

    采用require()方法;

    var math = require('math');
    
  2. 模块定义

    require():用来引入外部模块;

    exports:导出模块的方法或变量,唯一导出的出口;

    module:代表模块自身;

    // math.js
    exports.add = function (){
    	var sum = 0,
    	i = 0,
    	args = arguments,
    	l = args.lenght;
    	while(i<l){
    		sum += args[i++];
    	}
    	return sum;
    }
    
    //program.js
    var math = require("math");
    exports.increment = function (val){
    	return math.add(val,1);
    }
    
  3. 模块标识

    模块标识:

    就是传递给require()方法的参数,符合小驼峰命名的字符串,或者以**...**开头的相对路径或绝对路径,可以没有后缀.js。

2.Node的模块实现

Node中引入模块经历的三个步骤:

  1. 路径分析;
  2. 文件定位;
  3. 编译执行;

Node中,模块分为两种:

  1. 核心模块(Node提供的模块);

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

  2. 文件模块(用户编写的模块);

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

2.1优先从缓存加载

与前端浏览器会缓存静态脚本文件以提高性能一样,Node对引入过的模块都会进行缓存,以减少二次引入的开销。不同的是,浏览器仅缓存文件,而Node缓存的是编译和执行后的对象。

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

2.2路径分析和文件定位

  1. 模块标识符分析

    模块标识符在Node中的分类:

    1. 核心模块,如http、fs、path等;
    2. .或..开始的相对路径文件模块;
    3. 以/开始的绝对文件模块;
    4. 非路径形式的文件模块,如自定义的connect模块;

    核心模块

    核心模块的优先级仅次于缓存加载,在Node的源代码编译过程中,已经编译为二进制代码了,其加载过程最快。

    路径形式的文件模块

    在分析路径模块时,require()将路径转化为真实路径,以真实路径作为索引,将编译执行后的结果放在缓存中,以使二次加载更快,其加载速度慢于核心模块。

    自定义模块

    这类模块的查找最费时,也是所有方式最慢的一种。

    模块路径:

    Node在定位文件模块的具体文件时制定的查找策略,具体表现为一个路径组成的数组。

  2. 文件定位

    从缓存加载的优化策略使得二次引入时不需要路径分析、文件定位和编译执行的过程,大大提高了再次加载模块时的效率。

    文件定位过程中,需要注意的细节,包括文件扩展名的分析、目录和包的处理。

    文件扩展名分析

    require()在分析标识符的过程中,出现标识符中不包含文件扩展名的情况,Node会按.js、.json、.node的次序补足扩展名,依次尝试。

    在尝试的过程中,需要调用fs模块同步阻塞式低判断文件是否存在。

    目录分析和包

    在分析标识的过程中,require()通过分析文件扩展名之后,可能没有查找到对应的文件,但却得到一个目录,此时Node会将目录当做一个包来处理。

2.3模块编译

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

编译和执行时引入文件模块的最后一个阶段。定位到具体的文件后,Node会新建一个模块对象,然后根据路径载入并编译。对于不同的文件扩展名,载入方法也有所不同。

  • .js文件;通过fs模块同步读取文件后编译执行。
  • .node文件;这是用C/C++编写的扩展文件,通过dlopen()方法加载最后编译生成的文件。
  • .json文件;通过fs模块同步读取文件后,用JSON.parse()解析返回结果。
  • 其余扩展名文件;都被当做.js文件载入。

3.核心模块

核心模块分为两部分:

  1. C/C++编写的部分;
  2. JavaScript编写的部分;

3.1JavaScript核心模块的编译过程

JavaScript核心模块的编译过程:

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

    在此过程中,JavaScript代码已字符串的形式存储在Node的命名空间中,是不可直接执行的。在启动Node进程时,JavaScript代码直接加载进内存中。在加载的过程中,JavaScript核心模块经历标识符分析后,直接定位到内存中,比普通的文件模块从磁盘中一处一处查找要快得多。

  2. 编译JavaScript核心模块;

    在引入JavaScript核心模块的过程中,也经历了头尾包装的过程,然后才执行和导出exports对象。与文件模块区别的地方在于:获取源码的方式(核心模块从内存中加载)以及缓存执行结果的位置。

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

C/C++模块主内完成核心,JavaScript主外实现封装的模式是Node能够提高性能的常见方式。

  1. 内建模块的组织形式;

    内建模块的优势:

    1. C/C++编写,性能优于脚本语言;
    2. 在进行文件编译时,被编译成二进制文件。一旦Node开始执行,直接被加载进内存,无须再次做标识定位,文件定位,编译等过程,直接可执行。
  2. 内建模块的导出;

    在Node的所有模块类型中,存在一种依赖层级关系:

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

3.3核心模块的引入流程

核心模块的引入流程经历了C/C++层面的内建模块的定义,(JavaScript)核心模块的定义和引入以及(JavaScript)文件模块层面的引入。

3.4编写核心模块

核心模块被编译进二进制文件需要遵循一定规则。作为Node的使用者,几乎没有机会参与核心模块的开发。

4.C/C++扩展模块

JavaScript的一个典型的弱点就是位运算。

在JavaScript应用中,会频繁出现位运算的需求,包括转换、编码等过程,通过JavaScript实现,CPU资源会耗费很多。

4.1前提条件

  1. GYP项目生成工具;
  2. V8引擎C++库;
  3. libux库;
  4. Node内部库;
  5. 其他库;

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

普通的扩展模块与内建模块的区别在于无须将源代码编译进Node,而是通过dlopen()方式动态加载。

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

通过GYP工具实现。

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

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

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

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

5.模块调用栈

C/C++内建模块:属于最底层的模块,属于核心模块,主要通过API给JavaScript核心模块和第三方JavaScript文件模块的调用。

JavaScript核心模块的两个职责:

  1. 作为C/C++内建模块的封装层和桥接层,供文件模块调用;
  2. 纯粹的功能模块,不需要和底层打交道;

文件模块:通常由第三方编写,包括普通JavaScript模块的C/C++扩展模块,主要调用方向为普通JavaScript模块调用扩展模块。

模块调用栈

6.包与NPM

包和NPM是将模块联系起来的一种机制。

包组织模块示意图:

包组织模块示意图

CommonJS包的定义:

由包结构和包描述文件两个部分组成,前者用于组织包中的各种文件,后者用于描述包的相关信息,以供外部读取分析。

6.1包结构

包实际是一个存档文件,即一个目录直接打包为.zip和tar.gz格式的文件,安装后解压还原为目录。

包目录包含的文件:

  1. package.json:包描述文件;
  2. bin:用于存放可执行二进制文件的目录;
  3. lib:用于存放JavaScript代码的目录;
  4. doc:用于存放文档的目录;
  5. test:用于存放单元测试用例的代码;

6.2包描述文件和NPM

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

6.3NPM常用功能

对于Node而言,NPM帮助完成了第三方模块的发布、安装和依赖等。借助Node与第三方模块之间形成了很好的一个生态系统。

  1. 查看帮助;

    查看当前NPM版本:

    $ npm -v
    

    查看帮助:

    $ npm
    
  2. 安装依赖包

    安装依赖包是NPM最常见的用法,执行语句:

    $ npm install express
    

    1.全局模式安装

    $ npm install express -g
    

    2.从本地安装

    本地安装只需为NPM指明package.json文件的所在的位置即可。它可以是一个包含package.json的存档文件,也可以是个URL地址,也可以是个目录有package.json的目录的位置。

    3.从非官方源安装

    从非官方安装,可以通过镜像源安装。

    $ npm config set underscore --registry=http://registry.url
    

    镜像源安装指定默认源:

    $ npm config set registry http://registry.url
    
  3. NPM钩子命令

    C/C++模块实际上是编译后才能使用的。package.json中script字段的提出就是让包在安装或者卸载等过程中提供钩子机制。

  4. 发布包

    1. 编写模块;

    2. 初始化包描述文件;

      $ npm init
      
    3. 注册包仓库账号

      $ npm adduser
      
    4. 上传包

      $ npm publish .
      
    5. 安装包

      $ npm install hello_test_jackson --registy=http://npmjs.org
      
    6. 管理包权限

      多人进行发布

      $ npm owner ls eventproxy
      
  5. 分析包

    在使用NPM的过程中,或许你不能确认当前目录下能否通过require()顺利引入想要的包,执行npm ls分析包。

    $ npm ls
    

6.4局域NPM

为了同时能够享受NPM上众多的包,同时对自己的包进行保密和限制,现有的解决方案就是企业搭建自己的NPM仓库。

企业混合使用官方仓库和局域仓库的示意图:

混合使用官方仓库和局域仓库的示意图

对于企业内部而言,私有的可重用模块可以打包到局域NPM仓库,这样可以保持更新的中心化,不至于让各个小项目各自维护相同功能的模块,杜绝通过复制粘贴实现代码共享的行为。

6.5NPM潜在问题

NPM的潜在问题:

  1. 每个人都可以分享包平台上,鉴于开发人员水平不一,上面的包的质量也良莠不齐;
  2. Node代码可以运行在服务器端,需要考虑安全问题;

对于包的使用者而言,包质量和安全问题需要作为是否采纳模块的一个判断条件。

如何评判一个包的安全和质量?

  1. 开源社区内在的健康发展机制-口碑效应;
  2. Github中,模块项目的观察者数量和分支数量;
  3. 包中测试用例和文档的状况;

Kwalitee模块的考察点:

  1. 具备良好的测试;
  2. 具备良好的文档(README、API);
  3. 具备良好的测试覆盖率;
  4. 具备良好的编码规范;
  5. 更多条件;

7.前后端共用模块

7.1模块的侧重点

前后端JavaScript分别搁置在HTTP的两端,它们扮演的角色并不同。

浏览器端的JavaScript:

需要经历从同一个服务器端分发到多个客户端执行,瓶颈在于带宽,需要网络加载代码;

浏览器端的JavaScript:

相同的代码需要多次执行,瓶颈在于CPU和内存等资源,从磁盘中加载代码;

两者的加载的速度不在一个数量级别。

CommonJS为后端JavaScript制定的规范;

AMD为前端JavaScript制定的规范;

7.2AMD规范

AMD规范是CommonJS模块规范的一个延伸,定义如下;

define(id?,dependenceies?,factory)

模块的id和依赖是可选的,

与Node模块相似之处:

factory的内容就是实际代码的内容;

与Node模块不同之处:

  1. AMD需要define定义一个模块,Node实现中是隐式包装的;
  2. 内容需要通过返回的方式实现导出;

7.3CMD规范

CMD规范由国内的玉伯提出,与AMD规范的主要区别在于定义模块和依赖引入的部分。

AMD需要在声明模块的时候指定所有的依赖,通过形参传递到模块内容;

define(['dep1','dep2'],function (dep1,dep2){
	return function (){};
})

7.4兼容多种模块规范

为了让同一个模块可以运行在前后端,需要考虑兼容前端也实现了模块规范的环境。为了保持前后端的一致性,类库开发者需要将类库代码包装在一个闭包内。