Recently, I used KOA to build a Web server, including Session processing, routing design, project structure design, error processing, database reading and writing, etc. It is particularly cool to use. Here I make a summary for your reference. If there is anything wrong, welcome to discuss.

The project structure

-config.js-package. json -scripts/, used to store initial script files -logs/, used to store logs -static/, used to store front-end files -server/, used to store back-end codeCopy the code

server/

├─ App.js // Main file, Create the server, and use the corresponding middleware ├─ Controller // Response route, call the corresponding module in the Services, Return results │ ├ ─ ─ home. Js │ ├ ─ ─ strategy. Js │ └ ─ ─ the user. The js ├ ─ ─ models / / operation database │ ├ ─ ─ strategy. Js │ └ ─ ─ the user. The js ├ ─ ─ Routers / / routing definition │ ├ ─ ─ home. Js │ ├ ─ ─ routers. Js │ ├ ─ ─ the strategy. The js │ └ ─ ─ the user. The js ├ ─ ─ services/models/call, return the correct processing; To add such a module between the controller and the models, mainly considering the services of abstract │ ├ ─ ─ strategy. Js │ └ ─ ─ the user. The js └ ─ ─ utils / / public module, such as log, ├─ Datetime.js ├─ db.js ├─ log.js ├─ redis.jsCopy the code

The version of middleware used

    "bcrypt": "^ 3.0.6"."ioredis": "^ 4.9.5"."koa": "^ 2.7.0"."koa-bodyparser": "^ 2"."koa-redis": "^ 4.0.0"."koa-router": "^ 7.4.0"."koa-session-minimal": "^ 3.0.4"."koa-mysql-session": "^ hundreds." "."koa-static": "^ 5.0.0"."log4js": "^ 4.3.1." "."mysql": "^ 2.17.1"."superagent": "^ 5.0.6"
Copy the code


Create a web server

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 }; }}// Configure redis to store session information
const store = new redisStore({
  client: redis
});


app.use(bodyParser());
app.use(session({
  key: 'SESSION_ID'.store: store,
  cookie: {// Store the cookie configuration of the sessionId
    maxAge: 24*3600*1000.// Cookie validity period
    expires: ' '.// Cookie expiration time
    path: '/'.// Write the path to the cookie
    domain: config.domain, // Write the domain name of the cookie
    httpOnly: ' '.// Whether to obtain only in HTTP requests
    overwrite: ' '.// Whether overwriting is allowed
    secure: ' '.sameSite: ' '.signed: ' ',}})); app .use(logger)/ / the log processing
.use(handler)// Process the error message
.use(static(config.staticPath));// Configure the static resource path
app
.use(routers.routes())
.use(routers.allowedMethods());// Register the route

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

Copy the code


The Session

Since THE HTTP protocol cannot remember which user is using the protocol, another mechanism is needed to assist it. Session refers to the mechanism used by the server to remember which user is in use. Of course, the front-end also needs to be used (generally, the Session information, such as session_ID, is written into the cookie). HTTP protocol carries the cookie information. The server then uses the session_id to find the uid from mysql or Redis, so it knows who is using it.

Koa-session-minimal middleware has helped us deal with it. Of course, we need to tell it where the session information should be stored, which can be mysql or Redis.

Save session information to Redis

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

// Configure redis to store session information
const store = new redisStore({
  client: redis
});

app.use(session({
  key: 'SESSION_ID'.store: store,
  cookie: {// Store the cookie configuration of the sessionId
    maxAge: 24*3600*1000.// Cookie validity period
    expires: ' '.// Cookie expiration time
    path: '/'.// Write the path to the cookie
    domain: config.domain, // Write the domain name of the cookie
    httpOnly: ' '.// Whether to obtain only in HTTP requests
    overwrite: ' '.// Whether overwriting is allowed
    secure: ' '.sameSite: ' '.signed: ' ',}}));Copy the code

./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// Set the expiration time in seconds
})

module.exports = redis
Copy the code


Save session information to mysql

The problem with this method is that when many users log in continuously, there will be a lot of expired session information left in mysql, which needs to be cleaned up, which is quite troublesome. But redis can directly set an expiration time, very convenient, so I use Redis to store

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

// Configure mysql to store session information
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: {// Store the cookie configuration of the sessionId
    maxAge: 24*3600*1000.// Cookie validity period
    expires: ' '.// Cookie expiration time
    path: '/'.// Write the path to the cookie
    domain: 'localhost'.// Write the domain name of the cookie
    httpOnly: ' '.// Whether to obtain only in HTTP requests
    overwrite: ' '.// Whether overwriting is allowed
    secure: ' '.sameSite: ' '.signed: ' ',}}));Copy the code


Route Design

Koa supports registration and response of multiple routes, making it easy to design apis and processes based on modules

app.js

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

Phones.js combines routes from multiple modules in these routers

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;
Copy the code

Routers /user.js are user – related routes

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;
Copy the code


Design of project structure

Once the route is registered, it should respond to the route, return the result, correspond to the project,

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

The use of async/await

The use of async and await makes writing asynchronous code just like writing synchronous code, logically very clear and can solve callback hell very well.

Error handling

Errors in the project are handled in the controller by try/catch; Logical feeling is very clean, do not know what problem can have in actual project, the children’s shoes that has a lot of actual combat experience can say oh.

controller/user.js

Handles API related logic in controller;

Bcrypt was used in the project to encrypt and compare passwords;

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
}Copy the code

services/user.js

Because services can be multiple, you need a services module to encapsulate one layer;

In services, you should take into account various situations that can be retrieved from the database, such as the failure, failure, or success of retrieving user information from the user’s mobile phone, such as the following processing.

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
}
Copy the code

models/user.js

Models are similarly designed to encapsulate the database operations of the corresponding modules

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
}
Copy the code

utils/db.js

The operation module of mysql provides some interfaces for other modules to use

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
}Copy the code

utils/log.js

Use Log4JS to archive logs

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

const getCfg = (a)= >{
  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')
Copy the code