- 資訊首頁(yè) > 開(kāi)發(fā)技術(shù) > web開(kāi)發(fā) >
- 如何實(shí)現一個(gè)完整的Node.js RESTful API
這篇文章給大家分享的是有關(guān)如何實(shí)現一個(gè)完整的Node.js RESTful API的內容。小編覺(jué)得挺實(shí)用的,因此分享給大家做個(gè)參考,一起跟隨小編過(guò)來(lái)看看吧。
環(huán)境搭建
下載并安裝Node.js https://nodejs.org/en/
安裝npm
下載演示項目
git clone https://github.com/agelessman/ntask-api
進(jìn)入項目文件夾后運行
npm install
上邊命令會(huì )下載項目所需的插件,然后啟動(dòng)項目
npm start
訪(fǎng)問(wèn)接口文檔 http://localhost:3000/apidoc
程序入口
Express 這個(gè)框架大家應該都知道,他提供了很豐富的功能,我在這就不做解釋了,先看該項目中的代碼:
import express from "express" import consign from "consign" const app = express(); /// 在使用include或者then的時(shí)候,是有順序的,如果傳入的參數是一個(gè)文件夾 /// 那么他會(huì )按照文件夾中文件的順序進(jìn)行加載 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都會(huì )經(jīng)過(guò) Express 的加工和配置。在該項目中并沒(méi)有使用到views的地方。 Express 通過(guò)app對整個(gè)項目的功能進(jìn)行配置,但我們不能把所有的參數和方法都寫(xiě)到這一個(gè)文件之中,否則當項目很大的時(shí)候將急難維護。
我使用Node.js的經(jīng)驗是很少的,但上面的代碼給我的感覺(jué)就是極其簡(jiǎn)潔,思路極其清晰,通過(guò) consign 這個(gè)模塊導入其他模塊在這里就讓代碼顯得很優(yōu)雅。
@note:導入的順序很重要。
在這里,app的使用很像一個(gè)全局變量,這個(gè)我們會(huì )在下邊的內容中展示出來(lái),按序導入后,我們就可以通過(guò)這樣的方式訪(fǎng)問(wèn)模塊的內容了:
app.db app.auth app.libs....
模型設計
在我看來(lái),在開(kāi)始做任何項目前,需求分析是最重要的,經(jīng)過(guò)需求分析后,我們會(huì )有一個(gè)關(guān)于代碼設計的大的概念。
編碼的實(shí)質(zhì)是什么?我認為就是數據的存儲和傳遞,同時(shí)還需要考慮性能和安全的問(wèn)題
因此我們第二部的任務(wù)就是設計數據模型,同時(shí)可以反應出我們需求分析的成果。在該項目中有兩個(gè)模型, User 和 Task ,每一個(gè) task 對應一個(gè) user ,一個(gè) user 可以有多個(gè) task
用戶(hù)模型:
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; };
任務(wù)模型:
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; };
該項目中使用了系統自帶的 sqlite 作為數據庫,當然也可以使用其他的數據庫,這里不限制是關(guān)系型的還是非關(guān)系型的。為了更好的管理數據,我們使用 sequelize 這個(gè)模塊來(lái)管理數據庫。
為了節省篇幅,這些模塊我就都不介紹了,在google上一搜就出來(lái)了。在我看的Node.js的開(kāi)發(fā)中,這種ORM的管理模塊有很多,比如說(shuō)對 進(jìn)行管理的 mongoose 。很多很多,他們主要的思想就是Scheme。
在上邊的代碼中,我們定義了模型的輸出和輸入模板,同時(shí)對某些特定的字段進(jìn)行了驗證,因此在使用的過(guò)程中就有可能會(huì )產(chǎn)生來(lái)自數據庫的錯誤,這些錯誤我們會(huì )在下邊講解到。
Tasks.associate = (models) => { Tasks.belongsTo(models.Users); }; Users.associate = (models) => { Users.hasMany(models.Tasks); }; Users.isPassword = (encodedPassword, password) => { return bcrypt.compareSync(password, encodedPassword); };
hasMany 和 belongsTo 表示一種關(guān)聯(lián)屬性, Users.isPassword 算是一個(gè)類(lèi)方法。 bcrypt 模塊可以對密碼進(jìn)行加密編碼。
數據庫
在上邊我們已經(jīng)知道了,我們使用 sequelize 模塊來(lái)管理數據庫。其實(shí),在最簡(jiǎn)單的層面而言,數據庫只需要給我們數據模型就行了,我們拿到這些模型后,就能夠根據不同的需求,去完成各種各樣的CRUD操作。
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; };
上邊的代碼很簡(jiǎn)單,db是一個(gè)對象,他存儲了所有的模型,在這里是 User 和 Task 。通過(guò) sequelize.import 獲取模型,然后又調用了之前寫(xiě)好的associate方法。
上邊的函數調用之后呢,返回db,db中有我們需要的模型,到此為止,我們就建立了數據庫的聯(lián)系,作為對后邊代碼的一個(gè)支撐。
CRUD
CRUD在router中,我們先看看 router/tasks.js 的代碼:
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}); }); }); };
這些路由寫(xiě)起來(lái)比較簡(jiǎn)單,上邊的代碼中,基本思想就是根據模型操作CRUD,包括捕獲異常。但是額外的功能是做了authenticate,也就是授權操作。
這一塊好像沒(méi)什么好說(shuō)的,基本上都是固定套路。
授權
在網(wǎng)絡(luò )環(huán)境中,不能老是傳遞用戶(hù)名和密碼。這時(shí)候就需要一些授權機制,該項目中采用的是JWT授權(JSON Wbb Toknes),有興趣的同學(xué)可以去了解下這個(gè)授權,它也是按照一定的規則生成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 模塊生成一個(gè)token。
JWT在這也不多說(shuō)了,它由三部分組成,這個(gè)在它的官網(wǎng)中解釋的很詳細。
我覺(jué)得老外寫(xiě)東西一個(gè)最大的優(yōu)點(diǎn)就是文檔很詳細。要想弄明白所有組件如何使用,最好的方法就是去他們的官網(wǎng)看文檔,當然這要求英文水平還可以。
授權一般分兩步:
生成token
驗證token
如果從前端傳遞一個(gè)token過(guò)來(lái),我們怎么解析這個(gè)token,然后獲取到token里邊的用戶(hù)信息呢?
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 這兩個(gè)模塊。 passport 支持很多種授權。不管是iOS還是Node中,驗證都需要指定一個(gè)策略,這個(gè)策略是最靈活的一層。
授權需要在項目中提前進(jìn)行配置,也就是初始化, app.use(app.auth.initialize()); 。
如果我們想對某個(gè)接口進(jìn)行授權驗證,那么只需要像下邊這么用就可以了:
.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中一個(gè)很有用的思想就是middleware,我們可以利用這個(gè)手段做很多有意思的事情:
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表示進(jìn)行設置,app.use表示使用middleware。
測試
寫(xiě)測試代碼是我平時(shí)很容易疏忽的地方,說(shuō)實(shí)話(huà),這么重要的部分不應該被忽視。
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); }); }); }); }); });
測試主要依賴(lài)下邊的這幾個(gè)模塊:
import supertest from "supertest" import chai from "chai" import app from "../index" global.app = app; global.request = supertest(app); global.expect = chai.expect;
其中 supertest 用來(lái)發(fā)請求的, chai 用來(lái)判斷是否成功。
使用 mocha 測試框架來(lái)進(jìn)行測試:
"test": "NODE_ENV=test mocha test/**/*.js",
生成接口文檔
接口文檔也是很重要的一個(gè)環(huán)節,該項目使用的是 ApiDoc.js 。這個(gè)沒(méi)什么好說(shuō)的,直接上代碼:
/** * @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 */
大概就類(lèi)似與上邊的樣子,既可以做注釋用,又可以自動(dòng)生成文檔,一石二鳥(niǎo),我就不上圖了。
準備發(fā)布
到了這里,就只剩下發(fā)布前的一些操作了,
有的時(shí)候,處于安全方面的考慮,我們的API可能只允許某些域名的訪(fǎng)問(wèn),因此在這里引入一個(gè)強大的模塊 cors ,介紹它的文章,網(wǎng)上有很多,大家可以直接搜索,在該項目中是這么使用的:
app.use(cors({ origin: ["http://localhost:3001"], methods: ["GET", "POST", "PUT", "DELETE"], allowedHeaders: ["Content-Type", "Authorization"] }));
這個(gè)設置在本文的最后的演示網(wǎng)站中,會(huì )起作用。
打印請求日志同樣是一個(gè)很重要的任務(wù),因此引進(jìn)了 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來(lái)利用機器的多核,代碼如下:
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 模塊對數據進(jìn)行了gzip壓縮,這個(gè)使用起來(lái)比較簡(jiǎn)單:
app.use(compression());
最后,讓我們支持https訪(fǎng)問(wèn),https的關(guān)鍵就在于證書(shū),使用授權機構的證書(shū)是最好的,但該項目中,我們使用http://www.selfsignedcertificate.com這個(gè)網(wǎng)站自動(dòng)生成了一組證書(shū),然后啟用https的服務(wù):
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,我把前段的代碼也上傳到了這個(gè)倉庫https://github.com/agelessman/ntaskWeb,直接下載后,運行就行了。
API的代碼連接https://github.com/agelessman/ntask-api
免責聲明:本站發(fā)布的內容(圖片、視頻和文字)以原創(chuàng )、來(lái)自互聯(lián)網(wǎng)轉載和分享為主,文章觀(guān)點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權請聯(lián)系站長(cháng)郵箱:ts@56dr.com進(jìn)行舉報,并提供相關(guān)證據,一經(jīng)查實(shí),將立刻刪除涉嫌侵權內容。
Copyright ? 2009-2021 56dr.com. All Rights Reserved. 特網(wǎng)科技 版權所有 珠海市特網(wǎng)科技有限公司 粵ICP備16109289號
域名注冊服務(wù)機構:阿里云計算有限公司(萬(wàn)網(wǎng)) 域名服務(wù)機構:煙臺帝思普網(wǎng)絡(luò )科技有限公司(DNSPod) CDN服務(wù):阿里云計算有限公司 中國互聯(lián)網(wǎng)舉報中心 增值電信業(yè)務(wù)經(jīng)營(yíng)許可證B2 建議您使用Chrome、Firefox、Edge、IE10及以上版本和360等主流瀏覽器瀏覽本網(wǎng)站