Nodejs 结构和创建

本文参考自 《Nodejs 开发指南》 和 《Nodejs 实战》 两本书。

异步 I/O

Node.js 最大的特点就是采用了异步 I/O 与事件驱动的架构设计。

$.post('/resource.json', function(data){
    console.log(data);
})

这一小段代码是 jQuery 中的 Ajax 请求,我们知道这个结果函数是异步的,等服务器响应结果后才会执行。

对于一些高并发的需求,传统的解决方法是多线程模型,每个业务提供一个线程系统,通过切换和调度线程来实现高并发中的等待开销。但是这样做无疑又增加了线程管理和调度的开销。

Nodejs 采用的是单线程模型,对于所有的 I/O 操作(阻塞操作)都采用类似上面 Ajax 请求的异步的请求方式,避免了频繁切换线程。

Nodejs 的单线程模型

Nodejs 的异步机制是基于事件的,所有的磁盘 I/O、网络通讯、数据库查询都以非阻塞方式请求,返回的结果由事件循环来处理。Nodejs 进程在同一时刻只会处理一个事件,完成后立即进入事件循环检查并处理后面的事件。这样做的好处是,CPU 和内存在同一时间集中处理一件事,同时尽可能让耗时的 I/O 操作并行执行。

Nodejs 架构

Nodejs 用异步式 I/O 和事件驱动代替多线程,带来了客观的性能提升。Nodejs 除了用 V8 作为 JavaScript 引擎外,还使用了高效的 libev 和 libeio 库支持事件驱动和异步式 I/O.

Nodejs 架构

Nodejs 的开发者在 libev 和 libeio 的基础上抽象出了 libuv.

Nodejs 安装

Nodejs 的下载和安装请访问官网:https://nodejs.org/zh-cn/

因为我的系统是deepin的,所以下面是Ubuntn下的安装

sudo apt-get install nodejs
sudo apt-get install npm

npm 是 Node 包管理器,是一个由 Nodejs 官方提供的第三方包管理工具,就像 PHP 中的 Pear、Python 中的 PyPI 一样。npm 是一个完全由 JavaScript 实现的命令行工具,通过 Nodejs 执行。

如果我们有需要安装多个版本的 Nodejs 的需求,可以安装多版本管理器。n 是一个十分简洁的 Node 多版本管理器,他的名字就一个字母。

npm install -g -n

安装完 n 后,可以使用 n --help 查看使用说明。详细可以访问n主页

Nodejs 的 Helloword

新建一个 hw.js 文件

console.log('hello word');

然后执行下面命令

node hw.js

这就是 Nodejs 的 helloword 程序。

建立HTTP服务器

新建一个 app.js 的文件

var http = require('http');

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 prot 3000.');

接下来运行 node app.js 命令,打开浏览器访问 http://127.0.0.1:3000

Nodejs 服务器访问

用 Nodejs 实现最简单的 http 服务器就这样诞生了。这个程序中调用了 Nodejs 提供的 http 模块,对所有 HTTP 请求答复同样的内容并监听 3000 端口。

本文出自水寒的个人博客,转载请说明出处:https://dp2px.com

模块和包

模块(Module)和包(Package)是 Nodejs 最重要的支柱。开发一个具有一定规模的程序不可能只用一个文件,通常需要把各个功能拆分、封装、然后组合起来。Nodejs 提供了 require 函数来调用其他模块,而且模块都是基于文件的,机制十分简单。

模块

在 Nodejs 中模块和文件是一一对应的,一个 Nodejs 文件就是一个模块,这个文件可能是 JavaScript 代码、JSON 或者编译过的 C/C++ 扩展。

上面我们使用 var http = require('http'),其中 http 是 Nodejs 的一个核心模块,其内部是用 C++ 实现的,外部用 JavaScript 封装。

Nodejs 提供了 exports 和 require 两个对象,其中 exports 是模块公开的接口,require 用于获取模块的接口。我们在同一个目录下创建两个文件。

module.js 文件内容:

var name;
exports.setName = function(thyName){
    name = thyName;
}

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

getmodule.js 文件内容:

var myModule = require('./module');
myModule.setName('BYVoid');
myModule.sayHello();

运行 node getmodule.js,结果是:

Hello BYVoid

创建包

包是在模块基础上更深一步的抽象,Nodejs 的包类似于 C/C++ 的函数库或 Java 中的 jar 包。它将某个独立的功能封装起来,用于发布、更新、依赖管理和版本控制。

Nodejs 的包是一个目录,其中包含了一个 JSON 格式的包说明文件 package.json.

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

示例说明, 创建如下目录和文件:

.
│  main.js
│
└─lib
        index.js
        lib1.js
        lib2.js
//lib1.js
exports.showMsg = function(){
    console.log("msg --- lib1");
}

//lib2.js
exports.showMsg = function(){
    console.log("msg --- lib2");
}

//index.js
var lib2 = require('./lib2');
var lib1 = require('./lib1');

exports.showMsg = function(){
    console.log("msg --- index.js");
}

exports.l1 = lib1;
exports.l2 = lib2;

//main.js
var lib = require('./lib');

lib.showMsg();

lib.l1.showMsg();
lib.l2.showMsg();

可以看到上面的 lib 目录下的index.js中引入了 lib1lib2 并使用 exports 暴露给外部,然后使用 require('./lib') 引入 lib 目录下暴露的方法。

如果我们要更改这个入口文件名称或路径,可以使用 package.json 文件来配置,在 lib 目录新增一个 package.json 文件:

.
│  main.js
│
└─lib
        lib1.js
        lib2.js
        package.json
        rukou.js

package.json 文件内容如下:

{
    "name": "lib",
    "main": "./rukou.js"
}

接下来就可以使用 name 所对应的 main 路径了:

var lib = require('./lib');  //实际路径是  ./lib/rukou

lib.showMsg();

lib.l1.showMsg();
lib.l2.showMsg();

module.exports

事实上上面的 exports 对象指向的是 module.exports, Nodejs 为每一个模块提供了一个这样的变量。

var exports = module.exports

所以,在你的代码里面千万不要出现将 exports 变量指向一个值,因为这样等于切断了 exports 与 module.exports 的联系。这个联系正是它们不同的地方。

修改上面的 rukou.js 代码如下:

var lib2 = require('./lib2');
var lib1 = require('./lib1');

module.exports = function(){
    lib1.showMsg();
    lib2.showMsg();
    console.log("msg --- index.js");
}

然后就可以在 main.js 中这样使用了, 注意调用方法的不同:

var lib = require('./lib');

lib();

结论:如果要输出一个键值对象 {},可以利用 exports 这个已存在的空对象 {},并继续在上面添加新的键值;如果要输出一个函数或数组,必须直接对 module.exports 对象赋值。所以我们可以得出结论:直接对 module.exports 赋值,可以应对任何情况。

在这篇文章之后我还写了一篇如何模块化和结构化网络请求的文章 《Nodejs实现的一个网络请求模块化基础案例》

本系列源码在 GitHub 仓库:https://github.com/lxqxsyu/NodejsStudy