记录学习koa后台搭建的点点滴滴(基础目录根据狼叔的koa-generator生成,然后进行相应改造,瞎倒腾-_-),项目大概结构为: web=>router=>controller=>services=>model=>mongodb
1、目录结构
<!--log4js日记文件夹,可通过配置指定-->
dir:logs_development
<!--pm2日志文件夹,可通过配置指定-->
dir:pm2
<!--主代码文件放置点-->
dir:src
<!--项目启动入口(www.js)-->
dir:bin
<!--项目配置文件夹-->
dir:config
<!--控制器,router直接调用-->
dir:controller
<!--中间件,logjs、jwt、catch等工具链的包装-->
dir:middleware
<!--mongoose模型定义(定义库表)-->
dir:model
<!--静态文件目录(javascript、css、images等)-->
dir:public
<!--koa-router路由文件夹-->
dir:router
<!--数据服务层,供controller调用(为了一些通用数据接口的复用)-->
dir:services
<!--一些工具函数的定义-->
dir:utils
<!--模板文件-->
dir:views
<!--入口文件-->
file:app.js
<!--配置支持import/export语法-->
file:boot.js
<!--pm2配置文件-->
file:pm2.config.js
package.json启动命令
"scripts": {
"start": "node ./boot.js",
"dev": "./node_modules/.bin/nodemon ./boot.js",
"pm2:start": "pm2 start pm2.config.js",
"pm2:stop": "pm2 stop all",
"show": "pm2 list",
"test": "echo \"Error: no test specified\" && exit 1"
}
2、配置项目
2.1、启动文件bin/www.js
介绍:做一些服务启动前的任务=》以http.createServer创建koa实例服务
import http from 'http'
import app from '../app'
import config from '../config/env_config'
import { initLogPath } from '../config/log_config'
// 服务启动前检查log4js相关文件是否创建,否:创建相关文件夹
initLogPath()
// 配置服务端口号
let port = normalizePort(config.port || process.env.PORT || '3000');
// 创建http服务
let server = http.createServer(app.callback());
// 监听及注册事件
server.listen(port);
server.on('error', onError);
server.on('listening', onListening);
function normalizePort(val) {
let 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) {
if (error.syscall !== 'listen') {
throw error;
}
let 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');
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() {
let addr = server.address();
let bind = typeof addr === 'string'
? 'pipe ' + addr
: 'port ' + addr.port;
app.debug('www', 'Listening on ' + bind);
}
2.2 主程序app.js
实例化koa、加载各种koa中间件,数据库连接等操作。
/**
* App主入口
*/
import Koa from 'koa'
import views from 'koa-views'
import json from 'koa-json'
import onerror from 'koa-onerror'
import bodyparser from 'koa-bodyparser'
import koaStatic from 'koa-static'
// import logger from 'koa-logger'
import router_handle from './utils/router_handle'
import db_handle from './utils/db_handle'
import debug_handle from './utils/debug_handle'
import logMiddleware from './middleware/log4j'
import corsMiddleware from './middleware/cors'
import catchMiddleware from './middleware/catch'
import { jwtPre, jwtAuth } from './middleware/jwt'
// import session from './middleware/session'
import { isDev } from './config/env_config'
const app = new Koa()
// error handler
onerror(app)
// 注册debug功能
const debug = debug_handle(app, isDev)
// 注册路由信息
const router = router_handle(app)
// 注册数据库连接
db_handle()
// middlewares
app.use(bodyparser({
enableTypes: ['json', 'form', 'text']
}))
app.use(json({ pretty: false, param: 'pretty' }))
// app.use(logger())
// 配置静态资源目录
app.use(koaStatic(__dirname + '/public'))
// 配置页面资源目录(使用pug语法)
app.use(views(__dirname + '/views', {
extension: 'pug'
}))
// logger(log4js中间件包装)
app.use(logMiddleware)
// cors(跨域cors中间件包装)
app.use(corsMiddleware)
// catch(异常处理中间件包装)
app.use(catchMiddleware)
// jwt(jwt认证中间件包装)
app.use(jwtPre)
app.use(jwtAuth)
// routes(注册路由)
app.use(router.routes(), router.allowedMethods())
// error-handling
app.on('error', (err, ctx) => {
console.error('server error', err, ctx)
})
export default app
2.3、路由注册(urils/router_handle.js)
根据文件夹结构创建路由,默认index.js文件注册的为根路由,比如: router=>api=>index.js将创建/api的路由,router=>api=>users.js创建/api/users路由router=>index.js创建/的路由。
读取指定路由文件目录,解析文件夹及文件然后创建koa路由。
import path from 'path'
import Router from 'koa-router'
import { parsedir, resolvefiles } from './file_handle'
export default app => {
const router = new Router()
const files = parsedir(path.resolve(__dirname, '../router'))
resolvefiles(files, (file, routePathsStr, entity) => {
app.debug('router_handle', '路由注册路径:', `/${routePathsStr}`)
app.debug('router_handle', '路由注册文件:', `${file.fileName + file.extName}`)
// app.debug('router_handle','路由路径信息:', `${JSON.stringify(file, null, 2)}`)
router.use(`/${routePathsStr}`, entity.routes(), entity.allowedMethods())
})
return router
}
utils/file_handle.js,根据需要自己配置需要导出文件的信息
// utils/file_handle.js
/**
* 路径解析及加载内容(根据目录结构注册动态路由使用)
*/
import fs from 'fs'
import path from 'path'
/**
* 解析目录
*/
export function parsedir(dirpath = __dirname, pPathStr = '') {
const files = fs.readdirSync(dirpath)
const dirFiles = []
files.forEach(file => {
// 获取绝对路径
let absolutePath = path.join(dirpath, file)
// 获取路径信息
let stat = fs.lstatSync(absolutePath)
// 是否为文件夹
let isDirectory = stat.isDirectory()
// 截取当前模块名称(替换父模块路径)
let curModuleName = file.replace(`${pPathStr}/`, '')
// 父模块路径信息
let pPaths = pPathStr.split('/').filter(p => !!p)
let extName = null
// 路径为文件读取文件名称并设置当前模块名称为文件名(不包含后缀名)
if (!isDirectory) {
extName = path.extname(absolutePath)
curModuleName = curModuleName.replace(extName, '')
}
// pPaths.push(curModuleName)
// 是否为index文件
let isIndex = curModuleName === 'index'
// 定义路径对象
let fileOption = {
pPaths: pPaths,
pPathStr: pPaths.join('/'),
isDir: isDirectory,
path: absolutePath,
isIndex: isIndex,
extName: extName,
children: null,
fileName: null,
deep: 0
}
if (fileOption.isDir) {
let deep = 0
let nextPaths = [...pPaths.slice(0), curModuleName]
let children = parsedir(absolutePath, nextPaths.join('/'))
fileOption.children = children
if (!children.length) {
return
}
// 获取当前路径文件深度(有子路径则当前深度+1,深度遍历)
for (let c of fileOption.children) {
if (c.deep > deep) {
deep = c.deep
}
}
fileOption.deep = deep + 1
} else {
// 文件后缀名及文件名
fileOption.extName = path.extname(absolutePath)
fileOption.fileName = curModuleName
}
// index文件放置在尾部(确保路由最后注册)
if (fileOption.isIndex) {
dirFiles.push(fileOption)
} else {
dirFiles.unshift(fileOption)
}
})
return dirFiles
}
/**
* 解析字符串路径,执行回调
*/
export function resolvefiles(files, resolve, exp = /\w+\.js$/) {
if (!files || !resolve) return
if (typeof resolve !== 'function') return
if (typeof files === 'function') {
files = files()
}
if (typeof files === 'object') {
files = Object.values(files)
}
if (!files instanceof Array) {
files = [...files]
}
// 每个层级根据路径深度排序-降序(deep高的优先处理)
files = files.sort((a, b) => b.deep - a.deep)
for (let f of files) {
if (f.isDir) {
// 文件夹路径递归处理
resolvefiles(f.children, resolve, exp)
} else if (exp.test(f.path)) {
// 文件路径注册
let routePaths = f.pPaths.slice()
if (!f.isIndex) {
routePaths.push(f.fileName)
}
let file_entity = require(f.path).default
if (file_entity) {
resolve(f, routePaths.join('/').replace(/\/_/g, '/:'), file_entity)
}
} else {
console.log(`Url:${f.path} is illegal.`)
}
}
}
2.4、权限认证模块
使用JWT认证方式,依赖koa-jwt,jsonwebtoken模块,文件目录:middleware/jwt.js。
import jwt from 'koa-jwt'
// import CryptoJS from 'crypto-js'
import util from 'util'
import { sign, verify } from 'jsonwebtoken'
import { createToken } from '../utils/token_handle'
// 认证token函数
const verifyPromise = util.promisify(verify)
// JWT加密私钥
export const JWT_SECRET_KEY = 'JWT_SECRET_KEY'
// AES加密私钥
// http://www.esitecms.com/archives/cryptojs-aes
// export const AES_SECRET_KEY = CryptoJS.enc.Utf8.parse('AES_SECRET_KEY')
// export const AES_SECRET_KEY = 'AES_SECRET_KEY'
// // AES加密配置
// export const AES_CONFIG = {
// mode: CryptoJS.mode.ECB,
// padding: CryptoJS.pad.Pkcs7
// }
// 每次请求是否刷新token
export const RENEW_TOKEN = true
// token过期时间
export const EXPIRE_TIME = 60 * 60 * 1000
// cookie key值
export const COOKIE_KEY = 'JWT:SMB_WEB_TOKEN'
// token key值
export const TOKEN_KEY = 'JWT:TOKEN_KEY'
// cookie配置参数
export const COOKIR_CONFIG = { path: '/' }
// 无需认证路径
export const UNLESS_PATH = [/login|register/, '/']
/**
* 配置jwt认证头中间件
* @param {koa上下文} ctx
* @param {next} next
*/
export const jwtPre = async (ctx, next) => {
let header = ctx.header
let request = ctx.request
let debug = ctx.app.debug
try {
const token = ctx.cookies.get(COOKIE_KEY, COOKIR_CONFIG) || request.query.token || request.body.token
if (token) {
try {
// let jwtToken = CryptoJS.AES.decrypt(token, AES_SECRET_KEY, AES_CONFIG).toString(CryptoJS.enc.Utf8)
let paylod = await verifyPromise(token, JWT_SECRET_KEY)
let currentTime = Date.now() / 1000
let shouldFreshToken = currentTime - paylod.iat > EXPIRE_TIME / 2
if (RENEW_TOKEN && shouldFreshToken) {
createToken(ctx, { name: paylod.name, id: paylod.id })
}
ctx.authUser = paylod
} catch (err) {
debug('jwtMiddleware', `Token verify fail.`)
}
}
await next()
debug('jwtMiddleware', ctx._matchedRoute)
} catch (err) {
let { status } = err
if (status === 401) {
if (header['Req-Type'] === 'restful') {
ctx.body = {
code: 401,
message: 'JWT Authorization failure.'
}
} else {
ctx.redirect('/')
}
} else {
throw err
}
}
}
/**
* 获取jwt签名token
* @param {签名数据} payload
* @param {签名过期时间} expires
*/
export const jwtSign = (payload, expires = EXPIRE_TIME) => {
return sign(payload, JWT_SECRET_KEY, { expiresIn: expires })
}
/**
* 获取jwt认证中间件
*/
export const jwtAuth = jwt({ secret: JWT_SECRET_KEY, cookie: COOKIE_KEY, tokenKey: TOKEN_KEY }).unless({ path: UNLESS_PATH })
注意:中间件应用顺序:app.use(jwtPre);app.use(jwtAuth)
createToken负责创建jwttoken及写入cookie,并返回创建的token
import { jwtSign, COOKIE_KEY, EXPIRE_TIME, COOKIR_CONFIG } from '../middleware/jwt'
export function createToken(ctx, data) {
let token = jwtSign(data)
ctx.cookies.set(COOKIE_KEY, token, Object.assign({
maxAge: EXPIRE_TIME,
httpOnly: true,
overwrite: true
}, COOKIR_CONFIG))
return token
}
3、请求流程
1、启动服务:yarn dev
2、访问服务,这里是127.0.0.1::10000
3、请求根据app.js中use的中间件顺序依次执行:bodyparser=>json=>koaStatic=>logMiddleware=>corsMiddleware=>catchMiddleware=>jwtPre=>jwtAuth=>routes,然后以洋葱式的模式依次处理,这里配置了访问路径符合[/login|register/, '/']
格式的不进行jwt认证,当你访问/
时,根据创建的跟路由进行处理,没有认证信息跳转至登录页:
router.get('index', '/', async (ctx, next) => {
if (ctx.authUser) {
await ctx.render('index', {
title: 'Hello Koa 2!'
})
} else {
await ctx.render('login', {
title: 'Welcome to use smb system.',
type: 'EAP202'
})
}
})
4、无认证时,前端发起登录请求post:/api/login
import Router from 'koa-router'
import userCtrl from '../../controller/user'
const router = new Router()
router.post('/login', userCtrl.login)
router.get('/test', userCtrl.test)
router.get('/register', userCtrl.register)
export default router
userCtrl.login处理器接收并处理 controller/user.js=>
userCtrl根据用户名及密码调用userServices服务校验用户,通过则创建token:createToken,否则返回 --登录失败(账号或密码错误)信息
/**
* user业务逻辑层
*/
import * as userServices from '../services/user'
import { createToken } from '../utils/token_handle'
import { resOk, resErr } from '../utils/res_handle'
class UserController {
static async login(ctx) {
let {
username,
password
} = ctx.request.body
let result = await userServices.login({ name: username, password })
if (result) {
let token = createToken(ctx, { name: result.name, id: result._id })
resOk(ctx, { token })
} else {
resErr(ctx, '登录失败(账号或密码错误)')
}
}
static async test(ctx) {
ctx.body = { name: 'test' }
}
static async register(ctx) {
let user = {
name: 'czg1',
nickName: 'sb',
password: '46f9cbb4666fd9b109436288a339d72d',
profession: ['IT', 'YA'],
age: 10,
workYear: 3,
desc: 'SB of ZDJ',
sex: 'female'
}
let result = await userServices.register(user)
if (result) {
resOk(ctx, result, '注册成功')
} else {
resErr(ctx, '用户名已被注册')
}
}
}
export default UserController
services/user.js
userServices调用userModel进行数据库查询操作
import UserModel from '../model/User'
export const login = async ({ name, password }) => {
let user = await UserModel.findOne({ name, password }, { name: 1, _id: 1 })
return user
}
export const register = async (u) => {
let isExist = await UserModel.findOne({ name: u.name }, { _id: 1 })
if (isExist) {
return null
}
let userModel = new UserModel(u)
let user = await userModel.save()
console.log(user)
return user
}
model/User.js
基于mongoose定义库表结构及实例方法、静态方法。
/**
* User model领域层(对接数据库mongoose)
*/
import {
Schema,
model
} from 'mongoose'
const COLLECTION_NAME = 'User'
const UserSchema = new Schema({
name: String,
createAt: {
type: Date,
default: Date.now
},
nickName: String,
password: String,
profession: [String],
age: Number,
workYear: Number,
desc: String,
sex: {
type: String,
enum: ['male', 'female'],
default: 'male'
},
token: String
}, {
versionKey: false,
collation: COLLECTION_NAME
})
/**
* 定义实例方法
* let user =new User({sex:'male'})
* user.getUserBySex((err,users)={
* console.log(users)
* })
*/
UserSchema.methods.getUserBySex = function (cb) {
return this.model(COLLECTION_NAME).find({
sex: this.sex
}, cb)
}
/**
* 定义静态方法
* User.getUserBySex('male',(err,users)=>{
* console.log(users)
* })
*/
UserSchema.static.getUserBySex = function (sex, cb) {
return this.find({
sex
}, cb)
}
const User = model(COLLECTION_NAME, UserSchema)
export default User
4、总结
大体的项目结构就是这样了,做个记录防止遗忘,以后再看看,以晕死==