この記事では主に、完全な Node.js RESTful API の実装例を紹介します。編集者が非常に優れていると考えたので、参考として共有します。編集者をフォローして見てみましょう
前書き
この記事は、書籍『Building APIs with Node.js』の要約です。たとえば、プロジェクトの初期段階でネットワーク リクエストをすばやくシミュレートできるなど、Node.js でインターフェイスを作成すると非常に便利です。 js で書かれているからこそ、他の言語で書かれたバックエンドよりも iOS との直接的なつながりが近いのです。
この本は非常によく書かれており、著者のコーディングのアイデアが非常に明確です。全編英語で書かれていますが、読みやすいです。同時に、RESTful API の完全なロジック セットを完全に構築します。
私は、関数をデータまたはパラメーターとして渡す関数応答プログラムを作成することを好みます。
プログラムの構築から、エラー捕捉メカニズムの設計、プログラムのテストタスクまで、これは完全なプロセスです。この記事は非常に長くなるので、各コアコンセプトのコードを貼り付けます。
環境セットアップ
Node.jsのダウンロードとインストール https://nodejs.org/ja/
npmのインストール
デモプロジェクトをダウンロード
git clone https://github.com/agelessman/ntask-api
プロジェクトフォルダーに入って実行
npm install
上記のコマンドは、プロジェクトに必要なプラグインをダウンロードし、プロジェクトを開始します
npm start
インターフェイスドキュメントにアクセスします http://localhost:3000/apidoc
プログラムの入り口
Express 誰もがこのフレームワークを知っているはずですが、ここでは説明しません。まずプロジェクト内のコードを見てみましょう: モデル、ビュー、ルーターのいずれであっても。 、Express によって処理および構成されます。このプロジェクトではビューは使用されません。 Express はアプリを通じてプロジェクト全体の機能を設定しますが、すべてのパラメーターとメソッドをこのファイルに書き込むことはできません。そうしないと、プロジェクトが大規模になるとメンテナンスが困難になります。
@note: インポートの順序は重要です。
ここでは、アプリはグローバル変数のように使用されており、これを次のコンテンツで示します。順番にインポートした後、次の方法でモジュールのコンテンツにアクセスできます:
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;
モデル設計
私の意見では、プロジェクトを開始する前に要件分析が最も重要です。要件分析後、コード設計に関する大きなコンセプトが決まります。
エンコードの本質とは何ですか?データの保存と送信だと思いますが、パフォーマンスやセキュリティの問題も考慮する必要があります そこで、2番目のタスクは、需要分析の結果を反映できるデータモデルを設計することです。このプロジェクトにはユーザーとタスクの 2 つのモデルがあり、各タスクはユーザーに対応します。 ユーザー モデル:
タスク モデル:app.db app.auth app.libs....
もちろん、データベースとしてシステムに付属している他のデータベースも使用できます。リレーショナルか非リレーショナルかに制限はありません。データをより適切に管理するために、Sequelize モジュールを使用してデータベースを管理します。
上記のコードでは、モデルの出力テンプレートと入力テンプレートを定義し、いくつかの特定のフィールドを検証しました。そのため、使用中にデータベースからのエラーが発生する可能性があります。これらのエラーについては、以下で説明します。
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; };
データベース
上記ですでに知っているように、データベースを管理するために Sequelize モジュールを使用します。実際、最も単純なレベルでは、データベースはデータ モデルを提供するだけで、これらのモデルを取得した後、さまざまなニーズに応じてさまざまな CRUD 操作を実行できます。
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; };
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
总结
我觉得这本书写的非常好,我收获很多。它虽然并不复杂,但是该有的都有了,因此我可以自由的往外延伸。同时也学到了作者驾驭代码的能力。
自分が学んだことをまだ明確に説明できていないように感じます。間違いがあれば修正してください。
以上がNode.js RESTful API 実装の完全なサンプル共有の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。