Nodejs基础《Node Web开发》4-几种典型的简单应用
Web框架可以让你从复杂的HTTP协议中解脱,远离细节可以保持高效;
第4章 几种典型的简单应用
4.1 Math Wizard
注:这个示例将不依赖Web框架
URL规则:
/
:应用主页/square
:计算平方/mult
:计算乘法/factorial
:计算阶乘/fibonacci
:计算菲波那切数
创建一个项目文件夹:mkdir chap4-1
注:每一个页面都由一个独立的模块实现,服务器把请求分别路由到这些模块;
查看项目的源码
计算密集型功能的阻塞问题
值得注意的是,这里计算斐波那契数的过程是一个计算密集型的功能,它会阻止当前线程的Node事件循环的执行,影响Node对请求的响应:
- Node只是一个执行线程,处理程序必须能快速返回事件循环;通常异步编程能保证事件循环的正常执行;
- 常用的方案有两种:
- 一种是算法重构,即便没有优化空间,也可以将其切分并通过事件循环分派到不同回调函数里,将同步函数转换成一个含有对应回调的异步函数;
- 另一种则是创建一个后台服务,即实现一个后台运行的数学计算服务器;这个请求处理函数应可以异步调用数据服务或数据库、收集响应数据,然后发送给浏览器;(一个进行计算工作的后台服务器或使用负载均衡的服务器集群)
- 第二种 创建一个后台服务的方式使用场景原则:
- 最好把繁重的计算需求从直接面对用户的服务器移除,把它的资源留给用浏览器的交互;
- 用负载均衡做到多个服务器处理请求(云计算)
- 单独的服务器,可以结合缓存代理来使用,这将急剧提升已经被计算过的请求的响应速度;
“可以将其切分并通过事件循环分派到不同回调函数里,将同步函数转换成一个含有对应回调的异步函数” 实现前后的fibonacci函数实现对比:
// 同步
let fibonacci = function (n) {
if (n === 1) {
return 1;
} else if (n === 2) {
return 1;
} else if (n > 0) {
return fibonacci(n - 1) + fibonacci(n - 2)
} else {
return NaN;
}
}
// 异步驱动
let fibonacciAsync = function (n, done) {
if (n === 1) {
done(1);
} else if (n === 2) {
done(1);
} else if (n > 0) {
// process.nextTick方法将一个递归函数转换成各个步骤都由事件循环分派
// 这个函数通过事件循环调用回调函数,确保函数能快速进入事件循环
process.nextTick(function (){
fibonacciAsync(n-1, function(val1){
process.nextTick(function (){
fibonacciAsync(n-2, function(val2){
done(val1 + val2);
}
}
})
})
} else {
done(NaN);
}
}
- 我们应该注意到这个方式并没有对fibonacci的计算量有什么帮助,必要的计算量只是交给了事件循环去调度,区别在于,前者是同步计算,而process.nextTick是异步的,但都会使当前的Node进程占用的所有CPU负载,从而阻止Node对服务器请求的响应;
- 但这个示例展示了通过
事件循环分派工作
的技术,对于有些场景来说是非常实用的,虽然它并不符合这个示例中的案例;
关于process.nextTick()
- process.nextTick这个名字有点误导,它是在本轮循环执行的,而且是所有异步任务里面最快执行的;
- Node 执行完所有同步任务,接下来就会执行process.nextTick的任务队列;
- 基本上,如果你希望异步任务尽可能快地执行,那就使用 process.nextTick;
- 根据语言规格,Promise 对象的回调函数,会进入异步任务里面的”微任务”(microtask)队列。微任务队列追加在 process.nextTick 队列的后面,也属于本轮循环;
4.X
和已经成熟的Web服务器项目相比,Math Wizard缺少了什么?
- 不能对HTTP请求方式GET、POST、PUT进行不同的处理;
- 完善一点的话,还需要对错误URL提供一个404页面;
- 没有为URL和表单屏蔽注入式的脚本攻击;
- 不支持cookie处理,也没有使用cookie维持会话;
- 不记录请求、不支持身份验证;
- 没有处理html css js 图像 这样的静态文件;
- 没有对页面尺寸布局、执行及响应时间等施加限制;
好在,Node的web框架可以弥补大部分丢失的特性;
什么是“中间件”?
易于挂在和调用的模块,可以无序使用并为应用的快速开发提供常用的功能,如 请求的路由选择、身份验证、请求记录、cookie处理等;
中间件两种形式:
- 过滤器:处在中间层负责处理传入和传出的请求,但是本身不响应请求,如logger;
- 供应者:作为堆栈的终端,一个进入的请求最终会被供应者处理,并且供应者负责发送响应,如静态文件服务的中间件static;
注:本文中并未完成基于Connect中间件、Express框架的Math Wizard新的实现,总的来说就是通过中间件或框架的API来实现和处理原始Math Wizard的诸多不足,这里不继续讨论;
注:HTTP协议实现起来相当复杂,细节最好还是交给Web应用框架去做;
Nodejs基础《Node Web开发》5-简单的Web服务器、Event-Emitter和HTTP客户端
通过的简单的Web服务器的学习,来理解框架;
第5章 简单的Web服务器、Event-Emitter和HTTP客户端
5.1 通过EventEmitter发送和接收事件
EventEmitter对象在Node中很关键,很多类都是EventEmitter的子类,如HTTPServer和HTTPClient,它们会在HTTP协议的各个阶段发送相应的事件(data事件、end事件、close事件等);
使用EventEmitter的方法可以发送事件来表示一些状态,这些事件通过Node的事件循环,最终触发回调函数;
EventEmitter被定义在Node的事件模块中(events),这意味着使用前要require('events')
,但也可以不显示这样做;
const events = require('events');
const util = require('util');
// 定义一个类
function Pulser(){
events.EventEmitter.call(this);
}
// 使其继承EventEmitter
util.inherits(Pulser, events.EventEmitter);
Pulser.prototype.start = function(){
var self = this;
this.id = setInterval(function(){
// 每隔1s触发一次
self.emit('pluse');
},1000);
}
// 使用Pulser
var pulser = new Pulser();
// 进行事件监听
pulser.on('pulse', function(){
// 监听到触发的事件
})
pulser.start();
EventEmitter:
- 事件通过事件名标识;事件名可通过调用.emit方法进行设置,无需注册;事件名error通常表示发生错误时对应的事件;
- 对象使用.emit函数发送事件;
- 使用.on注册对应事件的监听器,参数是事件名及一个回调函数;
- 回调函数也可以传递参数:
self.emit('eventName',data1,data2,...)
emitter.on('eventName',function(data1,data2,...))
5.2 HTTP Sniffer——监听HTTP会话
httpsniffer.js
const util = require('util');
const url = require('url');
exports.sniffOn = function(server){
// server是一个给定的HTTP服务器
server.on('request',function(req,res){
// req.method
// req.httpVersion
// req.url
url.parse(req.url, true)
// req.headers
// req.trailers
});
server.on('close',function(errno){
});
server.on('checkContinue',function(req,res){
res.writeContinue();
});
server.on('upgrade',function(req,socket, head){
});
server.on('clientError',function(){
});
// 其他事件,如connection
}
HTTP Sniffer的使用:
const http = require('http');
const sniffer = require('./httpsniffer');
let server = ...
sniffer.sniffOn(server);
server.listen(3000);
5.3 基本的Web服务器
Basic Server的基本功能:
- 灵活的请求路由选择
- 自动提供解析后的URL对象;
- 自动提取主机头信息(用于虚拟机)
- 自动提取cookie头信息
- 支持favicon.ico请求
- 支持静态文件(HTML JS PNG GIF JPEG等)
- 灵活的服务器配置
注:mime模块可以用于生成正确的Content-Type头信息,使用
npm install mime
安装;
// MIME模块将根据给出的图标文件 确定正确的MIME类型
const mime = require('mime');
res.writeHead(200,{
'Content-Type':mime.getType(fname),
'Content-Length':buf.length
});
res.end(buf);
// Content-Type: image/vnd.microsoft.icon
Basic Server的实现:源码地址
Basic Server易扩展:
- 设置多个虚拟域
- 添加自定义处理程序
- 支持cookie头
- 实现HTTP基本验证和支持HTTPS请求
来具体看下:
1.Basic Server的虚拟主机配置
- 可以为每个不同的域名指定各自的内容文件夹;
- 也可以将一个域名挂在到另一个上;
server.useFavIcon("example.com","./example.com/favicon.ico");
server.docroot("example.com",'/','./example.com');
// 域名独立 内容也独立
server.useFavIcon("example2.com","./example2.com/favicon.ico");
server.docroot("example2.com",'/','./example2.com');
// 将一个域名挂在到另一个上,内容相同
server.useFavIcon("example2.com","./example.com/favicon.ico");
server.docroot("example2.com",'/','./example.com');
常见的需求非挂载域,而是把对一个域的请求重定向到两一个:
- 如,将www.example.com 重定向到 example.com;
- 又如,使用简短的url跳转到较长的URL;
实现方式:
- 在HTTP响应中发送301(永久移除)、302(临时移除)状态码,并指定Location头信息;这样浏览器就知道要跳转到另一个Web位置了;(参见redirector模块的实现)
- 核心代码如下:
res.writeHead(302, {'Location':re_url});
res.end();
Request URL: http://127.0.0.1:4080/1/ex1
Request Method: GET
Status Code: 302 Found
Remote Address: 127.0.0.1:4080
Referrer Policy: strict-origin-when-cross-origin
Connection: keep-alive
Date: Fri, 23 Oct 2020 07:38:06 GMT
Location: http://example1.com
Transfer-Encoding: chunked
5.4 MIME类型和MIME npm包
Web服务器的Content-Type,借用自MIME协议:
- MIME类型标准mime.types(包括600个Content-Type有关数据);
- 前文中我们已经见识过它的使用;
5.5 处理cookie
HTTP是无状态的协议,意味着服务器无法区分请求的发送端;普遍的做法是服务器发送cookie到客户端,cookie中定义标识用户身份的信息,后续的每一次请求,浏览器都会发送对应访问网站的cookie;
Basic Server项目中简单支持对浏览器发送的cookie的解析;
设置cookie:
res.setHeader('Set-Cookie2', ..cookie value..)
- cookie的值的格式是一个结构化的文本格式;
5.6 虚拟主机和请求路由
建立虚拟主机是同一个IP地址托管多个域名的方式:
- Node req对象有一个headers,其中包含了主机头,
Host:example.com
- 检查headers数组,映射请求到适当的域;
5.7 发送HTTP客户端请求
Node 还包含一个HTTP客户端对象,用来发送任何类型的HTTP请求;
创建一个wget.js:
const http = require('http');
const url = require('url');
const util = require('util');
// 获取命令行参数:准备请求的url
let argUrl = process.argv[2];
let parsedUrl = url.parse(argUrl, true);
let options = {
host:null,
port:-1,
path:null,
method:'GET'
};
// 指定请求url
options.host = parsedUrl.hostname;
options.port = parsedUrl.port;
options.path = parsedUrl.pathname;
if(parsedUrl.search){
options.path += '?' + parsedUrl.search;
}
// 发送请求
let req = http.request(options, function(res){
util.log(res.statusCode);
util.log(util.inspect(res.headers));
res.setEncoding('utf-8');
res.on('data',function(chunk){
// 数据到达
});
res.on('error',function(err){
});
})
req.on('error',function(err){
});
req.end();
命令行执行 node wget.js http://example.com
实际请求的options:
let options = {
host:'example.com',
port:80,
path:null,
method:'GET',
// 可以在headers中存放HTTP请求头信息
headers:{
'Cookie':'...'
}
};
- options对象通过host port path指定请求url;
- method必须是http动作(GET POST等);
- response对象是一个EventEmitter,能派发data、error事件;
- request是一种WritableStream,用于在HTTP请求中携带数据;
- HTTP请求中的数据格式通过MIME协议声明,如提交表单的Content-Type是
multipart/form-data
;
Nodejs基础《Node Web开发》6-存取数据
第6章 取数据
6.1 Node的数据存储引擎
Node除了在文件系统中读写文件功能,未能原生支持任何数据存储功能;使用其他数据存储系统需要使用数据库交互模块,如MySQL、MongoDB、SQLite3、REDIS等;
一般的,安装模块的同时还需要安装模块依赖的其他部分,包括数据库客户端,如MySQL模块依赖一个MySQL服务器和一个MySQL客户端库;
6.2 SQLite3——轻量级的进程内SQL引擎
SQLite3是一个无服务器且无需配置的SQL数据库引擎,仅仅是作为一个独立的库被链接到应用程序上;
node-sqlite3就是sqlite3的Node版本,可在github上查找;
安装SQLite3:
- 命名
$ npm install sqlite3
; - 安装前提需要系统上安装sqlite3库,以及包含原生代码(基于C语言)用于链接sqlite3的npm模块;Mac上已安装,Linux系统没有安装的可以通过包管理器命令进行安装,命令为
apt-get install libsqlite3
;
使用SQLite3:封装模块notesdb-sqlite3.js
const util = require('util');
const sqlite3 = require('sqlite3');
sqlite3.verbose();
let db = undefined;
exports.connect = function(callback){
db = new sqlite3.Database("Chap6.sqlite3",sqlite3.OPEN_READWRITE | sqlite3.OPEN_CREATE,function(err){
if(err){
util.log('Fail on creating db ' + err);
callback(err);
}else{
callback(null);
}
})
}
exports.disconnect = function(callback){
callback(null);
}
exports.setup = function(callback) {
db.run(`CREATE TABLE IF NOT EXISTS notes (ts DATETIME, author VARCHAR(255), note TEXT)`,function(err){
if(err){
util.log('Fail on creating table ' + err);
callback(err);
}else{
callback(null);
}
})
}
// 一个空便签对象 适合在不存在便签对象时使用
exports.emptyNote = {"ts":"",author:"",note:""};
exports.add = function(author, note, callback){
db.run(`INSERT INTO notes (ts, author, note)
VALUES (?, ?, ?);
`, [new Date(), author, note],function(error){
if(err){
util.log('Fail on add note ' + err);
callback(err);
}else{
callback(null);
}
})
}
exports.delete = function(ts,callback){
db.run(`DELETE FROM notes WHERE ts = ?;`,[ts],function(err){
if(err){
util.log('Fail on delete note ' + err);
callback(err);
}else{
callback(null);
}
})
}
exports.edit = function(ts,author,note,callback){
db.run(`UPDATE notes SET ts=?,author=?,note=? WHERE ts = ?;`,[ts,author,note,ts],function(err){
if(err){
util.log('Fail on updating note ' + err);
callback(err);
}else{
callback(null);
}
})
}
exports.allNotes = function(callback){
util.log(' in allNote ');
db.all(`SELECT * FROM notes`,callback);
}
// 每拿到一行数据 doEach执行一遍 读完所有数据 done执行;
exports.forAll = function(doEach,done){
db.each(`SELECT * FROM notes`,function(err, row){
if(err){
util.log('Fail on retrieve row ' + err);
done(err,null);
}else{
doneEach(null,row);
}
}, done)
}
exports.findNoteById = function(ts,callback){
var didOne = false;
db.each("SELECT * FROM notes WHERE ts = ?",[ts],function(err,row){
if(err){
util.log('Fail on retrieve note ' + err);
callback(err,null);
}else{
if(!didOne){
callback(null,row);
didOne = true;
}
}
})
}
注:async模块用于调用指定方法的同时调控一些其他操作,如async.series函数可以控制函数按顺序执行;
编辑一条便签的基于Express的局部代码示例:
// parseUrlParams用作路由中间件的处理函数 用于解析url中的参数
app.get('/del',parseUrlParams,function(req,res){
notesdb.delete(req.urlP.query.id, function(error){
if(error) throw error;
// 完成删除操作后 重定向到主页 完成页面刷新
res.redirect('/view')
})
})
注:当代数据库往往会支持分布式数据库访问、较高吞吐量、备份等功能;
6.3 Mongoose
MongoDB是“nosql”数据路,具有可扩展、高性能、开源、面向文档的数据库,使用JSON风格的文档;
Mongoose是用于访问MongoDB的模块之一,使用嵌入式文档,是一个类型灵活的系统,适用于字段输入。字段验证、虚拟字段等;
基于文档的数据库系统(如MongoDB)比SQL更接近现代编程语言和应用;
6.4 如何实现用户验证
许多应用都需要用户登录,然后进行一些特权操作;由于HTTP是一个无状态学医,验证用户的方式可以发送一个cookie到浏览器上,然后验证标识符;
cookie可包含应用中验证用户的数据:
let app = express.createServer();
// ...
// 在服务器对象配置中添加cookieParser中间件:查找和解析cookie,并将得到的值放到req对象中
app.use(express.cookieParser());
// ...
let checkAccess = function(req,res,next){
if(!req.cookies || !req.cookies.notesaccess || req.cookies.notesaccess !== "xxx"){
res.redirect('/login');
}else{
next();
}
}
// 在相关路由上添加 中间件函数checkAccess,以使每个便签url都能受到登录保护
app.get('/view', checkAccess, function(req,res){...})
注:不需要登录保护的URL也就不需要使用checkAccess路由中间件函数;
6.5 结语
Goodbye!