likes
comments
collection
share

nodejs启动http服务的三种方式

作者站长头像
站长
· 阅读数 21

我们首先通过一个石头剪刀布的游戏来看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路径中,把处理逻辑分成了三块,然后用中间件机制串起来,这样做的好处是我们可以把单独的逻辑提取出来,整个逻辑就非常清晰有条理。

洋葱圈的执行顺序

洋葱圈的执行顺序是从外到里,然后从里到外

nodejs启动http服务的三种方式

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()时,前面跟了一个awaitawait后面必须要接一个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
评论
请登录