微信公众号搜"智元新知"关注
微信扫一扫可直接关注哦!

实现一个完整的Node.js RESTful API的示例

前言

这篇文章算是对这本书的一个总结。用Node.js写接口对我来说是很有用的,比如在项目初始阶段,可以快速的模拟网络请求。正因为它用js写的,跟iOS直接的联系也比其他语言写的后台更加接近。

这本书写的极好,作者编码的思路极其清晰,整本书虽说是用英文写的,但很容易读懂。同时,它完整的构建了RESTful API的一整套逻辑。

我更加喜欢写一些函数响应式的程序,把函数当做数据或参数进行传递对我有着莫大的吸引力。

从程序的搭建,到设计错误捕获机制,再到程序的测试任务,这是一个完整的过程。这边文章将会很长,我会把每个核心概念的代码都黏贴上来。

环境搭建

下载并安装Node.js

安装npm

下载演示项目

rush:bash;"> git clone https://github.com/agelessman/ntask-api

进入项目文件夹后运行

rush:bash;"> npm install

上边命令会下载项目所需的插件,然后启动项目

rush:bash;"> npm start

访问接口文档 http://localhost:3000/apidoc

程序入口

Express 这个框架大家应该都知道,他提供了很丰富的功能,我在这就不做解释了,先看该项目中的代码

rush:js;"> import express from "express" import consign from "consign"

const app = express();

/// 在使用include或者then的时候,是有顺序的,如果传入的参数是一个文件
/// 那么他会按照文件夹中文件的顺序进行加载
consign({verbose: false})
.include("libs/config.js")
.then("db.js")
.then("auth.js")
.then("libs/middlewares.js")
.then("routers")
.then("libs/boot.js")
.into(app);

module.exports = app;

不管是models,views还是routers都会经过 Express 的加工和配置。在该项目中并没有使用到views的地方。 Express 通过app对整个项目的功能进行配置,但我们不能把所有的参数和方法都写到这一个文件之中,否则当项目很大的时候将急难维护。

我使用Node.js的经验是很少的,但上面的代码给我的感觉就是极其简洁,思路极其清晰,通过 consign 这个模块导入其他模块在这里就让代码显得很优雅。

@note:导入的顺序很重要。

在这里,app的使用很像一个全局变量,这个我们会在下边的内容中展示出来,按序导入后,我们就可以通过这样的方式访问模块的内容了:

rush:plain;"> app.db app.auth app.libs....

模型设计

在我看来,在开始做任何项目前,需求分析是最重要的,经过需求分析后,我们会有一个关于代码设计的大的概念。

编码的实质是什么?我认为就是数据的存储和传递,同时还需要考虑性能和安全的问题

因此我们第二部的任务就是设计数据模型,同时可以反应出我们需求分析的成果。在该项目中有两个模型, User 和 Task,每一个 task 对应一个 user,一个 user 可以有多个 task

用户模型:

