nodejs模块比Java多_一起读书《深入浅出nodejs》-node模块机制

node 模块机制

前言

说到node,就不免得提到JavaScript。JavaScript自诞生以来,经历了工具类库、组件库、前端框架、前端应用的变迁。通过无数开发人员的努力,JavaScript不断被类聚和抽象,从而更好的组织业务逻辑。而从另一个角度而言,大家之所以将其整来整去,还不是为了弥补其先天缺乏的一项功能:模块。

与其他高级语言相比,Java有类文件、Python有import机制、PHP有include和require。要想更好的将JavaScript运用在node上,解决这一缺陷至关重要。

CommonJS规范

经过多年的发展,为了解决JavaScript在服务端上暴露出来的种种缺陷,社区为JavaScript制定了相应的规范,其中CommonJS规范的提出算是最为重要的里程碑

CommonJS出发点

CommonJS规范的提出是为了解决JavaScript在后端的缺陷,如:

没有模块系统

标准库较少 ECMAscript仅仅定义了部分核心库,更多的是用在浏览器端。而服务器端的文件系统,I/O流等常见需求却没有标准的API。虽然W3C在推进ECMAscript的标准化,但它也仅限于浏览器端。

没有标准接口。 在JavaScript中,几乎没有定义过如web服务器或数据库之类的标准统一接口。

缺乏包管理系统。 这个问题导致了JavaScript应用中基本没有自动加载和安装依赖的能力。

CommonJS规范为Javascript开发大型应用指明了一条非常棒的道路,规范涵盖了模块、二进制、Buffer、字符集编码、I/O流、进程环境、文件系统、嵌套字、单元测试、Web服务器网关接口、包管理等。

模块规范

CommonJs对模块的定义主要分为:模块引用、模块定义、模块标识3个部分。

模块引用

var math = require('math')

在CommonJS规范中,存在require()方法,这个方法接受模块标识,引入一个模块的API到当前上下文中。

模块定义

// hello.js

exports.sayHello = function () {

console.log('hello world')

}

在另一个文件中,我们通过require()方法引入模块,就能调用定义的属性或变量了。

模块标识

模块标识其实就是传递给require()方法的参数,它必须是符合小驼峰命名的字符串或者以./、../开头的相对路径,或者绝对路径。它可以没有文件名后缀.js

CommonJS构建的这套模块导入和引入机制使得用户完全不必考虑变量污染。

node的模块实现

在node中引入模块,需要经历三个步骤

路径分析

文件定位

编译执行

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

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

而文件模块则是在运行时加载,需要完整的经历上面三个步骤,速度当然就比核心模块慢。

不论是核心模块还是文件模块,require()方法对相同的模块的二次加载都一律采用缓存优先的方式。

模块编译

我们知道每个模块文件中存在着require、exports、module这3个变量,但它们在模块文件中并没有定义,那么从何而来呢?并且在node的api文档中,我们知道每个模块中还有__filename,__dirname这两个变量的存在,它们又是从何而来呢?如果我们把直接定义模块的过程放在浏览器端,就会存在污染全局变量的情况

事实上,在编译过程中,node对获取的JavaScript文件内容进行了头尾包装。在头部添加了

(function (exports, require, module, __filename, __dirname){\n

在尾部添加了

\n})

一个正常的JavaScript文件会被包装成如下模样:

(function (exports, require, module, __filename, __dirname) {

var math = require('math)

exports.area = function (radius) {

return Math.PI * radius *radius

}

})

这样每个模块文件之间都进行了作用域隔离。包装之后的代码会通过vm原生模块的runInThisContext()方法执行,返回一个具体的funtion对象。最后将当前模块对象的exports属性、require()方法、module(模块对象自身),以及在文件定位中得到的完整文件路径和文件目录作为参数传递给这个function()执行。

这就是这些变量并没有定义在每个模块文件中却存在的原因。此外,也许有人会纠结为何存在exports的情况下,还存在module.exports。理想情况下,只要赋值给exports即可:

exports = function () {

// my class

}

但是通常都会得到一个失败的结果。其原因在于,exports对象是通过函数形参的方式传入的,直接赋值形参会改变形参的引用,但并不能改变作用域外的值。举个栗子:

var change = function (a) {

a = 100

console.log(a) // => 100

}

var a = 10

change(a)

console.log(a) // => 10

如果要达到require引入一个类的效果,我们应该赋值给module.exports对象。这个变通的方案不改变形参的引用。

包与NPM

node组织了自身的核心模块,也使得第三方文件模块可以有序地编写和使用。但是在第三方模块中,模块与模块之间仍然是分散在各地的,相互之间不能直接引用。而在模块之外,包和NPM则是将模块联系起来的一种机制。

CommonJS的包规范定义也十分简单,它由包结构和包描述文件两个部分组成。

包实际上是一个目录直接打包为.zip或tar.gz格式的文件,安装后解压还原为目录。完全符合CommonJS规范的包目录应该包含如下这些文件:

package.json:包描述文件

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

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

doc:用于存放文档的目录

test: 用于存放单元测试用例的目录。

前端模块规范

浏览器端通过网络加载代码,而服务器端从磁盘中加载,两者加载速度不在一个数量级上。纵观node的模块引入过程,几乎全是同步的。但如果前端模块也采用同步的方式来引入,那么用户体验上会出现很大的问题。UI在初始化过程中需要花费很多时间来等待脚本加载完成。

鉴于网络原因,CommonJS为后端JavaScript制定的规范并不完全适合前端的应用场景。经过一段争执之后,AMD规范最终在前端应用场景中胜出。

AMD规范

AMD规范是CommonJS模块规范的一个延申,它的模块定义如下:

define(id?, dependencies?, factory)

它的模块id和依赖是可选的,与Node模块相似的地方在于factory的内容就是实际代码的内容。下面的代码定义了一个简单的模块:

define(function () {

var exports = {}

exports.sayHello = function () {

alert('hello from module: ' + module.id)

return exports

}

})

不同之处在于AMD模块需要用define来明确定义一个模块,而在node实现中是隐式包装的,它们的目的是进行作用域隔离,仅在需要的时候被引入。另一个区别则是内容需要通过返回return的方式实现导出。

CMD规范

CMD规范是由国内的玉伯提出,与AMD规范主要区别在于定义模块和依赖引入部分。AMD需要在声明模块的时候指定所有的依赖,通过形参传递依赖到模块中。

define(['dep1', 'dep2'], function (dep1, dep2) {

return function () {}

})

与AMD模块规范相比,CMD模块更接近于node对CommonJS规范的定义:

define(factory)

在依赖部分,CMD支持动态引入,示例如下:

define(function (require, exports, module) {

// todo

})

require,exports和module通过形参传递给模块,在需要依赖模块时,随时调用require()引用即可。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值