原生 Node.js 应用
Node.js 是一个基于 Chrome V8 引擎的 JavaScript 运行环境
Node.js 使用了一个事件驱动、非阻塞式 I/O 的模型,使其轻量又高效
Node.js 的包管理器 npm,是全球最大的开源库生态系统
? 本文主要介绍构建一个 Node.js 应用的基本步骤和模块,并假定你已经对 Node.js Api 有一定的了解
? 本文引用部分代码作为例子,如果希望参看全部源码,欢迎去 github 查阅(如果觉得有一定帮助,欢迎star)
模块架构设计
整个 Node.js 应用的架构设计
node.js 应用构成
- 引入模块:通过
require
指令来引入 Node.js 模块 - 创建服务器:服务器用来监听客户端请求
- 接收请求和响应请求:接收客户端的HTTP请求,返回响应数据
// 通过 require 引入 http 模块,并将实例化的 HTTP 赋值给 http 变量
const http = require('http');
// 引入 url 模块,用来解析数据
const url = require('url');
function ylone(router, handleObj) {
const hostname = '127.0.0.1';
const port = 7777;
// http.createServer(function(){}) 方法创建服务器,并返回一个对象
const server = http.createServer((req, res) => {
const path = url.parse(req.url);
const pathName = path.pathname;
// 处理node.js每次自动请求favicon.ico
if (pathName !== '/favicon.ico') {
const content = router(handleObj, pathName, res, req);
}
});
// server.listen() 方法绑定主机和端口
server.listen(port, hostname, () => {
console.log(`服务运行在${hostname}:${port}`);
});
}
exports.ylone = ylone;
基于事件驱动的回调
-
http.createServer((req, res) => {...})
是一个典型的回调,事实上,这就是 Node.js 原生的工作方式 - 这里直接将一个匿名函数作为变量进行传递,绕开了“先定义,再传递”的圈子
- 像写 PHP 应用时:任何时候当有请求进入时,服务器(如Apache)就会为这个请求创建一个新的进程,并且从头至尾执行相应的 PHP 脚本
- 在 Node.js 中,无论何时当一个新的请求到达指定端口时,我们在服务器创建时传递的函数就会被调用
Node.js 模块
- 模块意味着将 Node.js 应用(如 http.js)抽象成一个模块,通过入口文件
index.js
去调用相应的模块来引导和启动应用 - 将代码模块化意味着我们将提供其功能的部分(如 一个函数)导出到请求这个模块的脚本内
const server = require('./http');
const router = require('./route');
const handle = require('./requestHandle');
var handleObj = {};
// 入口 Case
handleObj['/'] = handle.hello;
// 非阻塞 Case
handleObj['/vlone'] = handle.vlone;
// post Case
handleObj['/supreme'] = handle.supreme;
// get Case
handleObj['/adidas'] = handle.adidas;
server.ylone(router.router, handleObj);
路由
- 为了处理不同的 http 请求,我们需要通过创建一个路由模块来进行“路由选择”
- 为路由提供请求的 url 和其他需要的 get 和 post 参数,随后路由根据这些数据来执行相应的代码
- 我们所需要的数据都在
http.createServer((req, res) => {...})
的 req 参数中,为了解析 req,需要额外引入url
和querystring
Node.js 模块 - 在 index.js 内引入路由对象,将路由方法传递给 http 应用,在
http.createServer((req, res) => {...})
内解析 req 参数,然后调用 router 方法 - 理解以下函数式编程:将 router 对象传递给 index,在 index 内将 router 方法传递给 http,因为 http 并不关心 router 方法从哪来,只需要执行方法,然后完成业务,但是首先需要保证有这个对象
- 函数式编程最基本,最核心的即思想转换,由名词到动词,由对象到方法,行为驱动执行
function router(handleObj, pathName, res, req) {
if (typeof handleObj[pathName] === 'function') {
return handleObj[pathName](res, req);
} else {
res.writeHead(200, {
'Content-type': 'text/plain'
});
const content = '404 Not Found';
res.write(content);
res.end();
}
}
exports.router = router;
将路由分发到请求处理函数
- 需要创建一个新的 resquestHandlers 模块,封装各个处理函数来对应不同的请求
- 在 JavaScript 中通过对象键值对来封装 路径->方法 的映射关系
- 在 C++ 或者 C# 中,对象指的是类或者结构体的实例,对象根据其实例化的模板会拥有不同的属性和方法
- 在 JavaScript 中,对象是一个键值对集合
- 在入口文件(index.js)内引入 requestHandle,同时声明一个操作对象(handleObj),用来存储 路径->方法 的映射关系,最后将路由方法和操作对象传递给服务器应用(http.js)
- 在服务器应用内,获得浏览器请求的路径,调用路由方法(router),将操作对象(handleObj)和请求路径作为参数传递
- 在路由内(route.js)获取路径对应的函数,自执行函数
因为文章篇幅原因,这里只展示关键代码,源码参看 github
const { exec } = require('child_process');
const querystring = require('querystring');
const url = require('url');
function createHttp(type, res, val) {
const content = val;
const conType = {
plain: 'text/plain;charset=utf-8',
html: 'text/html',
};
// 为隐式的响应头设置值
res.writeHead(200, {
'Content-type': conType[type]
});
// 发送响应主体
res.write(content);
// http 完成响应
res.end();
}
... something else
function vlone(res) {
exec('node --version', (error, stdout, stderr) => {
if (error) {
console.log(error, stdout, stderr);
return;
}
const content = stdout;
const type = 'plain';
createHttp(type, res, content);
});
}
... something else
阻塞与非阻塞
A() 方法读取文件,因此需要一定的响应时间,B() 方法代表其他需要执行的代码
阻塞:在A() 执行的过程中,B() 处于等待状态,当A() 访问文件数据准备就绪后,B() 才开始执行
由上图可以看出,应用程序从进行系统调用到复制数据报到应用进程缓冲区的整段过程是阻塞的,直到数据报被复制到用户空间完成后,用户进程才解除阻塞状态,继续执行下一个应用程序
- 优点:能够及时返回数据,无延迟,方便调试
- 缺点:需要等待
非阻塞:在A() 执行的过程中,B() 同时执行,且当A() 访问文件数据准备就绪后,A() 会被执行完成
由上图可以看出,应用程序在调用过程中,如果数据报还没有准备就绪,会先返回一个错误信息(EWOULDBLOCK),此时当前进程可以执行其他方法,而不会阻塞。而 A() 会轮询内核,返回缓冲区数据是否准备就绪
- 优点:不需要等待,当前线程可以处理多个任务
- 缺点:增大了任务完成的响应延迟,因为任务可能在两次轮询间隔内完成,从而导致整体数据的吞吐量降低
以非阻塞方式进行请求响应
- 当前的应用交互方式:(请求处理程序 -> 请求路由 -> 服务器)将请求处理程序返回的内容(请求处理程序最终要显示给用户的内容)传递给HTTP服务器
- 当前这种交互方式的问题在于,如果请求处理程序中有 Node.js 封装的非阻塞方法A(),那么A() 在阻塞过程中,服务器就已经将数据返回了,并不会等到A() 执行完毕
- 为了解决上述问题,以非阻塞方式进行请求响应,相对于之前将数据传递给服务器的方式,现在我们需要将服务器(response对象)传递给生成数据的应用内,待数据准备完毕,再返回响应数据
- 这样可以同时请求两个路径(实际上就是触发两个函数方法),B() 并不会因为A()执行时间长而处于等待状态
处理 post 请求
- 创建一个表单元素,设置表单提交方法为 post, 每当用户提交表单时,则触发 supreme() 方法
- 处理 post 请求一般采用异步非阻塞方式,因为 post 请求一般会比较重,你无法控制用户输入的数据量,如果用阻塞的方式处理则必然会导致用户操作阻塞
- 为了实现非阻塞,Node.js 会将 post 数据拆分成数据块,然后通过出发特定事件,将这些数据块传递给回调函数
- 常用的 post 两个事件:data事件(新的数据块到达时触发),end事件(所有数据都已经接收完毕时触发)
- 通过在 request 对象上注册监听器(listener)来告诉应用当 post 事件触发时,应该触发哪些回调函数
处理 get 请求
- 通过 Node.js 封装的 url对象来解析 url 参数,获取关键数据
-
url.parse()
的第二参数 parseQueryString 如果为 true,则query
属性总是会通过 querystring 模块的 parse() 方法生成一个对象
some pieces
- 当写好 Node.js 脚本(如 ylone.js)后,通过
node ylone.js
命令执行脚本 - 在浏览器访问指定地址(如
http://localhost:7777/
)意味着向服务器发出请求,从而触发服务器创建时的回调函数 - 当访问网页(如
http://localhost:7777/
)时,控制台可能会输出两次 req 的数据,那是因为大部分浏览器会在访问网页时尝试读取 favicon.ico 文件 - 针对浏览器每次发送请求,都会默认请求一次
/favicon.ico
的问题,可以在 http 中对其进行过滤,不执行操作 - 如果希望在 Node.js 内的传递一个 html 片段,并渲染在浏览器上,需要将
res.writeHead(200, {'Content-type': 'text/plain'})
的 Content-type 设置为text/html
- Node.js 返回数据(response)在浏览器展示乱码,通过在
res.writeHead(200, {'Content-type': 'text/plain;charset=utf-8'})
加上charset=utf-8
配置解决
--Respect Node.js--
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。