Node.js 模块和包(Modules)

模块(Module)和包(Package)是 Node.js 最重要的支柱。开发一个具备必定规模的程序不可能只用一个文件,一般须要把各个功能拆分、封装,而后组合起来,模块正是为了实现这种方式而诞生的。在浏览器 Javascript 中,脚本模块的拆分和组合一般使用 HTML 的 script 标签来实现。Node.js 提供了 require 函数来调用其它模块,并且模块都是基于文件的。
html


Node.js 的模块和包常常被相提并论,由于模块和包是没有本质区别的,两个概念也时常混用。若是要辨析,那么能够把包理解成是实现某个功能模块的集合,用于发布和维护。对使用者来讲,模块和包的区别是透明的,所以常常不作区分。
node


一、什么是模块
git

模块是 Node.js 应用程序的基本组成部分,文件和模块是一一对应的。换言之,一个 Node.js 文件就是一个模块,这个文件多是 Javascript 代码、JSON 或者编译过的 C/C++ 扩展。github


例如 var http = require(' http '),其中,http 是 Node.js 的一个核心模块,其内部是用 C++ 实现的,外部用 Javascript 封装。咱们经过 require 函数获取了这个模块,而后才能使用其中的对象。
web




二、建立及加载模块express


2-一、建立模块npm

在 Node.js 中,建立一个模块很是简单,由于一个文件就是一个模块,例如:
编程

var hello = require('./hello');
hello.world();


上例中,代码 require(' ./hello ') 引入了当前目录下的 hello.js 文件(./ 表示当前目录,Node.js 默认后缀为 js)
json


Node.js 提供了两个 exports 和 require 两个对象,其中,exports 是模块公开的接口,require 用于从外部获取一个模块的接口,即获取模块的 exports 对象。数组


经过一个例子简单的了解下模块,建立一个 module.js 文件,代码以下:

var name;

exports.setName = function( uname ){
    name = uname;
};

exports.sayHello = function(){
    console.log('Hello' +name);
};


在同一目录下建立 getmoudle.js 文件,代码以下:

//引用当前目录下的 module.js 模块
var myModule = require('./module');

myModule.setName('Roger');
myModule.sayHello();


运行 node getmodule.js,控制台输出结果为:

wKiom1h1mUjRXUbJAAAIGe_mq3Y049.png


在这个例子中,module.js 经过 exports 对象 setName 和 sayHello 做为模块的访问接口,在 getmodule.js 中经过 require(' ./module ') 加载这个模块,而后就能够直接访问 module.js 中 exports 对象的成员函数了。


这种接口封装方式比许多语言要简洁的多,同时也不失优雅,未引入违反语义的特性,符合传统的编程逻辑。在这个基础上,咱们能够构建大型的应用程序,npm 提供的上万个模块都是经过这种方式搭建起来的。


2-二、单次加载

上面这个例子有点相似于建立一个对象,但实际上和对象又有本质的区别,由于 require 不会重复加载模块,也就是说不管调用多少次 require,得到的模块都是同一个。


新建一个 loadmoudle.js 文件,代码以下:

var hello1 = require('./module');
hello1.setName('Roger');

var hello2 = require('./module');
hello2.setName('Sarahling');

hello1.sayHello();


运行 node loadmodule.js,控制台输出结果为:

wKiom1h1nTvCxY5bAAAIX4PGt00812.png


为何不是“Hello Roger”呢?这是由于变量 hello1 和 hello2 指向的是同一个实例,所以 hello1.setName 的结果被 hello2.setName 覆盖,最终输出结果是由后者决定的。


2-三、覆盖 exports

有时候咱们只是想把一个对象封装到模块中,例如,新建一个 singleObject.js 文件,代码以下:

function Hello(){
    var name;
    this.setName = function(uname){
        name = uname;
    };

    this.sayHello = function(){
        console.log('Hello '+name);
    }
}

module.exports = Hello;


新建一个 getHello.js 文件,得到这个对象:

var Hello = require('./singleObject');

hello = new Hello();
hello.setName('Bob');
hello.sayHello();


运行 node getHello.js,控制台输出结果为:

wKioL1h1ovqDCl5tAAAHmhFLbQU352.png

