Используйте koa для создания веб-сервисов, от маршрутизации до MySql.

koa

Недавно я использовал koa для создания веб-сервера, включая обработку сеансов, проектирование маршрутизации, проектирование структуры проекта, обработку ошибок, чтение и запись базы данных и т. д. Это очень удобно в использовании. Вот краткое изложение для справки. Что не так , добро пожаловать на обсуждение.

Структура проекта

-config.js
-package.json
-scripts/,用来存放初始脚本文件
-logs/,用来存放日志
-static/,用来存放前端文件
-server/,用来存放后端的代码

server/

├── app.js//主文件,创建server,并使用对应的中间件
├── controller//响应路由,调用services中对应的模块,返回结果
│   ├── home.js
│   ├── strategy.js
│   └── user.js
├── models//操作数据库
│   ├── strategy.js
│   └── user.js
├── routers//路由的定义目录
│   ├── home.js
│   ├── routers.js
│   ├── strategy.js
│   └── user.js
├── services//调用models,返回正确的处理;之所以要增加这么一个在controller和models之间的模块,主要还是考虑到services的抽象
│   ├── strategy.js
│   └── user.js
└── utils//公共的模块,比如日志,数据库的读写
    ├── datetime.js
    ├── db.js
    ├── log.js
    └── redis.js

Используемая версия ПО промежуточного слоя

    "bcrypt": "^3.0.6",
    "ioredis": "^4.9.5",
    "koa": "^2.7.0",
    "koa-bodyparser": "^4.2.1",
    "koa-redis": "^4.0.0",
    "koa-router": "^7.4.0",
    "koa-session-minimal": "^3.0.4",
    "koa-mysql-session": "^0.0.2",
    "koa-static": "^5.0.0",
    "log4js": "^4.3.1",
    "mysql": "^2.17.1",
    "superagent": "^5.0.6"


создать веб-сервер

app.js

const Koa = require('koa');
const app = new Koa();
const static = require('koa-static');
const bodyParser = require('koa-bodyparser');
const routers = require('./routers/routers');
const config = require('../config');
const session = require('koa-session-minimal');
const redisStore = require('koa-redis')
const redis = require('./utils/redis')
const log = require('./utils/log')

const logger = async (ctx, next)=>{
  log.info(`[uid: ${ctx.session.uid}] ${ctx.request.method}, ${ctx.request.url}`);
  await next();
}
const handler = async (ctx, next)=>{
  try {
    await next();
  } catch (error) {
    ctx.response.status = error.statusCode || error.status || 500;
    ctx.response.body = {
      message: error.message
    };
  }
}
// 配置存储session信息的redis
const store = new redisStore({
  client: redis
});


app.use(bodyParser());
app.use(session({
  key: 'SESSION_ID',
  store: store,
  cookie: {// 存放sessionId的cookie配置
    maxAge: 24*3600*1000, // cookie有效时长
    expires: '',  // cookie失效时间
    path: '/', // 写cookie所在的路径
    domain: config.domain, // 写cookie所在的域名
    httpOnly: '', // 是否只用于http请求中获取
    overwrite: '',  // 是否允许重写
    secure: '',
    sameSite: '',
    signed: '',
  }
}));

app
.use(logger)//处理log
.use(handler)//处理出错信息
.use(static(config.staticPath));//配置静态资源路径
app
.use(routers.routes())
.use(routers.allowedMethods());//注册路由

app.listen(config.port, ()=>{
  log.info('server is running on port:'+config.port);
});


Обработка сеанса

Поскольку протокол http не может запомнить, какой пользователь использует протокол, для помощи необходим другой механизм; сеанс относится к механизму, используемому сервером для запоминания того, какой пользователь использует, конечно, ему также требуется внешний интерфейс для взаимодействия (обычно Запишите информацию о сеансе, такую ​​как session_id, в файл cookie), протокол http будет передавать информацию о файле cookie, а затем сервер найдет соответствующий uid из mysql или redis через session_id, чтобы он знал, кто его использует.

Приведенный выше механизм обработки сложен, но на самом деле он очень прост в использовании, промежуточное ПО koa-session-minimal уже обработало его за нас, конечно, нам нужно указать ему, где должна храниться информация о сеансе, что может быть mysql. или редис;

Хранить информацию о сеансе в Redis

const session = require('koa-session-minimal');
const redisStore = require('koa-redis')
const redis = require('./utils/redis')

