学习

在 5 分钟内使用 Express 和 Redis OM for Node.js 运行起来

Guy Royse
作者
Guy Royse, Redis 高级开发者倡导者

好的,这个标题是一个大胆的说法。这是一个阅读和遵循的教程。因此,可能需要 6 分钟或 4 分钟,具体取决于你的打字速度。无论如何,这应该能让你快速构建一些有用的东西,并能为更大的东西打下良好的基础。

哦,你可能想知道什么是 Redis OM。好吧,GitHub 上有一个详细的 README。去看看吧!

此外,这份文档,以及我们即将实现的 代码,以及 测试所需的数据 都在 GitHub 上。根据需要参考它们。

让我们构建一些东西#

那么,我们要构建什么呢?我们将构建一个 RESTful 服务,它允许你管理歌曲。它允许你对歌曲执行所有 CRUD 操作(对于未经初始化的人来说,即创建、读取、更新和删除)。此外,我们还将为该服务添加一些很酷的搜索端点。这样,我们可以按艺术家或流派、年份或特定歌词查找歌曲。

这个问题的测试数据有点棘手。大多数歌词受版权保护,获得许可在这么小的教程中使用它们并不是一个真正的选择。而且我们肯定希望能够搜索歌词。我们怎样才能找到那首“oooo ah ah ah ah”的歌呢?

幸运的是,我的朋友 Dylan Beattie 实际上是最初的 Rockstar 开发者。除了编写很酷的东西之外,他还写了 具有技术主题的模仿歌曲。而且,他允许我将它们用作测试数据。

卑微的开始#

我们使用 Redis 作为我们的数据库——这是 Redis OM 背后的全部想法。因此,你需要一些 Redis,特别是安装了 SearchJSON 最简单的方法是设置一个免费的 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"
  }
}

太棒了。设置完成。让我们写一些代码吧!

启动 Express 服务#

我喜欢在根目录使用一个小版本的名称端点来编写我的服务。这样,如果某个随机开发人员访问该服务的网站,他们就会得到一些线索。所以,让我们来做这件事吧

在项目的根目录中创建一个名为 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#

我们将使用 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 时,它会根据提供的模式信息向提供的实体类添加属性。以下是一个映射到我们的 SongSchema

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 中创建一些路由。

使用 Redis OM 进行 CRUD 操作#

让我们创建一个真正 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 });
});

现在我们有了将歌曲塞进 Redis 的方法,让我们开始塞吧。在 GitHub 上,有很多 JSON 文件 里面包含歌曲数据。(感谢 Dylan!)继续,将它们拉取下来,放在项目根目录下名为 songs 的文件夹中。

让我们使用 curl 来加载一首歌。我比较喜欢 HTML,这首歌的曲调是 AC/DC 的,所以让我们使用这首歌

$ 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,但除了响应头之外,它与纯文本没有区别。

使用 Redis OM 进行搜索#

所有 CRUD 操作都已完成。让我们添加一些搜索功能。搜索是 Redis OM 真正开始闪耀的地方。我们将创建路由来

  • 返回所有歌曲,就像,所有歌曲。
  • 获取特定艺人的歌曲,例如“Dylan Beattie and the Linebreakers”。
  • 获取属于特定类型的歌曲,例如“摇滚”或“电子音乐”。
  • 获取介于特定年份之间的歌曲,例如 80 年代的所有歌曲。
  • 获取歌词中包含特定词语的歌曲,例如“html”或“markdown”。

将歌曲加载到 Redis 中#

在开始之前,让我们用大量的歌曲来填充 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 服务器并提出问题。我一直在那里,很乐意提供帮助。

并且,如果您发现任何缺陷、错误,或者只是认为这个教程可以改进,请发送一个拉取请求或创建一个问题。

谢谢!

最后更新于 2024 年 2 月 22 日