注意,模块接口的惟一变化是使用 module.exports = Hello 代替了 exports.Hello = Hello。在外部引用模块时,其接口对象就是要输出的 Hello 对象自己,而不是原先的 exports。


事实上,exports 自己仅仅是一个普通的空对象,即{},它专门用来声明接口,本质上是经过它为模块闭包的内部创建一个有限的访问接口。由于它没有任何特殊的地方,因此可用其它东西来代替。


注意:不能够经过对 exports 直接赋值代替对 module.exports 赋值。exports 实际上只是一个和 module.exports 指向同一个对象的变量,它自己会在模块执行结束后释放,但 module 不会,所以只能经过指定 module.exports 来改变访问接口。



三、服务端的模块放在哪里


在 Node.js 初识 HTTP 模块 介绍中,咱们就已经见到了模块的使用,像这样

//第一步: 引入 http 模块
var http = require('http');
 
//第二步:建立一个服务器(requestListener 是一个函数,里面有2个参数,一个请求消息,一个响应消息)
var server = http.createServer(function(req, res){
 
});
 
//第三步:服务器监听本地的82端口
server.listen(8082, '127.0.0.1');


Node.js 中自带了一个叫作 http 的模块,咱们在代码中请求它并把返回值赋值给一个本地变量,这把咱们的本地变量变成了一个拥有全部 http 模块所提供的公共方法的对象。


Node.js 的 require 方法中文件查找策略以下:

因为 Node.js 中存在 4 类模块(原生模块和 3种文件模块),尽管 require 方法及其简单,可是内部的加载倒是十分复杂的,其加载优先级也各自不一样

wKioL1h1qJeRYNAfAAEd0EGAyCs470.png


3-一、从文件模块缓存中加载

尽管原生模块与文件模块的优先级不一样,可是都不会优先于从文件模块的缓存中加载已经存在的模块。


3-二、从原生模块加载

原生模块的优先级仅次于文件模块缓存的优先级。require方法在解析文件名以后,优先检查模块是否在原生模块列表中。以http模块为例,尽管在目录下存在一个http/http.js/http.node/http.json文件,require("http")都不会从这些文件中加载,而是从原生模块中加载。


原生模块也有一个缓存区,一样也是优先从缓存区加载。若是缓存区没有被加载过,则调用原生模块的加载方式进行加载和执行。


3-三、从文件加载

当文件模块缓存中不存在,并且不是原生模块的时候,Node.js会解析require方法传入的参数,并从文件系统中加载实际的文件,加载过程当中的包装和编译细节在前一节中已经介绍过,这里咱们将详细描述查找文件模块的过程,其中,也有一些细节值得知晓。


require方法接受如下几种参数的传递:

http、fs、path等,原生模块。

./mod或../mod,相对路径的文件模块。

/pathtomodule/mod,绝对路径的文件模块。

mod,非原生模块的文件模块。




四、建立包


包是在模块基础上更深一步的抽象,Node.js 的包相似于 C/C++ 的函数库或者 Java/.Net的类库。它将某个独立的功能封装起来,用于发布、更新、依赖管理和版本控制。Node.js 根据 CommonJS 规范实现了包机制,开发了 npm来解决包的发布和获取需求。


Node.js 的包是一个目录,其中包含一个 JSON 格式的包说明文件 package.json。严格符合CommonJS 规范的包应该具有如下特征:


package.json 必须在包的顶层目录下;

二进制文件应该在 bin 目录下;

JavaScript 代码应该在 lib 目录下;

文档应该在 doc 目录下;

单元测试应该在 test 目录下。


Node.js 对包的要求并无这么严格,只要顶层目录下有 package.json,并符合一些规范便可。固然为了提升兼容性,咱们仍是建议你在制做包的时候,严格遵照 CommonJS 规范。


4-一、做为文件夹的模块

模块与文件是一一对应的。文件不只能够是 JavaScript 代码或二进制代码,还能够是一个文件夹。最简单的包,就是一个做为文件夹的模块。下面咱们来看一个例子,创建一个叫作 somepackage 的文件夹,在其中建立 index.js,内容以下:

exports.hello = function(){
    console.log('Hello World!');
}


而后在 somepackage 以外创建 getpackage.js,内容以下:

