RESTful API風格
在開發(fā)之前先回顧一下,RESTful API 是什么? RESTful 是一種 API 設計風格,并不是一種強制規(guī)范和標準,它的特點在于請求和響應都簡潔清晰,可讀性強。不管 API 屬于哪種風格,只要能夠滿足需要,就足夠了。API 格式并不存在絕對的標準,只存在不同的設計風格。
API 風格
一般來說 API 設計包含兩部分: 請求和響應。
- 請求:請求URL、請求方法、請求頭部信息等。
- 響應:響應體和響應頭部信息。
先來看一個請求 url 的組成:
https://www.baidu.com:443/api/articles?id=1
// 請求方法:GET
// 請求協議:protocal: https
// 請求端口:port: 443
// 請求域名:host: www.baidu.com
// 請求路徑:pathname: /api/articles
// 查詢字符串:search: id=1
根據 URL 組成部分:請求方法、請求路徑和查詢字符串,我們有幾種常見的 API 風格。比如當刪除 id=1 的作者編寫的類別為 2 的所有文章時:
// 純請求路徑
GET https://www.baidu.com/api/articles/delete/authors/1/categories/2
// 一級使用路徑,二級以后使用查詢字符串
GET https://www.baidu.com/api/articles/delete/author/1?category=2
// 純查詢字符串
GET https://www.baidu.com/api/deleteArticles?author=1&category=2
// RESTful風格
DELETE https://www.baidu.com/api/articles?author=1&category=2
前面三種都是 GET 請求,主要的區(qū)別在于多個查詢條件時怎么傳遞查詢字符串,有的通過使用解析路徑,有的通過解析傳參,有的兩者混用。同時在描述 API 功能時,可以使用 articles/delete ,也可以使用 deleteArticles 。而第四種 RESTful API 最大的區(qū)別在于行為動詞 DELETE 的位置,不在 url 里,而在請求方法中.
RESTful設計風格
REST(Representational State Transfer 表現層狀態(tài)轉移) 是一種設計風格,而不是標準。主要用于客戶端和服務端的API交互,我認為它的約定大于它的定義,使得 api 在設計上有了一定的規(guī)范和原則,語義更加明確,清晰。
我們一起來看看 RESTFul API 有哪些特點:
- 基于“資源”,數據也好、服務也好,在
RESTFul設計里一切都是資源,
資源用URI(Universal Resource Identifier 通用資源標識)來表示。 - 無狀態(tài)性。
-
URL中通常不出現動詞,只有名詞。 -
URL語義清晰、明確。 - 使用
HTTP的GET、POST、DELETE、PUT來表示對于資源的增刪改查。 - 使用
JSON不使用XML。
舉個栗子,也就是后面要實現的 api 接口:
GET /api/blogs:查詢文章
POST /api/blogs:新建文章
GET /api/blogs/ID:獲取某篇指定文章
PUT /api/blogs/ID:更新某篇指定文章
DELETE /api/blogs/ID:刪除某篇指定文章
關于更多RESTful API 的知識,小伙伴們可以戳:這里。
項目初始化
什么是Koa2
Koa官方網址。官方介紹:Koa 是一個新的 web 框架,由 Express 幕后的原班人馬打造, 致力于成為 web 應用和 API 開發(fā)領域中的一個更小、更富有表現力、更健壯的基石。
Koa2的安裝與使用對Node.js的版本也是有要求的,因為node.js 7.6版本 開始完全支持async/await,所以才能完全支持Koa2。
Koa2 是 Koa 框架的最新版本,Koa3 還沒有正式推出,Koa1.X 正走在被替換的路上。Koa2 與 Koa1 的最大不同,在于 Koa1 基于 co 管理 Promise/Generator 中間件,而 Koa2 緊跟最新的 ES 規(guī)范,支持到了 Async Function(Koa1 不支持),兩者中間件模型表現一致,只是語法底層不同。
在 Express 里面,不同時期的代碼組織方式雖然大為不同,但縱觀 Express 多年的歷程,他依然是相對大而全,API 較為豐富的框架,它的整個中間件模型是基于 callback 回調,而 callback 常年被人詬病。
簡單來說 Koa 和 Express 的最大的區(qū)別在于 執(zhí)行順序 和 異步的寫法 ,同時這也映射了 js 語法在處理異步任務上的發(fā)展歷程。關于異步和兩種框架區(qū)別,不在這里做過多探討。來看看 Koa 中間件洋蔥圈模型:

