八、使用 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 代码,使用var
、require
、module.exports
等:
- 我们需要做的第一件事是将此代码转换为 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
- 迁移到 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
- 现在我们删除我们的
bin/www
目录,因为我们在文件末尾添加了app.listen(3000);
,然后您需要修改package.json
中的start
脚本:
"scripts": {
"start": "node app.js"
}
File: package.json
- 如果您尝试使用
npm start
运行应用,则会出现以下错误:
- 此错误是因为我们的 ES6 代码不能直接用于节点。我们需要使用 Babel 来编译我们的文件,并能够编写 ES6 代码。为此,我们需要在全球范围内安装
babel-cli
以及babel-preset-es2015
包:
npm install -g babel-cli
npm install babel-preset-es2015
- 要使其工作,我们需要创建一个名为
.babelrc
的新文件,并添加我们的es2015
预设:
{
"presets": ["es2015"]
}
File: .babelrc
- 现在您需要再次更改您的
start
脚本,并将node
切换到babel-node
:
"scripts": {
"start": "babel-node app.js"
}
File: package.json
- 如果您在终端上运行
npm start
,现在应该可以运行应用了。 - 在我们将代码更改为 ES6 之后,我们遇到了另一个问题。如果修改文件并将其保存在应用中,则该文件不会刷新。此外,如果由于某种原因我们的应用崩溃,那么我们的服务器将停止工作。解决此问题的方法是使用节点监视程序。最受欢迎的是
nodemon
:
npm install nodemon
- 您需要为此修改您的
start
脚本:
"scripts": {
"start": "nodemon app.js --exec babel-node"
}
File: package.json
- 现在,如果您对应用进行任何更改(例如,在
routes/index.js
文件中,您可以更改第 6 行中的文本Express
中的任何其他内容),您将看到服务器如何重新启动并刷新站点:
- 如您所见,绿色的第一条消息显示
starting babel-node app.js
,然后当它检测到更改时,显示由于更改而重新启动。。。现在我们可以看到我们网站上反映的变化:
- 因为我们的 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
- 如您所见,我将
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:
- 如果我们转到
http://localhost:3000/api
,我们将显示端点列表。这是可选的,但作为开发人员的参考非常有用:
- 如果您进入
http://localhost:3000/api/posts
,您将看到所有帖子:
- 此外,如果您点击
http://localhost:3000/api/post/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:
-
从下载您想要的 MongoDB 版本的二进制文件 https://www.mongodb.com/download-center#community 。
-
从下载的文件中提取文件;您可以使用终端并使用以下命令:
tar -zxvf mongodb-osx-ssl-x86_64-3.6.3.tgz
- 将提取的文件夹复制到 MongoDB 运行的位置:
mkdir -p mongodb
cp -R -n mongodb-osx-ssl-x86_64-3.6.3/ mongodb
- 确保二进制文件的位置在
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)"
- 如果已经安装了 Homebrew,或者刚刚安装了 Homebrew,则首先需要使用以下命令更新软件包数据库:
brew update
- 现在我们需要使用以下命令安装 MongoDB:
brew install mongodb
- 如果您想安装 MongoDB 的最新开发版本,那么您应该运行此命令(我不建议这样做,因为它可能有一些尚未修复的 bug,但这取决于您):
brew install mongodb --devel
运行 MongoDB
在我们第一次启动 MongoDB 之前,我们需要创建一个目录,mongod进程将在其中写入数据:
- 默认情况下,mongod 进程使用
/data/db
目录。要创建此文件夹,可以使用以下命令:
mkdir -p /data/db
- 现在我们需要设置数据目录的权限:
chmod -R 777 /data
- 在新终端(或选项卡)中,您需要运行以下操作:
mongod
- 如果没有收到错误,您可以在与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.
- 最后,如果要停止 MongoDB,请在
mongod
正在运行的终端中按Ctrl+C。 - 如果一切正常,您应该在终端中看到:
怎么做。。。
首先,我们需要创建一个新的数据库:
- 要创建新数据库或切换到现有数据库,您需要运行:
use <name of the database>
。让我们创建一个博客数据库:
use blog
- 现在我们需要创建一个名为posts的集合,您需要使用
db.<your-collection-name>.save({})
命令以 JSON 格式直接保存数据:
db.posts.save({ title: 'Post 1', slug: 'post-1', content: '<p>Content</p>' })
- 如您所见,我没有添加任何
id
值,这是因为 MongoDB 会自动为每一行创建一个唯一的 ID,称为_id
,这是一个随机散列。如果您想查看刚刚保存的数据,需要使用不带任何参数的find()
方法:
db.posts.find()
- 您应该看到这样的数据:
- 现在,假设您为 Post 2 添加了一个新行,并且希望通过指定 slug(Post-2)来查找该特定行。您可以这样做:
db.posts.find({ slug: 'post-2' })
- 您应该看到:
- 现在,让我们将第 2 篇文章的标题更改为我更新的第 2 篇文章。为此,我们需要按如下方式更新行:
db.posts.update({ slug: "post-2" }, { $set: { title: "My Updated Post 2" }})
- 第一个参数是查找要更新的行的查询,第二个参数使用
$set
修改字段。 - 最后,如果要删除特定行,可以按如下操作:
db.posts.remove({ "_id": ObjectId("5ad2e6ed4fa0d047639da616") })
- 建议删除行的方法是直接指定
_id
,以避免错误删除其他行,但也可以通过任何其他字段删除行。例如,假设您想使用 slug 移除 Post 1。您可以这样做:
db.posts.remove({ "slug": "post-1" })
- 现在您已经了解了如何使用 MongoDB 进行基本操作,让我们使用 Mongoose 库将 MongoDB 实现到 Node.js 中,Mongoose 库是 Node 的对象文档映射器(ODM。我们需要为此配方安装一些额外的软件包:
npm install mongoose body-parser slug
- 使用与上一个配方(
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
- 既然 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
- 现在,为了处理我们的模型,我们需要创建一个新的控制器(
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
- 最后,我们将修改我们的 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。主要针对POST
、PUT
和DELETE
方法,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:
- AMPPS(Max、Linux 和 Windows)–https://www.ampps.com
- XAMPP(Mac、Linux 和 Windows)–https://www.apachefriends.org
- WAMP 服务器(Windows)–http://www.wampserver.com
- MAMP(Mac)–https://www.mamp.info
其他开发人员更喜欢单独安装。如果您想这样做,可以直接从官网下载 MySQLhttps://dev.mysql.com/downloads/mysql/ 。
在这个配方中,我将使用 MySQL 工作台执行 SQL 查询。您可以从下载 https://www.mysql.com/products/workbench/ 。请随意使用任何其他 MySQL 管理员,或者如果您喜欢终端,您可以直接使用 MySQL 命令。
以下是更多 MySQL GUI 工具:
- phpMyAdmin–https://www.phpmyadmin.net
- 续集专业版–https://www.sequelpro.com
- Navicat–https://www.navicat.com
准备
要在 Node 上使用 MySQL,我们需要安装 sequelize 和 mysql2 软件包:
npm install sequelize mysql2 slug
怎么做。。。
- 我们需要做的第一件事是创建一个数据库,我们将其命名为 blog,并使用它:
CREATE DATABASE blog;
USE blog;
- 现在我们已经准备好了数据库,让我们使用 Node.js 来实现 MySQL。有很多方法可以将 MySQL 与 Node 一起使用,但是对于这个方法,我们将使用一个名为Sequelize的包,它是 MySQL 和其他数据库(如 SQLite、Postgres 和 MsSQL)的健壮 ORM。
- 我们需要做的第一件事是创建一个配置文件来添加数据库配置(主机、数据库、用户、密码等)。为此,您需要创建一个名为
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
- 我们可以重复使用 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
- 现在我们需要创建我们的博客模型(
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
- 创建数据库连接后,让我们创建 Post 模型。我们将创建一个名为 posts 的表,其中包含以下字段:
id
、title
、slug
、content
、author
和createdAt
,但 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
- 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
- 现在我们需要一种方法来更新帖子:
// 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
- 此外,我们还需要一种通过 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
- 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
- 我们在文件开头定义的
queryType
变量是为了避免从 Sequelize 获得第二个响应。默认情况下,如果您没有通过此queryType
Sequelize 将以多维数组的形式返回结果(第一个对象是结果,第二个对象是元数据对象)。让我们把所有的部分放在一起:
// 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-urlencoded
的Content-Type
标题,就像在 POST 方法中一样。最后一部分是“正文”选项卡,您可以在其中发送要替换的新数据,在本例中为新标题和新内容:
如果一切正常,您应该得到以下响应:
同样,如果要验证帖子是否正确更新,请转到浏览器中的/posts
端点:
如您所见,文章标题、内容和 slug 都已正确更新。
添加访问令牌以保护我们的 API
我们在最后两个菜谱中创建的 API 是公共的。这意味着每个人都可以从我们的服务器访问和获取信息,但是如果您想在 API 上添加一个安全层并获取平台上注册用户的信息,会发生什么情况?我们需要添加访问令牌验证来保护我们的 API,并且要做到这一点;我们必须使用JSON Web 令牌(JWT)。
准备
对于此配方,您需要为 Node.js 安装 JWT:
npm install jsonwebtoken
怎么做。。。
我们将主要使用为 MySQL 配方创建的相同代码,并添加一个安全层来验证我们的访问令牌:
- 我们需要做的第一件事是修改我们的配置文件(
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
- 下一步是在模型文件夹中创建一个
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
- 现在我们需要为用户创建一个表,并为用户保存一条记录:
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`)
);
- 我们可以使用此命令插入用户,更改用户名和密码。在此配方中,我们将使用 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
- 在我们创建了用户表并且有了注册用户之后,让我们使用
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
- 下一步是修改我们的 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 或会话中,但我不建议这样做,因为存在一些相关的安全问题。我的建议是,仅在用户连接到站点时使用本地存储来保存,然后在用户关闭浏览器后将其删除,但这同样取决于要添加到平台的安全类型。**
版权属于:月萌API www.moonapi.com,转载请注明出处