var somePackage = require('./somepackage');

somePackage.hello();


运行 getpackage.js,控制输出结果以下:

wKiom1h1q1zyJvtRAACBpoebTbw745.png

咱们使用这种方法能够把文件夹封装为一个模块,即所谓的包。包一般是一些模块的集合,在模块的基础上提供了更高层的抽象,至关于提供了一些固定接口的函数库。经过定制package.json,咱们能够建立更复杂、更完善、更符合规范的包用于发布。



4-二、package.json

在 somepackage 文件夹下,建立一个叫作 package.json 的文件,内容以下所示:

{
  "main" : "./lib/interface.js"
}


而后将 index.js 重命名为 interface.js 并放入 lib 子文件夹下。以一样的方式再次调用这个包,依然能够正常使用。

wKiom1h1rP6QIrxWAAB_HJPABs4737.png


Node.js 在调用某个包时,会首先检查包中 package.json 文件的 main 字段,将其做为包的接口模块,若是 package.json 或 main 字段不存在,会尝试寻找 index.js 或 index.node 做为包的接口。


package.json 是 CommonJS 规定的用来描述包的文件,彻底符合规范的 package.json 文件应该含有如下字段。

name:包的名称,必须是惟一的,由小写英文字母、数字和下划线组成,不能包含空格。

description:包的简要说明。

version:符合语义化版本识别规范的版本字符串。

keywords:关键字数组,一般用于搜索。

maintainers:维护者数组,每一个元素要包含 name、email (可选)、web (可选)字段。

contributors:贡献者数组,格式与maintainers相同。包的做者应该是贡献者数组的第一个元素。

bugs:提交bug的地址,能够是网址或者电子邮件地址。

licenses:许可证数组,每一个元素要包含 type (许可证的名称)和 url (连接到许可证文本的地址)字段。

repositories:仓库托管地址数组,每一个元素要包含 type (仓库的类型,如 git )、url (仓库的地址)和 path (相对于仓库的路径,可选)字段。

dependencies:包的依赖,一个关联数组,由包名称和版本号组成。


下面是一个彻底符合 CommonJS 规范的 package.json 示例:

{
  "name": "mypackage",
  "description": "Sample package for CommonJS. This package demonstrates the required
  elements of a CommonJS package.",
  "version": "0.7.0",
  "keywords": [
    "package",
    "example"
  ],
  "maintainers": [
    {
      "name": "Bill Smith",
      "email": "bills@example.com",
    }
  ],
  "contributors": [
    {
      "name": "BYVoid",
      "web": "http://www.byvoid.com/"
    }
  ],
  "bugs": {
    "mail": "dev@example.com",
    "web": "http://www.example.com/bugs"
  },
  "licenses": [
    {
      "type": "GPLv2",
      "url": "http://www.example.org/licenses/gpl.html"
    }
  ],
  "repositories": [
    {
      "type": "git",
      "url": "http://github.com/BYVoid/mypackage.git"
    }
  ],
  "dependencies": {
    "webkit": "1.2",
    "ssl": {
      "gnutls": ["1.0", "2.0"],
      "openssl": "0.9.8"
    }
  }
}



4-三、Node.js 包管理器

Node.js包管理器,即npm是 Node.js 官方提供的包管理工具,它已经成了 Node.js 包的标准发布平台,用于 Node.js 包的发布、传播、依赖控制。npm 提供了命令行工具,使你能够方便地下载、安装、升级、删除包,也可让你做为开发者发布并维护包。


(1)、获取一个包

使用 npm 安装包的命令格式为:

npm [install/i] [package_name]


好比你要安装 express,能够在命令行运行:

$ npm install express

$ npm i express


能够看到安装信息以下:

wKiom1h1wsWDHNThAAA1wp9nzG4572.png

wKioL1h1w2SgZKyeAABd3MoQzh8671.png


此时 express 就安装成功了,而且放置在当前目录的 node_moudles 子目录下。npm 在获取 express 的时候还将自动解析其依赖,并获取 express 依赖的 mime、mkdirp、qs 和 connect。


(2)、本地模式 和 全局模式

npm 在默认状况下会从 https://www.npmjs.com/   搜索或下载包,将包安装到当前目录的 node_moudles 子目录下。


