乐趣区

关于javascript:nodekoa2mongodb搭建RESTful-API风格后台

RESTful API 格调

在开发之前先回顾一下,RESTful API 是什么? RESTful 是一种 API 设计格调,并不是一种强制标准和规范,它的特点在于申请和响应都简洁清晰,可读性强。不论 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 申请,次要的区别在于多个查问条件时怎么传递查问字符串,有的通过应用解析门路,有的通过解析传参,有的两者混用。同时在形容 API 性能时,能够应用 articles/delete,也能够应用 deleteArticles。而第四种 RESTful API 最大的区别在于行为动词 DELETE 的地位,不在 url 里,而在申请办法中.

RESTful 设计格调

REST(Representational State Transfer 体现层状态转移) 是一种设计格调,而不是规范。次要用于客户端和服务端的 API 交互,我认为它的约定大于它的定义,使得 api 在设计上有了肯定的标准和准则,语义更加明确,清晰。

咱们一起来看看 RESTFul API 有哪些特点:

  • 基于“资源”,数据也好、服务也好,在 RESTFul 设计里一切都是资源,

资源用 URI(Universal Resource Identifier 通用资源标识) 来示意。

  • 无状态性。
  • URL 中通常不呈现动词,只有名词。
  • URL 语义清晰、明确。
  • 应用 HTTPGETPOSTDELETEPUT 来示意对于资源的 增删改查
  • 应用 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 开发畛域中的一个更小、更富裕表现力、更强壮的基石。

Koa2 的装置与应用对 Node.js 的版本也是有要求的,因为 node.js 7.6 版本 开始齐全反对 async/await,所以能力齐全反对 Koa2

Koa2Koa 框架的最新版本,Koa3 还没有正式推出,Koa1.X 正走在被替换的路上。Koa2Koa1 的最大不同,在于 Koa1 基于 co 治理 Promise/Generator 中间件,而 Koa2 紧跟最新的 ES 标准,反对到了 Async FunctionKoa1 不反对),两者中间件模型体现统一,只是语法底层不同。

Express 外面,不同期间的代码组织形式尽管大为不同,但纵观 Express 多年的历程,他仍然是绝对大而全,API 较为丰盛的框架,它的整个中间件模型是基于 callback 回调,而 callback 长年被人诟病。

简略来说 KoaExpress 的最大的区别在于 执行程序 异步的写法,同时这也映射了 js 语法在解决异步工作上的倒退历程。对于异步和两种框架区别,不在这里做过多探讨。来看看 Koa 中间件洋葱圈模型:

创立 Koa2 我的项目

创立文件 blog-api , 进入到该目录:

npm init

装置 Koa:

yarn add koa

装置 eslint, 这个抉择装置,能够依据本人的需要来标准本人的代码, 上面是我配置的 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/:

我的项目开发

目录构造

布局我的项目构造,创立对应的文件夹:

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,次要为设置环境变量兼容用的:

yarn add cross-env

package.jsonscripts 中减少脚本:

{
  "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

以开发模式运行

yarn dev
// 或者
npm run dev

koa 路由

路由 (Routing) 是由一个 URI(或者叫门路) 和一个特定的 HTTP 办法 (GET、POST 等) 组成的,波及到利用如何响应客户端对某个网站节点的拜访。

yarn add koa-router

接口对立以 /api 为前缀,比方:

http://localhost:3000/api/categories
http://localhost:3000/api/blogs

config 目录下创立 index.js:

// config/index.js
module.exports = {apiPrefix: '/api',};

routers 目录下创立 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 申请

接下来看一下参数传递,如果是申请 id1 的文章,咱们 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}`;
});

// 动静路由
router.get('/:id', async (ctx) => {ctx.body = ` 动静路由文章接口 id: ${ctx.params.id}`;
});

module.exports = router;

如图:

POST/PUT/DEL

GET 把参数蕴含在 URL 中,POST 通过 request body 传递参数。
为了方便使用 koa-body 来解决 POST 申请和文件上传,或者应用 koa-bodyparserkoa-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}`;
});

// 动静路由
router.get('/:id', async (ctx) => {ctx.body = ` 动静路由文章接口 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;

测试一下:


错误处理

在申请过程中,还须要将返回后果进行一下包装,产生异样时,如果接口没有提醒语, 状态码的返回必定是不敌对的,上面定义几个罕用的谬误类型。
error 目录下创立 api_error_map.jsapi_error_name.jsapi_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 目录下创立 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 {
      // 先去执行路由
      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());

// 监听 error
app.on('error', (err, ctx) => {
  // 在这里能够对错误信息进行一些解决,生成日志等。console.error('server error', err, ctx);
});

app.listen(3000);

在后续开发中,若遇到异样,将异样抛出即可。

连贯数据库

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 目录下创立 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 的所有查问操作返回的后果都是 querymongoose 封装的一个对象,并非一个残缺的 promise,而且与 ES6 规范的 promise 有所出入,因而在应用 mongoose 的时候,个别加上这句 mongoose.Promise = global.Promise

开发 API

Mongoose 的所有始于 Schema。在开发接口之前,那就先来构建模型,这里次要构建文章分类,和文章列表两种类型的接口,在字段上会比拟简陋,次要用于举例应用,小伙伴们能够触类旁通。
models 目录下创立 category.jsblog.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,
  }, // 状态
}, {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 目录下,定义一些对数据库增删改查的办法,创立 category.jsblog.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);

// 查 动静路由
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);

// 查 动静路由
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);
  });
};

/**
 * 查 动静路由 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;
  // 合并 路由中的参数 以及 发送过去的参数
  // 路由参数 以及发送的参数可能都有 id 以 发送的 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;
  // 合并 路由中的参数 以及 发送过去的参数
  // 路由参数 以及发送的参数可能都有 id 以 发送的 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;
  // 合并 路由中的参数 以及 发送过去的参数
  // 路由参数 以及发送的参数可能都有 id 以 发送的 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;
  // 合并 路由中的参数 以及 发送过去的参数
  // 路由参数 以及发送的参数可能都有 id 以 发送的 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 判断是否是空对象,numberAndCharacterid 格局做一个简略的查看。

// util/tool.js
/**
 * @desc 查看是否为空对象
 */
exports.isEmptyObject = (obj) => Object.keys(obj).length === 0;

/**
 * @desc 惯例正则校验表达式
 */
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 惯例正则校验办法
 */
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 目录下创立 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

对于集体博客前台开发能够戳这里:Nuxt 开发搭建博客

退出移动版