# Node常用框架

# 1、原生http模块

  • 服务启动:使用nodemon(本地)、ps2(生产)启动服务器
    • nodemon ./http.js,实现热更新,无需重启服务器
  • 监听路由:使用pathname进行分支处理
  • 获取get请求参数
    • 使用url库函数:url(req.url)路径和参数分割,url(req.url, true)路径和参数分割且参数解析
  • 获取post请求的参数
    • 使用curl发起post请求:curl -v -X POST -d "a=1&b=2" http://localhost:3000
    • 使用req的on和end事件获取请求体内容
  • 返回:使用res的write事件返回
  • 错误处理:监听error事件
const http = require('http')
const url = require('url')

const app = http.createServer(function(req, res) {
 //1、获取get请求内容
 console.log(req.method, req.url) // req.url是⼀个完整的,⽐如/order?id=4
 const { pathname, query } = url.parse(req.url, true) // true的作⽤是解析query字符串为对象
 console.log(pathname, query) //输出为/order,query为参数对象{id: 4}
  
 //2、获取post请求内容,即请求体内容
 // req 是⼀个可读流,有 data 和 end事件来获取请求体
 const requestDataArr = []
 		req.on('data', function(chunk /* Buffer */) {
 		requestDataArr.push(chunk) // ⼀条请求体
 })
 // ⽆论是否有请求体,都会执⾏end事件;express中可以使⽤中间件来获取请求体
 req.on('end', function(){
 		const requestDataBuffer = Buffer.concat(requestDataArr) // 拿到最终的请求体
 		// 请求已处理完成,res.end参数只能是字符串或者Buffer
 		res.end(`请求处理完毕${requestDataBuffer.toString()}\n`)
 })
  
 // 3、模拟⼀个路由; express⾃带路由系统
 if(pathname === '/') {
 // ⾸⻚的逻辑
 } else if (pathname === '/user') {
 // ⽤户中⼼的逻辑
 } else {
 // 404
 }

 //4、返回:res是⼀个可写流,有write和end⽅法
 res.setHeader('Content-Type', 'text/html;charset=utf-8') //避免中文出现乱码
 res.write('已收到请求\n')
 res.end(pathname)
})

app.listen(3000, () => {
 console.log('raw server starting at port 3000')
})

