模块机制
Web1.0时代,JavaScript脚本语言的两个主要功能:
- 表单验证;
- 网页特效;
Web2.0时代,前端工程师利用JavaScript大大提升了网页的用户体验,经历了工具类库、组件库、前端框架、前端应用的变迁。
JavaScript的先天缺陷:模块。
高级语言的模块化机制:
- Java-类文件;
- Python-import机制;
- Ruby-require;
- PHP-include和require;
1.CommonJS规范
commonJS的愿景:希望JavaScript能够在任何地方运行。
1.1CommonJS的出发点
JavaScript规范的缺陷:
- 没有模块系统;
- 标准库较少;
- 没有标准接口;
- 缺乏包管理系统;
CommonJS规范中,CommonJSAPI可以编写的应用:
- 服务端JavaScript应用程序;
- 命令行工具;
- 桌面图形界面应用程序;
- 混合应用;
1.2CommonJS的模块规范
-
模块引用
采用require()方法;
var math = require('math');
-
模块定义
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); }
-
模块标识
模块标识:
就是传递给require()方法的参数,符合小驼峰命名的字符串,或者以.、..开头的相对路径或绝对路径,可以没有后缀.js。
2.Node的模块实现
Node中引入模块经历的三个步骤:
- 路径分析;
- 文件定位;
- 编译执行;
Node中,模块分为两种:
- 核心模块(Node提供的模块);
核心模块部分在Node源代码的编译过程中,编译了二进制执行文件。在Node进程启动时,部分核心模块被直接加载进内存中,所以这部分核心模块引入时,文件定位和编译执行两个步骤可以省略掉,并且在路径分析中优先判断,它的加载速度是最快的。
- 文件模块(用户编写的模块);
文件模块在运行时动态加载,需要完整的路径分析、文件定位、编译执行过程,速度比核心模块慢。
2.1优先从缓存加载
与前端浏览器会缓存静态脚本文件以提高性能一样,Node对引入过的模块都会进行缓存,以减少二次引入的开销。不同的是,浏览器仅缓存文件,而Node缓存的是编译和执行后的对象。
不论是核心模块还是文件模块,require()方法对相同模块的二次加载都一律采用缓存优先的方式,这是第一优先级的。不同之处在于核心模块的缓存检查先于文件模块的缓存检查。
2.2路径分析和文件定位
-
模块标识符分析
模块标识符在Node中的分类:
- 核心模块,如http、fs、path等;
- .或..开始的相对路径文件模块;
- 以/开始的绝对文件模块;
- 非路径形式的文件模块,如自定义的connect模块;
核心模块
核心模块的优先级仅次于缓存加载,在Node的源代码编译过程中,已经编译为二进制代码了,其加载过程最快。
路径形式的文件模块
在分析路径模块时,require()将路径转化为真实路径,以真实路径作为索引,将编译执行后的结果放在缓存中,以使二次加载更快,其加载速度慢于核心模块。
自定义模块
这类模块的查找最费时,也是所有方式最慢的一种。
模块路径:
Node在定位文件模块的具体文件时制定的查找策略,具体表现为一个路径组成的数组。
-
文件定位
从缓存加载的优化策略使得二次引入时不需要路径分析、文件定位和编译执行的过程,大大提高了再次加载模块时的效率。
文件定位过程中,需要注意的细节,包括文件扩展名的分析、目录和包的处理。
文件扩展名分析
require()在分析标识符的过程中,出现标识符中不包含文件扩展名的情况,Node会按.js、.json、.node的次序补足扩展名,依次尝试。
在尝试的过程中,需要调用fs模块同步阻塞式低判断文件是否存在。
目录分析和包
在分析标识的过程中,require()通过分析文件扩展名之后,可能没有查找到对应的文件,但却得到一个目录,此时Node会将目录当做一个包来处理。
2.3模块编译
在Node中,每个文件模块都是一个对象。
编译和执行时引入文件模块的最后一个阶段。定位到具体的文件后,Node会新建一个模块对象,然后根据路径载入并编译。对于不同的文件扩展名,载入方法也有所不同。
- .js文件;通过fs模块同步读取文件后编译执行。
- .node文件;这是用C/C++编写的扩展文件,通过dlopen()方法加载最后编译生成的文件。
- .json文件;通过fs模块同步读取文件后,用JSON.parse()解析返回结果。
- 其余扩展名文件;都被当做.js文件载入。
3.核心模块
核心模块分为两部分:
- C/C++编写的部分;
- JavaScript编写的部分;
3.1JavaScript核心模块的编译过程
JavaScript核心模块的编译过程:
- 转存为C/C++代码;
在此过程中,JavaScript代码已字符串的形式存储在Node的命名空间中,是不可直接执行的。在启动Node进程时,JavaScript代码直接加载进内存中。在加载的过程中,JavaScript核心模块经历标识符分析后,直接定位到内存中,比普通的文件模块从磁盘中一处一处查找要快得多。
- 编译JavaScript核心模块;
在引入JavaScript核心模块的过程中,也经历了头尾包装的过程,然后才执行和导出exports对象。与文件模块区别的地方在于:获取源码的方式(核心模块从内存中加载)以及缓存执行结果的位置。
3.2C/C++核心模块的编译过程
C/C++模块主内完成核心,JavaScript主外实现封装的模式是Node能够提高性能的常见方式。
-
内建模块的组织形式;
内建模块的优势:
- C/C++编写,性能优于脚本语言;
- 在进行文件编译时,被编译二进制文件。一旦Node开始执行,直接被加载进内存,无须再次做标识定位,文件定位,编译等过程,直接可执行。
- 内建模块的导出;
在Node的所有模块类型中,存在一种依赖层级关系:
文件模块可能依赖核心模块,核心模块可能依赖内建模块。
3.3核心模块的引入流程
核心模块的引入流程经历了C/C++层面的内建模块的定义,(JavaScript)核心模块的定义和引入以及(JavaScript)文件模块层面的引入。
3.4编写核心模块
核心模块被编译进二进制文件需要遵循一定规则。作为Node的使用者,几乎没有机会参与核心模块的开发。
4.C/C++扩展模块
JavaScript的一个典型的弱点就是位运算。
在JavaScript应用中,会频繁出现位运算的需求,包括转换、编码等过程,通过JavaScript实现,CPU资源会耗费很多。
4.1前提条件
- GYP项目生成工具;
- V8引擎C++库;
- libux库;
- Node内部库;
- 其他库;
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核心模块的两个职责:
- 作为C/C++内建模块的封装层和桥接层,供文件模块调用;
- 纯粹的功能模块,不需要和底层打交道;
文件模块:通常由第三方编写,包括普通JavaScript模块的C/C++扩展模块,主要调用方向为普通JavaScript模块调用扩展模块。
6.包与NPM
包和NPM是将模块联系起来的一种机制。
包组织模块示意图:
CommonJS包的定义:
由包结构和包描述文件两个部分组成,前者用于组织包中的各种文件,后者用于描述包的相关信息,以供外部读取分析。
6.1包结构
包实际是一个存档文件,即一个目录直接打包为.zip和tar.gz格式的文件,安装后解压还原为目录。
包目录包含的文件:
- package.json:包描述文件;
- bin:用于存放可执行二进制文件的目录;
- lib:用于存放JavaScript代码的目录;
- doc:用于存放文档的目录;
- test:用于存放单元测试用例的代码;
6.2包描述文件和NPM
包描述文件用于表达非代码相关的信息,是个JSON格式的文件-package.json,位于包的根目录下,是包的重要组成部分。
6.3NPM常用功能
对于Node而言,NPM帮助完成了第三方模块的发布、安装和依赖等。借助Node与第三方模块之间形成了很好的一个生态系统。
-
查看帮助;
查看当前NPM版本:
$ npm -v
查看帮助:
$ npm
-
安装依赖包
安装依赖包是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
- NPM钩子命令
C/C++模块实际上是编译后才能使用的。package.json中script字段的提出就是让包在安装或者卸载等过程中提供钩子机制。
-
发布包
- 编写模块;
-
初始化包描述文件;
$ npm init
-
注册包仓库账号
$ npm adduser
-
上传包
$ npm publish .
-
安装包
$ npm install hello_test_jackson --registy=http://npmjs.org
-
管理包权限
多人进行发布
$ npm owner ls eventproxy
-
分析包
在使用NPM的过程中,或许你不能确认当前目录下能否通过require()顺利引入想要的包,执行npm ls分析包。
$ npm ls
6.4局域NPM
为了同时能够享受NPM上众多的包,同时对自己的包进行保密和限制,现有的解决方案就是企业搭建自己的NPM仓库。
企业混合使用官方仓库和局域仓库的示意图:
对于企业内部而言,私有的可重用模块可以打包到局域NPM仓库,这样可以保持更新的中心化,不至于让各个小项目各自维护相同功能的模块,杜绝通过复制粘贴实现代码共享的行为。
6.5NPM潜在问题
NPM的潜在问题:
- 每个人都可以分享包平台上,鉴于开发人员水平不一,上面的包的质量也良莠不齐;
- Node代码可以运行在服务器端,需要考虑安全问题;
对于包的使用者而言,包质量和安全问题需要作为是否采纳模块的一个判断条件。
如何评判一个包的安全和质量?
- 开源社区内在的健康发展机制-口碑效应;
- Github中,模块项目的观察者数量和分支数量;
- 包中测试用例和文档的状况;
Kwalitee模块的考察点:
- 具备良好的测试;
- 具备良好的文档(README、API);
- 具备良好的测试覆盖率;
- 具备良好的编码规范;
- 更多条件;
7.前后端共用模块
7.1模块的侧重点
前后端JavaScript分别搁置在HTTP的两端,它们扮演的角色并不同。
浏览器端的JavaScript:
需要经历从同一个服务器端分发到多个客户端执行,瓶颈在于带宽,需要网络加载代码;
浏览器端的JavaScript:
相同的代码需要多次执行,瓶颈在于CPU和内存等资源,从磁盘中加载代码;
两者的加载的速度不在一个数量级别。
CommonJS为后端JavaScript制定的规范;
AMD为前端JavaScript制定的规范;
7.2AMD规范
AMD规范是CommonJS模块规范的一个延伸,定义如下;
define(id?,dependenceies?,factory)
模块的id和依赖是可选的,
与Node模块相似之处:
factory的内容就是实际代码的内容;
与Node模块不同之处:
- AMD需要define定义一个模块,Node实现中是隐式包装的;
- 内容需要通过返回的方式实现导出;
7.3CMD规范
CMD规范由国内的玉伯提出,与AMD规范的主要区别在于定义模块和依赖引入的部分。
AMD需要在声明模块的时候指定所有的依赖,通过形参传递到模块内容;
define(['dep1','dep2'],function (dep1,dep2){
return function (){};
})
7.4兼容多种模块规范
为了让同一个模块可以运行在前后端,需要考虑兼容前端也实现了模块规范的环境。为了保持前后端的一致性,类库开发者需要将类库代码包装在一个闭包内。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。