nodejs启动http服务的三种方式
我们首先通过一个石头剪刀布的游戏来看nodejs
如何起一个服务的:
http.createServer
const url = require('url')
const querystring = require('querystring')
const game = require('./lib')
const http = require('http')
const fs = require('fs')
// 玩家赢的次数
let playerWon = 0
// 玩家连续出一样的值的次数
let sameCount = 0
let playerLastAction
http
.createServer(function (request, response) {
// 通过内置模块url,转换发送到该http服务上的http请求包的url,
// 将其分割成 协议(protocol)://域名(host):端口(port)/路径名(pathname)?请求参数(query)
const parseUrl = url.parse(request.url)
// 浏览器所有对这个服务器的请求,都会走到这个http.createServer的回调函数里
// 所以这里对不同的请求url做判断,就可以处理不同url的请求的返回
if (parseUrl.pathname == '/favicon.ico') {
// 如果请求url是浏览器icon,比如 http://localhost:3000/favicon.ico的情况
// 就返回一个200就好了
response.writeHead(200)
response.end()
return
}
if (parseUrl.pathname == '/game') {
// 如果请求url是游戏请求,比如 http://localhost:3000/game?action=rock的情况
// 就要把action解析出来,然后执行游戏逻辑
const query = querystring.parse(parseUrl.query)
const playerAction = query.action
// 如果统计的玩家胜利次数超过3
// 或者玩家出现过作弊的情况(sameCount=9代表玩家有过作弊行为)
if (playerWon >= 3 || sameCount == 9) {
response.writeHead(500)
response.end('我再也不和你玩了!')
return
}
// 当玩家操作与上次相同,则连续相同操作统计次数+1,否则统计清零
// 当玩家操作连续三次相同,则视为玩家作弊,把sameCount置为9代表有过作弊行为
if (playerLastAction && playerAction == playerLastAction) {
sameCount++
} else {
sameCount = 0
}
playerLastAction = playerAction
if (sameCount >= 3) {
response.writeHead(400)
response.end('你作弊!')
sameCount = 9
return
}
// 执行游戏逻辑
const gameResult = game(playerAction)
// 先返回头部
response.writeHead(200)
// 根据不同的游戏结果返回不同的说明
if (gameResult == 0) {
response.end('平局!')
} else if (gameResult == 1) {
response.end('你赢了!')
// 玩家胜利次数统计+1
playerWon++
} else {
response.end('你输了!')
}
}
// 如果访问的是根路径,则把游戏页面读出来返回出去
if (parseUrl.pathname == '/') {
fs.createReadStream(__dirname + '/index.html').pipe(response)
}
})
.listen(3000, () => {
console.log('server running at 3000')
})
http模块的属性和方法:
- createServer:返回http.Server的新实例,也就是起了一个服务。第一个参数是一个函数,当有一个请求过来的时候,就会触发request事件,然后就执行这个参数函数。
- response.writeHead:向请求发送响应头。
response
.writeHead(200, {
'Content-Length': Buffer.byteLength(body),
'Content-Type': 'text/plain'
})
.end('hello world')
该函数第一个参数是状态码,第二个参数是响应头headers。
此方法只能调用一次,并且必须在调用 response.end() 之前调用。
当已使用 response.setHeader() 设置时,则它们将与任何传给 response.writeHead() 的标头合并,其中传给 response.writeHead() 的标头优先。
-
response.end:此方法向服务器发出信号,表明所有响应头和正文都已发送;该服务器应认为此消息已完成。 response.end() 方法必须在每个响应上调用。
-
response.write: 通常使用
response.write
方法向前端返回数据,该方法可调用多次,你可以像console.log()
一样去使用它,返回的数据会被拼接到一起。
需要注意的是,必须调用response.end
方法结束请求,否则前端会一直处于等待状态,response.end
方法也可以用来向前端返回数据。
const server = http.createServer((request, response) => {
response.write('zhangsan')
response.write('lisi')
response.write('wangwu')
response.end('d')
})
express
我们学习一个库,首先需要了解这个库为了解决什么问题,它的主要功能有哪些?我们可以在npm上看下express的介绍,主要功能如下:
- 健壮的路由系统
- 简化http操作:它对http的重定向,缓存机制,内容协商等做了封装
- 支持多种模板引擎
- 提供了非常强大的脚手架,快速启动项目
下面使用express对石头剪刀布的游戏进行改造:
const fs = require('fs')
const game = require('./game')
const express = require('express')
// 玩家胜利次数,如果超过3,则后续往该服务器的请求都返回500
var playerWinCount = 0
// 玩家的上一次游戏动作
var lastPlayerAction = null
// 玩家连续出同一个动作的次数
var sameCount = 0
const app = express()
// 通过app.get设定 /favicon.ico 路径的路由
// .get 代表请求 method 是 get,所以这里可以用 post、delete 等。这个能力很适合用于创建 rest 服务
app.get('/favicon.ico', function (request, response) {
// 一句 status(200) 代替 writeHead(200); end();
response.status(200)
return
})
// 设定 /game 路径的路由
app.get(
'/game',
function (request, response, next) {
if (playerWinCount >= 3 || sameCount == 9) {
response.status(500)
response.send('我不会再玩了!')
return
}
// 通过next执行后续中间件
next()
// 当后续中间件执行完之后,会执行到这个位置
if (response.playerWon) {
playerWinCount++
}
},
function (request, response, next) {
// express自动帮我们把query处理好挂在request上
const query = request.query
const playerAction = query.action
if (!playerAction) {
response.status(400)
response.send()
return
}
if (lastPlayerAction == playerAction) {
sameCount++
if (sameCount >= 3) {
response.status(400)
response.send('你作弊!我再也不玩了')
sameCount = 9
return
}
} else {
sameCount = 0
}
lastPlayerAction = playerAction
// 把用户操作挂在response上传递给下一个中间件
response.playerAction = playerAction
next()
},
function (req, response) {
const playerAction = response.playerAction
const result = game(playerAction)
// 如果这里执行setTimeout,会导致前面的洋葱模型失效
// 因为playerWon不是在中间件执行流程所属的那个事件循环里赋值的
// setTimeout(()=> {
response.status(200)
if (result == 0) {
response.send('平局')
} else if (result == -1) {
response.send('你输了')
} else {
response.send('你赢了')
response.playerWon = true
}
// }, 500)
}
)
app.get('/', function (request, response) {
// send接口会判断你传入的值的类型,文本的话则会处理为text/html
// Buffer的话则会处理为下载
response.send(fs.readFileSync(__dirname + '/index.html', 'utf-8'))
})
app.listen(3000)
中间件-洋葱圈
express使用中间件机制对逻辑进行分开处理,这样就可以把单独的逻辑提炼出来,方便维护。
在上面的代码中当我们在game路径中,把处理逻辑分成了三块,然后用中间件机制串起来,这样做的好处是我们可以把单独的逻辑提取出来,整个逻辑就非常清晰有条理。
洋葱圈的执行顺序
洋葱圈的执行顺序是从外到里,然后从里到外:
function (request, response, next) {
if (playerWinCount >= 3 || sameCount == 9) {
response.status(500);
response.send('我不会再玩了!');
return;
}
// 通过next执行后续中间件
next();
// 当后续中间件执行完之后,会执行到这个位置
if (response.playerWon) {
playerWinCount++;
}
}
可以看到next()
后面的代码最后被执行。
洋葱圈的数据传递
如果上一个中间件要传递数据到下一个中间件里面,怎么办呢?可以把要传递到数据挂载到request对象上:
// 把用户操作挂在response上传递给下一个中间件
response.playerAction = playerAction
next();
express能否处理异步逻辑呢?
如下,我们把判断输赢的逻辑放在setTimeout里面执行,当执行到这个中间件的时候,不会等到setTimeout里面的回调执行完,而是直接跳过setTimeout继续执行next后面的代码。
function (req, response) {
const playerAction = response.playerAction;
const result = game(playerAction);
// 如果这里执行setTimeout,会导致前面的洋葱模型失效
// 因为playerWon不是在中间件执行流程所属的那个事件循环里赋值的
setTimeout(()=> {
response.status(200);
if (result == 0) {
response.send('平局')
} else if (result == -1) {
response.send('你输了')
} else {
response.send('你赢了')
response.playerWon = true;
}
}, 500)
}
这种异步的情况就会导致不符合洋葱圈的模型了,就诞生了koa框架。
koa
Koa is a middleware framework that can take two different kinds of functions as middleware: async function && common function
可以看出koa支持普通函数的中间件和异步函数的中间件。
// 异步函数
app.use(async (ctx, next) => {
const start = Date.now();
await next();
const ms = Date.now() - start;
console.log(`${ctx.method} ${ctx.url} - ${ms}ms`);
});
// 普通函数
app.use((ctx, next) => {
const start = Date.now();
// next是用来执行下游中间件的函数。它返回一个带有then函数的Promise,用于在完成后运行代码。
return next().then(() => {
const ms = Date.now() - start;
console.log(`${ctx.method} ${ctx.url} - ${ms}ms`);
});
})
为什么普通函数执行完了之后会返回一个promise
呢?
我们来看一下koa
中间件的执行流程:
function compose(middlewareList) {
return function(ctx) {
function dispatch(i) {
const fn = middlewareList[i]
try {
return Promise.resolve(
// fn 因为是个async函数本来返回一个promise,但是外面为什么还要包一层Promise.resolve呢,是因为为了防止用户传的中间件没有用async开头,那就不能用await next()
fn(ctx, dispatch.bind(null, i + 1))
)
} catch (err) {
return Promise.reject(err)
}
}
dispatch(0)
}
}
因为每次执行next()
时,前面跟了一个await
,await
后面必须要接一个promise
对象。因此,koa的中间件机制会对普通函数使用Promise.resolve
进行包裹,这样普通函数返回值就是一个promise
了。
koa使用async await
实现的中间件有如下能力:
- 有"暂停执行"的能力
- 在异步的情况下也符合洋葱圈模型
koa自身不带任何中间件
Koa is not bundled with any middleware.
比如路由系统,那么你需求单独去安装一个路由的中间件,express是自己集成了路由中间件,所以koa是一个特别轻量的框架系统。
npm install koa-mount
koa比express更加极致的request response的简化
每个中间件都接收一个Koa Context对象,该对象封装传入的request和对该消息的response。如下代码:
ctx.status = 200
ctx.body = 'hello world'
ctx.response.type简写ctx.type,ctx.response.body = ctx.body
// request
app.use(async (ctx, next) => {
if (!ctx.request.accepts('xml')) ctx.throw(406);
await next();
});
// response
app.use(async (ctx, next) => {
await next();
ctx.response.type = 'xml';
ctx.response.body = fs.createReadStream('really_large.xml');
});
使用koa改写上面的服务:
const koa = require('koa')
// 路由中间件
const mount = require('koa-mount')
const app = new koa()
app.use(
mount('/favicon.ico', function (ctx) {
// koa比express做了更极致的response处理函数
// 因为koa使用异步函数作为中间件的实现方式
// 所以koa可以在等待所有中间件执行完毕之后再统一处理返回值,因此可以用赋值运算符
ctx.status = 200
})
)
const gameKoa = new koa()
app.use(
// mount的第二个参数是一个中间件函数或者一个koa的实例,如果有多个中间件,那么需要创建一个koa的实例,然后在这个实例上挂载多个中间件
mount('/game', gameKoa)
)
// 利用use函数,进行中间件的注册
gameKoa.use(async function (ctx, next) {
if (playerWinCount >= 3) {
ctx.status = 500
ctx.body = '我不会再玩了!'
return
}
// 使用await 关键字等待后续中间件执行完成
await next()
// 就能获得一个准确的洋葱模型效果
if (ctx.playerWon) {
playerWinCount++
}
})
gameKoa.use(async function (ctx, next) {
const query = ctx.query
const playerAction = query.action
if (!playerAction) {
ctx.status = 400
return
}
if (sameCount == 9) {
ctx.status = 500
ctx.body = '我不会再玩了!'
}
if (lastPlayerAction == playerAction) {
sameCount++
if (sameCount >= 3) {
ctx.status = 400
ctx.body = '你作弊!我再也不玩了'
sameCount = 9
return
}
} else {
sameCount = 0
}
lastPlayerAction = playerAction
ctx.playerAction = playerAction
await next()
})
gameKoa.use(async function (ctx, next) {
const playerAction = ctx.playerAction
const result = game(playerAction)
// 对于一定需要在请求主流程里完成的操作,一定要使用await进行等待
// 否则koa就会在当前事件循环就把http response返回出去了
await new Promise(resolve => {
setTimeout(() => {
ctx.status = 200
if (result == 0) {
ctx.body = '平局'
} else if (result == -1) {
ctx.body = '你输了'
} else {
ctx.body = '你赢了'
ctx.playerWon = true
}
// 这里一定要resolve,不然外面一直处在等待的状态await
resolve()
}, 500)
})
})
app.use(
mount('/', function (ctx) {
ctx.body = fs.readFileSync(__dirname + '/index.html', 'utf-8')
})
)
app.listen(3000)
koa vs express
- express门槛更低,koa更加强大和优雅;
- express封装了更多的东西(包括路由,模板引擎),开发更加快捷,koa可定制性更高;
所以express更加适合于小型应用,koa适合大型的应用,或者更加可维护性的项目。
转载自:https://juejin.cn/post/7156425850669187108