module.exports = (sequelize,DataType) => {
"use strict";
const Users = sequelize.define("Users",{
id: {
type: DataType.INTEGER,primaryKey: true,autoIncrement: true
},name: {
type: DataType.STRING,allowNull: false,validate: {
notEmpty: true
}
},password: {
type: DataType.STRING,email: {
type: DataType.STRING,unique: true,validate: {
notEmpty: true
}
}
},{
hooks: {
beforeCreate: user => {
const salt = bcrypt.genSaltSync();
user.password = bcrypt.hashSync(user.password,salt);
}
}
});
Users.associate = (models) => {
Users.hasMany(models.Tasks);
};
Users.isPassword = (encodedPassword,password) => {
return bcrypt.compareSync(password,encodedPassword);
};

return Users;
};

任务模型:

{ "use strict"; const Tasks = sequelize.define("Tasks",title: { type: DataType.STRING,done: { type: DataType.BOOLEAN,defaultValue: false } }); Tasks.associate = (models) => { Tasks.belongsTo(models.Users); }; return Tasks; };

该项目中使用了系统自带sqlite 作为数据库,当然也可以使用其他的数据库,这里不限制是关系型的还是非关系型的。为了更好的管理数据,我们使用 sequelize 这个模块来管理数据库

为了节省篇幅,这些模块我就都不介绍了,在google上一搜就出来了。在我看的Node.js的开发中,这种ORM的管理模块有很多,比如说对 MongoDB 进行管理的 mongoose 。很多很多,他们主要的思想就是Scheme。

在上边的代码中,我们定义了模型的输出和输入模板,同时对某些特定的字段进行了验证,因此在使用的过程中就有可能会产生来自数据库错误,这些错误我们会在下边讲解到。

{ Tasks.belongsTo(models.Users); };

Users.associate = (models) => {
Users.hasMany(models.Tasks);
};
Users.isPassword = (encodedPassword,password) => {
return bcrypt.compareSync(password,encodedPassword);
};

hasMany 和 belongsTo 表示一种关联属性, Users.isPassword 算是一个方法。 bcrypt 模块可以对密码进行加密编码。

数据库

在上边我们已经知道了,我们使用 sequelize 模块来管理数据库。其实,在最简单的层面而言,数据库只需要给我们数据模型就行了,我们拿到这些模型后,就能够根据不同的需求,去完成各种各样的CRUD操作。

let db = null;

module.exports = app => {
"use strict";
if (!db) {
const config = app.libs.config;
const sequelize = new Sequelize(
config.database,config.username,config.password,config.params
);

db = {
sequelize,Sequelize,models: {}
};

const dir = path.join(__dirname,"models");

fs.readdirsync(dir).forEach(file => {
const modelDir = path.join(dir,file);
const model = sequelize.import(modelDir);
db.models[model.name] = model;
});

Object.keys(db.models).forEach(key => {
db.models[key].associate(db.models);
});
}
return db;
};

上边的代码很简单,db是一个对象,他存储了所有的模型,在这里是 User 和 Task 。通过 sequelize.import 获取模型,然后又调用了之前写好的associate方法

上边的函数调用之后呢,返回db,db中有我们需要的模型,到此为止,我们就建立了数据库的联系,作为对后边代码一个支撑。

CRUD

CRUD在router中,我们先看看 router/tasks.js 的代码

{ "use strict"; const Tasks = app.db.models.Tasks;

app.route("/tasks")
.all(app.auth.authenticate())

.get((req,res) => {
console.log(req.body: ${req.body});
Tasks.findAll({where: {user_id: req.user.id} })
.then(result => res.json(result))
.catch(error => {
res.status(412).json({msg: error.message});
});
})

.post((req,res) => {
req.body.user_id = req.user.id;
Tasks.create(req.body)
.then(result => res.json(result))
.catch(error => {
res.status(412).json({msg: error.message});
});
});

app.route("/tasks/:id")
.all(app.auth.authenticate())

.get((req,res) => {
Tasks.findOne({where: {
id: req.params.id,user_id: req.user.id
}})
.then(result => {
if (result) {
res.json(result);
} else {
res.sendStatus(412);
}
})
.catch(error => {
res.status(412).json({msg: error.message});
});
})

.put((req,res) => {
Tasks.update(req.body,{where: {
id: req.params.id,user_id: req.user.id
}})
.then(result => res.sendStatus(204))
.catch(error => {
res.status(412).json({msg: error.message});
});
})

.delete((req,res) => {
Tasks.destroy({where: {
id: req.params.id,user_id: req.user.id
}})
.then(result => res.sendStatus(204))
.catch(error => {
res.status(412).json({msg: error.message});
});
});
};

再看看 router/users.js 代码

{ "use strict"; const Users = app.db.models.Users;

app.route("/user")
.all(app.auth.authenticate())

.get((req,res) => {
Users.findById(req.user.id,{
attributes: ["id","name","email"]
})
.then(result => res.json(result))
.catch(error => {
res.status(412).json({msg: error.message});
});
})

.delete((req,res) => {
console.log(delete..........${req.user.id});
Users.destroy({where: {id: req.user.id}})
.then(result => {
console.log(result: ${result});
return res.sendStatus(204);
})
.catch(error => {
console.log(resultfsaddfsf);
res.status(412).json({msg: error.message});
});
});

app.post("/users",(req,res) => {
Users.create(req.body)
.then(result => res.json(result))
.catch(error => {
res.status(412).json({msg: error.message});
});
});
};

这些路由写起来比较简单,上边的代码中,基本思想就是根据模型操作CRUD,包括捕获异常。但是额外的功能是做了authenticate,也就是授权操作。

这一块好像没什么好说的,基本上都是固定套路。

授权

在网络环境中,不能老是传递用户名和密码。这时候就需要一些授权机制,该项目中采用的是JWT授权(JSON Wbb Toknes),有兴趣的同学可以去了解下这个授权,它也是按照一定的规则生成token。

因此对于授权而言,最核心的部分就是如何生成token。

module.exports = app => {
"use strict";
const cfg = app.libs.config;
const Users = app.db.models.Users;

app.post("/token",res) => {
const email = req.body.email;
const password = req.body.password;
if (email && password) {
Users.findOne({where: {email: email}})
.then(user => {
if (Users.isPassword(user.password,password)) {
const payload = {id: user.id};
res.json({
token: jwt.encode(payload,cfg.jwtSecret)
});
} else {
res.sendStatus(401);
}
})
.catch(error => res.sendStatus(401));
} else {
res.sendStatus(401);
}
});
};

