In diesem Artikel wird hauptsächlich ein Beispiel für die Implementierung einer vollständigen Node.js-RESTful-API vorgestellt. Der Herausgeber findet es recht gut, daher werde ich es jetzt mit Ihnen teilen und als Referenz verwenden. Folgen wir dem Editor und werfen wir einen Blick darauf.
Vorwort
Dieser Artikel ist eine Zusammenfassung des Buches Building APIs with Node.js. Das Schreiben von Schnittstellen mit Node.js ist für mich sehr nützlich. In der Anfangsphase des Projekts kann ich beispielsweise schnell Netzwerkanfragen simulieren. Gerade weil es in js geschrieben ist, ist seine direkte Verbindung zu iOS enger als die von Backends, die in anderen Sprachen geschrieben sind.
Dieses Buch ist sehr gut geschrieben und die Codierungsideen des Autors sind äußerst klar. Obwohl das gesamte Buch auf Englisch geschrieben ist, ist es leicht zu lesen. Gleichzeitig wird ein vollständiger Logiksatz für die RESTful-API erstellt.
Ich bevorzuge es, einige funktionale, responsive Programme zu schreiben. Die Übergabe von Funktionen als Daten oder Parameter ist für mich sehr attraktiv.
Von der Erstellung des Programms über den Entwurf des Fehlererkennungsmechanismus bis hin zum Testen des Programms ist dies ein vollständiger Prozess. Dieser Artikel wird sehr lang sein, ich werde den Code jedes Kernkonzepts einfügen.
Umgebungseinrichtung
Node.js https://nodejs.org/en/ herunterladen und installieren
npm installieren
Laden Sie das Demoprojekt herunter
git clone https://github.com/agelessman/ntask-api
Geben Sie den Projektordner ein und führen Sie
npm install
npm start
Programmeintrag
Es bietet viele Funktionen, die ich hier nicht erklären werde Zuerst den Code im Projekt: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;
app.db app.auth app.libs....
Modelldesign
Meiner Meinung nach ist die Anforderungsanalyse das Wichtigste, bevor wir mit jedem Projekt beginnen, nach der Anforderungsanalyse werden wir sie haben ein großes Konzept zum Code-Design. Was ist die Essenz der Kodierung? Ich denke, es geht um die Speicherung und Übertragung von Daten, und wir müssen auch Leistungs- und Sicherheitsaspekte berücksichtigen Daher besteht unsere zweite Aufgabe darin, ein Datenmodell zu entwerfen, das die Ergebnisse unserer Bedarfsanalyse widerspiegeln kann. In diesem Projekt gibt es zwei Modelle: Benutzer und Aufgabe. Jede Aufgabe entspricht einem Benutzer. Benutzermodell:import bcrypt from "bcrypt" 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, allowNull: false, validate: { notEmpty: true } }, email: { type: DataType.STRING, unique: true, allowNull: false, 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; };
module.exports = (sequelize, DataType) => { "use strict"; const Tasks = sequelize.define("Tasks", { id: { type: DataType.INTEGER, primaryKey: true, autoIncrement: true }, title: { type: DataType.STRING, allowNull: false, validate: { notEmpty: true } }, done: { type: DataType.BOOLEAN, allowNull: false, defaultValue: false } }); Tasks.associate = (models) => { Tasks.belongsTo(models.Users); }; return Tasks; };
Tasks.associate = (models) => { Tasks.belongsTo(models.Users); }; Users.associate = (models) => { Users.hasMany(models.Tasks); }; Users.isPassword = (encodedPassword, password) => { return bcrypt.compareSync(password, encodedPassword); };
Datenbank
Wie wir oben bereits wissen, verwenden wir das Sequelize-Modul zur Verwaltung der Datenbank. Tatsächlich muss uns die Datenbank auf der einfachsten Ebene nur Datenmodelle zur Verfügung stellen. Nachdem wir diese Modelle erhalten haben, können wir verschiedene CRUD-Vorgänge je nach Bedarf ausführen.import fs from "fs" import path from "path" import Sequelize from "sequelize" 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; };
CRUD
CRUD ist im Router, schauen wir uns zuerst den Code von router/tasks.js an:module.exports = app => { "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
的代码:
module.exports = app => { "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。
import jwt from "jwt-simple" module.exports = app => { "use strict"; const cfg = app.libs.config; const Users = app.db.models.Users; app.post("/token", (req, 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过来,我们怎么解析这个token,然后获取到token里边的用户信息呢?
import passport from "passport"; import {Strategy, ExtractJwt} from "passport-jwt"; 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()); 。
如果我们想对某个接口进行授权验证,那么只需要像下边这么用就可以了:
.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}); }); })
配置
Node.js中一个很有用的思想就是middleware,我们可以利用这个手段做很多有意思的事情:
import bodyParser from "body-parser" import express from "express" import cors from "cors" import morgan from "morgan" import logger from "./logger" import compression from "compression" import helmet from "helmet" 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。
测试
写测试代码是我平时很容易疏忽的地方,说实话,这么重要的部分不应该被忽视。
import jwt from "jwt-simple" 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", () => { describe("status 200", () => { 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); }); }); }); }); });
测试主要依赖下边的这几个模块:
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 测试框架来进行测试:
"test": "NODE_ENV=test mocha test/**/*.js",
生成接口文档
接口文档也是很重要的一个环节,该项目使用的是 ApiDoc.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", * "email": "James@mc.com", * "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 ,介绍它的文章,网上有很多,大家可以直接搜索,在该项目中是这么使用的:
app.use(cors({ origin: ["http://localhost:3001"], methods: ["GET", "POST", "PUT", "DELETE"], allowedHeaders: ["Content-Type", "Authorization"] }));
这个设置在本文的最后的演示网站中,会起作用。
打印请求日志同样是一个很重要的任务,因此引进了 winston 模块。下边是对他的配置:
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 }) ] });
打印的结果大概是这样的:
{"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","message":"Tue Sep 26 2017 19:16:43 GMT+0800 (CST) Executing (default): SELECT `id`, `name`, `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`, `name`, `password`, `email`, `created_at`, `updated_at` FROM `Users` AS `Users` WHERE `Users`.`id` = 342;","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`, `name`, `password`, `email`, `created_at`, `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来利用机器的多核,代码如下:
import cluster from "cluster" import os from "os" 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} disconnected`); }); // Exit cluster.on("exit", worker => { "use strict"; console.log(`Cluster ${worker.process.pid} is dead`); cluster.fork(); }); } else { require("./index"); }
在数据传输上,我们使用 compression 模块对数据进行了gzip压缩,这个使用起来比较简单:
app.use(compression());
最后,让我们支持https访问,https的关键就在于证书,使用授权机构的证书是最好的,但该项目中,我们使用http://www.selfsignedcertificate.com这个网站自动生成了一组证书,然后启用https的服务:
import https from "https" import fs from "fs" 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 模块:
app.use(helmet());
前端程序
为了更好的演示该API,我把前段的代码也上传到了这个仓库https://github.com/agelessman/ntaskWeb,直接下载后,运行就行了。
API的代码连接https://github.com/agelessman/ntask-api
总结
我觉得这本书写的非常好,我收获很多。它虽然并不复杂,但是该有的都有了,因此我可以自由的往外延伸。同时也学到了作者驾驭代码的能力。
Ich habe das Gefühl, dass ich noch nicht in der Lage bin, klar zu erklären, was ich gelernt habe. Sollten Fehler vorhanden sein, korrigieren Sie diese bitte.
Das obige ist der detaillierte Inhalt vonEin vollständiges Beispiel für die gemeinsame Nutzung der RESTful-API-Implementierung von Node.js. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!