2.Node学习:特性、模块和包

一. 特性

1. 内置HTTP服务器

Node.js内置了HTTP服务器支持(Node本身就是服务器),可以轻而易举地实现一个网站和服务器的组合,这和PHP等不一样,因为在使用PHP的时候,必须先搭建一个Apache或者nginx之类的HTTP服务器,然后通过服务器的模块加载或CGI调用,才能将 PHP 脚本的执行结果呈现给用户

2.Node学习:特性、模块和包

而当使用Node.js 时,不用额外搭建一个HTTP 服务器,因为Node.js 本身就内建了一个

var http = require('http');
http.createServer(function(req,res){
	console.log("请求到达");
	res.writeHead('Content-Type','text/html');
	res.write('<p>Hello</p>');
	res.end('<p>Node.js</p>');
}).listen(3000);
console.log("success 127.0.0.1:3000");

这就建立了一个最简单的服务器,调用http模块,监听3000端口,并对所有请求答复同样的内容

注:当我们刷新浏览器的时候,会发现“请求到达”出现两次,这说明接收到了两次请求,这是因为除了对根目录的请求外,很多浏览器都会试图下载favicon图标,通过请求 http://localhost:3000/favicon.ico

2. 单线程、异步式I/O(非阻塞式I/O)

线程是程序中一个单一的顺序控制流程,是程序的调度单位,在单个程序中同时运行多个线程完成不同的工作,称为多线程

线程执行中的磁盘读写、网络通信或数据库查询,统称为I/O 操作,IO操作通常要耗费较长的时间,传统架构下会采用多线程模型,也就是为每一个业务逻辑提供一个线程,通过线程切换来弥补同步式I/O调用的时间开销

注:也就是,当某个线程进行I/O操作时,操作系统会剥夺这个线程的CPU 控制权,使其暂停执行,同时将控制权让给其他的工作线程(这种线程调度方式称为阻塞),当I/O 操作完毕时,操作系统将这个线程的阻塞状态解除,恢复其对CPU的控制权,令其继续执行。这种模式就是同步式I/O或阻塞式I/O

Node.js使用的是单线程模型,对于所有的I/O操作都采用异步式的请求方式,当线程遇到I/O 操作时,不会以阻塞的方式等待I/O 操作的完成或数据的返回,而只是将I/O 请求发送给操作系统,继续执行下一条语句。当操作系统完成I/O 操作时,以事件的形式通知执行I/O 操作的线程,线程会在特定时候处理这个事件。为了处理异步I/O,线程必须有事件循环,不断地检查有没有未处理的事件,依次予以处理

注:同步式I/O,一个线程只能处理一项任务,会造成阻塞,要想提高吞吐量必须增加线程,因此造成不必要的内存开销,同时因为线程的来回切换而导致了CPU的占用

注:异步式I/O,一个线程永远在执行计算操作,I/O操作不会造成阻塞,而是直接执行后续的语句,当I/O操作完成时,以回调函数的形式执行指定逻辑,因此线程的CPU 利用率永远是100%

3. 基于事件的回调函数

同步读取文件API

var fs = require('fs'); 
var data = fs.readFileSync('file.txt', 'utf-8'); 
console.log(data); 
console.log('end.'); 

运行结果如下所示

contents of the file
end. 

过程如下,读取文件(阻塞等待读取完成),将文件的内容作为函数的返回值赋给 data 变量并输出

异步读取文件API

var fs = require('fs'); 
fs.readFile('file.txt', 'utf-8', function(err, data) { 
	console.log(data); 
}); 
console.log('end.'); 

运行的结果如下:

end
Contents of the file

过程如下,读取文件(不等待读取完成),将读取的请求发送给系统,继而执行后续语句,执行完以后进入事件循环监听,当fs接收到I/O 请求完成的事件时,事件循环会主动调用回调函数来完成后续操作

注:一个恰当的比喻就是,我从网上买了一个东西,之后,我就干我自己的事情,当货物送到的时候,快递员才通知我去取

注:并不是所有的API 都提供了同步和异步版本,node.js不鼓励使用同步I/O