//错误处理
app.on('error', (err) => {
 console.log(err)
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45

# 2、express框架

# 1、路由

使用路由中间件app.get、app.post

  • res:send方法返回
  • req:query属性或者请求参数、params属性获取请求路径的参数

# 2、中间件

  • 使用:app.use
    • 请求体解析:使用bodyParse中间件,给req添加body对象
    • 静态资源中间件:app.use("/static", express.static(__dirname + '/public')),自带
    • cookie处理:cookie-parser中间件,给req和res添加cookie对象
    • 路由中间件:自带,app.use('/user', (req, res, next) => {})
      • 以/user开头
      • next表示执行下一个中间件
const express = require('express')
const app = express()
const cookieParser = require('cookie-parser')

//2、中间件
app.use("/static", express.static(__dirname + '/public'));
// 解析 application/json
app.use(bodyParser.json()); 
// 解析 application/x-www-form-urlencoded
app.use(bodyParser.urlencoded());
app.use(cookieParser());
// ⽐如访问/user 开头的⻚⾯需要鉴权
app.use('/user', (req, res, next) => {
 console.log('todo 访问/user⻚⾯需要验证身份')
  if(req.query.id){
    next() //继续执行下一个中间件
  }else{
    next(new Error('鉴权失败'))
  }
})
//错误处理中间件
app.use('/user', (err, req, res, next) => {
 console.log('todo 访问/user⻚⾯需要验证身份')
 res.send(err.message)
})

//1、路由
app.get('/', (req, res) => {
 res.send('⾸⻚')
})

app.get('/user/:userId', (req, res) => {
 console.log(req.query, req.params)
 res.send('⽤户⻚')
})

app.post('/user', (req, res) => {
 console.log(req.body)
 console.log('cookie: ', req.headers.cookie, req.cookies)
 res.cookie('age', '5')
 res.send('⽤户⻚post\n')
})

app.listen(3001, () => {
 console.log(`express-server started at 3001`)
})

app.on('error', function(err){
  consolg.log('error: ', err)
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
  • 中间件执行流程和原理
    • 洋葱模型
    • 异步模式:即使下一个中间件有异步处理,当前中间件不会等待下一个中间件处理完成
//正常函数执行
function a () {
 console.log('1-1')
 b()
 console.log('1-2')
}
function b () {
 console.log('2-1')
 c()
 console.log('2-2')
}
function c () {
 console.log('3-1')
 console.log('3-2')
}
a() //1-1 2-1 3-1 3-2 2-2 1-2

// 路由中间件
app.get('/test', (req, res, next) => {
 console.log('1-1')
 next()
 console.log('1-2')
}, (req, res, next) => {
 console.log('2-1')
 next()
 console.log('2-2')
}, (req, res, next) => {
 console.log('3-1')
 next()
 console.log('3-2')
})
app.get('/test', (req, res) => {
 console.log('test')
 res.send('end')
})
//1-1 2-1 3-1 test 3-2 2-2 1-2

app.get('/test', (req, res, next) => {
 console.log('1-1')
 next()
 console.log('1-2')
}, (req, res, next) => {
 console.log('2-1')
 next()
 console.log('2-2')
}, (req, res, next) => {
 console.log('3-1')
 await sleep(3)
 //后面函数会延迟执行
 next()
 console.log('3-2')
})
app.get('/test', (req, res) => {
 console.log('test')
 res.send('end')
})
//1-1 2-1 3-1 2-2 1-2 test 3-2

app.get('/test', (req, res, next) => {
 console.log('1-1')
 next()
 console.log('1-2')
}, async (req, res, next) => {//即使下一个中间件有异步处理,当前中间件不会等待下一个中间件处理完成
 console.log('2-1')
 await next() //不会等待中间件的异步执行完成 Promise.resolve().then(()=> console.log('2-2'))
 console.log('2-2')
}, (req, res, next) => {
 console.log('3-1')
 await sleep(3)
 //后面函数会延迟执行
 next()
 console.log('3-2')
})
app.get('/test', (req, res) => {
 console.log('test')
 res.send('end')
})
//1-1 2-1 3-1 1-2 2-2 test 3-2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78

# 3、next实现

内部中间件:参考/express/lib/router/route.js Route.prototype.dispatch方法

外部中间件:参考/express/lib/router/index.js proto.handle⽅法

// 模型抽象: [ [a, b, c], [d] ] a -> b -> c -> d
// 1、一维数组模式:内部中间件
// 先实现内部: [a, b, c],先⼿动执⾏a(),a中调⽤next()执⾏b,b中调⽤next()执⾏c
const handlers = [
   function a (next) {
     console.log('1-1')
     next()
     console.log('1-2')
   },
   function b (next) {
     console.log('2-1')
     next()
     console.log('2-2')
   },
   function c (next) {
     console.log('3-1')
     console.log('3-2')
   }
]

function innerProcess() {
 	 let idx = 0;
 	 function next() {
 			if(idx >= handlers.length) return;
 			handlers[idx++](next) // 执⾏a() i = 1; 执⾏b() i = 2; 执⾏c() i = 3;
 		}
 	 next() // idx = 0
}
innerProcess()

// 2、二维数组形式:外部中间件
// 模型抽象: [ [a, b, c], [d] ] a -> b -> c -> d
const handlers = [
 [
   function a (next) {
     console.log('1-1')
     next()
     console.log('1-2')
   },
   function b (next) {
     console.log('2-1')
     next()
     console.log('2-2')
   },
   function c (next) {
     console.log('3-1')
     next()
     console.log('3-2')
   }
 ],
 [
   function d (next) {
   	 console.log('4')
   }
 ]
]

function innerProcess(innerHandlers, out) {
 		let idx = 0;
 		function next() {
 			if(idx >= innerHandlers.length) return out(); // 执⾏外部的函数,将使⽤权返还 c -> d
 			innerHandlers[idx++](next) // 执⾏a() i = 1; 执⾏b() i = 2; 执⾏c() i = 3;
 		}
 		next() // idx = 0
}

function outerProcess() {
 		let idx = 0;
 		function next() {
 				if(idx >= handlers.length) return;
        //将二维数组中的每个数组对象传入innerProcess执行,执行结束执行next继续传入下一个数组对象
 				innerProcess(handlers[idx++], next) 
 		}
 		next()
}

outerProcess()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77

# 4、子路由

使用:require('express').Router()

app.use('/api', apiHandler) //api开头前缀

//apiHandler.js
const router = require('express').Router()
router.post('/news', (req, res) => {
 		res.send(req.path) //这里的path为/news
})
router.get('/list', (req, res) => {
 		res.send(req.path)
})
module.exports = router
1
2
3
4
5
6
7
8
9
10
11

# 5、错误处理

错误处理是在⼀个特殊签名的中间件中完成的,⼀般被添加到链的最后

  • 可以通过next传递错误
  • 可以是代码运⾏错误
  • 使用最后的中间件捕获
// 可以通过next传递错误
app.get('/test', (req, res, next) => {
   console.log('test')
   next(new Error('500 can only greet "world"'))
   // res.send('end')
})
// 可以是代码运⾏错误
app.use((req, res) => {
   if (req.query.greet !== 'world') {
      throw new Error('can only greet "world"')
   }
   res.status(200)
   res.send(`Hello ${req.query.greet} from Express`)
})
// 会被最后的中间件捕获,4个参数即可不必放置在最后
app.use((err, req, res, next) => {
   if (!err) {
      next()
      return
   }
   console.log('Error handler:', err.message)
   res.status(500)
   res.send('Uh-oh: ' + err.message)
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

# 3、Koa框架

# 1、基本用法

  • 需先建对象与express不同:const app = new Koa()
  • ctx(context)封装了res和req
    • 返回使用ctx.body,可使用async和await
      • ctx.body最终会转换为res.end
      • 执行时机:洋葱模型的最外层,第一个中间件promise有结果后,将ctx.body转换为res.end()
      • 异步使用时,不可放置异步函数中,需放置在await的promise中
    • 也可直接使用ctx.res和ctx.req
    • 常用属性:req、res、request、response
  • ctx属性:
const Koa = require('koa') // ⼀个类
const app = new Koa()

// 中间件,可以使⽤async await
app.use(async function(ctx){
 	// throw new Error('出错了')
  // ctx封装了req和res的功能,这⾥相当于res.end
  // 1、有输出,ctx.body在promise结果后
 	ctx.body = await Promise.resolve('zhuwa') 
  // 2、无输出
  setTimeout(()=>{
    ctx.body = 'zhuwa'
  }, 3000)
  
})

// 错误事件
app.on('error', function(err, ctx) {
 	console.log(err)
 	ctx.res.end(`500 错误`)
})

app.listen(3002, () => {
 	console.log('koa server started at 3002')
})

// req request res response的区别
app.use(async function(ctx){
   console.log(ctx.req.path) 			// 原⽣的req
   console.log(ctx.request.path) 	// koa封装的request
   console.log(ctx.request.req.path)
   console.log(ctx.path) 					// 代理到request
   ctx.body = await Promise.resolve('zhuwa') // ctx封装了req和res的功能,这⾥相当于res.end
   //ctx.throw(401, "err msg");
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

# 2、中间件

使用app.use,回调函数只能传一个,多个需多次注册

异步流程会等待后续中间件有结果时返回

app.use(async function(ctx, next) {
   console.log('1-1')
   await next()
   console.log('1-2')
})
app.use(async function(ctx, next) {
   console.log('2-1')
   await next()
   console.log('2-2')
})
app.use(async function(ctx, next) {
   console.log('3-1')
   await next()
   console.log('3-2')
})
//输出:1-1、2-1、3-1、3-2、2-2、1-2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
  • 手写koa-compose

核心代码参考:koa-compose/index.js

/**
* 函数:compose
* @params middleware: Function[]
* @return function fnCompose
* 
* 函数:fnCompose
* 作用:执⾏第0个,并传⼊ctx和⼀个next函数,让第0个⾃⼰决定何时执⾏第1个
* @params ctx next函数
* @return promise
* function fnCompose(ctx, next){ return promise }
*
* 函数:next
* 作用:执⾏第i个中间件,并⽤promise包裹起来
* @params i表示执⾏第⼏个
* @return 返回promise
* function next (i) {
* 	// 执⾏当前的fn,将ctx, next传⼊(绑定下⼀个i, i + 1)
* 	return promise
* }
*/

/**
* 注意的细节:边界情况处理
* 1、middleware必须是数组,其中每⼀项必须是fuction
* 2、最后⼀个中间件调⽤了next,i === length
* 3、⼀个中间件多次调⽤next会报错
* 异常捕获
*/
const handlers = [
  async function(ctx, next) {
     console.log('1-1')
     await next()
     console.log('1-2')
	},
  async function(ctx, next) {
     console.log('2-1')
     await next()
     console.log('2-2')
	},
  async function(ctx, next) {
  	  console.log('3-1')
   		await sleep(3)
   	  console.log('3-2')
  }
]

const compose = function(middleware) {
   let index = -1 //next只能调用一次方法
   return function(ctx, next) {
        function disptach(i) {
            if(index >= i) return Promise.reject('next multiples callback')
            index = i
            const fn = middleware[i]
            if(i === middleware.length) fn = next
            if(!fn) return Promise.resolve()
            try {
                return Promise.resolve(fn(ctx, disptach.bind(null, i + 1)))
            } catch (err) {
                return Promise.reject(err)
            }
        }
        return disptach(0)
   }
}

const fnCompose = compose(handlers)
const context = {}
fnCompose(context).then(()=>{
  console.log('通过ctx.body 设置 res.end')
}).catch(err => {
  console.log('err', err)
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72

# 3、与express比较

框架 express koa
模型 线性模型 洋葱模型
表现 同步表现为洋葱模型,但遇到异步,即使使⽤了 await next()也不会等待,会继续向下执⾏,因为next()返回undefined ⽆论同步,还是异步,都是洋葱模型,可以使⽤ await和next() 来等待下⼀个中间件执⾏完成
原理 使⽤回调, next(),返回undefined next(),返回promise,会把中间件⽤promise包裹起来
错误 错误处理是在⼀个特殊签名的中间件中完成的,它必须被添加到链的后⾯才能捕获到前⾯的错误 可以使⽤try catch 捕获中间件错误
注意 await next() 也不会等待下⼀个中间件异步完成 要使⽤await来等待异步操作;注意res.end的时机,等最外层的中间件(也就是第⼀个中间件)的promise有结果后,响应就会被返回 ctx.body ->res.end(ctx.body)

# 4、中间件编写

//编写⼀个计算请求的时⻓的中间件
// koa 中间件
app.use(async function(ctx, next) {
   console.time('serviceTime')
   await next()
   console.timeEnd('serviceTime')
})

// express 中间件
app.use(function(req, res, next) {
   console.time('service')
   next()
   res.once('close', () => {
   		console.timeEnd('service')
   });
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# 5、错误处理

// 可以使⽤try catch 捕获中间件错误
// 在最顶层捕获未处理的错误
app.use(async (ctx, next) => {
   try {
   		await next();
   } catch (err) {
   		const status = err.status || 500;
   		ctx.status = status;
   		ctx.type = "html";
   		ctx.body = `
   			<b>${status}</b> ${err}
   		`;
   		// emmit
   		ctx.app.emit("error", err, ctx);
   }
});

app.use(async (ctx, next) => {
   try {
       await next()
   } catch (err) {
       ctx.status = 400
       ctx.body = `Uh-oh: ${err.message}`
       console.log('Error handler:', err.message)
   }
})

app.use(async (ctx) => {
 		if (ctx.query.greet !== 'world') {
 				throw new Error('can only greet "world"')
 		}
    console.log('Sending response')
    ctx.status = 200
    ctx.body = `Hello ${ctx.query.greet} from Koa`
})

// 也可以使⽤总的错误事件来统⼀处理
// 错误事件
app.on('error', function(err) {
 		console.log(err)
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41

# 6、常用中间件

  • koa-router:路由
  • koa-static:静态资源
  • koa-mounut:子路由
  • koa-body:body解析
  • koa-parameter:参数校验
//1、koa-router
const KoaRouter = require('koa-router')
router.get('/page', async ctx => {
 		ctx.body = await new Promise((resolve, reject) => {
 				resolve('ok')
 		})
})
// 注册中间件
app.use(router.routes())
// 添加前缀
const router = new Router({ prefix: '/user' })
// 重定向
router.get('/home', async ctx => {
 	ctx.redirect('http://baidu.com');
});
// 路由参数
router.get('/:id', async ctx => {
   ctx.body = {
     msg: 'index page',
     params: ctx.params.id // 通过 ctx.params.id 获取到的是 1
   };
});
router.get('/', async ctx => {
   ctx.body = {
     msg: 'index page',
     query: ctx.query // ctx.query会获取url后⾯携带的参数对象
   };
});
// allowedMethod
// 如果没有这⼀⾏, 当使⽤其他请求⽅式请求user时, 会提示404
// 如果有这⼀⾏, 当使⽤其他请求⽅式请求user时, 提示405 method not allowed
app.use(router.allowedMethod());

//2、koa-static
const KoaStatic = require('koa-static')
app.use(KoaStatic(path.resolve(__dirname, '../dist/assets')))

//3、koa-mounut
const KoaMount = require('koa-mount')
// 添加前缀
app.use(KoaMount('/assets', KoaStatic(path.resolve(__dirname, '../dist/assets'))))

//4、koa-body
const bodyparser = require('koa-body');
// 在注册此中间件之后, 会在 ctx 上下⽂注⼊ ctx.request.body 属性 ⽤于获取客户端传递来的数据
app.use(bodyparser());
router.post('/', async ctx => {
   ctx.body = {
   code: 200,
   		//body 可以获取form表单数据, json数据
   		msg: ctx.request.body
   }
})

//5、koa-parameter
const parameter = require('koa-parameter');
parameter(app);
router.post('/', async ctx => {
   //接收⼀个对象
   ctx.verifyParams({
     // 校验的简写形式
     username: 'string',
     password: { type: 'string', required: true } // 或者也可以这样
   })
   ctx.body = { code: 1 }
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66

# 4、Sequalize

orm框架(对象关系映射)

# 1、安装

npm install --save sequelize
# 根据具体使⽤的数据库选择安装以下某个包
npm install --save pg pg-hstore # Postgres
npm install --save mysql2
npm install --save mariadb
npm install --save sqlite3
npm install --save tedious # Microsoft SQL Server
1
2
3
4
5
6
7

# 2、连接数据库

// 配置数据库连接
const sequelize = new Sequelize({
 	dialect: 'sqlite',
 	storage: path.join(__dirname, '../database.sqlite')
});
// 连接数据库
const connectDatabase = async () => {
   try {
   		await sequelize.authenticate();
   		console.log('数据库连接成功');
   } catch (error) {
   		console.error('数据库连接失败:', error);
   }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 3、model

作⽤:⽤来描述数据库表的结构

const User = sequelize.define('User', {
   // Model attributes
   id: {
     type: DataTypes.INTEGER(11),
     primaryKey: true, // 主键
     autoIncrement: true, 
   },
   firstName: {
     type: DataTypes.STRING,
     allowNull: false
   },
   lastName: {
     type: DataTypes.STRING
     // allowNull 默认为 true
   },
   age: {
   		type: DataTypes.INTEGER
   }
}, {
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# 4、增删改查

// 1、增加
// service
async function addUser(User, userInfo) {
 		console.log('addUser', User, userInfo)
 		const user = await User.create(userInfo);
 		return user
}
// controler
app.post('/user', async (req, res) => {
   const result = await services.User.addUser(req.body)
   res.send(result)
})

//2、改
// service
function updateUser(User, id, userInfo) {
 		console.log('updateUser', User, userInfo)
 		return User.update(
       {
       ...userInfo,
       },
       {
         where: {
            id, //查询条件
         }
     	 }
    )
}
// controller
app.put('/user/:id', async (req, res) => {
   // console.log(req.params.id)
   const result = await services.User.updateUser(req.params.id, req.body)
   res.send(result)
})

//3、查找
function getAllUsers(User) {
   return User.findAll(
   // {
   // attributes: ['id', 'firstName', 'lastName']
   // }
   )
}
function getUserById(User, id) {
   // return User.findOne({
   // where: {
   // id,
   // }
   // });
   return User.findByPk(id)
}
// 获取列表
app.get('/user', async (req, res) => {
   const result = await services.User.getAllUsers({})
   res.send(result)
})
// 获取⼀个
app.get('/user/:id', async (req, res) => {
   const userId = req.params.id
   if(!userId) {
     res.send('error id not found')
     return;
   }
   const result = await services.User.getUserById(userId)
   res.send(result)
})

//4、删除
async function deleteUserById(User, id) {
 		try {
 			const result = await User.destroy({
         where: { //where是指定查询条件
            id,
         } 
 			})
 			return result
 		} catch (e) {
 			return Promise.reject(e)
 		}
}
// 删除
app.delete('/user/:id', async (req, res) => {
 		try {
 				 const result = await services.User.deleteUserById(req.params.id)
         if(result === 1) {
         		res.send('delete success')
         } else {
         		res.send('delete fail')
         } 
 		} catch(e) {
 				res.send(e)
 		}
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
Last Updated: 8/22/2021, 10:45:45 PM