国产探花免费观看_亚洲丰满少妇自慰呻吟_97日韩有码在线_资源在线日韩欧美_一区二区精品毛片,辰东完美世界有声小说,欢乐颂第一季,yy玄幻小说排行榜完本

首頁 > 編程 > JavaScript > 正文

實現一個完整的Node.js RESTful API的示例

2019-11-19 15:15:38
字體:
來源:轉載
供稿:網友

前言

這篇文章算是對Building APIs with Node.js這本書的一個總結。用Node.js寫接口對我來說是很有用的,比如在項目初始階段,可以快速的模擬網絡請求。正因為它用js寫的,跟iOS直接的聯系也比其他語言寫的后臺更加接近。

這本書寫的極好,作者編碼的思路極其清晰,整本書雖說是用英文寫的,但很容易讀懂。同時,它完整的構建了RESTful API的一整套邏輯。

我更加喜歡寫一些函數響應式的程序,把函數當做數據或參數進行傳遞對我有著莫大的吸引力。

從程序的搭建,到設計錯誤捕獲機制,再到程序的測試任務,這是一個完整的過程。這邊文章將會很長,我會把每個核心概念的代碼都黏貼上來。

環境搭建

下載并安裝Node.js https://nodejs.org/en/

安裝npm

下載演示項目

git clone https://github.com/agelessman/ntask-api

進入項目文件夾后運行

npm install

上邊命令會下載項目所需的插件,然后啟動項目

npm start

訪問接口文檔 http://localhost:3000/apidoc

程序入口

Express 這個框架大家應該都知道,他提供了很豐富的功能,我在這就不做解釋了,先看該項目中的代碼:

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的使用很像一個全局變量,這個我們會在下邊的內容中展示出來,按序導入后,我們就可以通過這樣的方式訪問模塊的內容了:

app.dbapp.authapp.libs....

模型設計

在我看來,在開始做任何項目前,需求分析是最重要的,經過需求分析后,我們會有一個關于代碼設計的大的概念。

編碼的實質是什么?我認為就是數據的存儲和傳遞,同時還需要考慮性能和安全的問題

因此我們第二部的任務就是設計數據模型,同時可以反應出我們需求分析的成果。在該項目中有兩個模型, User 和 Task ,每一個 task 對應一個 user ,一個 user 可以有多個 task

用戶模型:

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;};

該項目中使用了系統自帶的 sqlite 作為數據庫,當然也可以使用其他的數據庫,這里不限制是關系型的還是非關系型的。為了更好的管理數據,我們使用 sequelize 這個模塊來管理數據庫。

為了節省篇幅,這些模塊我就都不介紹了,在google上一搜就出來了。在我看的Node.js的開發中,這種ORM的管理模塊有很多,比如說對 MongoDB 進行管理的 mongoose 。很多很多,他們主要的思想就是Scheme。

在上邊的代碼中,我們定義了模型的輸出和輸入模板,同時對某些特定的字段進行了驗證,因此在使用的過程中就有可能會產生來自數據庫的錯誤,這些錯誤我們會在下邊講解到。

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 表示一種關聯屬性, Users.isPassword 算是一個類方法。 bcrypt 模塊可以對密碼進行加密編碼。

數據庫

在上邊我們已經知道了,我們使用 sequelize 模塊來管理數據庫。其實,在最簡單的層面而言,數據庫只需要給我們數據模型就行了,我們拿到這些模型后,就能夠根據不同的需求,去完成各種各樣的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;};

上邊的代碼很簡單,db是一個對象,他存儲了所有的模型,在這里是 User 和 Task 。通過 sequelize.import 獲取模型,然后又調用了之前寫好的associate方法。

上邊的函數調用之后呢,返回db,db中有我們需要的模型,到此為止,我們就建立了數據庫的聯系,作為對后邊代碼的一個支撐。

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});   }); });};

這些路由寫起來比較簡單,上邊的代碼中,基本思想就是根據模型操作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

總結

我覺得這本書寫的非常好,我收獲很多。它雖然并不復雜,但是該有的都有了,因此我可以自由的往外延伸。同時也學到了作者駕馭代碼的能力。

我覺得我還達不到把所學所會的東西講明白。有什么錯誤的地方,還請給予指正。

以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支持武林網。

發表評論 共有條評論
用戶名: 密碼:
驗證碼: 匿名發表
主站蜘蛛池模板: 云阳县| 大洼县| 泾川县| 宁晋县| 扎赉特旗| 武鸣县| 图木舒克市| 大名县| 时尚| 贺州市| 平山县| 泸水县| 新疆| 克东县| 赞皇县| 北川| 遵化市| 牙克石市| 东阳市| 宝应县| 阳泉市| 邮箱| 长顺县| 萍乡市| 乌什县| 唐河县| 海城市| 工布江达县| 贞丰县| 木兰县| 湟中县| 青冈县| 铁岭市| 建阳市| 怀集县| 罗山县| 宜川县| 沙田区| 克什克腾旗| 巴林左旗| 区。|