REST API的设计

前言

客户端通过请求URL,传递参数,去获取指定的数据,这就是API(ApplicationProgramInterface)。

API是前端和客户端操作后端数据的一种方式,一种接口。当一个Web应用以API的方式对外提供功能和接口时,整个应用的架构模式是这样的:

但是,URL怎么约定,参数以什么编码方式传,响应数据的格式是什么样的,以及响应码怎么约定,API版本升级怎么设计,这些问题是设计这些API的后端人员不得不考虑的问题。

2000年,Roy Fielding博士在论文中提出REST(Representational State Transfer)风格的软件API架构模式。该模式对上面的问题提出了针对性的解决方案,迅速被大家接受,逐渐流行起来。

REST是一种API的设计模式,它将数据库的数据看做是一个个的资源。客户端的请求就是对这些资源的操作,这些操作无外乎增删改查四种。这种模式简单易读,易于维护。

这种设计模式提倡我们遵循以下原则。

数据格式

REST提倡数据格式应该使用轻量级,易于阅读的json格式。这要求我们除了GET请求方式外,其他请求方式的请求体都应该是json,响应体也是json。

所以,请求头和响应头的Content-Type都应该为application/json

请求方式

REST将请求看做是对某个资源的增删改查操作,正好对应了Http请求的GET,POST, PUT, DELETE 4种请求方法。

对应关系如下:

  • 获取资源:GET方式
  • 创建资源:POST方式
  • 更新资源:PUT方
  • 删除资源:DELETE方式

URL设计

REST提倡声明式的URL设计。URL设计需要考虑层次化,可扩展,易于维护。每一个URL都表示对某个资源的操作,假设我们要操作服务器的博客数据,可以这样设计:

  • 通用的资源操作方式:
    // 获取所有博客
    GET /api/blogs
    // 获取指定博客
    GET /api/blogs/1234
    // 创建博客
    POST /api/blogs
    // 更新指定博客
    PUT /api/blogs/1234
    // 删除指定博客
    DELETE /api/blogs/1234
  • 资源的过滤操作,使用查询参数来限定条件:
    // 分页获部分博客
    GET /api/blogs?page=1&size=15&sort=time
  • 按层次组织资源:
    // 获取某个博客下面的所有评论
    GET /api/blogs/143/comments

    层次化的设计有可能会让URL太长,不便于阅读,比如:

    // 获取某个博客下面的某个评论的某个回复
    GET /api/blogs/123/comments/12343/replys/2231

    所以,我们也可以使用扁平化的方式设计所有的资源:

    // 获取某个博客的所有评论,使用查询参数来限定条件
    GET /api/comments?blogId=123
    // 获取某个评论的所有回复
    GET /api/replys?commentId=123
  • API的版本升级。

    我们的API会不断的更新迭代,比如:获取所有订单的URL参数多加了一个时间戳。如果我们直接更改原来接口的逻辑,会导致旧版本的客户端获取失败。因此,通过添加一个路径/v1来让我们的API更加维护性。
    // 获取所有订单, 第一版
    GET /api/v1/orders
    // 获取所有订单, 第二版
    GET /api/v2/orders

响应设计

响应设计分为数据结构设计和响应码设计。

  • 响应数据结构设计

    客户端对资源的操作有可能成功,也有可能失败。以前我一直使用使用这样的数据结构:

    {
    code: 1/0, // 会有很多标识码,11, 12, 13 ...
    msg: "获取成功/获取失败",
    data: [...]/{}
    }

    整体的结构一定不能变,数据的变化体现在data字段。这样非常方便客户端的解析或者序列化(静态语言)。

  • 响应码设计

    响应码分为http响应码和逻辑响应码。http响应码很简单,如果成功就200,服务器出错就500。而逻辑响应码则是需要自己来定义,比如:1表示用户不存在,2表示密码错误。

前后端分离

在以前的时代,用户请求一个网页,服务端通过JSP技术,或者其他模板引擎技术动态渲染后,返回给用户。它们看起来像这样:

