衆所周知,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的代碼優化,就先分享到這裏,未完待續,後續會不斷補充。歡迎點贊、留言!