那么,我们要构建什么呢?我们将构建一个 RESTful 服务,它允许你管理歌曲。它允许你对歌曲执行所有 CRUD 操作(对于未经初始化的人来说,即创建、读取、更新和删除)。此外,我们还将为该服务添加一些很酷的搜索端点。这样,我们可以按艺术家或流派、年份或特定歌词查找歌曲。
这个问题的测试数据有点棘手。大多数歌词受版权保护,获得许可在这么小的教程中使用它们并不是一个真正的选择。而且我们肯定希望能够搜索歌词。我们怎样才能找到那首“oooo ah ah ah ah”的歌呢?
幸运的是,我的朋友 Dylan Beattie 实际上是最初的 Rockstar 开发者。除了编写很酷的东西之外,他还写了 具有技术主题的模仿歌曲。而且,他允许我将它们用作测试数据。
我们使用 Redis 作为我们的数据库——这是 Redis OM 背后的全部想法。因此,你需要一些 Redis,特别是安装了 Search 和 JSON。 最简单的方法是设置一个免费的 Redis Cloud 实例。但是,你也可以使用 Docker:
$ docker run -p 6379:6379 redis/redis-stack:latest
我假设你对 Node.js 比较了解,因此你应该能够自己安装它。我们将使用在 Node v14.8.0 中引入的模块的顶级 await 功能,因此请确保你拥有该版本或更高版本。如果你没有,那就去获取吧。
一旦你有了它,就可以创建一个项目了
$ npm init
给它一个名称、版本和描述。使用任何你喜欢的。我把它命名为“Metalpedia”。
安装 Express 和 Redis OM for Node.js
$ npm install express redis-om --save
并且,为了让我们轻松一点,我们将使用 nodemon:
$ npm install nodemon --save-dev
现在这些东西已经安装好了,让我们在我们的 package.json
中设置一些其他细节。首先,将“type”设置为“module”,以便我们能够使用 ES6 模块:
"type": "module",
由 npm init
生成的“test”脚本对我们来说没有太大用处。用调用 nodemon
的“start”脚本替换它。这将允许我们构建的服务在每次更改文件时自动重启。非常方便
"scripts": {
"start": "nodemon server.js"
},
我喜欢使我的包私有,这样它们就不会意外地被推送到 NPM
"private": true,
哦,你不需要“main”条目。我们不是在构建要共享的包。所以,可以删除它。
现在,你应该有一个看起来像这样的 package.json
{
"name": "metalpedia",
"version": "1.0.0",
"description": "Sample application for building a music repository backed by Redis and Redis OM.",
"type": "module",
"scripts": {
"start": "nodemon server.js"
},
"author": "Guy Royse <guy@guyroyse.com> (https://guyroyse.com/)",
"license": "MIT",
"private": true,
"dependencies": {
"express": "^4.17.1",
"redis-om": "^0.2.0"
},
"devDependencies": {
"nodemon": "^2.0.14"
}
}
太棒了。设置完成。让我们写一些代码吧!
我喜欢在根目录使用一个小版本的名称端点来编写我的服务。这样,如果某个随机开发人员访问该服务的网站,他们就会得到一些线索。所以,让我们来做这件事吧
在项目的根目录中创建一个名为 server.js
的文件,并用以下内容填充它
import express from 'express';
// create an express app and use JSON
let app = new express();
app.use(express.json());
// setup the root level GET to return name and version from package.json
app.get('/', (req, res) => {
res.send({
name: process.env.npm_package_name,
version: process.env.npm_package_version,
});
});
// start listening
app.listen(8080);
现在我们有足够的资源来实际运行一些东西了。所以,让我们运行它吧
$ npm start
然后,在你的浏览器中访问 https://localhost:8080/
。你应该看到类似以下内容:
{
"name": "metalpedia",
"version": "1.0.0"
}
或者,使用 curl
(以及 json_pp
如果你想变得更花哨)访问你的服务
$ curl -X GET https://localhost:8080 -s | json_pp
{
"name": "metalpedia",
"version": "1.0.0"
}
酷。让我们添加一些 Redis。
我们将使用 Redis OM 将 Redis 中的歌曲 JSON 数据映射到 JavaScript 对象。
在项目的根目录中创建一个名为 song-repository.js
的文件。在其中,导入你将需要的 Redis OM 的所有部分
import { Entity, Schema, Client, Repository } from 'redis-om';
实体是你所使用的类——要映射的事物。它们是你创建、读取、更新和删除的东西。任何扩展 Entity
的类都是实体。我们现在将使用一行代码定义我们的 Song 实体,但我们稍后会添加更多内容:
class Song extends Entity {}
模式定义了实体上的字段、它们的类型以及它们如何在内部映射到 Redis。默认情况下,实体映射到 Redis 中的哈希,但我们希望我们的实体使用 JSON。当创建 Schema
时,它会根据提供的模式信息向提供的实体类添加属性。以下是一个映射到我们的 Song
的 Schema
let schema = new Schema(Song, {
title: { type: 'string' }, // the title of the song
artist: { type: 'string' }, // who performed the song
genres: { type: 'string[]' }, // array of strings for the genres of the song
lyrics: { type: 'text' }, // the full lyrics of the song
music: { type: 'text' }, // who wrote the music for the song
year: { type: 'number' }, // the year the song was releases
duration: { type: 'number' }, // the duration of the song in seconds
link: { type: 'string' }, // link to a YouTube video of the song
});
客户端用于连接到 Redis。创建一个 Client
并将您的 Redis URL 传递给构造函数。如果您没有指定 URL,它将默认设置为 redis://localhost:6379
。客户端有方法可以 .open
、.close
和 .execute
原生 Redis 命令,但我们只是要打开它
let client = await new Client().open();
请记住,我在文档开头提到的 顶层等待 内容?它就在这里!
现在我们拥有了创建 Repository
所需的所有组件。仓库是 Redis OM 的主要接口。它们为我们提供了读取、写入和删除实体的方法。创建一个仓库,并确保将其导出,因为当我们开始使用 Express 的内容时,您将需要它:
export let songRepository = client.fetchRepository(schema);
我们快完成仓库的设置了。但是我们还需要创建一个索引,否则我们将无法搜索任何内容。我们通过调用 .createIndex
来做到这一点。如果索引已经存在并且相同,此函数将不会执行任何操作。如果它不同,它将删除它并创建一个新的索引。在真实的运行环境中,您可能希望在 CI/CD 的一部分中创建您的索引。但对于本示例,我们将把它们塞进我们的主代码中:
await songRepository.createIndex();
我们拥有了与 Redis 交互所需的一切。现在,让我们用它在 Express 中创建一些路由。
让我们创建一个真正 RESTful 的 API,其中 CRUD 操作分别映射到 PUT、GET、POST 和 DELETE。我们将使用 Express 路由器 来完成此操作,因为这使得我们的代码简洁明了。所以,在项目文件夹的根目录下创建一个名为 song-router.js
的文件。然后添加导入并创建一个 Router
import { Router } from 'express';
import { songRepository as repository } from './song-repository.js';
export let router = Router();
此路由需要添加到 server.js
的 /song
路径下,所以接下来我们来做。将以下代码行添加到 server.js
的顶部,和其他导入一起,以导入歌曲路由
import { router as songRouter } from './song-router.js';
还添加一行代码来调用 .use
,以便我们即将实现的路由能够被使用
app.use('/song', songRouter);
现在我们的 server.js
应该看起来像这样
import express from 'express';
import { router as songRouter } from './song-router.js';
// create an express app and use JSON
let app = new express();
app.use(express.json());
// bring in some routers
app.use('/song', songRouter);
// setup the root level GET to return name and version from package.json
app.get('/', (req, res) => {
res.send({
name: process.env.npm_package_name,
version: process.env.npm_package_version,
});
});
// start listening
app.listen(8080);
现在,让我们开始在 song-router.js
中添加一些路由。我们首先要创建一个歌曲,因为您需要在 Redis 中拥有歌曲才能执行任何读取、更新或删除操作。添加下面的 PUT 路由。此路由将调用 .createEntity
来创建一个实体,设置新创建实体的所有属性,然后调用 .save
来持久化它
router.put('/', async (req, res) => {
// create the Song so we can save it
let song = repository.createEntity();
// set all the properties, converting missing properties to null
song.title = req.body.title ?? null;
song.artist = req.body.artist ?? null;
song.genres = req.body.genres ?? null;
song.lyrics = req.body.lyrics ?? null;
song.music = req.body.music ?? null;
song.year = req.body.year ?? null;
song.duration = req.body.duration ?? null;
song.link = req.body.link ?? null;
// save the Song to Redis
let id = await repository.save(song);
// return the id of the newly created Song
res.send({ id });
});
$ curl -X PUT -H "Content-Type: application/json" -d "@songs/html.json" https://localhost:8080/song -s | json_pp
您应该会得到新插入的歌曲的 ID
{
"id": "01FKRW9WMVXTGF71NBEM3EBRPY"
}
我们确实在发送 HTML。如果您有 redis-cli
在手,或者想要使用 RedisInsight,您可以看看 Redis 是如何存储这个数据的
> json.get Song:01FKRW9WMVXTGF71NBEM3EBRPY
"{\"title\":\"HTML\",\"artist\":\"Dylan Beattie and the Linebreakers\",\"genres\":[\"blues rock\",\"hard rock\",\"parody\",\"rock\"],\"lyrics\":\"W3C, RFC, a JIRA ticket and a style guide.\\\\nI deploy with FTP, run it all on the client side\\\\nDon\xe2\x80\x99t need Ruby, don\xe2\x80\x99t need Rails,\\\\nAin\xe2\x80\x99t nothing running on my stack\\\\nI\xe2\x80\x99m hard wired, for web scale,\\\\nYeah, I\xe2\x80\x99m gonna bring the 90s back\\\\n\\\\nI\xe2\x80\x99m shipping HTML,\\\\nHTML,\\\\nI\xe2\x80\x99m shipping HTML,\\\\nHTML\xe2\x80\xa6\\\\n\\\\nNo logins, no trackers,\\\\nNo cookie banners to ignore\\\\nI ain\xe2\x80\x99t afraid of, no hackers\\\\nJust the occasional 404\\\\nThey hatin\xe2\x80\x99, what I do,\\\\nBut that\xe2\x80\x99s \xe2\x80\x98cos they don\xe2\x80\x99t understand\\\\nMark it up, break it down,\\\\nRemember to escape your ampersands\xe2\x80\xa6\\\\n\\\\nI\xe2\x80\x99m shipping HTML,\\\\nHTML,\\\\nI\xe2\x80\x99m shipping HTML,\\\\nHTML\xe2\x80\xa6\\\\n\\\\n(But it\xe2\x80\x99s really just markdown.)\",\"music\":\"\\\"Highway to Hell\\\" by AC/DC\",\"year\":2020,\"duration\":220,\"link\":\"https://www.youtube.com/watch?v=woKUEIJkwxI\"}"
是的。看起来像 JSON。
创建完后,让我们添加一个 GET 路由,以便从 HTTP 中读取这首歌,而不是使用 redis-cli
router.get('/:id', async (req, res) => {
// fetch the Song
let song = await repository.fetch(req.params.id);
// return the Song we just fetched
res.send(song);
});
现在您可以使用 curl
或浏览器来加载 https://localhost:8080/song/01FKRW9WMVXTGF71NBEM3EBRPY
以获取这首歌
$ curl -X GET https://localhost:8080/song/01FKRW9WMVXTGF71NBEM3EBRPY -s | json_pp
您应该会得到这首歌的 JSON 数据
{
"link": "https://www.youtube.com/watch?v=woKUEIJkwxI",
"genres": ["blues rock", "hard rock", "parody", "rock"],
"entityId": "01FKRW9WMVXTGF71NBEM3EBRPY",
"title": "HTML",
"lyrics": "W3C, RFC, a JIRA ticket and a style guide.\\nI deploy with FTP, run it all on the client side\\nDon’t need Ruby, don’t need Rails,\\nAin’t nothing running on my stack\\nI’m hard wired, for web scale,\\nYeah, I’m gonna bring the 90s back\\n\\nI’m shipping HTML,\\nHTML,\\nI’m shipping HTML,\\nHTML…\\n\\nNo logins, no trackers,\\nNo cookie banners to ignore\\nI ain’t afraid of, no hackers\\nJust the occasional 404\\nThey hatin’, what I do,\\nBut that’s ‘cos they don’t understand\\nMark it up, break it down,\\nRemember to escape your ampersands…\\n\\nI’m shipping HTML,\\nHTML,\\nI’m shipping HTML,\\nHTML…\\n\\n(But it’s really just markdown.)",
"duration": 220,
"artist": "Dylan Beattie and the Linebreakers",
"music": "\"Highway to Hell\" by AC/DC",
"year": 2020
}
现在我们已经可以读取和写入,让我们实现 REST 中剩下的 HTTP 动词。REST... 明白了吗?
这是使用 POST 路由进行更新的代码。您会注意到,这段代码与 GET 路由几乎完全相同。您可以将其重构为一个辅助函数,但由于这只是一个教程,所以我现在将跳过这一步。
router.post('/:id', async (req, res) => {
// fetch the Song we are replacing
let song = await repository.fetch(req.params.id);
// set all the properties, converting missing properties to null
song.title = req.body.title ?? null;
song.artist = req.body.artist ?? null;
song.genres = req.body.genres ?? null;
song.lyrics = req.body.lyrics ?? null;
song.music = req.body.music ?? null;
song.year = req.body.year ?? null;
song.duration = req.body.duration ?? null;
song.link = req.body.link ?? null;
// save the Song to Redis
let id = await repository.save(song);
// return the id of the Song we just saved
res.send({ id });
});
以及 curl
命令来尝试它,将 Dylan 的替换为,这首歌的曲调是,由 Village People 演唱
$ curl -X POST -H "Content-Type: application/json" -d "@songs/d-m-c-a.json" https://localhost:8080/song/01FKRW9WMVXTGF71NBEM3EBRPY -s | json_pp
您应该会得到已更新歌曲的 ID
{
"id" : "01FKRW9WMVXTGF71NBEM3EBRPY"
}
最后,让我们实现一个 DELETE 路由
router.delete('/:id', async (req, res) => {
// delete the Song with its id
await repository.remove(req.params.id);
// respond with OK
res.type('application/json');
res.send('OK');
});
并测试一下
$ curl -X DELETE https://localhost:8080/song/01FKRW9WMVXTGF71NBEM3EBRPY -s
OK
这只会返回“OK”,从技术上讲是 JSON,但除了响应头之外,它与纯文本没有区别。
所有 CRUD 操作都已完成。让我们添加一些搜索功能。搜索是 Redis OM 真正开始闪耀的地方。我们将创建路由来
在开始之前,让我们用大量的歌曲来填充 Redis,这样我们就有东西可以搜索了。我编写了一个简短的 shell 脚本,它使用我们刚刚创建的服务器将 GitHub 上的所有歌曲数据加载到 Redis 中。它只是在循环中调用 curl 。它在 GitHub 上,所以去 获取它 并将它放在项目根目录中。然后运行它:
$ ./load-data.sh
您应该会看到类似这样的内容
{"id":"01FM310A8AVVM643X13WGFQ2AR"} <- songs/big-rewrite.json
{"id":"01FM310A8Q07D6S7R3TNJB146W"} <- songs/bug-in-the-javascript.json
{"id":"01FM310A918W0JCQZ8E57JQJ07"} <- songs/d-m-c-a.json
{"id":"01FM310A9CMJGQHMHY01AP0SG4"} <- songs/enterprise-waterfall.json
{"id":"01FM310A9PA6DK4P4YR275M58X"} <- songs/flatscreens.json
{"id":"01FM310AA2XTEQV2NZE3V7K3M7"} <- songs/html.json
{"id":"01FM310AADVHEZXF7769W6PQZW"} <- songs/lost-it-on-the-blockchain.json
{"id":"01FM310AASNA81Y9ACFMCGP05P"} <- songs/meetup-2020.json
{"id":"01FM310AB4M2FKTDPGEEMM3VTV"} <- songs/re-bass.json
{"id":"01FM310ABFGFYYJXVABX2YXGM3"} <- songs/teams.json
{"id":"01FM310ABW0ANYSKN9Q1XEP8BJ"} <- songs/tech-sales.json
{"id":"01FM310AC6H4NRCGDVFMKNGKK3"} <- songs/these-are-my-own-devices.json
{"id":"01FM310ACH44414RMRHPCVR1G8"} <- songs/were-gonna-build-a-framework.json
{"id":"01FM310ACV8C72Y69VDQHA12C1"} <- songs/you-give-rest-a-bad-name.json
请注意,此脚本不会擦除任何数据。因此,您之前在其中拥有的任何歌曲都将保留在其中,与这些歌曲并排存在。如果您多次运行此脚本,它会很乐意再次添加歌曲。
就像歌曲的 CRUD 操作一样,我们首先需要创建一个路由器。这次我们将文件命名为 songs-router.js
。注意复数。像以前一样,将所有导入和导出添加到该文件中
import { Router } from 'express';
import { songRepository as repository } from './song-repository.js';
export let router = Router();
将此路由器添加到 server.js
中的 /songs
下,就像我们以前做的那样。同样,请注意复数。现在您的 server.js
应该看起来像这样
import express from 'express';
import { router as songRouter } from './song-router.js';
import { router as songsRouter } from './songs-router.js';
// create an express app and use JSON
let app = new express();
app.use(express.json());
// bring in some routers
app.use('/song', songRouter);
app.use('/songs', songsRouter);
// setup the root level GET to return name and version from package.json
app.get('/', (req, res) => {
res.send({
name: process.env.npm_package_name,
version: process.env.npm_package_version,
});
});
// start listening
app.listen(8080);
现在我们可以添加一些搜索路由了。我们通过在仓库上调用 .search
来启动搜索。然后我们调用 .where
来添加任何我们想要的过滤器,如果我们想添加的话。一旦我们指定了过滤器,我们就调用 .returnAll
来获取所有匹配的实体。
这是最简单的搜索,它只返回所有内容。继续将其添加到 songs-router.js
中
router.get('/', async (req, res) => {
let songs = await repository.search().returnAll();
res.send(songs);
});
然后使用 curl
或浏览器来尝试一下
$ curl -X GET https://localhost:8080/songs -s | json_pp
我们可以通过调用 .where
和 .eq
来搜索特定字段。此路由会找到所有特定艺人的歌曲。请注意,您必须指定艺人的完整名称,才能使此操作生效
router.get('/by-artist/:artist', async (req, res) => {
let artist = req.params.artist;
let songs = await repository.search().where('artist').eq(artist).returnAll();
res.send(songs);
});
然后使用 curl
或浏览器来尝试一下
$ curl -X GET https://localhost:8080/songs/by-artist/Dylan%20Beattie -s | json_pp
类型存储为字符串数组。您可以使用 .contains
来查看数组是否包含该类型
router.get('/by-genre/:genre', async (req, res) => {
let genre = req.params.genre;
let songs = await repository
.search()
.where('genres')
.contains(genre)
.returnAll();
res.send(songs);
});
并尝试一下
$ curl -X GET https://localhost:8080/songs/by-genre/rock -s | json_pp
$ curl -X GET https://localhost:8080/songs/by-genre/parody -s | json_pp
此路由允许您获取介于两个年份之间的所有歌曲。非常适合查找所有 80 年代的热门歌曲。当然,Dylan 的所有歌曲都是比这更近的,所以当我们尝试它时,我们将缩小范围
router.get('/between-years/:start-:stop', async (req, res) => {
let start = Number.parseInt(req.params.start);
let stop = Number.parseInt(req.params.stop);
let songs = await repository
.search()
.where('year')
.between(start, stop)
.returnAll();
res.send(songs);
});
当然,还要尝试一下
$ curl -X GET https://localhost:8080/songs/between-years/2020-2021 -s | json_pp
让我们添加最后一个路由来查找歌词中包含特定词语的歌曲,使用 .match
router.get('/with-lyrics/:lyrics', async (req, res) => {
let lyrics = req.params.lyrics;
let songs = await repository
.search()
.where('lyrics')
.match(lyrics)
.returnAll();
res.send(songs);
});
我们也可以尝试一下,获取所有包含“html”和“markdown”两个词语的歌曲
$ curl -X GET https://localhost:8080/songs/with-lyrics/html%20markdown -s | json_pp
就这样。在这个教程中,我带您了解了一些基础知识。但您应该深入研究。如果您想了解更多信息,请继续查看 Redis OM for Node.js on GitHub。它更详细地解释了 Redis OM for Node.js 的功能。
如果您有任何问题或遇到困难,请随时加入 Redis Discord 服务器并提出问题。我一直在那里,很乐意提供帮助。
并且,如果您发现任何缺陷、错误,或者只是认为这个教程可以改进,请发送一个拉取请求或创建一个问题。
谢谢!