打开APP
userphoto
未登录

开通VIP,畅享免费电子书等14项超值服

开通VIP
一个完整的Node.js RESTful API

前言

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

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

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

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

环境搭建

下载并安装Node.jshttps://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....

模型设计

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

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

因此我们第二部的任务就是设计数据模型,同时可以反应出我们需求分析的成果。在该项目中有两个模型,UserTask,每一个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);};

hasManybelongsTo表示一种关联属性,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是一个对象,他存储了所有的模型,在这里是UserTask。通过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);        }    };};

这就用到了passportpassport-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

总结

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

我觉得我还达不到把所学所会的东西讲明白。

有什么错误的地方,还请给予指正。

本站仅提供存储服务,所有内容均由用户发布,如发现有害或侵权内容,请点击举报
打开APP,阅读全文并永久保存 查看更多类似文章
猜你喜欢
类似文章
【热】打开小程序,算一算2024你的财运
koa2实现jwt登录
bd- token的详解
登录鉴权的三种方式:token、jwt、session简洁例子
第 82 天:Python Web 开发之 JWT 简介
详解 Cookie,Session,Token
什么是 JSON Web Token
更多类似文章 >>
生活服务
热点新闻
分享 收藏 导长图 关注 下载文章
绑定账号成功
后续可登录账号畅享VIP特权!
如果VIP功能使用有故障,
可点击这里联系客服!

联系客服