上边代码中,在得到邮箱和密码后,再使用 jwt-simple 模块生成一个token。

JWT在这也不多说了,它由三部分组成,这个在它的官网中解释的很详细。

我觉得老外写东西一个最大的优点就是文档很详细。要想弄明白所有组件如何使用,最好的方法就是去他们的官网看文档,当然这要求英文水平还可以。

授权一般分两步:

如果从前端传递一个token过来,我们怎么解析这个token,然后获取到token里边的用户信息呢?

module.exports = app => {
const Users = app.db.models.Users;
const cfg = app.libs.config;
const params = {
secretorKey: cfg.jwtSecret,jwtFromrequest: ExtractJwt.fromAuthHeaderAsBearerToken()
};
var opts = {};
opts.jwtFromrequest = ExtractJwt.fromAuthHeaderWithScheme("JWT");
opts.secretorKey = cfg.jwtSecret;

const strategy = new Strategy(opts,(payload,done) => {
Users.findById(payload.id)
.then(user => {
if (user) {
return done(null,{
id: user.id,email: user.email
});
}
return done(null,false);
})
.catch(error => done(error,null));
});
passport.use(strategy);

return {
initialize: () => {
return passport.initialize();
},authenticate: () => {
return passport.authenticate("jwt",cfg.jwtSession);
}
};
};

这就用到了 passport 和 passport-jwt 这两个模块。 passport 支持很多种授权。不管是iOS还是Node中,验证都需要指定一个策略,这个策略是最灵活的一层。

授权需要在项目中提前进行配置,也就是初始化, app.use(app.auth.initialize()); 。

如果我们想对某个接口进行授权验证,那么只需要像下边这么用就可以了:

.get((req,res) => {
console.log(req.body: ${req.body});
Tasks.findAll({where: {user_id: req.user.id} })
.then(result => res.json(result))
.catch(error => {
res.status(412).json({msg: error.message});
});
})

配置

Node.js中一个很有用的思想就是middleware,我们可以利用这个手段做很多有意思的事情:

module.exports = app => {
"use strict";
app.set("port",3000);
app.set("json spaces",4);
console.log(err ${JSON.stringify(app.auth)});
app.use(bodyParser.json());
app.use(app.auth.initialize());
app.use(compression());
app.use(helmet());
app.use(morgan("common",{
stream: {
write: (message) => {
logger.info(message);
}
}
}));
app.use(cors({
origin: ["http://localhost:3001"],methods: ["GET","POST","PUT","DELETE"],allowedHeaders: ["Content-Type","Authorization"]
}));
app.use((req,res,next) => {
// console.log(header: ${JSON.stringify(req.headers)});
if (req.body && req.body.id) {
delete req.body.id;
}
next();
});

app.use(express.static("public"));
};

上边的代码中包含了很多新的模块,app.set表示进行设置,app.use表示使用middleware。

测试

写测试代码是我平时很容易疏忽的地方,说实话,这么重要的部分不应该被忽视。

describe("Routes: Users",() => {
"use strict";
const Users = app.db.models.Users;
const jwtSecret = app.libs.config.jwtSecret;
let token;

beforeEach(done => {
Users
.destroy({where: {}})
.then(() => {
return Users.create({
name: "Bond",email: "Bond@mc.com",password: "123456"
});
})
.then(user => {
token = jwt.encode({id: user.id},jwtSecret);
done();
});
});

describe("GET /user",() => {
describe("status 200",() => {
it("returns an authenticated user",done => {
request.get("/user")
.set("Authorization",JWT ${token})
.expect(200)
.end((err,res) => {
expect(res.body.name).to.eql("Bond");
expect(res.body.email).to.eql("Bond@mc.com");
done(err);
});
});
});
});

describe("DELETE /user",() => {
describe("status 204",() => {
it("deletes an authenticated user",done => {
request.delete("/user")
.set("Authorization",JWT ${token})
.expect(204)
.end((err,res) => {
console.log(err: ${err});
done(err);
});
});
});
});

describe("POST /users",() => {
it("creates a new user",done => {
request.post("/users")
.send({
name: "machao",email: "machao@mc.com",password: "123456"
})
.expect(200)
.end((err,res) => {
expect(res.body.name).to.eql("machao");
expect(res.body.email).to.eql("machao@mc.com");
done(err);
});
});
});
});
});