// 配置存储session信息的redis
const store = new redisStore({
  client: redis
});

app.use(session({
  key: 'SESSION_ID',
  store: store,
  cookie: {// 存放sessionId的cookie配置
    maxAge: 24*3600*1000, // cookie有效时长
    expires: '',  // cookie失效时间
    path: '/', // 写cookie所在的路径
    domain: config.domain, // 写cookie所在的域名
    httpOnly: '', // 是否只用于http请求中获取
    overwrite: '',  // 是否允许重写
    secure: '',
    sameSite: '',
    signed: '',
  }
}));

./utils/redis.js

const config = require('../../config')
const redisConfig = config.redis
const Redis = require('ioredis')

let redis = new Redis({
  host: redisConfig.HOST,
  port: redisConfig.PORT,
  password: redisConfig.PASSWORD,
  family: redisConfig.FAMILY,
  db: redisConfig.DB,
  ttl: redisConfig.TTL//设置过期时间,单位是秒
})

module.exports = redis


Хранить информацию о сеансе в mysql

У этого метода есть проблема. Когда много пользователей входят в систему постоянно, в mysql остается много просроченной и бесполезной информации о сеансе. Это необходимо очистить, что более проблематично. Но в Redis можно напрямую установить время истечения, что очень удобно, поэтому я использую Redis для хранения

const session = require('koa-session-minimal');
const MysqlSession = require('koa-mysql-session');

// 配置存储session信息的mysql
const store = new MysqlSession({
  user: config.database.USERNAME,
  password: config.database.PASSWORD,
  database: config.database.DATABASE,
  host: config.database.HOST,
});

app.use(session({
  key: 'SESSION_ID',
  store: store,
  cookie: {// 存放sessionId的cookie配置
    maxAge: 24*3600*1000, // cookie有效时长
    expires: '',  // cookie失效时间
    path: '/', // 写cookie所在的路径
    domain: 'localhost', // 写cookie所在的域名
    httpOnly: '', // 是否只用于http请求中获取
    overwrite: '',  // 是否允许重写
    secure: '',
    sameSite: '',
    signed: '',
  }
}));


дизайн маршрутизации

koa поддерживает регистрацию и ответ нескольких маршрутов, что очень удобно для оформления апи и обработки по модулю

app.js

const routers = require('./routers/routers');
app
.use(routers.routes()).use(routers.allowedMethods());

routers/routers.js, который объединяет здесь маршруты из нескольких модулей

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

const home = require('./home');
const user = require('./user');
const strategy = require('./strategy');

const router = new Router();

router.use(home.routes(), home.allowedMethods());
router.use(user.routes(), user.allowedMethods());
router.use(strategy.routes(), strategy.allowedMethods());

module.exports = router;

routers/user.js, пользовательские маршруты

const Router = require('koa-router');
const router = new Router();
const userCtl = require('../controller/user');

router.get('/user/list', userCtl.getUserList);
router.get('/user/info', userCtl.getUserInfo);
router.post('/user/signin', userCtl.userSignin);
router.post('/user/signup', userCtl.userSignup);

module.exports = router;


проектирование структуры проекта

После того, как маршрут прописан, он должен реагировать на маршрут, возвращать результат и соответствовать проекту.

routers/user.js --> controller/user.js -->  services/user.js --> models/user.js --> utils/db.js

Использование асинхронного/ожидания

Использование async и await делает написание асинхронного кода таким же, как написание синхронного кода.Логика очень ясна, и она может очень хорошо решить ситуацию с адом обратных вызовов.

обработка ошибок

Ошибки в проекте обрабатываются в контроллере и перехватываются try/catch, логика кажется очень чистой, я не знаю, будут ли какие-либо проблемы в реальном проекте, а детская обувь с большим практическим опытом может говорить о Это.

controller/user.js

логика, связанная с API, обрабатывается в контроллере;

В проекте используется bcrypt для шифрования и сравнения паролей;

const userService = require('../services/user')
const bcrypt = require('bcrypt')
const log = require('../utils/log')

