Node.js内置了HTTP服务器支持(Node本身就是服务器),可以轻而易举地实现一个网站和服务器的组合,这和PHP等不一样,因为在使用PHP的时候,必须先搭建一个Apache或者nginx之类的HTTP服务器,然后通过服务器的模块加载或CGI调用,才能将 PHP 脚本的执行结果呈现给用户
而当使用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
线程是程序中一个单一的顺序控制流程,是程序的调度单位,在单个程序中同时运行多个线程完成不同的工作,称为多线程
线程执行中的磁盘读写、网络通信或数据库查询,统称为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%
同步读取文件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
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)事件,执行完毕后再返回事件循环,事件循环会检查事件队列中有没有未处理的事件,直到程序结束
开发具有一定规模的程序不可能只用一个文件,通常需要把各个功能拆分、封装,然后组合起来,模块正是为了实现这种方式而诞生的
在浏览器JavaScript 中,脚本模块的拆分和组合通常使用HTML 的script 标签来实现。
注:浏览器端,可以借助require.js或者sea.js来完成模块化开发
模块都是基于文件的,一个Node.js文件(JS文件、JSON文件或者编译过的C/C++文件)就是一个模块
NodeJS的模块机制参照了CommonJS规范的module规范
用于加载目标模块,获取目标模块的接口,返回目标模块的导出对象(module.exports)
接收一个模块名字符串作为参数
模块名可使用相对路径(./ 或者 ../),或者是绝对路径( / 或C:之类的盘符),模块名中的.js扩展名可以省略
var f1 = require('./foo');
var f2 = require('./foo.js');
在node.js中,模块大概可以分为核心模块(内置模块)和文件模块
被编译成二进制代码,引用的时候只需require模块表示符即可,不做路径解析,直接返回核心模块的导出对象
var http=require('http');
是指.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
我们所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
通过module对象可以访问到当前模块的一些相关信息,但最多的用途是替换当前模块的导出对象,改变访问接口
例如模块导出对象默认是一个普通对象,如果想改成一个函数的话,可以使用以下方式
module.exports = function () { };
注:module.exports是一个空对象{},仅仅用来声明接口
通过命令行参数传递给NodeJS以启动程序的模块成为主模块,主模块负责调度组成整个程序的其它模块完成工作
引入一个模块仿佛创建了一个对象,然后使用该对象的属性或方法,但是又有着本质区别,这是因为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两次而被初始化两次
注:记住——所有模块在执行过程中只初始化一次
所以,一个模块的加载过程基本如下图所示
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,并符合一定规范即可,当然,为了提高兼容性,还是建议严格遵守以上规范
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模块)
所以全局模式就是安装命令行程序
总而言之,当我们要把某个包作为工程运行时的一部分时,通过本地模式获取,如果要
在命令行下使用,则使用全局模式安装
通过使用 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
待续….
待续….