創(chuàng)建Koa2項目
創(chuàng)建文件 blog-api ,進入到該目錄:
npm init
安裝 Koa:
yarn add koa
安裝 eslint, 這個選擇安裝,可以根據自己的需求來規(guī)范自己的代碼,下面是我配置的 eslint:
yarn add eslint -D
yarn add eslint-config-airbnb-base -D
yarn add eslint-plugin-import -D
根目錄下新建文件 .eslintrc.js 和 .editorconfig:
// .eslintrc.js
module.exports = {
root: true,
globals: {
document: true,
},
extends: 'airbnb-base',
rules: {
'no-underscore-dangle': 0,
'func-names': 0,
'no-plusplus': 0,
},
};
// .editorconfig
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
在根目錄下新建文件 app.js:
const Koa = require('koa');
const app = new Koa();
app.use(async (ctx) => {
ctx.body = 'Hello World';
});
app.listen(3000);
通過命令啟動項目:
node app.js
在瀏覽器打開 http://localhost:3000/:

項目開發(fā)
目錄結構
規(guī)劃項目結構,創(chuàng)建對應的文件夾:
blog-api
├── bin // 項目啟動文件
├── config // 項目配置文件
├── controllers // 控制器
├── dbhelper // 數據庫操作
├── error // 錯誤處理
├── middleware // 中間件
├── models // 數據庫模型
├── node_modules
├── routers // 路由
├── util // 工具類
├── README.md // 說明文檔
├── package.json
├── app.js // 入口文件
└── yarn.lock
自動重啟
在編寫調試項目,修改代碼后,需要頻繁的手動close掉,然后再重新啟動,非常繁瑣。安裝自動重啟工具 nodemon :
yarn add nodemon -D
再安裝 cross-env,主要為設置環(huán)境變量兼容用的 :
yarn add cross-env
在 package.json 的 scripts 中增加腳本:
{
"name": "blog-api",
"version": "1.0.0",
"description": "個人博客后臺api",
"main": "app.js",
"scripts": {
"dev": "cross-env NODE_ENV=development nodemon ./app.js",
"rc": "cross-env NODE_ENV=production nodemon ./app.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "Mingme <419654548@qq.com>",
"license": "ISC",
"dependencies": {
"cross-env": "^7.0.2",
"koa": "^2.13.0",
"koa-router": "^10.0.0"
},
"devDependencies": {
"eslint": "^7.13.0",
"eslint-config-airbnb-base": "^14.2.1",
"eslint-plugin-import": "^2.22.1",
"nodemon": "^2.0.6"
}
}
這時候就能通過我們設置的腳本運行項目,修改文件保存后就會自動重啟了。
以生產模式運行
yarn rc
// 或者
npm run rc
以開發(fā)模式運行
yarn dev
// 或者
npm run dev
koa 路由
路由(Routing)是由一個 URI(或者叫路徑) 和一個特定的 HTTP 方法 (GET、POST 等) 組成的,涉及到應用如何響應客戶端對某個網站節(jié)點的訪問。
yarn add koa-router
接口統一以 /api 為前綴,比如:
http://localhost:3000/api/categories
http://localhost:3000/api/blogs
在 config 目錄下創(chuàng)建 index.js:
// config/index.js
module.exports = {
apiPrefix: '/api',
};
在 routers 目錄下創(chuàng)建 index.js , category.js , blog.js :
// routers/category.js
const router = require('koa-router')();
router.get('/', async (ctx) => {
// ctx 上下文 context ,包含了request 和response等信息
ctx.body = '我是分類接口';
});
module.exports = router;
// routers/blog.js
const router = require('koa-router')();
router.get('/', async (ctx) => {
ctx.body = '我是文章接口';
});
module.exports = router;
// routers/index.js
const router = require('koa-router')();
const { apiPrefix } = require('../config/index');
const blog = require('./blog');
const category = require('./category');
router.prefix(apiPrefix);
router.use('/blogs', blog.routes(), blog.allowedMethods());
router.use('/categories', category.routes(), category.allowedMethods());
module.exports = router;
在 app.js 中修改代碼,引入路由:
// app.js
const Koa = require('koa');
const app = new Koa();
const routers = require('./routers/index');
// routers
app.use(routers.routes()).use(routers.allowedMethods());
app.listen(3000);
本地啟動項目,看看效果:


根據不同的路由顯示不同的內容,說明路由沒問題了。
GET 請求
接下來看一下參數傳遞,假如是請求 id 為 1 的文章,我們 GET 請求一般這么寫:
http://localhost:3000/api/blogs/1
http://localhost:3000/api/blogs?id=1
// routers/blog.js
const router = require('koa-router')();
router.get('/', async (ctx) => {
/**
在 koa2 中 GET 傳值通過 request 接收,但是接收的方法有兩種:query 和 querystring。
query:返回的是格式化好的參數對象。
querystring:返回的是請求字符串。
*/
ctx.body = `我是文章接口id: ${ctx.query.id}`;
});
// 動態(tài)路由
router.get('/:id', async (ctx) => {
ctx.body = `動態(tài)路由文章接口id: ${ctx.params.id}`;
});
module.exports = router;
如圖:


POST/PUT/DEL
GET 把參數包含在 URL 中,POST 通過 request body 傳遞參數。
為了方便使用 koa-body 來處理 POST 請求和文件上傳,或者使用 koa-bodyparser 和 koa-multer 也可以。
yarn add koa-body
為了統一數據格式,使數據JSON化,安裝 koa-json:
yarn add koa-json
使用 koa-logger 方便調試:
yarn add koa-logger
在 app.js 里引入中間件:
const Koa = require('koa');
const path = require('path');
const app = new Koa();
const koaBody = require('koa-body');
const json = require('koa-json');
const logger = require('koa-logger');
const routers = require('./routers/index');
// middlewares
app.use(koaBody({
multipart: true, // 支持文件上傳
formidable: {
formidable: {
uploadDir: path.join(__dirname, 'public/upload/'), // 設置文件上傳目錄
keepExtensions: true, // 保持文件的后綴
maxFieldsSize: 2 * 1024 * 1024, // 文件上傳大小
onFileBegin: (name, file) => { // 文件上傳前的設置
console.log(`name: ${name}`);
console.log(file);
},
},
},
}));
app.use(json());
app.use(logger());
// routers
app.use(routers.routes()).use(routers.allowedMethods());
app.listen(3000);
在 routers/blog.js 下添加路由:
// routers/blog.js
const router = require('koa-router')();
router.get('/', async (ctx) => {
ctx.body = `我是文章接口id: ${ctx.query.id}`;
});
// 動態(tài)路由
router.get('/:id', async (ctx) => {
ctx.body = `動態(tài)路由文章接口id: ${ctx.params.id}`;
});
router.post('/', async (ctx) => {
ctx.body = ctx.request.body;
});
router.put('/:id', async (ctx) => {
ctx.body = `PUT: ${ctx.params.id}`;
});
router.del('/:id', async (ctx) => {
ctx.body = `DEL: ${ctx.params.id}`;
});
module.exports = router;
測試一下:



錯誤處理
在請求過程中,還需要將返回結果進行一下包裝,發(fā)生異常時,如果接口沒有提示語,狀態(tài)碼的返回肯定是不友好的,下面定義幾個常用的錯誤類型。
在 error 目錄下創(chuàng)建 api_error_map.js 、 api_error_name.js 、 api_error.js:
// error/api_error_map.js
const ApiErrorNames = require('./api_error_name');
const ApiErrorMap = new Map();
ApiErrorMap.set(ApiErrorNames.NOT_FOUND, { code: ApiErrorNames.NOT_FOUND, message: '未找到該接口' });
ApiErrorMap.set(ApiErrorNames.UNKNOW_ERROR, { code: ApiErrorNames.UNKNOW_ERROR, message: '未知錯誤' });
ApiErrorMap.set(ApiErrorNames.LEGAL_ID, { code: ApiErrorNames.LEGAL_ID, message: 'id 不合法' });
ApiErrorMap.set(ApiErrorNames.UNEXIST_ID, { code: ApiErrorNames.UNEXIST_ID, message: 'id 不存在' });
ApiErrorMap.set(ApiErrorNames.LEGAL_FILE_TYPE, { code: ApiErrorNames.LEGAL_FILE_TYPE, message: '文件類型不允許' });
ApiErrorMap.set(ApiErrorNames.NO_AUTH, { code: ApiErrorNames.NO_AUTH, message: '沒有操作權限' });
module.exports = ApiErrorMap;
// error/api_error_name.js
const ApiErrorNames = {
NOT_FOUND: 'not_found',
UNKNOW_ERROR: 'unknow_error',
LEGAL_ID: 'legal_id',
UNEXIST_ID: 'unexist_id',
LEGAL_FILE_TYPE: 'legal_file_type',
NO_AUTH: 'no_auth',
};
module.exports = ApiErrorNames;
// error/api_error.js
const ApiErrorMap = require('./api_error_map');
/**
* 自定義Api異常
*/
class ApiError extends Error {
constructor(errorName, errorMsg) {
super();
let errorInfo = {};
if (errorMsg) {
errorInfo = {
code: errorName,
message: errorMsg,
};
} else {
errorInfo = ApiErrorMap.get(errorName);
}
this.name = errorName;
this.code = errorInfo.code;
this.message = errorInfo.message;
}
}
module.exports = ApiError;
在 middleware 目錄下創(chuàng)建 response_formatter.js 用來處理 api 返回數據的格式化:
// middleware/response_formatter.js
const ApiError = require('../error/api_error');
const ApiErrorNames = require('../error/api_error_name');
const responseFormatter = (apiPrefix) => async (ctx, next) => {
if (ctx.request.path.startsWith(apiPrefix)) {
try {
// 先去執(zhí)行路由
await next();
if (ctx.response.status === 404) {
throw new ApiError(ApiErrorNames.NOT_FOUND);
} else {
ctx.body = {
code: 'success',
message: '成功!',
result: ctx.body,
};
}
} catch (error) {
// 如果異常類型是API異常,將錯誤信息添加到響應體中返回。
if (error instanceof ApiError) {
ctx.body = {
code: error.code,
message: error.message,
};
} else {
ctx.status = 400;
ctx.response.body = {
code: error.name,
message: error.message,
};
}
}
} else {
await next();
}
};
module.exports = responseFormatter;
順便安裝 koa 的錯誤處理程序 hack :
yarn add koa-onerror
在 app.js 中添加代碼:
const Koa = require('koa');
const path = require('path');
const app = new Koa();
const onerror = require('koa-onerror');
const koaBody = require('koa-body');
const json = require('koa-json');
const logger = require('koa-logger');
const responseFormatter = require('./middleware/response_formatter');
const { apiPrefix } = require('./config/index');
const routers = require('./routers/index');
// koa的錯誤處理程序hack
onerror(app);
// middlewares
app.use(koaBody({
multipart: true, // 支持文件上傳
formidable: {
formidable: {
uploadDir: path.join(__dirname, 'public/upload/'), // 設置文件上傳目錄
keepExtensions: true, // 保持文件的后綴
maxFieldsSize: 2 * 1024 * 1024, // 文件上傳大小
onFileBegin: (name, file) => { // 文件上傳前的設置
console.log(`name: ${name}`);
console.log(file);
},
},
},
}));
app.use(json());
app.use(logger());
// response formatter
app.use(responseFormatter(apiPrefix));
// routers
app.use(routers.routes()).use(routers.allowedMethods());
// 監(jiān)聽error
app.on('error', (err, ctx) => {
// 在這里可以對錯誤信息進行一些處理,生成日志等。
console.error('server error', err, ctx);
});
app.listen(3000);
在后續(xù)開發(fā)中,若遇到異常,將異常拋出即可。
連接數據庫
mongoDB 數據庫的安裝教程 :Linux 服務器(CentOS)安裝配置mongodb+node。
mongoose : nodeJS 提供連接 mongodb 的一個庫。
mongoose-paginate :mongoose 的分頁插件。
mongoose-unique-validator :可為 Mongoose schema 中的唯一字段添加預保存驗證。
yarn add mongoose
yarn add mongoose-paginate
yarn add mongoose-unique-validator
在 config/index.js 中增加配置:
module.exports = {
port: process.env.PORT || 3000,
apiPrefix: '/api',
database: 'mongodb://localhost:27017/test',
databasePro: 'mongodb://root:123456@110.110.110.110:27017/blog', // mongodb://用戶名:密碼@服務器公網IP:端口/庫的名稱
};
在 dbhelper 目錄下創(chuàng)建 db.js:
const mongoose = require('mongoose');
const config = require('../config');
mongoose.Promise = global.Promise;
const IS_PROD = ['production', 'prod', 'pro'].includes(process.env.NODE_ENV);
const databaseUrl = IS_PROD ? config.databasePro : config.database;
/**
* 連接數據庫
*/
mongoose.connect(databaseUrl, {
useUnifiedTopology: true,
useNewUrlParser: true,
useFindAndModify: false,
useCreateIndex: true,
config: {
autoIndex: false,
},
});
/**
* 連接成功
*/
mongoose.connection.on('connected', () => {
console.log(`Mongoose 連接成功: ${databaseUrl}`);
});
/**
* 連接異常
*/
mongoose.connection.on('error', (err) => {
console.log(`Mongoose 連接出錯: ${err}`);
});
/**
* 連接斷開
*/
mongoose.connection.on('disconnected', () => {
console.log('Mongoose 連接關閉!');
});
module.exports = mongoose;
在 app.js 中引入:
...
const routers = require('./routers/index');
require('./dbhelper/db');
// koa的錯誤處理程序hack
onerror(app);
...
啟動項目就可以看到log提示連接成功:

這里說一下在 db.js 中有這么一行代碼:
mongoose.Promise = global.Promise;
加上這個是因為:mongoose 的所有查詢操作返回的結果都是 query ,mongoose 封裝的一個對象,并非一個完整的 promise,而且與 ES6 標準的 promise 有所出入,因此在使用 mongoose 的時候,一般加上這句 mongoose.Promise = global.Promise。
開發(fā) API
Mongoose 的一切始于 Schema 。在開發(fā)接口之前,那就先來構建模型,這里主要構建文章分類,和文章列表兩種類型的接口,在字段上會比較簡陋,主要用于舉例使用,小伙伴們可以舉一反三。
在 models 目錄下創(chuàng)建 category.js 和 blog.js:
// models/category.js
const mongoose = require('mongoose');
const mongoosePaginate = require('mongoose-paginate');
const uniqueValidator = require('mongoose-unique-validator');
const schema = new mongoose.Schema({
name: {
type: String,
unique: true,
required: [true, '分類 name 必填'],
},
value: {
type: String,
unique: true,
required: [true, '分類 value 必填'],
},
rank: {
type: Number,
default: 0,
},
}, {
timestamps: { createdAt: 'createdAt', updatedAt: 'updatedAt' },
});
// 自動增加版本號
/* Mongoose 僅在您使用時更新版本密鑰save()。如果您使用update(),findOneAndUpdate()等等,Mongoose將不會 更新版本密鑰。
作為解決方法,您可以使用以下中間件。參考 https://mongoosejs.com/docs/guide.html#versionKey */
schema.pre('findOneAndUpdate', function () {
const update = this.getUpdate();
if (update.__v != null) {
delete update.__v;
}
const keys = ['$set', '$setOnInsert'];
Object.keys(keys).forEach((key) => {
if (update[key] != null && update[key].__v != null) {
delete update[key].__v;
if (Object.keys(update[key]).length === 0) {
delete update[key];
}
}
});
update.$inc = update.$inc || {};
update.$inc.__v = 1;
});
schema.plugin(mongoosePaginate);
schema.plugin(uniqueValidator);
module.exports = mongoose.model('Category', schema);
// models/blog.js
const mongoose = require('mongoose');
const uniqueValidator = require('mongoose-unique-validator');
const mongoosePaginate = require('mongoose-paginate');
const schema = new mongoose.Schema({
title: {
type: String,
unique: true,
required: [true, '必填字段'],
}, // 標題
content: {
type: String,
required: [true, '必填字段'],
}, // 內容
category: {
type: mongoose.Schema.Types.ObjectId,
required: [true, '必填字段'],
ref: 'Category',
}, // 分類_id,根據這個id我們就能從 category 表中查找到相關數據。
status: {
type: Boolean,
default: true,
}, // 狀態(tài)
}, {
timestamps: { createdAt: 'createdAt', updatedAt: 'updatedAt' },
toJSON: { virtuals: true },
});
// 虛擬字段:根據_id查找對應表中的數據。
schema.virtual('categoryObj', {
ref: 'Category',
localField: 'category',
foreignField: '_id',
justOne: true,
});
// 自動增加版本號
/* Mongoose 僅在您使用時更新版本密鑰save()。如果您使用update(),findOneAndUpdate()等等,Mongoose將不會 更新版本密鑰。
作為解決方法,您可以使用以下中間件。參考 https://mongoosejs.com/docs/guide.html#versionKey */
schema.pre('findOneAndUpdate', function () {
const update = this.getUpdate();
if (update.__v != null) {
delete update.__v;
}
const keys = ['$set', '$setOnInsert'];
Object.keys(keys).forEach((key) => {
if (update[key] != null && update[key].__v != null) {
delete update[key].__v;
if (Object.keys(update[key]).length === 0) {
delete update[key];
}
}
});
update.$inc = update.$inc || {};
update.$inc.__v = 1;
});
schema.plugin(mongoosePaginate);
schema.plugin(uniqueValidator);
module.exports = mongoose.model('Blog', schema);
在 dbhelper 目錄下,定義一些對數據庫增刪改查的方法,創(chuàng)建 category.js 和 blog.js:
// dbhelper/category.js
const Model = require('../models/category');
// TODO: 此文件中最好返回 Promise。通過 .exec() 可以返回 Promise。
// 需要注意的是 分頁插件本身返回的就是 Promise 因此 Model.paginate 不需要 exec()。
// Model.create 返回的也是 Promise
/**
* 查找全部
*/
exports.findAll = () => Model.find().sort({ rank: 1 }).exec();
/**
* 查找多個 篩選
*/
exports.findSome = (data) => {
const {
page = 1, limit = 10, sort = 'rank',
} = data;
const query = {};
const options = {
page: parseInt(page, 10),
limit: parseInt(limit, 10),
sort,
};
const result = Model.paginate(query, options);
return result;
};
/**
* 查找單個 詳情
*/
exports.findById = (id) => Model.findById(id).exec();
/**
* 增加
*/
exports.add = (data) => Model.create(data);
/**
* 更新
*/
exports.update = (data) => {
const { id, ...restData } = data;
return Model.findOneAndUpdate({ _id: id }, {
...restData,
},
{
new: true, // 返回修改后的數據
}).exec();
};
/**
* 刪除
*/
exports.delete = (id) => Model.findByIdAndDelete(id).exec();
// dbhelper/blog.js
const Model = require('../models/blog');
// TODO: 此文件中最好返回 Promise。通過 .exec() 可以返回 Promise。
// 需要注意的是 分頁插件本身返回的就是 Promise 因此 Model.paginate 不需要 exec()。
// Model.create 返回的也是 Promise
const populateObj = [
{
path: 'categoryObj',
select: 'name value',
},
];
/**
* 查找全部
*/
exports.findAll = () => Model.find().populate(populateObj).exec();
/**
* 查找多個 篩選
*/
exports.findSome = (data) => {
const {
keyword, title, category, status = true, page = 1, limit = 10, sort = '-createdAt',
} = data;
const query = {};
const options = {
page: parseInt(page, 10),
limit: parseInt(limit, 10),
sort,
populate: populateObj,
};
if (status !== 'all') {
query.status = status === true || status === 'true';
}
if (title) {
query.title = { $regex: new RegExp(title, 'i') };
}
if (category) {
query.category = category;
}
// 關鍵字模糊查詢 標題 和 content
if (keyword) {
const reg = new RegExp(keyword, 'i');
const fuzzyQueryArray = [{ content: { $regex: reg } }];
if (!title) {
fuzzyQueryArray.push({ title: { $regex: reg } });
}
query.$or = fuzzyQueryArray;
}
return Model.paginate(query, options);
};
/**
* 查找單個 詳情
*/
exports.findById = (id) => Model.findById(id).populate(populateObj).exec();
/**
* 新增
*/
exports.add = (data) => Model.create(data);
/**
* 更新
*/
exports.update = (data) => {
const { id, ...restData } = data;
return Model.findOneAndUpdate({ _id: id }, {
...restData,
}, {
new: true, // 返回修改后的數據
}).exec();
};
/**
* 刪除
*/
exports.delete = (id) => Model.findByIdAndDelete(id).exec();
編寫路由:
// routers/category.js
const router = require('koa-router')();
const controller = require('../controllers/category');
// 查
router.get('/', controller.find);
// 查 動態(tài)路由
router.get('/:id', controller.detail);
// 增
router.post('/', controller.add);
// 改
router.put('/:id', controller.update);
// 刪
router.del('/:id', controller.delete);
module.exports = router;
// routers/blog.js
const router = require('koa-router')();
const controller = require('../controllers/blog');
// 查
router.get('/', controller.find);
// 查 動態(tài)路由
router.get('/:id', controller.detail);
// 增
router.post('/', controller.add);
// 改
router.put('/:id', controller.update);
// 刪
router.del('/:id', controller.delete);
module.exports = router;
在路由文件里面我們只定義路由,把路由所對應的方法全部都放在 controllers 下:
// controllers/category.js
const dbHelper = require('../dbhelper/category');
const tool = require('../util/tool');
const ApiError = require('../error/api_error');
const ApiErrorNames = require('../error/api_error_name');
/**
* 查
*/
exports.find = async (ctx) => {
let result;
const reqQuery = ctx.query;
if (reqQuery && !tool.isEmptyObject(reqQuery)) {
if (reqQuery.id) {
result = dbHelper.findById(reqQuery.id);
} else {
result = dbHelper.findSome(reqQuery);
}
} else {
result = dbHelper.findAll();
}
await result.then((res) => {
if (res) {
ctx.body = res;
} else {
throw new ApiError(ApiErrorNames.UNEXIST_ID);
}
}).catch((err) => {
throw new ApiError(err.name, err.message);
});
};
/**
* 查 動態(tài)路由 id
*/
exports.detail = async (ctx) => {
const { id } = ctx.params;
if (!tool.validatorsFun.numberAndCharacter(id)) {
throw new ApiError(ApiErrorNames.LEGAL_ID);
}
await dbHelper.findById(id).then((res) => {
if (res) {
ctx.body = res;
} else {
throw new ApiError(ApiErrorNames.UNEXIST_ID);
}
}).catch((err) => {
throw new ApiError(err.name, err.message);
});
};
/**
* 添加
*/
exports.add = async (ctx) => {
const dataObj = ctx.request.body;
await dbHelper.add(dataObj).then((res) => {
ctx.body = res;
}).catch((err) => {
throw new ApiError(err.name, err.message);
});
};
/**
* 更新
*/
exports.update = async (ctx) => {
const ctxParams = ctx.params;
// 合并 路由中的參數 以及 發(fā)送過來的參數
// 路由參數 以及發(fā)送的參數可能都有 id 以 發(fā)送的 id 為準,如果沒有,取路由中的 id
const dataObj = { ...ctxParams, ...ctx.request.body };
await dbHelper.update(dataObj).then((res) => {
if (res) {
ctx.body = res;
} else {
throw new ApiError(ApiErrorNames.UNEXIST_ID);
}
}).catch((err) => {
throw new ApiError(err.name, err.message);
});
};
/**
* 刪除
*/
exports.delete = async (ctx) => {
const ctxParams = ctx.params;
// 合并 路由中的參數 以及 發(fā)送過來的參數
// 路由參數 以及發(fā)送的參數可能都有 id 以 發(fā)送的 id 為準,如果沒有,取路由中的 id
const dataObj = { ...ctxParams, ...ctx.request.body };
if (!tool.validatorsFun.numberAndCharacter(dataObj.id)) {
throw new ApiError(ApiErrorNames.LEGAL_ID);
}
await dbHelper.delete(dataObj.id).then((res) => {
if (res) {
ctx.body = res;
} else {
throw new ApiError(ApiErrorNames.UNEXIST_ID);
}
}).catch((err) => {
throw new ApiError(err.name, err.message);
});
};
// controllers/blog.js
const dbHelper = require('../dbhelper/blog');
const tool = require('../util/tool');
const ApiError = require('../error/api_error');
const ApiErrorNames = require('../error/api_error_name');
/**
* 查
*/
exports.find = async (ctx) => {
let result;
const reqQuery = ctx.query;
if (reqQuery && !tool.isEmptyObject(reqQuery)) {
if (reqQuery.id) {
result = dbHelper.findById(reqQuery.id);
} else {
result = dbHelper.findSome(reqQuery);
}
} else {
result = dbHelper.findAll();
}
await result.then((res) => {
if (res) {
ctx.body = res;
} else {
throw new ApiError(ApiErrorNames.UNEXIST_ID);
}
}).catch((err) => {
throw new ApiError(err.name, err.message);
});
};
/**
* 查 詳情
*/
exports.detail = async (ctx) => {
const { id } = ctx.params;
if (!tool.validatorsFun.numberAndCharacter(id)) {
throw new ApiError(ApiErrorNames.LEGAL_ID);
}
await dbHelper.findById(id).then(async (res) => {
if (res) {
ctx.body = res;
} else {
throw new ApiError(ApiErrorNames.UNEXIST_ID);
}
}).catch((err) => {
throw new ApiError(err.name, err.message);
});
};
/**
* 增
*/
exports.add = async (ctx) => {
const dataObj = ctx.request.body;
await dbHelper.add(dataObj).then((res) => {
ctx.body = res;
}).catch((err) => {
throw new ApiError(err.name, err.message);
});
};
/**
* 改
*/
exports.update = async (ctx) => {
const ctxParams = ctx.params;
// 合并 路由中的參數 以及 發(fā)送過來的參數
// 路由參數 以及發(fā)送的參數可能都有 id 以 發(fā)送的 id 為準,如果沒有,取路由中的 id
const dataObj = { ...ctxParams, ...ctx.request.body };
await dbHelper.update(dataObj).then((res) => {
if (res) {
ctx.body = res;
} else {
throw new ApiError(ApiErrorNames.UNEXIST_ID);
}
}).catch((err) => {
throw new ApiError(err.name, err.message);
});
};
/**
* 刪
*/
exports.delete = async (ctx) => {
const ctxParams = ctx.params;
// 合并 路由中的參數 以及 發(fā)送過來的參數
// 路由參數 以及發(fā)送的參數可能都有 id 以 發(fā)送的 id 為準,如果沒有,取路由中的 id
const dataObj = { ...ctxParams, ...ctx.request.body };
if (!tool.validatorsFun.numberAndCharacter(dataObj.id)) {
throw new ApiError(ApiErrorNames.LEGAL_ID);
}
await dbHelper.delete(dataObj.id).then((res) => {
if (res) {
ctx.body = res;
} else {
throw new ApiError(ApiErrorNames.UNEXIST_ID);
}
}).catch((err) => {
throw new ApiError(err.name, err.message);
});
};
上面使用了兩個方法,isEmptyObject 判斷是否是空對象,numberAndCharacter 對 id 格式做一個簡單的檢查。
// util/tool.js
/**
* @desc 檢查是否為空對象
*/
exports.isEmptyObject = (obj) => Object.keys(obj).length === 0;
/**
* @desc 常規(guī)正則校驗表達式
*/
exports.validatorsExp = {
number: /^[0-9]*$/,
numberAndCharacter: /^[0-9a-zA-Z]+$/,
nameLength: (n) => new RegExp(`^[\\u4E00-\\u9FA5]{${n},}$`),
idCard: /^(\d{6})(\d{4})(\d{2})(\d{2})(\d{3})([0-9]|X)$/,
backCard: /^([1-9]{1})(\d{15}|\d{18})$/,
phone: /^1[3456789]\d{9}$/,
email: /^\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$/,
};
/**
* @desc 常規(guī)正則校驗方法
*/
exports.validatorsFun = {
number: (val) => exports.validatorsExp.number.test(val),
numberAndCharacter: (val) => exports.validatorsExp.numberAndCharacter.test(val),
idCard: (val) => exports.validatorsExp.idCard.test(val),
backCard: (val) => exports.validatorsExp.backCard.test(val),
};
到此,分類和文章相關的接口基本完成了,測試一下:



鑒權
這里我使用的是 token 來進行身份驗證的: jsonwebtoken
根據路由對一些非 GET 請求的接口做 token 驗證。
// app.js
...
// 檢查請求時 token 是否過期
app.use(tokenHelper.checkToken([
'/api/blogs',
'/api/categories',
...
], [
'/api/users/signup',
'/api/users/signin',
'/api/users/forgetPwd',
]));
...
// util/token-helper.js
const jwt = require('jsonwebtoken');
const config = require('../config/index');
const tool = require('./tool');
// 生成token
exports.createToken = (user) => {
const token = jwt.sign({ userId: user._id, userName: user.userName }, config.tokenSecret, { expiresIn: '2h' });
return token;
};
// 解密token返回userId,userName用來判斷用戶身份。
exports.decodeToken = (ctx) => {
const token = tool.getTokenFromCtx(ctx);
const userObj = jwt.decode(token, config.tokenSecret);
return userObj;
};
// 檢查token
exports.checkToken = (shouldCheckPathArray, unlessCheckPathArray) => async (ctx, next) => {
const currentUrl = ctx.request.url;
const { method } = ctx.request;
const unlessCheck = unlessCheckPathArray.some((url) => currentUrl.indexOf(url) > -1);
const shouldCheck = shouldCheckPathArray.some((url) => currentUrl.indexOf(url) > -1) && method !== 'GET';
if (shouldCheck && !unlessCheck) {
const token = tool.getTokenFromCtx(ctx);
if (token) {
try {
jwt.verify(token, config.tokenSecret);
await next();
} catch (error) {
ctx.status = 401;
ctx.body = 'token 過期';
}
} else {
ctx.status = 401;
ctx.body = '無 token,請登錄';
}
} else {
await next();
}
};
在注冊個登錄的時候生成設置 token :
// controllers/users.js
/**
* @desc 注冊
*/
...
exports.signUp = async (ctx) => {
const dataObj = ctx.request.body;
await dbHelper.signUp(dataObj).then((res) => {
const token = tokenHelper.createToken(res);
const { password, ...restData } = res._doc;
ctx.res.setHeader('Authorization', token);
ctx.body = {
token,
...restData,
};
}).catch((err) => {
throw new ApiError(err.name, err.message);
});
};
/**
* @desc 登錄
*/
exports.signIn = async (ctx) => {
const dataObj = ctx.request.body;
await dbHelper.signIn(dataObj).then((res) => {
const token = tokenHelper.createToken(res);
const { password, ...restData } = res;
ctx.res.setHeader('Authorization', token);
ctx.body = {
token,
...restData,
};
}).catch((err) => {
throw new ApiError(err.name, err.message);
});
};
...
項目部署
部署就比較簡單了,將項目文件全部上傳到服務器上,然后全局安裝 pm2,用 pm2 啟動即可。
在 bin 目錄下創(chuàng)建 pm2.config.json :
{
"apps": [
{
"name": "blog-api",
"script": "./app.js",
"instances": 0,
"watch": false,
"exec_mode": "cluster_mode"
}
]
}
在 package.json 中添加啟動腳本:
{
...
"scripts": {
"dev": "cross-env NODE_ENV=development nodemon ./app.js",
"rc": "cross-env NODE_ENV=production nodemon ./app.js",
"pm2": "cross-env NODE_ENV=production NODE_LOG_DIR=/tmp ENABLE_NODE_LOG=YES pm2 start ./bin/pm2.config.json",
"test": "echo \"Error: no test specified\" && exit 1"
},
...
}
然后,cd 到項目根目錄:
npm run pm2
關于個人博客前臺開發(fā)可以戳這里:Nuxt 開發(fā)搭建博客