const userSignin = async (ctx)=>{
  let result = {
    success: false,
    message: '',
    data: null,
  }, message = ''

  if (ctx.session.uid) {
    message = 'aleady login.'
    result.data = {
      uid: ctx.session.uid
    }
  } else {
    try {
      let formData = ctx.request.body
      let res = await userService.signin(formData)
      if (res) {
        if (bcrypt.compareSync(formData.password, res.password)) {
          ctx.session = {uid: res.id}
          result.success = true
          result.data = {uid: res.id, name: res.name}
        } else {
          message = 'phone or password error.'
        }
      } else {
        message = 'no such user.'
      }
      
    } catch (error) {
      message = 'login failed'
      log.error(message+', '+error)
    }
  }
  
  result.message = message
  ctx.body = result
}

module.exports = {
  getUserList,
  getUserInfo,
  userSignin,
  userSignup
}

services/user.js

Поскольку службы могут быть разнообразными, необходимо инкапсулировать слой с модулем служб;

В службах следует учитывать различные ситуации, полученные из базы данных, такие как получение информации о пользователе через мобильный телефон пользователя, которая может дать сбой, не может быть получена или может быть получена успешно, например следующая обработка.

const userModel = require('../models/user')

const signin = async (formData)=>{
  return new Promise((resolve, reject)=>{
    userModel.getUserByPhone(formData.phone)
    .then(res=>{
      if (Array.isArray(res)  && res.length> 0) {
        resolve(res[0])
      } else {
        resolve(null)
      }
    }, err=>{
      reject(err)
    })
  })
}

module.exports = {
  getUserList,
  getUserInfoById,
  signin,
  checkIsUserAdmin,
  modifyUser
}

models/user.js

Модели также аналогичны, но также инкапсулируют операции с базой данных соответствующих модулей.

const db = require('../utils/db')


const getUserList = async ()=> {
  let keys = ['id', 'phone', 'level', 'avatar', 'login_count', 'create_time', 'last_login_time']
  return await db.selectKeys('user_info', keys)
}

const getUserInfoById = async (uid)=> {
  let keys = ['id', 'phone', 'level', 'avatar', 'login_count', 'create_time', 'last_login_time']
  return await db.selectKeysById('user_info', keys, uid)
}

const getUserByPhone = async (phone)=> {
  let _sql = "select * from user_info where phone="+phone+" limit 1"
  return await db.query(_sql)
}

const modifyUser = async (user)=> {
  return await db.updateData('user_info', user, user.id)
}


module.exports = {
  getUserList,
  getUserInfoById,
  getUserByPhone,
  modifyUser
}

utils/db.js

Операционный модуль mysql предоставляет некоторые внешние интерфейсы для использования другими модулями.

const config = require('./../../config')
const dbConfig = config.database
const mysql = require('mysql')

const pool = mysql.createPool({
  host: dbConfig.HOST,
  user: dbConfig.USERNAME,
  password: dbConfig.PASSWORD,
  database: dbConfig.DATABASE
})

let query = (sql, values)=>{
  return new Promise((resolve, reject)=>{
    pool.getConnection((err, connection)=>{
      if (err) {
        resolve(err)
      } else {
        connection.query(sql, values, (err, rows)=>{
          if (err) {
            reject(err)
          } else {
            resolve(rows)
          }
          connection.release()
        })
      }
    })
  })
}

let createTable = sql => {
  return query(sql, [])
}

let selectAll = (table)=>{
  let _sql = "select * from ??"
  return query(_sql, [table])
}

let selectAllById = (table, id)=>{
  let _sql = "select * from ?? where id = ?"
  return query(_sql, [table, id])
}

let selectKeys = (table, keys)=>{
  let _sql = "select ?? from ??"
  return query(_sql, [keys, table])
}


module.exports = {
  query,
  createTable,
  selectAll,
  selectAllById,
  selectKeys,
  selectKeysById,
  selectKeysByKey,
  insertData,
  insertBatchData,
  updateData,
  deleteDataById
}

utils/log.js

Используйте log4js для архивирования и классификации журналов

const config = require('../../config')
const debug = config.debug
const logPath = config.logPath
const log4js = require('log4js')

const getCfg = ()=>{
  var cfg = {}
  if (debug) {
    cfg.type = 'console'
  } else {
    cfg.type = 'file'
    cfg.filename = logPath+'/server-'+new Date().toLocaleDateString()+'.log'
  }
  return cfg
}

log4js.configure({
  appenders: {
    access: getCfg()
  },
  categories: {
    default: {appenders: ['access'], level: 'info'}
  }
})

module.exports = log4js.getLogger('access')