测试主要依赖下边的这几个模块:

rush:js;"> import supertest from "supertest" import chai from "chai" import app from "../index"

global.app = app;
global.request = supertest(app);
global.expect = chai.expect;

其中 supertest 用来发请求的, chai 用来判断是否成功。

使用 mocha 测试框架来进行测试:

rush:js;"> "test": "NODE_ENV=test mocha test/**/*.js",

生成接口文档

接口文档也是很重要的一个环节,该项目使用的是 ApiDoc.js 。这个没什么好说的,直接上代码

rush:js;"> /** * @api {get} /tasks List the user's tasks * @apiGroup Tasks * @apiHeader {String} Authorization Token of authenticated user * @apiHeaderExample {json} Header * { * "Authorization": "xyz.abc.123.hgf" * } * @apiSuccess {Object[]} tasks Task list * @apiSuccess {Number} tasks.id Task id * @apiSuccess {String} tasks.title Task title * @apiSuccess {Boolean} tasks.done Task is done? * @apiSuccess {Date} tasks.updated_at Update's date * @apiSuccess {Date} tasks.created_at Register's date * @apiSuccess {Number} tasks.user_id The id for the user's * @apiSuccessExample {json} Success * HTTP/1.1 200 OK * [{ * "id": 1,* "title": "Study",* "done": false,* "updated_at": "2016-02-10T15:46:51.778Z",* "created_at": "2016-02-10T15:46:51.778Z",* "user_id": 1 * }] * @apiErrorExample {json} List error * HTTP/1.1 412 Precondition Failed */

/**

  • @api {post} /users Register a new user
  • @apiGroup User
  • @apiParam {String} name User name
  • @apiParam {String} email User email
  • @apiParam {String} password User password
  • @apiParamExample {json} Input
  • {
  • "name": "James", "email": "James@mc.com", "password": "123456"
  • }
  • @apiSuccess {Number} id User id
  • @apiSuccess {String} name User name
  • @apiSuccess {String} email User email
  • @apiSuccess {String} password User encrypted password
  • @apiSuccess {Date} update_at Update's date
  • @apiSuccess {Date} create_at Rigister's date
  • @apiSuccessExample {json} Success
  • {
  • "id": 1, "name": "James", "updated_at": "2016-02-10T15:20:11.700Z",* "created_at": "2016-02-10T15:29:11.700Z"
  • }
  • @apiErrorExample {json} Rergister error
  • HTTP/1.1 412 Precondition Failed
    */

大概就类似与上边的样子,既可以做注释用,又可以自动生成文档,一石二鸟,我就不上图了。

准备发布

到了这里,就只剩下发布前的一些操作了,

有的时候,处于安全方面的考虑,我们的API可能只允许某些域名的访问,因此在这里引入一个强大的模块 cors ,介绍它的文章,网上有很多,大家可以直接搜索,在该项目中是这么使用的:

rush:js;"> app.use(cors({ origin: ["http://localhost:3001"],"Authorization"] }));

这个设置在本文的最后的演示网站中,会起作用。

打印请求日志同样是一个很重要的任务,因此引进了 winston 模块。下边是对他的配置:

rush:js;"> import fs from "fs" import winston from "winston"

if (!fs.existsSync("logs")) {
fs.mkdirsync("logs");
}

module.exports = new winston.Logger({
transports: [
new winston.transports.File({
level: "info",filename: "logs/app.log",maxsize: 1048576,maxFiles: 10,colorize: false
})
]
});

打印的结果大概是这样的:

