1 node 简介
1 什么是Node.js?
是一个可以让 JavaScript 运行在服务器端的平台。
它可以让JavaScript 脱离浏览器的束缚运行在服务器环境下,就像运行 Python、 Perl、 PHP、 Ruby程序一样。
你可以用 Node.js 轻松地进行服务器端应用开发, Python、 Perl、 PHP、 Ruby 能做的事情 Node.js 几乎都能做,而且可以做得更好。
Node.js 是一个为实时Web(Real-time Web)应用开发而诞生的平台,它从诞生之初就充分考虑了在实时响应、超大规模数据要求下架构的可扩展性。
这使得它摒弃了传统平台依靠多线程来实现高并发的设计思路,而采用了单线程、异步式I/O、事件驱动式的程序设计模型。
这些特性不仅带来了巨大的性能提升,还减少了多线程程序设计的复杂性,进而提高了开发效率。
Node.js 有着强大而灵活的包管理器(node package manager, npm),目前已经有上万个第三方模块,其中有网站开发框架,有 MySQL、 MongoDB等数据库接口,有模板语言解析、 CSS 生成工具、加密、调试支持,甚至还有图形用户界面和操作系统 API工具。
Node.js 可以作为服务器向用户提供服务,与 PHP、 Python、 Ruby on Rails 相比,它跳过了 Apache、 Nginx 等 HTTP服务器,直接面向前端开发。
Node.js 中所谓的 JavaScript 只是 Core JavaScript,或者说是 ECMAScript 的一个实现,不包含 DOM、 BOM 或者 Client JavaScript。
这是因为 Node.js 不运行在浏览器中,所以不需要使用浏览器中的许多特性。
历史上将 JavaScript移植到浏览器外的计划不止一个,但Node.js 是最出色的一个。
随着 Node.js 的成功,各种浏览器外的 JavaScript 实现逐步兴起,因此产生了 CommonJS 规范。
CommonJS 试图拟定一套完整的 JavaScript 规范,以弥补普通应用程序所需的 API,譬如文件系统访问、命令行、模块管理、函数库集成等功能。
Node.js 的部份实现遵循了CommonJS规范,但由于两者还都处于诞生之初的快速变化期,也会有不一致的地方。
Node.js 的 JavaScript 引擎是 V8,来自 Google Chrome 项目。
Node.js 不运行在浏览器中,所以也就不存在 JavaScript 的浏览器兼容性问题,你可以放心地使用 JavaScript 语言的所有特性。
正如 JavaScript 为客户端而生, Node.js 为网络而生。
Node.js能做什么?
Node.js 能做的远不止开发一个网站那么简单,使用 Node.js,你可以轻松地开发:
- 具有复杂逻辑的网站;
- 基于社交网络的大规模 Web 应用;
- Web Socket 服务器;
- TCP/UDP 套接字应用程序;
- 命令行工具;
- 交互式终端程序;
- 带有图形用户界面的本地应用程序;
- 单元测试工具;
- 客户端 JavaScript 编译器。
Node.js 内建了 HTTP 服务器支持,也就是说你可以轻而易举地实现一个网站和服务器的组合。
这个服务器不仅可以用来调试代码,而且它本身就可以部署到产品环境,它的性能足以满足要求。
Node.js 还可以部署到非网络应用的环境下,比如一个命令行工具。
Node.js 还可以调用C/C++ 的代码,这样可以充分利用已有的诸多函数库,也可以将对性能要求非常高的部分用C/C++ 来实现。
2 异步式 I/O 与事件驱动
对于高并发的解决方案,传统的架构是多线程模型,也就是为每个业务逻辑提供一个系统线程,通过系统线程切换来弥补同步式 I/O 调用时的时间开销。
Node.js 使用的是单线程模型,对于所有 I/O 都采用异步式的请求方式,避免了频繁的上下文切换。
Node.js 在执行的过程中会维护一个事件队列,程序在执行时进入事件循环等待下一个事件到来,每个异步式 I/O 请求完成后会被推送到事件队列,等待程序进程进行处理。
例如,对于简单而常见的数据库查询操作,按照传统方式实现的代码如下:
res = db.query('SELECT * from some_table');
res.output();
以上代码在执行到第一行的时候,线程会阻塞,等待数据库返回查询结果,然后再继续处理。
对于高并发的访问,一方面线程长期阻塞等待,另一方面为了应付新请求而不断增加线程,因此会浪费大量系统资源,同时线程的增多也会占用大量的 CPU 时间来处理内存上下文切换,而且还容易遭受低速连接攻击。
看看Node.js是如何解决这个问题的:
db.query('SELECT * from some_table', function(res) { res.output(); });
这段代码中 db.query 的第二个参数是一个函数,我们称为回调函数。
进程在执行到db.query 的时候,不会等待结果返回,而是直接继续执行后面的语句,直到进入事件循环。
当数据库查询结果返回时,会将事件发送到事件队列,等到线程进入事件循环以后,才会调用之前的回调函数继续执行后面的逻辑。
Node.js 进程在同一时刻只会处理一个事件,完成后立即进入事件循环检查并处理后面的事件。
这样做的好处是,CPU 和内存在同一时间集中处理一件事,同时尽可能让耗时的 I/O 操作并行执行。
如下图:
3 CommonJS 规范与实现
正如当年为了统一 JavaScript 语言标准,人们制定了 ECMAScript 规范一样,如今为了统一 JavaScript 在浏览器之外的实现, CommonJS 诞生了。
CommonJS 试图定义一套普通应用程序使用的API,从而填补 JavaScript 标准库过于简单的不足。
CommonJS 的终极目标是制定一个像 C++ 标准库一样的规范,使得基于 CommonJS API 的应用程序可以在不同的环境下运行,就像用 C++ 编写的应用程序可以使用不同的编译器和运行时函数库一样。
为了保持中立, CommonJS 不参与标准库实现,其实现交给像 Node.js 之类的项目来完成。
CommonJS 规范包括了模块(modules)、包(packages)、系统(system)、二进制(binary)、控制台(console) 、编码(encodings) 、文件系统(filesystems) 、套接字(sockets) 、单元测试(unit testing)等部分。
Node.js 是目前 CommonJS 规范最热门的一个实现,常见的还有mongodb等。
4 node 的 REPL 模式
REPL (Read-eval-print loop),即输入—求值—输出循环。
5 一个简单的hello程序
在C:\node-demo下创建一个hello.js文件。
文件内容很简单,就一行代码:
console.log('Hello Node.js');
然后进入cmd:
6 建立HTTP服务器
在成功运行 PHP 之前先要配置一个功能强大而复杂的 HTTP服务器,譬如 Apache、 IIS 或 Nginx,还需要将 PHP 配置为 HTTP 服务器的模块,
或者使用FastCGI 协议调用 PHP 解释器。这种架构是“浏览器 - HTTP 服务器 - PHP 解释器”的组织方式,而Node.js采用了一种不同的组织方式。
Node.js 将“ HTTP服务器”这一层抽离,直接面向浏览器用户。
现在我们创建一个app.js,内容如下:
//调用Node.js提供的http模块 var http = require('http'); //对所有HTTP请求答复同样的内容并监听3000端口。 http.createServer(function(req, res) { res.writeHead(200, {'Content-Type': 'text/html'}); res.write('<h1>Node.js</h1>'); res.end('<p>Hello World</p>'); }).listen(3000); console.log("HTTP server is listening at port 3000.");
接下来运行 node app.js命令,打开浏览器访问 http://127.0.0.1:3000,即可看到下图所示的内容。
Content-Type,用于定义网络文件的类型和网页的编码,决定文件接收方将以什么形式、什么编码读取这个文件。
7 理解异步 - 回调函数
让我们看看在 Node.js 中如何用异步的方式读取一个文件,下面是一个例子:
var fs = require('fs'); fs.readFile('file.txt', 'utf-8', function(err, data) { if (err) { console.error(err); } else { console.log(data); } }); console.log('end.');
在C:\node-demo目录下创建一个file.txt文件,然后运行node:
异步式读取文件就稍微有些违反直觉了, end.先被输出。
fs.readFile 接收了三个参数,第一个是文件名,第二个是编码方式,第三个是一个函数,我们称这个函数为回调函数。
fs.readFile 调用时所做的工作只是将异步式 I/O 请求发送给了操作系统,然后立即返回并执行后面的语句,执行完以后进入事件循环监听事件。当 fs 接收到 I/O 请求完成的
事件时,事件循环会主动调用回调函数以完成后续工作。因此我们会先看到 end.,再看到file.txt 文件的内容。
8 事件
Node.js 所有的异步 I/O 操作在完成时都会发送一个事件到事件队列。在开发者看来,事件由 EventEmitter 对象提供。
下面我们用一个简单的例子说明 EventEmitter的用法:
var EventEmitter = require('events').EventEmitter; var event = new EventEmitter(); //定义some_event监听器 event.on('some_event', function() { console.log('some_event occured.'); }); //在1000毫秒以后向event对象发送事件some_event setTimeout(function() { event.emit('some_event'); }, 1000);
运行这段代码, 1秒后控制台输出了 some_event occured.。
9 模块
模块(Module)和包(Package)是 Node.js 最重要的支柱。
开发一个具有一定规模的程序不可能只用一个文件,通常需要把各个功能拆分、封装,然后组合起来,模块正是为了实现这种方式而诞生的。
在浏览器 JavaScript 中,脚本模块的拆分和组合通常使用 HTML 的script 标签来实现。
Node.js 提供了require 函数来调用其他模块,而且模块都是基于文件的,机制十分简单。
模块和包有什么区别?
我们经常把 Node.js 的模块和包相提并论,因为模块和包是没有本质区别的,两个概念也时常混用。
如果要辨析,那么可以把包理解成是实现了某个功能模块的集合,用于发布和维护。对使用者来说,模块和包的区别是透明的,因此经常不作区分。
一个Node.js 文件就是一个模块,这个文件可能是 JavaScript 代码、 JSON 或者编译过的 C/C++ 扩展。
var http = require('http')
其中 http是 Node.js 的一个核心模块,其内部是用 C++ 实现的,外部用 JavaScript 封装。我们通过require 函数获取了这个模块,然后才能使用其中的对象。
10 创建和加载模块
在C:\node-demo目录下创建一个module.js 的文件,内容是:
var name; exports.setName = function(thyName) { name = thyName; }; exports.sayHello = function() { console.log('Hello ' + name); };
在同一目录下创建 getmodule.js,内容是:
var myModule = require('./module'); myModule.setName('larry'); myModule.sayHello();
运行node getmodule.js,结果是:
Hello larry
在以上示例中,module.js 通过 exports 对象把 setName 和 sayHello 作为模块的访问接口,在 getmodule.js 中通过require('./module') 加载这个模块,然后就可以直接访问 module.js 中 exports 对象的成员函数了。
npm 提供的上万个模块都是通过这种简单的方式搭建起来的。
另一种方式:
module.js如下:
function Hello() { var name; this.setName = function (thyName) { name = thyName; }; this.sayHello = function () { console.log('Hello ' + name); }; }; module.exports = Hello;
getModule.js如下:
var Hello = require('./module'); hello = new Hello(); hello.setName('larry'); hello.sayHello();
11 创建包
包是在模块基础上更深一步的抽象, Node.js 的包类似于 C/C++ 的函数库或者 Java/.Net的类库。
它将某个独立的功能封装起来,用于发布、更新、依赖管理和版本控制。 Node.js 根据 CommonJS 规范实现了包机制,开发了 npm来解决包的发布和获取需求。
现在我们创建一个最简单的包:
先建立一个叫做 somepackage 的文件夹,在其中创建 index.js,
//somepackage/index.js
exports.hello = function() { console.log('Hello.'); };
然后在 somepackage 之外建立 getpackage.js,内容如下:
//getpackage.js
var somePackage = require('./somepackage'); somePackage.hello();
运行 node getpackage.js,控制台将输出结果 Hello.。
我们使用这种方法可以把文件夹封装为一个模块,即所谓的包。
包通常是一些模块的集合,在模块的基础上提供了更高层的抽象,相当于提供了一些固定接口的函数库。
通过定制package.json,我们可以创建更复杂、更完善、更符合规范的包用于发布。
Node.js 在调用某个包时,会首先检查包中 package.json 文件的 main 字段,将其作为包的接口模块,
如果 package.json 或 main 字段不存在,会尝试寻找 index.js 或 index.node 作为包的接口。
12 本地模式 - 全局模式
默认情况下我们使用 npminstall命令就是采用本地模式,即把包安装到当前目录的 node_modules 子目录下。
Node.js的 require 在加载模块时会尝试搜寻 node_modules 子目录,因此使用 npm 本地模式安装的包可以直接被引用。
npm 还有另一种不同的安装模式被成为全局模式,使用方法为:
npm [install/i] -g [package_name]
与本地模式的不同之处就在于多了一个参数 -g。
为什么要使用全局模式呢?
1 许多程序都有可能用到它,为了减少多重副本而使用全局模式。
2 因为本地模式不会注册 PATH 环境变量,需要在命令行中运行时不方便。
本文主要内容来自:Node.js开发指南