众所周知,koa2是基于nodejs的一款很是轻量级的服务端框架,其简单易上手的特性更是大大节省了前端人员开发服务端api的成本。尽管许多功能可以实现,可是做为一个有素养的开发人员,代码的层次性、后期可维护性都是须要考虑周到的。前端
实话说,按照koa官方文档来照葫芦画瓢,咱们的代码是写不漂亮的。node
这里须要咱们在编码以前有一个很是清晰的认识:咱们的代码如何组织?如何分层?如何复用?ios
在经历一系列的思考斟酌以及一些项目的实践以后,我总结了一些关于koa的开发技巧,可以大幅度的提升项目的代码质量,不再用让同伴笑话代码写的烂啦!程序员
以前咱们的路由老是手动注册的?大概是这样的:web
//app.js
const Koa = require('koa');
const app = new Koa();
const user = require('./app/api/user');
const store = require('./app/api/store');
app.use(user.routes());
app.use(classic.routes());
复制代码
对于写过koa项目的人来讲,这段代码是否是至关熟悉呢?其实如今只有两个路由文件还好,但实际上这样的文件数量庞大到必定的程度,再像这样引入再use方式未免会显得繁琐拖沓。那有没有办法让这些文件自动被引入、自动被use呢?数据库
有的。如今让咱们来安装一个很是好用的包:npm
npm install require-directory --save
复制代码
如今只须要这么作:json
//...
const Router = require('koa-router');
const requireDirectory = require('require-directory');
//module为固定参数,'./api'为路由文件所在的路径(支持嵌套目录下的文件),第三个参数中的visit为回调函数
const modules = requireDirectory(module, './app/api', {
visit: whenLoadModule
});
function whenLoadModule(obj) {
if(obj instanceof Router) {
app.use(obj.routes());
}
}
复制代码
因而可知,好的代码是能够提高效率的,这样的自动加载路由省去了不少注册配置的功夫,是否是很是酷炫?axios
相信不少人都这样作:路由注册代码写在了入口文件app.js中,之后进行相应中间件的导入也是写在这个文件。可是对于入口文件来讲,咱们是不但愿让它变得十分臃肿的,所以咱们能够适当地将一些操做抽离出来。后端
在根目录下建一个文件夹core,之后一些公共的代码都存放在这里。
//core/init.js
const requireDirectory = require('require-directory');
const Router = require('koa-router');
class InitManager {
static initCore(app) {
//把app.js中的koa实例传进来
InitManager.app = app;
InitManager.initLoadRouters();
}
static initLoadRouters() {
//注意这里的路径是依赖于当前文件所在位置的
//最好写成绝对路径
const apiDirectory = `${process.cwd()}/app/api`
const modules = requireDirectory(module, apiDirectory, {
visit: whenLoadModule
});
function whenLoadModule(obj) {
if(obj instanceof Router) {
InitManager.app.use(obj.routes())
}
}
}
}
module.exports = InitManager;
复制代码
如今在app.js中
const Koa = require('koa');
const app = new Koa();
const InitManager = require('./core/init');
InitManager.initCore(app);
复制代码
能够说已经精简不少了,并且功能的实现照样没有问题。
有时候,在两种不一样的环境下,咱们须要作不一样的处理,这时候就须要咱们提早在全局中注入相应的参数。
首先在项目根目录中,建立config文件夹:
//config/config.js
module.exports = {
environment: 'dev'
}
复制代码
//core/init.js的initManager类中增长以下内容
static loadConfig() {
const configPath = process.cwd() + '/config/config.js';
const config = require(configPath);
global.config = config;
}
复制代码
如今经过全局的global变量中就能够取到当前的环境啦。
在服务端api编写的过程当中,异常处理是很是重要的一环,由于不可能每一个函数返回的结果都是咱们想要的。不管是语法的错误,仍是业务逻辑上的错误,都须要让异常抛出,让问题以最直观的方式暴露,而不是直接忽略。关于编码风格,《代码大全》里面也强调过,在一个函数遇到异常时,最好的方式不是直接return false/null,而是让异常直接抛出。
而在JS中,不少时候咱们都在写异步代码,例如定时器,Promise等等,这就会产生一个问题,若是用try/catch的话,这样的异步代码中的错误咱们是没法捕捉的。例如:
function func1() {
try {
func2();
} catch (error) {
console.log('error');
}
}
function func2() {
setTimeout(() => {
throw new Error('error')
}, 1000)
}
func1();
复制代码
执行这些代码,你会发现过了一秒后程序直接报错,console.log('error')并无执行,也就是func1并无捕捉到func2的异常。这就是异步的问题所在。
那怎么解决这个坑呢?
最简便的方式是采起async-await。
async function func1() {
try {
await func2();
} catch (error) {
console.log('error');
}
}
function func2() {
return new Promise((resolve, reject) => {
setTimeout(() => {
reject()
}, 1000)
})
}
func1();
复制代码
在这里的异步函数被Promise封装,而后reject触发func1中的catch,这就捕捉到了func2中的异常。庆幸的是,像func2这样的异步代码,如今经常使用的库(如axios、sequelize)已经为咱们封装好了Promise对象,不用咱们本身去封装了,直接去经过async-await的方式去try/catch就好了。
忠告: 经过这种方式,只要是异步代码,执行以前必需要加await,不加会报Unhandled promise rejection的错误。血的教训!
//middlewares/exception.js
//这里的工做是捕获异常生成返回的接口
const catchError = async (ctx, next) => {
try {
await next();
} catch (error) {
if(error.errorCode) {
ctx.body = {
msg: error.msg,
error_code: error.errorCode,
request: `${ctx.method} ${ctx.path}`
};
} else {
//对于未知的异常,采用特别处理
ctx.body = {
msg: 'we made a mistake',
};
}
}
}
module.exports = catchError;
复制代码
到入口文件使用这个中间件。
//app.js
const catchError = require('./middlewares/exception');
app.use(catchError)
复制代码
接着咱们来以HttpException为例生成特定类型的异常。
//core/http-exception.js
class HttpException extends Error {
//msg为异常信息,errorCode为错误码(开发人员内部约定),code为HTTP状态码
constructor(msg='服务器异常', errorCode=10000, code=400) {
super()
this.errorCode = errorCode
this.code = code
this.msg = msg
}
}
module.exports = {
HttpException
}
复制代码
//app/api/user.js
const Router = require('koa-router')
const router = new Router()
const { HttpException } = require('../../core/http-exception')
router.post('/user', (ctx, next) => {
if(true){
const error = new HttpException('网络请求错误', 10001, 400)
throw error
}
})
module.exports = router;
复制代码
返回的接口这样:
这样就抛出了一个特定类型的错误。可是在业务中错误的类型是很是复杂的,如今我就把我编写的一些Exception类分享一下,供你们来参考:
//http-exception.js
class HttpException extends Error {
constructor(msg = '服务器异常', errorCode=10000, code=400) {
super()
this.error_code = errorCode
this.code = code
this.msg = msg
}
}
class ParameterException extends HttpException{
constructor(msg, errorCode){
super(400, msg='参数错误', errorCode=10000);
}
}
class NotFound extends HttpException{
constructor(msg, errorCode) {
super(404, msg='资源未找到', errorCode=10001);
}
}
class AuthFailed extends HttpException{
constructor(msg, errorCode) {
super(404, msg='受权失败', errorCode=10002);
}
}
class Forbidden extends HttpException{
constructor(msg, errorCode) {
super(404, msg='禁止访问', errorCode=10003);
this.msg = msg || '禁止访问';
this.errorCode = errorCode || 10003;
this.code = 404;
}
}
module.exports = {
HttpException,
ParameterException,
Success,
NotFound,
AuthFailed,
Forbidden
}
复制代码
对于这种常常须要调用的错误处理的代码,有必要将它放到全局,不用每次都导入。
如今的init.js中是这样的:
const requireDirectory = require('require-directory');
const Router = require('koa-router');
class InitManager {
static initCore(app) {
//入口方法
InitManager.app = app;
InitManager.initLoadRouters();
InitManager.loadConfig();
InitManager.loadHttpException();//加入全局的Exception
}
static initLoadRouters() {
// path config
const apiDirectory = `${process.cwd()}/app/api/v1`;
requireDirectory(module, apiDirectory, {
visit: whenLoadModule
});
function whenLoadModule(obj) {
if (obj instanceof Router) {
InitManager.app.use(obj.routes());
}
}
}
static loadConfig(path = '') {
const configPath = path || process.cwd() + '/config/config.js';
const config = require(configPath);
global.config = config;
}
static loadHttpException() {
const errors = require('./http-exception');
global.errs = errors;
}
}
module.exports = InitManager;
复制代码
JWT(即Json Web Token)目前最流行的跨域身份验证解决方案之一。它的工做流程是这样的:
1.前端向后端传递用户名和密码
2.用户名和密码在后端核实成功后,返回前端一个token(或存在cookie中)
3.前端拿到token并进行保存
4.前端访问后端接口时先进行token认证,认证经过才能访问接口。
那么在koa中咱们须要作哪些事情?
在生成token阶段:首先是验证帐户,而后生成token令牌,传给前端。
在认证token阶段: 完成认证中间件的编写,对前端的访问作一层拦截,token认证事后才能访问后面的接口。
先安装两个包:
npm install jsonwebtoken basic-auth --save
复制代码
//config.js
module.exports = {
environment: 'dev',
database: {
dbName: 'island',
host: 'localhost',
port: 3306,
user: 'root',
password: 'fjfj'
},
security: {
secretKey: 'lajsdflsdjfljsdljfls',//用来生成token的key值
expiresIn: 60 * 60//过时时间
}
}
//utils.js
//生成token令牌函数,uid为用户id,scope为权限等级(类型为数字,内部约定)
const generateToken = function(uid, scope){
const { secretKey, expiresIn } = global.config.security
//第一个参数为用户信息的js对象,第二个为用来生成token的key值,第三个为配置项
const token = jwt.sign({
uid,
scope
},secretKey,{
expiresIn
})
return token
}
复制代码
//前端传token方式
//在请求头中加上Authorization:`Basic ${base64(token+":")}`便可
//其中base64为第三方库js-base64导出的一个方法
//middlewares/auth.js
const basicAuth = require('basic-auth');
const jwt = require('jsonwebtoken');
class Auth {
constructor(level) {
Auth.USER = 8;
Auth.ADMIN = 16;
this.level = level || 1;
}
//注意这里的m是一个属性
get m() {
return async (ctx, next) => {
const userToken = basicAuth(ctx.req);
let errMsg = 'token不合法';
if(!userToken || !userToken.name) {
throw new global.errs.Forbidden();
}
try {
//将前端传过来的token值进行认证,若是成功会返回一个decode对象,包含uid和scope
var decode = jwt.verify(userToken.name, global.config.security.secretKey);
} catch (error) {
// token不合法
// 或token过时
// 抛异常
errMsg = '//根据状况定义'
throw new global.errs.Forbidden(errMsg);
}
//将uid和scope挂载ctx中
ctx.auth = {
uid: decode.uid,
scope: decode.scope
};
//如今走到这里token认证经过
await next();
}
}
}
module.exports = Auth;
复制代码
在路由相应文件中编写以下:
//中间件先行,若是中间件中认证未经过,则不会走到路由处理逻辑这里来
router.post('/xxx', new Auth().m , async (ctx, next) => {
//......
})
复制代码
在开发的过程,当项目的目录愈来愈复杂的时候,包的引用路径也变得愈来愈麻烦。曾经就出现过这样的导入路径:
const Favor = require('../../../models/favor');
复制代码
甚至还有比这个更加冗长的导入方式,做为一个有代码洁癖的程序员,实在让人看的很是不爽。其实经过绝对路径process.cwd()的方式也是能够解决这样一个问题的,可是当目录深到必定程度的时候,导入的代码也很是繁冗。那有没有更好的解决方式呢?
使用module-alias将路径别名就能够。
npm install module-alias --save
复制代码
//package.json添加以下内容
"_moduleAliases": {
"@models": "app/models"
},
复制代码
而后在app.js引入这个库:
//引入便可
require('module-alias/register');
复制代码
如今引入代码就变成这样了:
const Favor = require('@models/favor');
复制代码
简洁清晰了许多,也更容易让人维护。
当一个业务要进行多项数据库的操做时,拿点赞功能为例,首先你得在点赞记录的表中增长记录,而后你要将对应对象的点赞数加1,这两个操做是必需要一块儿完成的,若是有一个操做成功,另外一个操做出现了问题,那就会致使数据不一致,这是一个很是严重的安全问题。
咱们但愿若是出现了任何问题,直接回滚到未操做以前的状态。这个时候建议用数据库事务的操做。利用sequelize的transaction是能够完成的,把业务部分的代码贴一下:
async like(art_id, uid) {
//查找是否有重复的
const favor = await Favor.findOne({
where: { art_id, uid }
}
);
//有重复则抛异常
if (favor) {
throw new global.errs.LikeError('你已经点过赞了');
}
//db为sequelize的实例
//下面是事务的操做
return db.transaction(async t => {
//1.建立点赞记录
await Favor.create({ art_id, uid }, { transaction: t });
//2.增长点赞数
const art = await Art.getData(art_id, type);//拿到被点赞的对象
await art.increment('fav_nums', { by: 1, transaction: t });//加1操做
});
}
复制代码
sequelize中的transaction大概就是这样作的,官方文档是promise的方式,看起来实在太不美观,改为async/await方式会好不少,可是千万不要忘了写await。
关于koa2的代码优化,就先分享到这里,未完待续,后续会不断补充。欢迎点赞、留言!