在使用 npm 安装包的时候,有两种模式:本地模式和全局模式。默认状况下咱们使用 npm install 命令就是采用本地模式,即把包安装到当前目录的 node_modules 子目录下。Node.js 的 require 在加载模块时会尝试搜寻 node_modules 子目录,所以使用 npm 本地模式安装的包能够直接被引用。


npm 还有另外一种不一样的安装模式被成为全局模式,使用方法为:

npm [install/i] -g [package_name]


与本地模式的不一样之处就在于多了一个参数 -g。


为何要使用全局模式呢?多数时候并非由于许多程序都有可能用到它,为了减小多重副本而使用全局模式,而是由于本地模式不会注册 PATH 环境变量。举例说明,咱们安装supervisor 是为了在命令行中运行它,譬如直接运行 supervisor script.js,这时就须要在 PATH环境变量中注册 supervisor。npm 本地模式仅仅是把包安装到 node_modules 子目录下,其中 的 bin 目录没有包含在 PATH 环境变量中,不能直接在命令行中调用。而当咱们使用全局模式安装时,npm 会将包安装到系统目录,譬如 /usr/local/lib/node_modules/,同时 package.json 文件中 bin 字段包含的文件会被连接到 /usr/local/bin/。/usr/local/bin/ 是在PATH 环境变量中默认定义的,所以就能够直接在命令行中运行 supervisor script.js命令了。


本地模式和全局模式的特色以下表所示:

模式 可经过 require 使用
注册PATH
本地模式
全局模式


总而言之,当咱们要把某个包做为工程运行时的一部分时,经过本地模式获取,若是要 在命令行下使用,则使用全局模式安装。


(3)、建立全局连接

npm 提供了一个有趣的命令 npm link(不支持 Windows),它的功能是在本地包和全局包之间建立符号连接。咱们说过使用全局模式安装的包不能直接经过 require 使用,但经过 npm link命令能够打破这一限制。举个例子,咱们已经经过 npm install -g express 安装了 express,这时在工程的目录下运行命令:

$ npm link express
./node_modules/express -> /usr/local/lib/node_modules/express


咱们能够在 node_modules 子目录中发现一个指向安装到全局的包的符号连接。经过这 种方法,咱们就能够把全局包当本地包来使用了。


除了将全局的包连接到本地之外,使用 npm link命令还能够将本地的包连接到全局。使用方法是在包目录( package.json 所在目录)中运行 npm link 命令。若是咱们要开发一个包,利用这种方法能够很是方便地在不一样的工程间进行测试。



(4)、包的发布

npm 能够很是方便地发布一个包,比 pip、gem、pear 要简单得多。在发布以前,首先须要让咱们的包符合 npm 的规范,npm 有一套以 CommonJS 为基础包规范,但与 CommonJS并不彻底一致,其主要差异在于必填字段的不一样。经过使用 npm init 能够根据交互式问答产生一个符合标准的 package.json,例如建立一个名为 byvoidmodule 的目录,而后在这个目录中运行npm init:


wKiom1h1yJjhtEIzAABWVNPd3fg872.png

这样就在 byvoidmodule 目录中生成一个符合 npm 规范的 package.json 文件。建立一个index.js 做为包的接口,一个简单的包就制做完成了。


在发布前,咱们还须要得到一个帐号用于从此维护本身的包,使用 npm adduser 根据提示输入用户名、密码、邮箱,等待帐号建立完成。完成后可使用 npm whoami 测验是否已经取得了帐号。


接下来,在 package.json 所在目录下运行 npm publish,稍等片刻就能够完成发布了。


打开浏览器,访问 http://search.npmjs.org/ 就能够找到本身刚刚发布的包了。如今咱们能够在世界的任意一台计算机上使用 npm install byvoidmodule 命令来安装它。下图是npmjs.org上包的描述页面。

wKioL1h1yarTnddAAAEwRhMWehI250.png


若是你的包未来有更新,只须要在 package.json 文件中修改 version 字段,而后从新使用 npm publish 命令就好了。若是你对已发布的包不满意(好比咱们发布的这个毫无心义的包),可使用 npm unpublish 命令来取消发布。

相关文章
相关标签/搜索