4. 事件

node.js所有的异步I/O 操作在完成时都会发送一个事件到事件队列,事件由EventEmitter 对象提供。前面提到的fs.readFile的回调函数就是是通过 EventEmitter 来实现的

var EventEmitter = require('events').EventEmitter ; 
var event = new EventEmitter(); 
event.on('some_event', function() { 
	console.log('sssss');   //注册事件监听器
}); 
setTimeout(function() { 
	event.emit('some_event');    //触发事件
}, 1000);   //1秒后控制台输出sssss

node.js程序由事件循环开始,到事件循环结束,所有的逻辑都是事件的回调函数,所以node.js始终在事件循环中,程序入口就是事件循环第一个事件的回调函数。事件的回调函数在执行的过程中,可能会发出I/O 请求或直接发射(emit)事件,执行完毕后再返回事件循环,事件循环会检查事件队列中有没有未处理的事件,直到程序结束

二. 模块(module)

开发具有一定规模的程序不可能只用一个文件,通常需要把各个功能拆分、封装,然后组合起来,模块正是为了实现这种方式而诞生的

在浏览器JavaScript 中,脚本模块的拆分和组合通常使用HTML 的script 标签来实现。

注:浏览器端,可以借助require.js或者sea.js来完成模块化开发

模块都是基于文件的,一个Node.js文件(JS文件、JSON文件或者编译过的C/C++文件)就是一个模块

NodeJS的模块机制参照了CommonJS规范的module规范

1. require()函数

用于加载目标模块,获取目标模块的接口,返回目标模块的导出对象(module.exports)

接收一个模块名字符串作为参数

模块名可使用相对路径(./ 或者 ../),或者是绝对路径( / 或C:之类的盘符),模块名中的.js扩展名可以省略

var f1 = require('./foo');
var f2 = require('./foo.js');

在node.js中,模块大概可以分为核心模块(内置模块)和文件模块

1) 核心模块(内置模块/原生模块)

被编译成二进制代码,引用的时候只需require模块表示符即可,不做路径解析,直接返回核心模块的导出对象

var http=require('http');

2) 文件模块(三种)

是指.js、.json、.node文件,在引用文件模块的时候要加上文件的路径

相对路径: . / 表示当前目录、 .. / 表示上级目录

绝对路径: / 表示当前盘符根目录或者C: (D: 、 E: 等)表示指定盘符根目录

注:与客户端css和js不同,node里当前目录用./表示

如果不加路径的话,则该模块要么是核心模块,要么是从一个node_modules文件夹加载,且加载模块的搜索路径如下

如果'/aaa/bbb/ccc/foo.js' 中的文件调用了 require('bar.js') ,node将依次在下面的位置进行搜索

/aaa/bbb/ccc/node_modules/bar.js

/aaa/bbb/node_modules/bar.js

/aaa/node_modules/bar.js

/node_modules/bar.js

为什么要这么做呢?这是因为当项目的子目录内的文件需要使用某个模块时,可以上溯到父目录,从而避免产生多余的副本

最后,NodeJS允许通过 NODE_PATH 环境变量来指定额外的模块搜索路径

NODE_PATH环境变量中包含一到多个目录路径(路径之间在Windows下使用";"分隔,在Linux下使用":"分隔)

NODE_PATH=/home/user/lib;/home/lib

当使用require('foo')的方式加载模块时,则NodeJS依次尝试以下路径

 /home/user/lib/foo.js
/home/lib/foo/bar.js

此外,对于json文件,可以使用以下方式加载和使用

var data = require('./data.json');

如果想让文件夹(包)作为模块(也要加路径,都则就会按照不加路径方式处理),则首先在文件夹的根目录下建立package.json文件,它标识了一个主模块。一个package.json中的内容可能如下:

{
	"name" : "some-library",  
	"main" : "./lib/some-library.js"   //主模块的位置
} 

注:npm获取的包通常就是这种方式来加载的

注:如果在这个目录下没有package.json文件,node将试图从这个目录下加载index.js或index.node

