Node基础 2(Node Web开发【下】)

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!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值