Koa2系列第一篇:详解生成器

1,394 阅读4分钟

什么是Koa2?

koa-generator

Koa2提供了一个生成器koa-generator,用于Koa2开发者生成一个项目骨架,方便开发者开箱即用。

生成器主要功能包括:

  • 约定目录结构
  • 集成一些基础的、必要的中间件
  • app.js作为入口文件
  • bin/www作为启动入口
  • 支持静态服务器
  • 支持routes路由目录
  • 支持views视图目录,默认pug作为模板引

package.json


{
  "name": "koa-demo",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "start": "node bin/www", // 代码变动需要重启node进程
    "dev": "./node_modules/.bin/nodemon bin/www", // 代码变动,通过nodemon自动重启node进程
    "prd": "pm2 start bin/www",// 生产环境 pm2启动
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "dependencies": {
    "debug": "^4.1.1", // 根据Debug环境变量输出调试日志
    "koa": "^2.7.0", 
    "koa-bodyparser": "^4.2.1",// 解析body,主要针对post请求
    "koa-convert": "^1.2.0", // 兼容Koa2中间件写法
    "koa-json": "^2.0.2", // 对json更好对的支持
    "koa-logger": "^3.2.0", // 开发阶段的日志模块
    "koa-onerror": "^4.1.0", // 错误处理模块
    "koa-router": "^7.4.0", // 路由
    "koa-static": "^5.0.0", // HTTP静态服务器
    "koa-views": "^6.2.0", // 视图渲染
    "pug": "^2.0.3" // 模板引擎
  },
  "devDependencies": {
    "nodemon": "^1.19.1" // 自动重启node进程
  }
}

bin/www

bin/www是应用的启动入口。


#!/usr/bin/env node

/**
 * Module dependencies.启动文件
 */

var app = require('../app');
var debug = require('debug')('demo:server');
var http = require('http');

/**
 * Get port from environment and store in Express.
 */

var port = normalizePort(process.env.PORT || '3000');
// app.set('port', port);

/**
 * Create HTTP server.
 * http://nodejs.cn/api/http.html#http_class_http_server
 * 返回的是一个http.Server实例,继承自: <net.Server>
 * http://nodejs.cn/api/net.html#net_class_net_server
 * 而<net.Server>继承自: <EventEmitter>
 * http://nodejs.cn/api/events.html#events_class_eventemitter
 */
var server = http.createServer(app.callback());

/**
 * Listen on provided port, on all network interfaces.
 */

// 启动 HTTP 服务器用于监听连接
server.listen(port);
// on方法来自于<EventEmitter>
// error、listening事件来自于<net.Server>
server.on('error', onError);
server.on('listening', onListening);

/**
 * Normalize a port into a number, string, or false.
 */

function normalizePort(val) {
  var port = parseInt(val, 10);

  if (isNaN(port)) {
    // named pipe
    return val;
  }

  if (port >= 0) {
    // port number
    return port;
  }

  return false;
}

/**
 * Event listener for HTTP server "error" event.
 */

function onError(error) {
  // 关于失败的系统调用描述:http://nodejs.cn/api/errors.html#errors_error_syscall
  // https://man7.org/linux/man-pages/man2/syscalls.2.html
  if (error.syscall !== 'listen') {
    throw error;
  }

  var bind = typeof port === 'string'
    ? 'Pipe ' + port
    : 'Port ' + port;

  // handle specific listen errors with friendly messages
  switch (error.code) {
    case 'EACCES':
      // 拒绝访问)
      console.error(bind + ' requires elevated privileges');
      // http://nodejs.cn/api/process.html#process_process_exit_code
      // 调用 process.exit() 将强制进程尽快退出,即使还有尚未完全完成的异步操作,包括对 process.stdout 和 process.stderr 的 I/O 操作。
      process.exit(1);
      break;
    case 'EADDRINUSE':
      // 地址已经被使用
      console.error(bind + ' is already in use');
      process.exit(1);
      break;
    default:
      throw error;
  }
}

/**
 * Event listener for HTTP server "listening" event.
 */

function onListening() {
  // http://nodejs.cn/api/net.html#net_server_address
  var addr = server.address();
  var bind = typeof addr === 'string'
    ? 'pipe ' + addr
    : 'port ' + addr.port;
  debug('Listening on ' + bind);
}

app.js


const Koa = require('koa')
const app = new Koa()
const views = require('koa-views')
const json = require('koa-json')
const onerror = require('koa-onerror')
const bodyparser = require('koa-bodyparser')
const logger = require('koa-logger')

const index = require('./routes/index')
const users = require('./routes/users')

// 错误处理
onerror(app)

// 挂载中间件
app.use(bodyparser({
  enableTypes:['json', 'form', 'text']
}))
app.use(json())
app.use(logger())
app.use(require('koa-static')(__dirname + '/public'))

app.use(views(__dirname + '/views', {
  extension: 'pug'
}))

// 日志中间件
app.use(async (ctx, next) => {
  const start = new Date()
  await next()
  const ms = new Date() - start
  console.log(`${ctx.method} ${ctx.url} - ${ms}ms`)
})

// 路由
app.use(index.routes(), index.allowedMethods())
app.use(users.routes(), users.allowedMethods())

// error-handling
app.on('error', (err, ctx) => {
  console.error('server error', err, ctx)
});

module.exports = app

Koa2推荐的中间件写法是:


app.use(async(ctx, next) => {
   // ...
})

路由

路由本身也是一种中间件,不过我们由于职责拆分,将其独立到routes目录下。


const router = require('koa-router')()

// 视图渲染
router.get('/', async (ctx, next) => {
  await ctx.render('index', {
    title: 'Hello Koa 2!'
  })
})

// ctx.body是一个字符串
router.get('/string', async (ctx, next) => {
  ctx.body = 'koa2 string'
})

// ctx.body是一个json
router.get('/json', async (ctx, next) => {
  ctx.body = {
    title: 'koa2 json'
  }
})

module.exports = router

ctx.render方法不是Koa2自身的,而是koa-views中间件绑定到ctx上下文的。可见很多时候,我们可以通过中间件在ctx挂载一些业务方法,比如统一处理接口响应的json约定格式方法。

静态服务器


// app.js

app.use(require('koa-static')(__dirname + '/public'))

静态服务器主要存放一些静态资源,这里是通过koa-static中间件实现,它是基于koa-send模块的封装。

koa-static推荐与koa-router结合使用,这样就不需要每个请求都经过koa-static。


// koa-router支持多个中间件
router.get('/public', async(ctx,netx) => {
}, staticServer(resolve('./public)))

视图

关于模板引擎的配置


app.use(views(__dirname + '/views', {
  extension: 'pug'
}))

单纯的html不够强大,不能提供很好的复用。而模板引擎就是基于复用思想的一种定义模板的语法,使用时候它把数据与模板一起编译,生成html。


// 视图渲染
router.get('/', async (ctx, next) => {  
  // ctx.render方法第一个参数是模板相对路径,相对于views目录下 
  // 第二个参数就是传入到模板的数据
  await ctx.render('index', {
    title: 'Hello Koa 2!'
  })
})

模板引擎工作原理:

  • ctx.render()方法指定需要渲染的模板路径
  • 根据路径读取该文件模板
  • 通过模板引擎编译器把数据与模板内容一起编译为HTML字符串
  • 将响应头的Content-Type设置text/html; charset=utf-8
  • 通过http模块底层的res.write以及res.end方法把html字符串写到浏览器

ps: 推荐我的公众号:xyz编程日记