这样做的坏处是,后端和前端融合在一起,哪怕前端代码不是由后端人员来写,仍然需要2方人员进行必要的交流和沟通,降低了开发效率;而且前端和后端共同部署,灵活性差;而且后台处理静态资源的性能较差,也不应该去处理静态资源压缩,缓存等问题,应该交给代理服务器来处理,比如Nginx。

前后端分离最极致作法是:前端和后端各自独立编写,独立部署,通过API接口进行交互。他们看起来像这样:

项目实战:电商管理后台

目标,实现一个有账户模块,权限模块,商品和分类模块和订单模块,给商家和管理员使用的后台系统。

项目采用前后端分离的开发方式,本身只实现API功能,并没有提供界面。

项目结构搭建和实施流程

在之前TODO项目的基础上,增加一个middleware包和test包,前者用来存放中间件的包,因为权限管理需要用中间件来实现;后者是测试相关包。

  1. 每个模块的实现顺序为:model层 --> service层 --> router层。
  2. 单元测试:service层写脚本测试;router层使用postman测试。

配置文件的环境切换

开发环境和生产环境的配置一般是不一样的,比如端口配置,数据库配置。一般我们是通过环境变量NODE_ENV来区分。为了能够动态切换配置,就需要根据当前NODE_ENV的值来加载不同的配置对象。

做法就是:

  1. 建立config目录,创建dev.jsprod.js分别存放对应的配置信息
  2. 编写index.js,实现动态切换配置的逻辑。

目录结构:

编写入口文件

添加依赖:

npm i body-parser express express-async-errors mongoose morgan

编写app.jsdb.js文件。

app.js

//引入dib
require('./db') const config = require('./config');
const morgan = require('morgan')
const bodyParser = require('body-parser');
const express = require('express')
// 引入异常捕获处理
require('express-async-errors'); const app = express(); //注册中间件
// log中间件
app.use(morgan('combined')); //注册自定义的中间件 // 注册body-parser中间件
// parse application/x-www-form-urlencoded
app.use(bodyParser.urlencoded({ extended: false }));
// parse application/json
app.use(bodyParser.json()); // 注册路由
app.use("/users", require('./router/account')); // 注册异常处理中间件
app.use((err, req, res, next)=>{
res.fail(err.toString())
}); //启动
app.listen(config.PORT);

db.js

const config = require('./config');
const mongoose = require('mongoose');
mongoose.connect(`mongodb://127.0.0.1/${config.DB}`); const db = mongoose.connection; db.on('error', (err)=>{
console.log(err);
}); db.on("open", ()=>{
console.log("mongodb connect successfully!");
});

账户模块

先编写model,再service,最后router;最后对service和router进行测试。

REST中间件

为了方便进行REST结果的返回,我们编写一个res_md.js中间件,作用是给每个res对象安装2个方法,注意该中间件注册的顺序尽量放在前面。代码如下:

module.exports = function (req, res, next) {
res.success = (data = null) =>{
res.send({
code: 0,
msg: "操作成功",
data: data
})
};
res.fail = (msg)=>{
res.send({
code: -1,
msg: msg
})
}; next();
};

账户model

const mongoose = require('mongoose')
const schema = new mongoose.Schema({
username: {
type: String,
required: [true, "用户名不能缺少"]
},
password: {
type: String,
required: [true, "密码不能缺少"]
},
age: {
type: Number,
min: [0, "年龄不能小于0"],
max: [120, "年龄不能大于120"]
},
role: {
type: Number,
default: 0 // 0是商家, 10086是管理员
},
created:{
type: Date,
default: Date.now()
}
}); module.exports = mongoose.model('user', schema);

账户service