rush:plain;"> {"level":"info","message":"::1 - - [26/Sep/2017:11:16:23 +0000] \"GET /tasks HTTP/1.1\" 200 616\n","timestamp":"2017-09-26T11:16:23.089Z"} {"level":"info","message":"::1 - - [26/Sep/2017:11:16:43 +0000] \"OPTIONS /user HTTP/1.1\" 204 0\n","timestamp":"2017-09-26T11:16:43.583Z"} {"level":"info","message":"Tue Sep 26 2017 19:16:43 GMT+0800 (CST) Executing (default): SELECT `id`,`name`,`password`,`email`,`created_at`,`updated_at` FROM `Users` AS `Users` WHERE `Users`.`id` = 342;","timestamp":"2017-09-26T11:16:43.592Z"} {"level":"info",`email` FROM `Users` AS `Users` WHERE `Users`.`id` = 342;","timestamp":"2017-09-26T11:16:43.596Z"} {"level":"info","message":"::1 - - [26/Sep/2017:11:16:43 +0000] \"GET /user HTTP/1.1\" 200 73\n","timestamp":"2017-09-26T11:16:43.599Z"} {"level":"info","message":"::1 - - [26/Sep/2017:11:16:49 +0000] \"OPTIONS /user HTTP/1.1\" 204 0\n","timestamp":"2017-09-26T11:16:49.658Z"} {"level":"info","message":"Tue Sep 26 2017 19:16:49 GMT+0800 (CST) Executing (default): SELECT `id`,"timestamp":"2017-09-26T11:16:49.664Z"} {"level":"info","message":"Tue Sep 26 2017 19:16:49 GMT+0800 (CST) Executing (default): DELETE FROM `Users` WHERE `id` = 342","timestamp":"2017-09-26T11:16:49.669Z"} {"level":"info","message":"::1 - - [26/Sep/2017:11:16:49 +0000] \"DELETE /user HTTP/1.1\" 204 -\n","timestamp":"2017-09-26T11:16:49.714Z"} {"level":"info","message":"::1 - - [26/Sep/2017:11:17:04 +0000] \"OPTIONS /token HTTP/1.1\" 204 0\n","timestamp":"2017-09-26T11:17:04.905Z"} {"level":"info","message":"Tue Sep 26 2017 19:17:04 GMT+0800 (CST) Executing (default): SELECT `id`,`updated_at` FROM `Users` AS `Users` WHERE `Users`.`email` = 'xiaoxiao@mc.com' LIMIT 1;","timestamp":"2017-09-26T11:17:04.911Z"} {"level":"info","message":"::1 - - [26/Sep/2017:11:17:04 +0000] \"POST /token HTTP/1.1\" 401 12\n","timestamp":"2017-09-26T11:17:04.916Z"}

性能上,我们使用Node.js自带的cluster来利用机器的多核,代码如下:

const CPUS = os.cpus();

if (cluster.isMaster) {
// Fork
CPUS.forEach(() => cluster.fork());

// Listening connection event
cluster.on("listening",work => {
"use strict";
console.log(Cluster ${work.process.pid} connected);
});

// disconnect
cluster.on("disconnect",work => {
"use strict";
console.log(Cluster ${work.process.pid} <a href="https://www.jb51.cc/tag/dis/" target="_blank" class="keywords">dis</a>connected);
});

// Exit
cluster.on("exit",worker => {
"use strict";
console.log(Cluster ${worker.process.pid} is dead);
cluster.fork();
});

} else {
require("./index");
}

在数据传输上,我们使用 compression 模块对数据进行了gzip压缩,这个使用起来比较简单:

rush:js;"> app.use(compression());

最后,让我们支持https访问,https的关键就在于证书,使用授权机构的证书是最好的,但该项目中,我们使用这个网站自动生成了一组证书,然后启用https的服务:

module.exports = app => {
"use strict";
if (process.env.NODE_ENV !== "test") {

const credentials = {
key: fs.readFileSync("44885970_www.localhost.com.key","utf8"),cert: fs.readFileSync("44885970_www.localhost.com.cert","utf8")
};

app.db.sequelize.sync().done(() => {

https.createServer(credentials,app)
.listen(app.get("port"),() => {
console.log(NTask API - Port ${app.get("port")});
});
});
}
};

当然,处于安全考虑,防止攻击,我们使用了 helmet 模块:

前端程序

为了更好的演示该API,我把前段的代码也上传到了这个仓库,直接下载后,运行就行了。

API的代码连接nofollow" target="_blank" href="https://github.com/agelessman/ntask-api">https://github.com/agelessman/ntask-api

总结

我觉得这本书写的非常好,我收获很多。它虽然并不复杂,但是该有的都有了,因此我可以自由的往外延伸。同时也学到了作者驾驭代码的能力。

我觉得我还达不到把所学所会的东西讲明白。有什么错误的地方,还请给予指正。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持编程之家。

原文地址:https://www.jb51.cc/nodejs/36029.html

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