2. exports对象

我们所require()的模块不可能没有要求,必须要有导出对象

exports 是模块公开的接口,require 用于从外部获取一个模块的接口,即获取模块的 exports 对象

exports对象指向当前模块的导出对象(module.exports),用于导出公有方法和属性,通过require函数使用别的模块时,其实就是使用别的模块的导出对象(module.exports)

exports对象保存的实际上是一个指向module.exports对象的指针,等同在每个模块头部,有一行这样的命令

var exports = module.exports;

所以可以向exports对象添加方法

exports.area = function () {};

但是不能直接将exports变量指向一个函数,因为它切断了exports与module.exports之间的连接,导出的是module.exports,因此只能通过指定module.exports 来改变访问接口

exports = function (){ };  //错误
module.exports = function (){ };  //正确,但改变了暴露的接口

注:如果觉得exports与module.exports之间的区别很难分清,一个简单的处理方法,就是放弃使用exports,只使用module.exports

3. module对象

通过module对象可以访问到当前模块的一些相关信息,但最多的用途是替换当前模块的导出对象,改变访问接口

例如模块导出对象默认是一个普通对象,如果想改成一个函数的话,可以使用以下方式

module.exports = function () {  };

注:module.exports是一个空对象{},仅仅用来声明接口

4. 主模块

通过命令行参数传递给NodeJS以启动程序的模块成为主模块,主模块负责调度组成整个程序的其它模块完成工作

5. 模块初始化(单次加载)

引入一个模块仿佛创建了一个对象,然后使用该对象的属性或方法,但是又有着本质区别,这是因为require不会重复加载模块

一个模块中的JS代码仅在模块第一次被使用时执行一次,并在执行过程中初始化模块的导出对象。之后,缓存起来的导出对象被重复利用

注:无论调用多少次 require,获得的都是同一个导出对象的实例,所以require初次加载模块时候是阻塞的(初次加载之后会被缓存,所以加载之后就不是阻塞的了)

target.js内容如下

var i = 0;
function count() {
    return ++i;
}
exports.count = count;  //该模块定义了一个私有变量,并导出一个公有方法

main.js内容如下

