八、使用 MongoDB、MySQL 和 Node.js 创建 API

本章将介绍以下配方:

  • 使用 Express 创建基本 API
  • 使用 MongoDB 构建数据库
  • 用 MySQL 构建数据库
  • 添加访问令牌以保护我们的 API

介绍

来自 Node.js 官方网站(https://nodejs.org

Node.js is a JavaScript runtime built on Chrome's V8 JavaScript engine. Node.js uses an event-driven, non-blocking I/O model that makes it lightweight and efficient. Node.js' package ecosystem, npm, is the largest ecosystem of open source libraries in the world.

Node.js 被广泛用作 web 应用的后端,因为它易于创建 API,并且其性能优于 Java、PHP 或 Ruby 等技术。通常,使用 Node.js 最常用的方法是使用一个名为 Express 的框架。

来自快递官方网站(https://expressjs.com

Express is a minimal and flexible Node.js web application framework that provides a robust set of features for web and mobile applications.

使用 Express 创建基本 API

Express 是最流行的 Node.js 框架,易于安装和使用。在此配方中,我们将使用 Express 创建、配置和安装一个基本 API。

准备

首先,我们需要安装 Node。您需要访问官方网站www.nodejs.org,然后下载 Node.js。有两个版本:LTS长期支持版本)和当前版本,具有最新功能。在我看来,选择 LTS 版本总是更好,但这取决于您。

安装节点后,可以通过在终端中运行以下命令来检查您的版本:

node -v
v10.8.0

此外,默认情况下,节点包括节点包管理器(npm)。您可以使用此命令检查您的版本:

npm -v
6.3.0

现在我们需要安装 Express。为此,有一个名为express-generator的包,它允许我们用一个简单的命令创建一个 Express 应用。我们需要在全球范围内安装它:

npm install -g express-generator

安装express-generator后,我们可以创建一个 Express 应用。我通常更喜欢在我的 Mac 上的主文件夹中创建一个名为projects的目录,或者如果您使用 Windows,您可以在C:\projects处创建:

express my-first-express-app

运行命令后,您将看到如下内容:

如果您按照说明运行应用,您将看到 Express 应用在http://localhost:3000处运行:

 cd my-first-express-app
 npm install
npm start 

您将看到以下视图:

怎么做。。。

express-generator默认生成的代码为 ES5 代码,使用varrequiremodule.exports等:

  1. 我们需要做的第一件事是将此代码转换为 ES6。为此,我们首先修改app.js文件。这是此文件的原始代码:
  var createError = require('http-errors');
  var express = require('express');
  var path = require('path');
  var cookieParser = require('cookie-parser');
  var logger = require('morgan');

  var indexRouter = require('./routes/index');
  var usersRouter = require('./routes/users');

  var app = express();

  // view engine setup
  app.set('views', path.join(__dirname, 'views'));
  app.set('view engine', 'jade');

  app.use(logger('dev'));
  app.use(express.json());
  app.use(express.urlencoded({ extended: false }));
  app.use(cookieParser());
  app.use(express.static(path.join(__dirname, 'public')));

  app.use('/', indexRouter);
  app.use('/users', usersRouter);

  // catch 404 and forward to error handler
  app.use(function(req, res, next) {
    next(createError(404));
  });

  // error handler
  app.use(function(err, req, res, next) {
    // set locals, only providing error in development
    res.locals.message = err.message;
    res.locals.error = req.app.get('env') === 'development' ? err : {};

    // render the error page
    res.status(err.status || 500);
    res.render('error');
  });

 module.exports = app;

File: app.js

  1. 迁移到 ES6 时,我们应该有以下代码:
  import createError from 'http-errors';
  import express from 'express';
  import path from 'path';
  import cookieParser from 'cookie-parser';
  import logger from 'morgan';

  import indexRouter from './routes/index';
  import usersRouter from './routes/users';

  const app = express();

  // view engine setup
  app.set('views', path.join(__dirname, 'views'));
  app.set('view engine', 'jade');

  app.use(logger('dev'));
  app.use(express.json());
  app.use(express.urlencoded({ extended: false }));
  app.use(cookieParser());
  app.use(express.static(path.join(__dirname, 'public')));

  app.use('/', indexRouter);
  app.use('/users', usersRouter);

  // catch 404 and forward to error handler
  app.use((req, res, next) => {
    next(createError(404));
  });

  // error handler
  app.use((err, req, res, next) => {
    // set locals, only providing error in development
    res.locals.message = err.message;
    res.locals.error = req.app.get('env') === 'development' ? err : {};

    // render the error page
    res.status(err.status || 500);
    res.render('error');
  });

  // Listening port
  app.listen(3000);

File: app.js

  1. 现在我们删除我们的bin/www目录,因为我们在文件末尾添加了app.listen(3000);,然后您需要修改package.json中的start脚本:
  "scripts": {
    "start": "node app.js"
  }

File: package.json

  1. 如果您尝试使用npm start运行应用,则会出现以下错误:

  1. 此错误是因为我们的 ES6 代码不能直接用于节点。我们需要使用 Babel 来编译我们的文件,并能够编写 ES6 代码。为此,我们需要在全球范围内安装babel-cli以及babel-preset-es2015包:
    npm install -g babel-cli
 npm install babel-preset-es2015
  1. 要使其工作,我们需要创建一个名为.babelrc的新文件,并添加我们的es2015预设:
    {
      "presets": ["es2015"]
    }

File: .babelrc

  1. 现在您需要再次更改您的start脚本,并将node切换到babel-node
  "scripts": {
    "start": "babel-node app.js"
  }

File: package.json

  1. 如果您在终端上运行npm start,现在应该可以运行应用了。
  2. 在我们将代码更改为 ES6 之后,我们遇到了另一个问题。如果修改文件并将其保存在应用中,则该文件不会刷新。此外,如果由于某种原因我们的应用崩溃,那么我们的服务器将停止工作。解决此问题的方法是使用节点监视程序。最受欢迎的是nodemon
    npm install nodemon
  1. 您需要为此修改您的start脚本:
  "scripts": {
    "start": "nodemon app.js --exec babel-node"
  }

File: package.json

  1. 现在,如果您对应用进行任何更改(例如,在routes/index.js文件中,您可以更改第 6 行中的文本Express中的任何其他内容),您将看到服务器如何重新启动并刷新站点:

  1. 如您所见,绿色的第一条消息显示starting babel-node app.js,然后当它检测到更改时,显示由于更改而重新启动。。。现在我们可以看到我们网站上反映的变化:

  1. 因为我们的 Express 应用是作为 API 而不是常规网站创建的,所以我们需要删除许多多余的内容,例如views文件夹和模板引擎,并且我们需要进行一些结构更改以使其更易于处理。让我们看看我们的app.js文件现在是什么样子:
  // Dependencies
  import express from 'express';
  import path from 'path';

  // Controllers
  import apiController from './controllers/api';

  // Express Application
  const app = express();

  // Middlewares
  app.use(express.json());
  app.use(express.urlencoded({ extended: false }));

  // Routes
  app.use('/api', apiController);

  // Listening port
  app.listen(3000);

File: app.js

  1. 如您所见,我将routes目录重命名为controllers,同时删除了该文件夹中的users.js文件,并将index.js重命名为api.js。让我们创建一个 API 来处理博客:
  import express from 'express';

  const router = express.Router();

  // Mock data, this should come from a database....
  const posts = [
    {
      id: 1,
      title: 'My blog post 1',
      content: '<p>Content</p>',
      author: 'Carlos Santana'
    },
    {
      id: 2,
      title: 'My blog post 2',
      content: '<p>Content</p>',
      author: 'Cristina Rojas'
    },
    {
      id: 3,
      title: 'My blog post 3',
      content: '<p>Content</p>',
      author: 'Carlos Santana'
    }
  ];

  router.get('/', (req, res, next) => {
    res.send(`
      <p>API Endpoints:</p>
      <ul>
        <li>/api/posts</li>
        <li>/api/post/:id</li>
      </ul>
    `);
  });

  router.get('/posts', (req, res, next) => {
    res.json({
      response: posts
    });
  });

  router.get('/post/:id', (req, res, next) => {
    const { params: { id } } = req;

    const singlePost = posts.find(post => post.id === Number(id));

    if (!singlePost) {
      res.send({
        error: true,
        message: 'Post not found'
      });
    }

    res.json({
      response: [singlePost]
    });
  });

 export default router;

File: controllers/api.js

它是如何工作的。。。

现在让我们测试一下我们的新 API:

  1. 如果我们转到http://localhost:3000/api,我们将显示端点列表。这是可选的,但作为开发人员的参考非常有用:

  1. 如果您进入http://localhost:3000/api/posts,您将看到所有帖子:

  1. 此外,如果您点击http://localhost:3000/api/post/1,您将获得列表的第一个帖子:

  1. 最后,如果您试图获取我们的数据中不存在的帖子(http://localhost:3000/api/post/99,那么我们将返回一个错误:

使用 MongoDB 构建数据库

MongoDB 是最流行的 NoSQL 数据库。它是免费的(开源)和面向文档的。在这个配方中,我们将安装 MongoDB,创建一个数据库,创建一个文档,并插入一些数据,使用 Node.js 使用 Mongoose 库显示信息。

准备

首先,我们需要安装 MongoDB。在这个食谱中,我将向您展示使用 Mac 安装它的最简单方法,如果您使用 Linux 或 Windows,我将为您提供一些安装它的链接。

From the MongoDB official documentation (https://docs.mongodb.com/manual/tutorial/install-mongodb-on-os-x): "Starting in version 3.0, MongoDB only supports MacOS version 10.7 (Lion) and later on Intel x86-64." 

手动安装 MongoDB 社区版(困难之路)

此安装适用于 Mac 和 Linux:

  1. 下载您想要的 MongoDB 版本的二进制文件 https://www.mongodb.com/download-center#community

  2. 从下载的文件中提取文件;您可以使用终端并使用以下命令:

    tar -zxvf mongodb-osx-ssl-x86_64-3.6.3.tgz
  1. 将提取的文件夹复制到 MongoDB 运行的位置:
    mkdir -p mongodb
 cp -R -n mongodb-osx-ssl-x86_64-3.6.3/ mongodb
  1. 确保二进制文件的位置在PATH变量中。您可以在 shell 的rc文件中添加以下行,例如~/.bashrc~/.bash_profile
    export PATH=<your-mongodb-install-directory>/bin:$PATH 

使用自制软件安装 MongoDB 社区版(简易方法)

Homebrew 是 Mac 的软件包管理器(也称为 macOS 的缺失软件包管理器,易于安装。访问官方网站(https://brew.sh),您将在那里找到一个安装它时应该运行的命令,如下所示:

 /usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
  1. 如果已经安装了 Homebrew,或者刚刚安装了 Homebrew,则首先需要使用以下命令更新软件包数据库:
    brew update
  1. 现在我们需要使用以下命令安装 MongoDB:
 brew install mongodb
  1. 如果您想安装 MongoDB 的最新开发版本,那么您应该运行此命令(我不建议这样做,因为它可能有一些尚未修复的 bug,但这取决于您):
    brew install mongodb --devel

运行 MongoDB

在我们第一次启动 MongoDB 之前,我们需要创建一个目录,mongod进程将在其中写入数据:

  1. 默认情况下,mongod 进程使用/data/db目录。要创建此文件夹,可以使用以下命令:
    mkdir -p /data/db
  1. 现在我们需要设置数据目录的权限:
    chmod -R 777 /data
  1. 在新终端(或选项卡)中,您需要运行以下操作:
    mongod
  1. 如果没有收到错误,您可以在与mongod相同的主机上启动 Mongo shell(在新的终端或选项卡中):
    mongo --host 127.0.0.1:127017

If you get an error like this: Error: Port number 127017 out of range parsing HostAndPort from "127.0.0.1:127017", then just run mongo without --host flag.

  1. 最后,如果要停止 MongoDB,请在mongod正在运行的终端中按Ctrl+C
  2. 如果一切正常,您应该在终端中看到:

怎么做。。。

首先,我们需要创建一个新的数据库:

  1. 要创建新数据库或切换到现有数据库,您需要运行:use <name of the database>。让我们创建一个博客数据库:
    use blog
  1. 现在我们需要创建一个名为posts的集合,您需要使用db.<your-collection-name>.save({})命令以 JSON 格式直接保存数据:
   db.posts.save({ title: 'Post 1', slug: 'post-1', content: '<p>Content</p>' })
  1. 如您所见,我没有添加任何id值,这是因为 MongoDB 会自动为每一行创建一个唯一的 ID,称为_id,这是一个随机散列。如果您想查看刚刚保存的数据,需要使用不带任何参数的find()方法:
   db.posts.find()
  1. 您应该看到这样的数据:

  1. 现在,假设您为 Post 2 添加了一个新行,并且希望通过指定 slug(Post-2)来查找该特定行。您可以这样做:
   db.posts.find({ slug: 'post-2' })
  1. 您应该看到:

  1. 现在,让我们将第 2 篇文章的标题更改为我更新的第 2 篇文章。为此,我们需要按如下方式更新行:
   db.posts.update({ slug: "post-2" }, { $set: { title: "My Updated Post 2" }})
  1. 第一个参数是查找要更新的行的查询,第二个参数使用$set修改字段。
  2. 最后,如果要删除特定行,可以按如下操作:
   db.posts.remove({ "_id": ObjectId("5ad2e6ed4fa0d047639da616") })
  1. 建议删除行的方法是直接指定_id,以避免错误删除其他行,但也可以通过任何其他字段删除行。例如,假设您想使用 slug 移除 Post 1。您可以这样做:
   db.posts.remove({ "slug": "post-1" })
  1. 现在您已经了解了如何使用 MongoDB 进行基本操作,让我们使用 Mongoose 库将 MongoDB 实现到 Node.js 中,Mongoose 库是 Node 的对象文档映射器ODM。我们需要为此配方安装一些额外的软件包:
 npm install mongoose body-parser slug
  1. 使用与上一个配方(Repository: Chapter08/Recipe1/my-first-express-app相同的代码,我们将 Mongoose 连接到 Node.js。我们需要做的第一件事是修改app.js
  // Dependencies
  import express from 'express';
  import path from 'path';
  import mongoose from 'mongoose';
  import bodyParser from 'body-parser';

  // Controllers
  import apiController from './controllers/api';

  // Express Application
  const app = express();

  // Middlewares
  app.use(bodyParser.json());
  app.use(bodyParser.urlencoded({ extended: false }));

  // Mongoose Connection (blog is our database)
  mongoose.connect('mongodb://localhost/blog');

  // Routes
  app.use('/api', apiController);

  // Listening port
  app.listen(3000);

File: app.js

  1. 既然 Mongoose 已经连接到数据库,我们需要创建一个模型来处理我们的博客文章。为此,您需要创建一个src/models/blog.js文件:
// Dependencies
import mongoose, { Schema } from 'mongoose';
import slug from 'slug';

// Defining the post schema...
const postSchema = new Schema({
  title: String,
  slug: { type: String, unique: true },
  content: { type: String, required: true },
  author: String,
  createdAt: Date
});

// Adding a custom method...
postSchema.methods.addAuthor = function(author) {
 /**
 * NOTE: Probably you are thinking, why I'm using function 
   * and not an arrow function?
 * Is because arrow functions does not bind their own context
 * that means this actually refers to the originating context
 */
  this.author = author;

  return this.author;
};
//Before save we create the slug and we add the current date...
postSchema.pre('save', function(next) {
  this.slug = slug(this.title, { lower: 'on' });
  this.createdAt = Date.now();

  next();
});

// Creating our Model...
const Post = mongoose.model('Post', postSchema);

export default Post;

File: src/models/blog.js

  1. 现在,为了处理我们的模型,我们需要创建一个新的控制器(src/controllers/blog.js,我们将在其中添加保存、更新、删除、查找所有帖子或查找单个帖子的方法:
  // Dependencies
  import slugFn from 'slug';
  import Post from '../models/blog';

 export function createPost(title, content, callback) {
    // Creating a new post...
    const newPost = new Post({
      title,
      content
    });

    // Adding the post author...
    newPost.addAuthor('Carlos Santana');

    // Saving the post into the database...
    newPost.save(error => {
      if (error) {
        console.log(error);
        callback(error, true);
      }

      console.log('Post saved correctly!');
      callback(newPost);
    });
  }

  // Updating a post...
  export function updatePost(slug, title, content, callback) {
    const updatedPost = {
      title,
      content,
      slug: slugFn(title, { lower: 'on' })
    };
    Post.update({ slug }, updatedPost, (error, affected) => {
      if (error) {
        console.log(error);
        callback(error, true);
      }

      console.log('Post updated correctly!');
      callback(affected);
    });
  }

  // Removing a post by slug...
  export function removePost(slug, callback) {
    Post.remove({ slug }, error => {
      if (error) {
        console.log(error);
        callback(error, true);
      }

      console.log('Post removed correctly!');
      callback(true);
    });
  }

  // Find all posts...
  export function findAllPosts(callback) {
    Post.find({}, (error, posts) => {
      if (error) {
        console.log(error);

        return false;
      }

      console.log(posts);
      callback(posts);
    });
  }

  // Find a single post by slug...
  export function findBySlug(slug, callback) {
    Post.find({ slug }, (error, post) => {
      if (error) {
        console.log(error);

        return false;
      }

      console.log(post);
      callback(post);
    });
  }

File: src/controllers/blog.js

  1. 最后,我们将修改我们的 API 控制器(src/controllers/api.js,以删除我们在上一个配方中创建的伪数据,并从实际的 MongoDB 数据库中获取数据:
  import express from 'express';
  import {
    createPost,
    findAllPosts,
    findBySlug,
    removePost,
    updatePost
  } from './blog';

  const router = express.Router();

  // GET Endpoints
  router.get('/', (req, res, next) => {
    res.send(`
      <p>API Endpoints:</p>
      <ul>
        <li><a href="/api/posts">/api/posts</a></li>
        <li><a href="/api/post/1">/api/post/:id</a></li>
      </ul>
    `);
  });

  router.get('/posts', (req, res, next) => {
    findAllPosts(posts => {
      res.json({
        response: posts
      });
    });
  });

  router.get('/post/:slug', (req, res, next) => {
    const { params: { slug } } = req;

    findBySlug(slug, singlePost => {
      console.log('single', singlePost);
      if (!singlePost || singlePost.length === 0) {
        res.send({
          error: true,
          message: 'Post not found'
        });
      } else {
        res.json({
          response: [singlePost]
        });
      }
    });
  });

  // POST Endpoints
  router.post('/post', (req, res, next) => {
    const { title, content } = req.body;

    createPost(title, content, (data, error = false) => {
      if (error) {
        res.json({
          error: true,
          message: data
        });
      } else {
        res.json({
          response: {
            saved: true,
            post: data
          }
        });
      }
    });
  });

  // DELETE Endpoints
  router.delete('/post/:slug', (req, res, next) => {
    const { params: { slug } } = req;

    removePost(slug, (removed, error) => {
      if (error) {
        res.json({
          error: true,
          message: 'There was an error trying to remove this 
          post...'
        });
      } else {
        res.json({
          response: {
            removed: true
          }
        })
      }
    });
  });

  // PUT Endpoints
  router.put('/post/:slug', (req, res, next) => {
    const { params: { slug }, body: { title, content } } = req;

    updatePost(slug, title, content, (affected, error) => {
      if (error) {
        res.json({
          error: true,
          message: 'There was an error trying to update the post'
        });
      } else {
        res.json({
          response: {
            updated: true,
            affected
          }
        })
      }
    });
  });

  export default router;

File: src/controllers/api.js

它是如何工作的。。。

您需要安装邮递员(https://www.getpostman.com 或任何其他 REST 客户端测试 API。主要针对POSTPUTDELETE方法,GET 方法可以在任何浏览器上轻松验证。

获取方法终结点

获取/发布。可以使用浏览器测试此端点。转到http://localhost:3000/api/posts。我已手动插入三行:

如果你想在邮递员身上测试,那么写下相同的 URL(http://localhost:3000/api/posts,选择GET方法,点击发送按钮:

GET/post/:slug。该端点也是一个GET,您需要在 URL 上传递 slug(友好 URL)。例如,第一行的 slug,My blog post 1,就是 My-blog-post-1。slug 是一个友好的 URL,它的值与标题相同,但使用小写字母,没有特殊字符,空格替换为破折号(-)。在我们的模型中,我们将 slug 定义为一个唯一的字段。这意味着同一个 slug 不能有多个 post。

让我们在浏览器中转到http://localhost:3000/api/post/my-blog-post-1。如果 slug 存在于数据库中,您将看到以下信息:

但是,如果试图查找数据库中不存在的 slug,则会出现以下错误:

POST 方法终结点

POST方法通常用于将新数据插入数据库时。

岗位/岗位。对于这个端点,我们需要使用邮递员来通过身体发送数据。为此,需要在 Postman 中选择 POST 方法。使用 URLhttp://localhost:3000/api/post,然后点击标题,需要添加标题Content-Type的值application/x-www-form-urlencoded

设置标题后,转到 Body 选项卡并选择 raw 选项,您可以发送如下信息:

现在,您可以点击发送按钮,查看服务返回的响应:

如果所有操作都正确,您应该会得到一个响应,其中保存的节点设置为 true,并且post节点包含有关保存的 post 的信息。现在,如果您试图用相同的数据(相同的标题)再次点击发送按钮,将导致错误,因为您记得,我们的 slug 必须是唯一的:

如果我们没有直接添加该节点,您可能想知道__v是什么。这就是versionKey,它是 Mongoose 首次创建文档时在每个文档上设置的属性。此键的值包含文档的内部版本。您可以更改或删除此文档属性的名称。默认值为__v

如果要更改,可以在定义新架构时执行以下操作:

    // If you want to change the name of the versionKey
    new Schema({...}, { versionKey: '_myVersion' });

或者如果您想删除它,您可以将false传递到versionKey,但我不建议这样做,因为您无法控制每次更新文档时的版本更改:

    // If you want to remove it you can do:
    new Schema({...}, { versionKey: false });

删除方法终结点

顾名思义,DELETE方法用于删除数据库中的行。

删除/发布/:slug。在 Postman 中,我们需要选择DELETE方法,在 URL 中,您需要传递要删除的帖子的 slug。例如,让我们删除帖子 my-blog-post-2。如果您正确地删除了它,您将得到一个响应,其中删除的节点设置为 true:

如果您想验证帖子是否已被删除,可以再次转到/posts端点,您将看到它不再出现在 JSON 中:

放置方法终结点

最后一种方法是PUT,通常用于更新数据库中的一行。

PUT/post/:slug。在 Postman 中,您需要选择 PUT 方法,然后选择要编辑的文章的 URL。让我们编辑 my-blog-post-3;URL 将为http://localhost:3000/api/post/my-blog-post-3。在 Headers 选项卡上,就像在POST方法中一样,您需要添加一个Content-Type标题,其值为 application/x-www-form-urlencoded。在“正文”选项卡中,发送要替换的新数据(在本例中为新标题和新内容):

如果一切正常,您应该得到以下响应:

同样,如果要验证帖子是否正确更新,请转到浏览器中的/posts端点:

如您所见,文章标题、内容和 slug 都已正确更新。

用 MySQL 构建数据库

MySQL 是最流行的数据库。它是一个开源关系数据库管理系统(RDBMS)。MySQL 通常是 LAMP(Linux、Apache、MySQL、PHP/Python/Perl)堆栈的核心组件;许多捆绑包包括 MySQL:

其他开发人员更喜欢单独安装。如果您想这样做,可以直接从官网下载 MySQLhttps://dev.mysql.com/downloads/mysql/

在这个配方中,我将使用 MySQL 工作台执行 SQL 查询。您可以从下载 https://www.mysql.com/products/workbench/ 。请随意使用任何其他 MySQL 管理员,或者如果您喜欢终端,您可以直接使用 MySQL 命令。

以下是更多 MySQL GUI 工具:

准备

要在 Node 上使用 MySQL,我们需要安装 sequelize 和 mysql2 软件包:

    npm install sequelize mysql2 slug

怎么做。。。

  1. 我们需要做的第一件事是创建一个数据库,我们将其命名为 blog,并使用它:
CREATE DATABASE blog;
 USE blog;
  1. 现在我们已经准备好了数据库,让我们使用 Node.js 来实现 MySQL。有很多方法可以将 MySQL 与 Node 一起使用,但是对于这个方法,我们将使用一个名为Sequelize的包,它是 MySQL 和其他数据库(如 SQLite、Postgres 和 MsSQL)的健壮 ORM。
  2. 我们需要做的第一件事是创建一个配置文件来添加数据库配置(主机、数据库、用户、密码等)。为此,您需要创建一个名为config/index.js的文件:
  export default {
    db: {
      dialect: 'mysql', // 'mysql'|'sqlite'|'postgres'|'mssql'
      host: 'localhost', // Your host, by default is localhost
      database: 'blog', // Your database name
      user: 'root', // Your MySQL user, by default is root
      password: '123456' // Your Db password, sometimes by default                  
                         //is empty.
    }
  };

File: config/index.js

  1. 我们可以重复使用 MongoDB 配方中使用的相同 API 控制器:
  import express from 'express';
  import {
    createPost,
    findAllPosts,
    findBySlug,
    removePost,
    updatePost
  } from './blog';

  const router = express.Router();

 // GET Methods
  router.get('/', (req, res, next) => {
    res.send(`
      <p>API Endpoints:</p>
      <ul>
        <li><a href="/api/posts">/api/posts</a></li>
        <li><a href="/api/post/1">/api/post/:id</a></li>
      </ul>
    `);
  });

  router.get('/posts', (req, res, next) => {
    findAllPosts(posts => {
      res.json({
        response: posts
      });
    });
  });

  router.get('/post/:slug', (req, res, next) => {
    const { params: { slug } } = req;

    findBySlug(slug, singlePost => {
      console.log('single', singlePost);
      if (!singlePost || singlePost.length === 0) {
        res.send({
          error: true,
          message: 'Post not found'
        });
      } else {
        res.json({
          response: [singlePost]
        });
      }
    });
  });

  // POST Methods
  router.post('/post', (req, res, next) => {
    const { title, content } = req.body;

    createPost(title, content, (data, error = false) => {
      if (error) {
        res.json({
          error: true,
          details: error
        });
      } else {
        res.json({
          response: {
            saved: true,
            post: data
          }
        });
      }
    });
  });

  // DELETE Methods
  router.delete('/post/:slug', (req, res, next) => {
    const { params: { slug } } = req;

    removePost(slug, (removed, error) => {
      if (error) {
        res.json({
          error: true,
          message: 'There was an error trying to remove this post...'
        });
      } else {
        res.json({
          response: {
            removed: true
          }
        })
      }
    });
  });

  // PUT Methods
  router.put('/post/:slug', (req, res, next) => {
    const { params: { slug }, body: { title, content } } = req;

    updatePost(slug, title, content, (affected, error) => {
      if (error) {
        res.json({
          error: true,
          message: 'There was an error trying to update the post'
        });
      } else {
        res.json({
          response: {
            updated: true,
            affected
          }
        })
      }
    });
  });

  export default router;

File: controllers/api.js

  1. 现在我们需要创建我们的博客模型(models/blog.js。让我们分段构建它;第一件事是连接到我们的数据库:
  // Dependencies
  import Sequelize from 'sequelize';
  import slug from 'slug';

  // Configuration
  import config from '../config';

  // Connecting to the database
  const db = new Sequelize(config.db.database, config.db.user, 
  config.db.password, {
    host: config.db.host,
    dialect: config.db.dialect,
    operatorsAliases: false
  });

File: models/blog.js

  1. 创建数据库连接后,让我们创建 Post 模型。我们将创建一个名为 posts 的表,其中包含以下字段:idtitleslugcontentauthorcreatedAt,但 Sequelize 默认情况下会在添加DATE字段时自动创建一个名为updatedAt的额外字段,该字段将在每次更新行时更改:
  // This will remove the extra response
  const queryType = {
    type: Sequelize.QueryTypes.SELECT
  };

  // Defining our Post model...
  const Post = db.define('posts', {
    id: {
      type: Sequelize.INTEGER,
      autoIncrement: true,
      primaryKey: true
    },
    title: {
      type: Sequelize.STRING,
      allowNull: false,
      validate: {
        notEmpty: {
          msg: 'The title is empty',
        }
      }
    },
    slug: {
      type: Sequelize.STRING,
      allowNull: false,
      unique: true,
      validate: {
        notEmpty: {
          msg: 'The slug is empty',
        }
      }
    },
    content: {
      type: Sequelize.TEXT,
      allowNull: false,
      validate: {
        notEmpty: {
          msg: 'The content is empty'
        }
      }
    },
    author: {
      type: Sequelize.STRING,
      allowNull: false,
      validate: {
        notEmpty: {
          msg: 'Who is the author?',
        }
      }
    },
    createdAt: {
      type: Sequelize.DATE,
      defaultValue: Sequelize.NOW
    },
  });

File: models/blog.js

  1. sequelize 最酷的事情之一是,我们可以在字段为空(notEmpty时使用自定义消息添加验证。现在,我们将添加一个方法来创建新帖子:
  // Creating new post...
 export function createPost(title, content, callback) {
    // .sync({ force: true }), if you pass force this will     
    // drop the table every time.
 db
      .sync()
      .then(() => {
        Post.create({
          title,
          slug: title ? slug(title, { lower: 'on' }) : '',
          content,
          author: 'Carlos Santana'
        }).then(insertedPost => {
          console.log(insertedPost);
          callback(insertedPost.dataValues);
        }).catch(error => {
          console.log(error);
          callback(false, error);
        });
      });
  }

File: models/blog.js

  1. 现在我们需要一种方法来更新帖子:
  // Updating a post...
  export function updatePost(slg, title, content, callback) {
    Post.update(
      {
        title,
        slug: slug(title, { lower: 'on' }),
        content
      },
      {
        where: { slug: slg }
      }
    ).then(rowsUpdated => {
      console.log('UPDATED', rowsUpdated);
      callback(rowsUpdated);
    }).catch(error => {
      console.log(error);
      callback(false, error);
    });
  }

File: models/blog.js

  1. 此外,我们还需要一种通过 slug 删除帖子的方法:
  // Removing a post by slug...
  export function removePost(slug, callback) {
    Post.destroy({
      where: {
        slug
      }
    }).then(rowDeleted => {
      console.log('DELETED', rowDeleted);
      callback(rowDeleted);
    }).catch(error => {
      console.log(error);
      callback(false, error);
    });
  }

File: models/blog.js

  1. Sequelize 还直接支持 SQL 查询。让我们创建两个方法,一个用于查找所有帖子,另一个用于使用 SQL 查询逐段查找帖子:
 // Find all posts...
  export function findAllPosts(callback) {
    db.query('SELECT * FROM posts', queryType).then(data => {
      callback(data);
    });
  }

  // Find a single post by slug...
  export function findBySlug(slug, callback) {
    db.query(`SELECT * FROM posts WHERE slug = '${slug}'`, queryType).then(data => {
      callback(data);
    });
  }

File: models/blog.js

  1. 我们在文件开头定义的queryType变量是为了避免从 Sequelize 获得第二个响应。默认情况下,如果您没有通过此queryTypeSequelize 将以多维数组的形式返回结果(第一个对象是结果,第二个对象是元数据对象)。让我们把所有的部分放在一起:
  // Dependencies
  import Sequelize from 'sequelize';
  import slug from 'slug';

  // Configuration
  import config from '../config';

  // Connecting to the database
  const db = new Sequelize(config.db.database, config.db.user, 
  config.db.password, {
    host: config.db.host,
    dialect: config.db.dialect,
    operatorsAliases: false // This is to avoid the warning:       
   //sequelize 
   //deprecated String based operators are now deprecated.
  });

  // This will remove the extra metadata object
  const queryType = {
    type: Sequelize.QueryTypes.SELECT
  };

  // Defining our Post model...
  const Post = db.define('posts', {
    id: {
      type: Sequelize.INTEGER,
      autoIncrement: true,
      primaryKey: true
    },
    title: {
      type: Sequelize.STRING,
      allowNull: false,
      validate: {
        notEmpty: {
          msg: 'The title is empty',
        }
      }
    },
    slug: {
      type: Sequelize.STRING,
      allowNull: false,
      unique: true,
      validate: {
        notEmpty: {
          msg: 'The slug is empty',
        }
      }
    },
    content: {
      type: Sequelize.TEXT,
      allowNull: false,
      validate: {
        notEmpty: {
          msg: 'The content is empty'
        }
      }
    },
    author: {
      type: Sequelize.STRING,
      allowNull: false,
      validate: {
        notEmpty: {
          msg: 'Who is the author?',
        }
      }
    },
    createdAt: {
      type: Sequelize.DATE,
      defaultValue: Sequelize.NOW
    },
  });

  // Creating new post...
  export function createPost(title, content, callback) {
    db
      .sync()
      .then(() => {
        Post.create({
          title,
          slug: title ? slug(title, { lower: 'on' }) : '',
          content,
          author: 'Carlos Santana'
        }).then(insertedPost => {
          console.log(insertedPost);
          callback(insertedPost.dataValues);
        }).catch((error) => {
          console.log(error);
          callback(false, error);
        });
      });
  }

  // Updating a post...
 export function updatePost(slg, title, content, callback) {
    Post.update(
      {
        title,
        slug: slug(title, { lower: 'on' }),
        content
      },
      {
        where: { slug: slg }
      }
    ).then(rowsUpdated => {
      console.log('UPDATED', rowsUpdated);
      callback(rowsUpdated);
    }).catch(error => {
      console.log(error);
      callback(false, error);
    });
  }

  // Removing a post by slug...
  export function removePost(slug, callback) {
    Post.destroy({
      where: {
        slug
      }
    }).then(rowDeleted => {
      console.log('DELETED', rowDeleted);
      callback(rowDeleted);
    }).catch(error => {
      console.log(error);
      callback(false, error);
    });
  }

  // Find all posts...
  export function findAllPosts(callback) {
    db.query('SELECT * FROM posts', queryType).then(data => {
      callback(data);
    });
  }

  // Find a single post by slug...
  export function findBySlug(slug, callback) {
    db.query(`SELECT * FROM posts WHERE slug = '${slug}'`, queryType).then(data => {
      callback(data);
    });
  }

File: models/blog.js

它是如何工作的。。。

它将以与 MongoDB 配方相同的方式工作,只是结果略有不同。要测试 API,您需要安装 Postman(https://www.getpostman.com )。

POST 方法终结点

POST 方法通常用于将新数据插入数据库时。

岗位/岗位。对于该端点,我们需要使用 Postman 通过请求主体发送数据。为此,需要在 Postman 中选择 POST 方法。输入 URLhttp://localhost:3000/api/post,点击表头,需要添加一个Content-Type表头,其值为application/x-www-form-urlencoded

设置好表头后,进入Body页签,选择raw选项,可以发送如下信息:

现在,您可以点击发送按钮,查看服务返回的响应:

如果所有操作都正确,则应该得到一个响应,其中保存的节点设置为 true,而 post 节点则包含有关保存的 post 的信息。如果您试图用相同的数据(相同的标题)再次点击发送按钮,将导致错误,因为您记得,我们的 slug 必须是唯一的:

The text in this image is not relevant. The purpose of the image is to give you a glimpse of how the error looks like. Try in your Postman, and you will see the same error as the image.

获取方法终结点

获取/发布。可以使用浏览器测试此端点。转到http://localhost:3000/api/posts。我已经用createPost方法手动插入了三行:

如果你想在邮递员身上测试,那么写下相同的 URL(http://localhost:3000/api/posts,选择GET方法,点击发送按钮:

GET/post/:slug

该端点也是 GET,您需要在 URL 中传递 slug(友好 URL)。例如,第一行的 slug,My blog post 1,是 My-blog-post-1。slug 是一个友好的 URL,其值与标题相同,但为小写,没有特殊字符,空格用破折号替换(-。在我们的模型中,我们将 slug 定义为一个唯一字段,这意味着同一 slug 不能有多个 post。

让我们在浏览器中转到http://localhost:3000/api/post/my-blog-post-1。如果 slug 存在于数据库中,您将看到以下信息:

但如果试图查看数据库中不存在的 slug,则会出现以下错误:

删除方法终结点

顾名思义,DELETE方法用于删除数据库中的行。

删除/发布/:slug。在 Postman 中,我们需要选择DELETE方法,在 URL 中,您需要传递要删除的帖子的 slug。例如,让我们删除 my-blog-post-2。如果您正确删除了它,您应该会得到一个响应,其中删除的节点的值为 true:

如果您想验证帖子是否已被删除,可以再次转到/posts端点,您将看到它不再出现在 JSON 中:

放置方法终结点

最后一种方法是PUT,通常用于更新数据库中的一行。

PUT /post/:slug

在 Postman 中,您需要首先选择 PUT 方法,然后选择要编辑的文章的 URL。让我们编辑 my-blog-post-3;所以 URL 将是http://localhost:3000/api/post/my-blog-post-3。在 Headers 选项卡中,您需要添加值为application/x-www-form-urlencodedContent-Type标题,就像在 POST 方法中一样。最后一部分是“正文”选项卡,您可以在其中发送要替换的新数据,在本例中为新标题和新内容:

如果一切正常,您应该得到以下响应:

同样,如果要验证帖子是否正确更新,请转到浏览器中的/posts端点:

如您所见,文章标题、内容和 slug 都已正确更新。

添加访问令牌以保护我们的 API

我们在最后两个菜谱中创建的 API 是公共的。这意味着每个人都可以从我们的服务器访问和获取信息,但是如果您想在 API 上添加一个安全层并获取平台上注册用户的信息,会发生什么情况?我们需要添加访问令牌验证来保护我们的 API,并且要做到这一点;我们必须使用JSON Web 令牌JWT)。

准备

对于此配方,您需要为 Node.js 安装 JWT:

    npm install jsonwebtoken

怎么做。。。

我们将主要使用为 MySQL 配方创建的相同代码,并添加一个安全层来验证我们的访问令牌:

  1. 我们需要做的第一件事是修改我们的配置文件(config/index.js,添加一个安全节点,其中包含我们将用于创建令牌的secretKey,并添加令牌的过期时间:
 export default {
    db: {
      dialect: 'mysql', // The database engine you want to use
      host: 'localhost', // Your host, by default is localhost
      database: 'blog', // Your database name
      user: 'root', // Your MySQL user, by default is root
      password: '123456' // Your MySQL password
    },
    security: {
      secretKey: 'C0d3j0bs', // Secret key
      expiresIn: '1h' // Expiration can be: 30s, 30m, 1h, 7d, etc.
    }
  };

File: config/index.js

  1. 下一步是在模型文件夹中创建一个db.js文件,以分离数据库连接并在模型之间共享。以前,我们只有博客模型,但现在我们也要创建一个用户模型文件:
  // Configuration
  import config from '../config';
  import Sequelize from 'sequelize';

  export const db = new Sequelize(
    config.db.database, 
    config.db.user,
    config.db.password, 
    {
      host: config.db.host,
      dialect: config.db.dialect,
      operatorsAliases: false
    }
  );

File: models/db.js

  1. 现在我们需要为用户创建一个表,并为用户保存一条记录:
    CREATE TABLE users (
      id int(11) UNSIGNED NOT NULL AUTO_INCREMENT,
      username varchar(255) NOT NULL,
      password varchar(255) NOT NULL,
      email varchar(255) NOT NULL,
      fullName varchar(255) NOT NULL,
      PRIMARY KEY (`id`)
    );
  1. 我们可以使用此命令插入用户,更改用户名和密码。在此配方中,我们将使用 SHA1 算法加密密码:
 INSERT INTO users (id, username, password, email, fullName) 
    VALUES (
      NULL, 
      'czantany', 
 SHA1('123456'), 
      'carlos@milkzoft.com', 
      'Carlos Santana'
    );

    // The SHA1 hash generated for the 123456 password is      
    // 7c4a8d09ca3762af61e59520943dc26494f8941b
  1. 在我们创建了用户表并且有了注册用户之后,让我们使用login方法创建我们的用户模型:
  // Dependencies
  import Sequelize from 'sequelize';

  // Db Connection
  import { db } from './db';

  // This will remove the extra response
  const queryType = {
    type: Sequelize.QueryTypes.SELECT
  };

  // Login
  export function login(username, password, callback) {
    db.query(`
      SELECT id, username, email, fullName
      FROM users
      WHERE username = '${username}' AND password = '${password}'
    `, queryType).then(data => callback(data));
  }

File: models/user.js

  1. 下一步是修改我们的 API 控制器,添加一个login端点来生成令牌,并添加一个函数来验证令牌。然后我们将保护我们的一个端点(/api/posts
  // Dependencies
  import express from 'express';
  import jwt from 'jsonwebtoken';

  // Models
  import {
    createPost,
    findAllPosts,
    findBySlug,
    removePost,
    updatePost
  } from '../models/blog';
  import { login } from '../models/user';

  // Configuration
  import config from '../config';

  // Extracting the secretKey and the expiresIn
  const { security: { secretKey, expiresIn } } = config;

  const router = express.Router();

  // Token Validation
  const validateToken = (req, res, next) => {
    if (req.headers['access-token']) {
      // The token should come as 'Bearer <access-token>'
      req.accessToken = req.headers['access-token'].split(' ')[1];

 // We just need the token that's why we split the string by       
     //space 
      // and we got the token in the position 1 of the array 
      //generated 
      // by the split method.
      return next();
    } else {
      res.status(403).send({ 
 error: 'You must send an access-token header...'
      });
    }
  }

  // POST login - This will generate a new token
  router.post('/login', (req, res) => {
    const { username, password } = req.body;

    login(username, password, data => {
      if (Object.keys(data).length === 0) {
        res.status(403).send({ error: 'Invalid login' });
      }

      // Creating the token with the 
      // user data + secretKey + expiration time
      jwt.sign({ data }, secretKey, { expiresIn }, (error, 
 accessToken) => {
        res.json({
          accessToken
        });
      });
    });
  });

  // We pass validateToken as middleware and then we verify with   
  //  req.accessToken
    router.get('/posts', validateToken, (req, res, next) => {
      jwt.verify(req.accessToken, secretKey, (error, userData) => {
        if (error) {
          console.log(error);
          res.status(403).send({ error: 'Invalid token' });
        } else {
          findAllPosts(posts => {
            res.json({
              response: posts,
              user: userData
            });
          });
        }
      });
    });

    // From here all the others endpoints are public...
    router.get('/post/:slug', (req, res, next) => {
      const { params: { slug } } = req;

      findBySlug(slug, singlePost => {
        console.log('single', singlePost);
        if (!singlePost || singlePost.length === 0) {
          res.send({
            error: true,
            message: 'Post not found'
          });
        } else {
          res.json({
            response: [singlePost]
          });
        }
      });
    });

 // POST Methods
    router.post('/post', (req, res, next) => {
      const { title, content } = req.body;

      createPost(title, content, (data, error = false) => {
        if (error) {
          res.json({
            error: true,
            details: error
          });
        } else {
          res.json({
            response: {
              saved: true,
              post: data
            }
          });
        }
      });
    });

    // DELETE Methods
    router.delete('/post/:slug', (req, res, next) => {
      const { params: { slug } } = req;

      removePost(slug, (removed, error) => {
        if (error) {
          res.json({
            error: true,
            message: 'There was an error trying to remove this 
            post...'
          });
        } else {
          res.json({
            response: {
              removed: true
            }
          });
        }
      });
    });

    // PUT Methods
    router.put('/post/:slug', (req, res, next) => {
      const { params: { slug }, body: { title, content } } = req;

      updatePost(slug, title, content, (affected, error) => {
        if (error) {
          res.json({
            error: true,
            message: 'There was an error trying to update the post'
          });
        } else {
          res.json({
            response: {
              updated: true,
              affected
            }
          });
        }
      });
    });

 export default router;

File: controllers/api.js

它是如何工作的。。。

如果要测试 API 的安全性,首先需要执行POST/api/login方法获取新令牌。和以前一样,我们可以和邮递员一起做。

您需要选择 POST 方法,然后写入 URLhttp://localhost:3000/api/login并添加一个Content-Type头,其值为application/x-www-form-urlencoded,以便能够通过请求体发送数据:

然后,在主体选项卡上,我们需要发送我们的数据(用户名和密码)以及数据库中的用户信息。在这里,我们手动执行此过程,但最终,此信息应来自您网站上的登录表单:

如果您为您的用户传递了正确的信息,您应该会得到accessToken,但是如果由于某种原因登录失败或者用户或密码不正确,您将得到如下错误:

一旦您获得新的accessToken(请记住,此令牌仅在 1 小时内有效;过期后,您需要创建一个新的令牌),您需要复制令牌,然后将其作为头发送(作为访问令牌,格式为Bearer <access-token>),发送到受保护的端点(/api/posts

*

发送正确格式的载体[空格]至关重要。请记住,我们正在使用空格来获取令牌。如果你做的一切都是正确的,那么你应该从服务那里得到来自博客的帖子和用户信息的响应(这可能位于不同的端点,但对于这个例子,我只是在这里添加了用户数据)。

正如您在用户数据中所看到的,我们从数据库中获取信息,并添加了两个新字段:iat(发布时间)和exp(令牌到期时间)。但是如果我们的令牌过期或者用户发送了不正确的访问令牌,会发生什么呢?在这些场景中,我们将返回一个错误:

还有更多。。。

如您所见,令牌验证很容易实现,并且在处理私有数据时为我们的 API 添加了一个安全层。您可能会问,保存生成的访问令牌的最佳位置是哪里。有些人将访问令牌保存在 cookie 或会话中,但我不建议这样做,因为存在一些相关的安全问题。我的建议是,仅在用户连接到站点时使用本地存储来保存,然后在用户关闭浏览器后将其删除,但这同样取决于要添加到平台的安全类型。**