Node.js学习笔记【三】
Koa简介
一句话简介
基于Node.js的下一代Web框架
- 基于Node.js:node.js模块
- 下一代:蚕食第一代Web框架Express的市场
- Web框架:不是命令行工具、不是算法
官网简介
- 由Express幕后的原班人马打造
- Web应用和API开发领域
- 更小、更富有表现力、更健壮
- 利用
async函数
,丢弃回调函数(koa2
利用ES7的async/await
特性,极大的解决了我们在做nodejs开发的时候异步给我们带来的烦恼。 ) - 增强错误处理:try catch
- 没有捆绑任何中间件:开发者按照自己的意愿选择自己需要的中间件
- 快速而愉快地编写程序
相关知识
在学习koa2前还要了解ES6的相关语法。
作用域
ES5
中作用域有:全局作用域、函数作用域。没有块作用域的概念。
ES6
中新增了块级作用域。块作用域由 { } 包括,if语句和 for语句里面的{ }也属于块作用域。
let、const和var
var
、let
---定义变量(值可以改变)
const
---定义常量(必须给初始值,且值不能修改)
let
和const
有块级作用域,var
没有
//实例1
for(let i=1;i<5;i++){
// i只能在这里使用
}
console.log(i);//报错
//实例2
//使用 let 依次打印1、2、3、4、5。因为每一次循环都形成一个块(局部作用域),不同的i
for(let i=1;i<=5;i++){
setTimeout(()=>{
console.log(i);
},1000*i)
}
//实例3
// 使用 var 只会打印5次6。因为内存中就只有一个i,for很快执行完了,定时器属于异步任务后执行,1秒钟之后才执行1个定时器。
for(var i=1;i<=5;i++){
setTimeout(()=>{
console.log(i);
},1000*i)
}
除此之外,还需要注意:
let/const
定义的变量不会出现变量提升,而var
定义的变量会提升- 相同作用域中,
let
和const
不能出现重复声明。而var
就可以重复声明
箭头函数
语法
()=>{}
- ()和=>之间不能换行
=>是由一个=和>组合而成的,中间不能有空格
=>和{}之间能换行
写法特点
形参如果只有一个,可以省略小括号
函数体只有一行代码,可以省略大括号,并且自带 return 效果
let fn = function(x){
return x*x;
}
let fn = (x)=>{
return x*x;
}
let fn = x=>x*x;
console.log(fn(5));//25
其他特点
1.函数内没有argument对象,可以使用剩余参数
获取全部的实参(ES6有个剩余参数,在普通函数也能用)
let fn = (...c)=>{
// console.log(arguments);//报错
console.log(c);// [3, 2, 1, 9, 7]
}
fn(3,2,1,9,7);
2.箭头函数内部没有自己的this
;使用this需要向查找普通变量那样去查找
箭头函数this是在定义的时候绑定的,内部的this就算外层代码的this。
setTimeout的外层是fn2,fn2的this就是obj
let obj = {
fn1:function(){
setTimeout(function(){
console.log(this);//window
},1000)
},
fn2:function(){
// 这里的this指向谁? ===> obj对象
setTimeout(()=>{
console.log(this);//obj对象
},1000)
}
}
obj.fn1();//window
obj.fn2();//obj
以前的this
指向:普通函数中的this ==> window
定时器中的this ===> window
自调用函数中的this ===> window
obj.fn()里面的this ===> obj对象
事件处理函数中this ===> 事件源
构造函数中的this ===> 实例对象
3.箭头函数不能当做构造函数
//构造函数
function Person(){
this.age = 20
}
Promise
- promise只有三种状态,未完成,完成(fulfilled)和失败(rejected)。
- promise的状态可以由未完成转换成完成,或者未完成转换成失败。
- promise的状态转换只发生一次
所谓Promise
,简单说就是一个容器
,里面保存着某个未来才会结束的事件(通常是一个异步操作
)的结果。
ES6 规定,Promise对象是一个构造函数
,用来生成Promise实例。
const promise = new Promise(function(resolve, reject) { // ... some code
if (/* 异步操作成功 */){
resolve(value);
} else {
reject(error);
}
});
Promise构造函数
接受一个函数作为参数,该函数的两个参数分别是resolve和reject。它们是两个函数,由 JavaScript 引擎提供,不用自己部署。
resolve函数
的作用是,将Promise对象的状态从“未完成”变为“成功”(即从 pending 变为 resolved),在异步操作成功时调用,并将异步操作的结果,作为参数传递出去;
reject函数
的作用是,将Promise对象的状态从“未完成”变为“失败”(即从 pending 变为 rejected),在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去。
Promise
实例生成以后,可以用then
方法分别指定resolved
状态和rejected
状态的回调函数或者返回处理进度信息
promise.then(function(fulfilled){
//当promise状态变成fulfilled时,调用此函数
},function(rejected){
//当promise状态变成rejected时,调用此函数
},function(progress){
//当返回进度信息时,调用此函数
});
使用Promise
包装了一个读取文件的异步操作
const fs = require('fs'); function readFileAsync(path){ return new Promise((resolve,reject)=>{ fs.readFile(path,(err,data)=>{ if(err) reject(err) else resolve(data) }) }) }
readFileAsync('./package.json').then(data=>{
data=JSON.parse(data)
console.log(data.name);
}).catch(err=>{
console.log(err);
})
参考文章:ES6 Promise用法讲解
处理异步方式
处理异步场景有以下几种方式:(例子为读取文件的异步操作)
- 通过
回调函数
实现异步操作
const fs = require('fs') function readFile(cb){ fs.readFile('../package.json',(err,data)=>{ if(err) return cb(err) cb(null,data) }) }
readFile((err,data)=>{
if(!err){
data = JSON.parse(data)
console.log(data.name);
}else{
console.log("读取文件失败");
}
})
- 使用
Promise
const fs = require('fs') function readFileAsync(path){ return new Promise((resolve,reject)=>{ fs.readFile(path,(err,data)=>{ if(err) reject(err) else resolve(data) }) }) }
readFileAsync('./package.json').then(data=>{
data=JSON.parse(data)
console.log(data.name);
}).catch(err=>{
console.log(err);
})
- 利用
co库
+生成器函数(generator function)
const fs = require('fs') const co = require('co') const util= require('util')
co(function *(){
let data = yield util.promisify(fs.readFile)('../package.json')
data = JSON.parse(data)
console.log(data.name);
})
Async
const fs = require('fs')
const util= require('util')
const readAsync = util.promisify(fs.readFile)
async function init(){
let data = await readAsync('../package.json')
data = JSON.parse(data)
console.log(data.name);
}
init()
安装koa2
使用npm install koa
命令安装koa2
然后就可以使用koa2啦😊
//引入Koa
const Koa = require('koa')
const app = new Koa()
//配置中间件(可以先当做路由)
app.use(async(ctx,next)=>{
ctx.body = 'Hello World'
})
//监听端口
app.listen(8080)
上下文(Context)
Koa
提供一个Context
对象,表示一次对话的上下文
(包括 HTTP 请求和 HTTP 回复)。通过加工这个对象,就可以控制返回给用户的内容。ctx
是context
的缩写中文一般叫成上下文,这个在所有语言里都有的名词,可以理解为上(request
)下(response
)沟通的环境,所以koa中把它们两都封装进了ctx对象
这个ctx
对象下有4个主要的属性,它们分别是
- ctx.req:原生的req对象
- ctx.res:原生的res对象
- ctx.request:koa自己封装的request对象
- ctx.response:koa自己封装的response对象
其中koa自己封装的和原生的最大的区别在于,koa自己封装的请求和响应对象的内容不仅囊括原生的还有一些其独有的东西。
Koa Context 将 node 的 request
和 response
对象封装到单个对象中,为编写 Web 应用程序和 API 提供了许多有用的方法。 这些操作在 HTTP 服务器开发中频繁使用,它们被添加到此级别而不是更高级别的框架,这将强制中间件重新实现此通用功能。
koa中的next
配置中间件时通常会将参数定义为(ctx,next)
,而这个next
函数主要负责将控制权交给下一个中间件,如果当前中间件没有终结请求,并且next没有被调用,那么当前中间件的请求将被挂起,等到next()后的中间件执行完再返回继续执行。
总结来说就是:从第一个中间件开始执行,遇到next进入下一个中间件,一直执行到最后一个中间件,在逆序,执行上一个中间件next之后的代码,一直到第一个中间件执行结束才发出响应。
实例代码:
const one = (ctx, next) => { console.log('>> one'); next(); console.log('<< one'); }
const two = (ctx, next) => {
console.log('>> two');
next();
console.log('<< two');
}const three = (ctx, next) => {
console.log('>> three');
next();
console.log('<< three');
}
app.use(one);
app.use(two);
app.use(three);
输出结果:
>> one
>> two
>> three
<< three
<< two
<< one
中间件
Koa 的最大特色,就是中间件(middleware)
。在中间件系统的实现上,Koa
中间件通过async/await
来在不同中间件之间交换控制权,工作机制和栈
结构非常相似。Koa
把很多async函数组成一个处理链
,每个async函数都可以做一些自己的事情,然后用await next()
来调用下一个async函数。我们把每个async函数称为middleware
,这些middleware
可以组合起来,完成很多有用的功能。
Koa中使用app.use()
用来加载中间件,基本上Koa 所有的功能都是通过中间件实现的。
在Koa中,中间件是指连贯整个 Koa 应用程序,并共享资源的独立插件”,注意两个词,“连贯”与“共享资源”,与上面的代码一一对应,“连贯”对应“next”,“共享资源对应context”。
每个中间件默认接受两个参数,第一个参数是 Context
对象,第二个参数是next
函数。只要调用next
函数,就可以把执行权转交给下一个中间件。每一个中间件就相当于洋葱的一层,请求从最外层进去,然后从最里层出来,每个中间件都会执行两次。
Koa 中通过 next
贯穿整个应用程序:
const Koa = require('koa'); const app = new Koa();
const mid1 = async(ctx,next)=>{
ctx.type = 'text/html;charset=utf-8;'
await next()
ctx.body = ctx.body + ' !'
};const mid2 = async(ctx,next)=>{
ctx.body = 'Hello'
await next()
};const mid3 = async(ctx,next)=>{
ctx.body = ctx.body+' World'
};app.use(mid1);
app.use(mid2);
app.use(mid3);
app.listen(8080);
Koa中的koa-compose
在了解koa-compose前先了解下纯函数
和尾递归
概念
纯函数
一个函数的返回结果只依赖其参数,并且执行过程中没有副作用。在状态数据多变的环境中,可以带来强的可控性。
function pure(x){
return x+1;
}
console.log(pure(1));
console.log(pure(1));
console.log(pure(1));
console.log(pure(1));
尾递归
若一个函数在尾位置调用本身(或是一个尾调用本身的其他函数等),则称这种情况为尾递归,是递归的一种特殊情形。而形式上只要是最后一个return语句返回的是一个完整函数,它就是尾递归。
尾递归的特点是最后返回的值是一个递归的函数调用,这样执行完就会在调用栈中销毁,不会占据调用栈。
基础递归
:
function tail(i){ if(i>3) return console.log('修改前 ',i); tail(i+1) console.log('修改后 ',i); }
tail(0);
修改前 0
修改前 1
修改前 2
修改前 3
修改后 3
修改后 2
缺点:在这个过程,函数记录了太多的堆栈深度和状态,比较浪费性能。
使用尾递归
改进:
function tail(i){ if(i>3) return console.log('修改前 ',i); return tail(i+1) }
tail(0);
修改前 0
修改前 1
修改前 2
修改前 3
很容易看出, 普通的线性递归
比尾递归
更加消耗资源,,在实现上说, 每次重复的过程调用都使得调用链条不断加长。系统不得不使用栈进行数据保存和恢复,而尾递归就不存在这样的问题。
koa-compose
koa 的 koa-compose
利用递归实现了 Promise 的链式执行
,不管中间件中是同步还是异步都通过 Promise 转成异步链式执行
。
compose核心代码
function compose (middleware) {
return function (context, next) {
let index = -1
return dispatch(0)
function dispatch (i) {
if (i <= index) return Promise.reject(new Error('next() called multiple times'))
index = i
let fn = middleware[i]
if (i === middleware.length) fn = next
if (!fn) return Promise.resolve()
try {
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
} catch (err) {
return Promise.reject(err)
}
}
}
}
可以看到
compose
其实就是一个尾递归
函数 ; 递归相对低效且消耗系统资源- 返回的是一个Promise.resolve包装之后的调用,而不是同步的调用,所以这是一个
异步递归
,异步递归比同步递归的好处是可以被打断,如果中间有一些优先级更高的微任务,那么可以先执行别的微任务 compose
是函数复合
,把n个middleware复合成一个,参数依然是context和next,这种复合之后依然是一个middleware,还可以继续进行复合。
compose
是一个工具函数,Koa.js 的中间件
通过这个工具函数组合后,按app.use()
的顺序同步执行,也就是形成了 洋葱圈式
的调用。
koa-session
session同之前介绍的一样,是一种记录客户状态的机制。
使用npm install koa-session
安装体验koa-session
const Koa = require('koa'); const session = require('koa-session'); const app = new Koa() //设置cookie key app.keys = ['Hello']
app.use(session(app))
app.use(ctx=>{
if (ctx.path === '/favicon.ico') return
let n =ctx.session.views || 0
ctx.session.views = ++n
ctx.body = n+' views'
})
app.listen(8888)