const User = require('../model/user');
const config = require('../config')
const crypto = require('lxj-crypto') /**
* 根据用户名获取某个用户
* @param username
* @returns {Promise<*>}
*/
async function getUserByUsername(username) {
return await User.findOne({username: username}).select("-__v")
} async function checkCanInsertUser(user) {
//检查密码是否为空
if(!user.password || user.password.length===0){
throw Error("密码不能为空")
}
//检查用户是否已经存在
let res = await getUserByUsername(user.username);
if(res){
throw Error("用户名已经存在")
}
} /**
* 添加普通用户
* @param user
* @returns {Promise<void>}
*/
async function addUser(user) {
await checkCanInsertUser(user); user.role = 0;
user.created = Date.now(); //对密码进行加密,加密的方式:使用username作为随机数对password进行哈希
user.password = crypto.md5Hmac(user.password, user.username)
await User.create(user)
} async function deleteUser(id) {
let res = await User.deleteOne({_id:id});
if(!res || res.n===0){
throw Error("删除用户失败")
}
} /**
* 登录的方法
* @param user
* @returns token
*/
async function login(user) {
// 1. 对密码进行加密
user.password = crypto.md5Hmac(user.password, user.username)
// 2. 进行查询
let res = await User.findOne({username:user.username, password: user.password});
if(!res){
throw Error("用户名或者密码错误")
} // 说明用户名和密码验证成功,需要生产token返回给客户端,以后客户端的header中带上这个token
// token 生产方法:用aes进行对指定的data加密
let tokenData = {
username: user.username,
expire: Date.now() + config.TokenDuration
};
let token = crypto.aesEncrypt(JSON.stringify(tokenData), config.TokenKey);
return token
} module.exports = {
getUserByUsername,
addUser,
deleteUser,
login
};

账户router

let router = require("express").Router();
let accountService = require('../service/accout') router.get("/:username", async (req, res)=>{
let user = await accountService.getUserByUsername(req.params.username);
res.success(user);
}); // 登录
router.post("/login", async (req, res)=>{
let token = await accountService.login(req.body);
res.success({token});
}); // 注册
router.post("/register", async (req, res)=>{
await accountService.register(req.body)
res.success()
}); router.delete("/:id", async (req, res)=>{
await accountService.deleteUser(req.params.id)
res.success()
}); module.exports = router;

NodeJs04的更多相关文章

随机推荐

  1. 2017.10.10 java中的继承与多态(重载与重写的区别)

    1. 类的继承 继承是面向对象编程技术的主要特征之一,也是实现软件复用的重要手段,使用继承特性子类(subclass) 可以继承父类(superclass)中private方法和属性,继承的目的是使程 ...

  2. 第一个C#程序Hello World

    一.编写第一个C#程序——Hello World1. 启动Microsoft Visual Studio 2010.2. 点击“文件”菜单,选择“新建”项,在弹出的子菜单中选择“项目”命令.3. 弹出 ...

  3. How good are detection proposals, really?

    How good are detection proposals, really? J. Hosang, R. Benenson, B. Schiele Oral at BMVC 2014 http: ...

  4. 1、SpringBoot+Mybatis整合------简单CRUD的实现

    编译工具:STS 代码下载链接:https://github.com/theIndoorTrain/SpringBoot_Mybatis01/commit/b757cd9bfa4e2de551b2e9 ...

  5. Qt.5.9.6移植

    工具及软件包 交叉编译工具链 arm-2014.05-29-arm-none-linux-gnueabi-i686-pc-linux-gnu.tar.bz2 软件包 dbus-1.10.0.tar.g ...

  6. C++使用GDI+实现图片格式转换

    主要是我在设置壁纸时遇到的个小问题,因为设置壁纸只能是bmp格式的图片,不可能我喜欢的壁纸就都是bmp格式的,就想怎么转换一下图片的格式,于是就在百度搜怎么弄,搜到了可行方法,却没有实现代码,有些看起 ...

  7. 使用docker搭建“企业级镜像仓库”Harbor

    一.前沿 docker的官方镜像仓库registry,功能比较单一,不太好用,特别是删除镜像操作,不够友好. Harbor是一个用于存储和分发Docker镜像的企业级Registry服务器,通过添加一 ...

  8. 转:比较spring cloud和dubbo,各自的优缺点是什么

    原文:https://blog.csdn.net/u010664947/article/details/80007767 dubbo由于是二进制的传输,占用带宽会更少 springCloud是http ...

  9. Maven - 依赖范围<scope></scope>

    6种:

  10. 前端vue项目部署到tomcat,一刷新报错404解决方法

    公司前端写的后台部署到tomcat webapps目录下后,无法进行刷新,一刷新就会报错404,自动跳的404页面.在网上查了下,官方说是HTML5 History 模式引发的问题,但是解决方案中,并 ...