var counter1 = require('./target');
var counter2 = require(''./target');
console.log(counter1.count());   // 1
console.log(counter2.count());   // 2
console.log(counter2.count());   // 3

输出1、2、3,可见target.js并没有因为被require两次而被初始化两次

注:记住——所有模块在执行过程中只初始化一次

所以,一个模块的加载过程基本如下图所示

2.Node学习:特性、模块和包

三. 包(packgae)

JS模块的基本单位是单个JS文件,而复杂些的模块由多个子模块组成

为了便于管理和使用,我们可以把由多个子模块组成的大模块称做包,并把所有子模块放在同一个目录里,用于发布、更新、依赖管理和版本控制

注:包就是代码组织和部署的一种依托方式,在模块的基础上提供了更高层次的抽象

在组成一个包的所有子模块中,需要有一个入口模块(主模块),入口模块的导出对象被作为包的导出对象

例如,cat目录定义了一个包,其中包含3个子模块,head.js、body.js、main.js,其中main.js作为入口模块

var head = require('./head);
var body = require(''./body);

在其它模块里使用包的时候,只需require('/xxx/xxx/xxx/cat/main')即可

但是入口模块名称出现在路径里并不好,因为我们要让包看起来像是整个模块

require('/xxx/xxx/xxx/cat');

此时,我们可以将主模块命名为index.js 或 index.node,这样处理后,就只需要把包目录路径传递给require函数,感觉上整个目录被当作单个模块使用,更有整体感

如果想自定义入口模块的文件名和存放位置,就需要在包目录下创建一个package.json文件,并在其中指定入口模块(main字段)的路径,这样就可以直接require包名了,Node.js会根据package.json文件
的main字段自动寻找主模块

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 规范)

bin文件夹:存放二进制文件

doc文件夹:存放文档

lib文件夹:存放JavaScript代码

node_modules文件夹:存放第三方包

test文件夹:存放测试用例

package.json文件:元数据文件

README.md文件:说明文件

注:Node.js对包的要求没有上面这么严格,只要包含package.json,并符合一定规范即可,当然,为了提高兼容性,还是建议严格遵守以上规范

1. NPM(Node包管理器)

npm(Node Package Manager)即node包管理器(通常随Node.js自动安装),用于Node.js包的下载,发布、传播、依赖控制

允许用户从NPM服务器下载别人编写的三方包到本地使用

允许用户从NPM服务器下载并安装别人编写的命令行程序到本地使用

允许用户将自己编写的包或命令行程序上传到NPM服务器供别人使用

可以使用命令

npm -v

来测试是否成功安装

代理设置(如果需要的话)

npm config set proxy=http://127.0.0.1:8087     //设为自己可用的代理即可
npm config set registry=http://registry.npmjs.org

下载第三方包(本地模式)

npm install package_name  
npm     i   package_name  

本地模式下载的包在命令运行目录的node_modules文件夹下

注:npm 在获取包的时候还将自动解析其依赖,并获取其依赖的包(package.json的作用)此后,只需require(“package_name”)即可,无需指定第三方包的路径(路径解析规则见前)

npm install package_name  --save

这样的话,不仅安装了指定的包,还将该包的依赖加入到了package.json的依赖列表中

注:这个 --save 参数,是Node.js中共有的,安装所有模块时,都可以这样使用,功能同上

注:使用 npm info 模块名 version 可以查看该模块的最新版本

下载第三方包(命令行工具,全局模式)

npm install –g  package_name 
npm    i    –g  package_name

全局模式下载的包在C:\Users\用户名\AppData\Roaming\npm\node_modules下

全局模式目的并不是让其它模块引用该包(不能被 require 获得),其最重要的是注册 PATH 环境变量(本地模式不会),从而能在命令行里使用(例如supervisor模块)

所以全局模式就是安装命令行程序

本地or全局

总而言之,当我们要把某个包作为工程运行时的一部分时,通过本地模式获取,如果要
在命令行下使用,则使用全局模式安装

本地&&全局

通过使用 npm link package_name 可以本地包和全局包之间创建符号链接,通过这种方法,我们就可以把全局包当本地包来使用了

除了将全局的包链接到本地以外,使用 npm link命令还可以将本地的包链接到全局。使用方法是在包目录(package.json 所在目录)中运行npm link 命令

npm link 命令不支持Windows

指定版本号

以上方式默认下载最新版第三方包,可以使用 @ 指定要下载的版本

npm  install  [email protected] (指定版本)
npm  install  express@4 (指定区间的最新版本)
npm  install  [email protected] (指定区间的最新版本)
npm  install  [email protected] (指定区间的最新版本)

当使用的第三方包很多时,以上方式会很繁琐,因此NPM对package.json做了扩展,允许在其中申明三方包依赖

{
	“name” : ”hello world” ,
	“main” : “./echo.js”,
	“dependencies” : {
		“express” : “4.2.0”,
	  	“argv” : “0.0.2”
	}
}

这样,在当前目录下,就可以使用 npm install 命令批量安装第三方包了

在package.json中,版本号表示方式略有不同

“express” : ” * ”     指定最新版本
“express” : ”4.2.0”   指定具体版本
“express” : ”~4.2.0”  指定最低版本
…. 

注:更重要的是,当这个包被别的包使用时,仍然会根据依赖关系进一步下载第三方包,这种关系使得我们可以直接使用三方包,不需要自己去解决所有包的依赖关系

版本号分为X.Y.Z三位(主版本号、次版本号、补丁版本号)

如果只是修复bug,需要更新Z位

如果是新增了功能,但是向下兼容,需要更新Y位

如果有大变动,向下不兼容,需要更新X位

移除包

本地安装的模块通过

npm uninstall package_name

全局安装的模块通过

npm uninstall –g package_name

升级包

本地安装的模块通过

npm update package_name

全局安装的模块通过

npm update –g package_name

发布包

待续….

四. 调试

待续….

你可能感兴趣的:(node)