Skip to content

Latest commit

 

History

History
1276 lines (849 loc) · 59.2 KB

File metadata and controls

1276 lines (849 loc) · 59.2 KB

五、使用微服务构建 Web Messenger

在当今的应用开发世界中,微服务已经成为设计和构建分布式系统的标准。像 Netflix 这样的公司率先实现了这一转变,并彻底改变了软件公司的运营方式,从拥有小型自主团队到设计易于扩展的系统。

在本章中,我将指导您完成创建两个微服务的过程,这两个微服务将协同工作,以创建一个使用 Redis 作为数据存储的消息传递 web 应用。消息将在一段可配置的时间后自动过期,因此在本章中,我们将其称为 TempMessenger。

在本章中,我们将介绍以下主题:

  • 什么是 Nameko?
  • 创建您的第一个 Nameko 微服务
  • 存储消息
  • Nameko 依赖提供程序
  • 保存消息
  • 检索所有邮件
  • 在 web 浏览器中显示消息
  • 通过POST请求发送消息
  • 浏览器轮询消息

临时通讯员目标

在开始之前,让我们为应用定义一些目标:

  • 用户可以访问网站并发送消息
  • 用户可以看到其他人发送的消息
  • 消息在可配置的时间后自动过期

为了实现这一点,我们将使用 Nameko——一个 Python 的微服务框架。

If at any point during this chapter you would like to refer to all of the code in this chapter in its entirety, feel free to see it, with tests, at: http://url.marcuspen.com/github-ppb.

要求

为了参与本章,您的本地机器将需要以下各项:

所有其他要求将在我们完成本章时安装。

本章中的所有说明都是针对 macOS 或 Debian/Ubuntu 系统定制的。但是,我注意到只使用跨平台依赖项。

Throughout this chapter, there will be blocks of code. Different types of code will have their own prefixes, which are as follows: $: To be executed in your terminal, always within your virtualenv >>>: To be executed in your Nameko/Python shell No prefix: Block of Python code to be used in your editor

什么是 Nameko?

Nameko 是一个开源框架,用于在 Python 中构建微服务。使用 Nameko,您可以通过AMQP高级消息队列协议)创建使用RPC远程过程调用)相互通信的微服务。

放射性同位素

RPC 代表远程过程调用,我将用一个基于电影院预订系统的简短示例简要说明这一点。在这个影院预订系统中,有许多微服务,但我们将重点关注负责管理预订的预订服务和负责发送电子邮件的电子邮件服务。预订服务和电子邮件服务都存在于不同的机器上,并且都不知道另一台在哪里。在进行新预订时,预订服务需要向用户发送电子邮件确认,因此它会向电子邮件服务进行远程过程调用,这可能类似于以下内容:

def new_booking(self, user_id, film, time): 
    ... 
    self.email_service.send_confirmation(user_id, film, time) 
    ... 

请注意,在前面的代码中,预订服务是如何进行调用的,就好像它正在执行本地代码一样?它不关心网络或协议,甚至不提供需要发送到哪个电子邮件地址的详细信息。对于预订服务,电子邮件地址和任何其他与电子邮件相关的概念都是无关的!这使得预订服务能够遵守罗伯特·C·马丁在其文章《面向对象设计的原则》(中引入的单一责任原则http://url.marcuspen.com/bob-ood ),其中规定:

"A Class should have only one reason to change"

此引用的范围也可以扩展到微服务,这是我们在开发微服务时应该记住的。这将允许我们保持微服务的自包含性和内聚性。如果电影院决定改变其电子邮件提供商,那么唯一需要改变的服务就是电子邮件服务,将所需的工作保持在最低限度,从而降低出现错误和可能停机的风险。

然而,与其他技术(如 REST)相比,RPC 确实有其缺点,主要的缺点是很难看到调用何时是远程的。人们可能会在没有意识到的情况下进行不必要的远程呼叫,这可能会很昂贵,因为他们通过网络并使用外部资源。因此,在使用 RPC 时,重要的是使它们明显不同。

Nameko 如何使用 AMQP

AMQP 代表高级消息队列协议,Nameko 将其用作我们的 RPC 的传输。当我们的 Nameko 服务彼此创建 rpc 时,请求被放置在消息传递队列中,然后被目标服务使用。Nameko 服务使用工人消费和执行请求;创建 RPC 时,目标服务将生成一个新的工作进程来执行任务。一旦完成,它就死了。由于可以有多个工人同时执行任务,Nameko 可以扩展到其可用的工人数量。如果所有工作线程都已耗尽,则消息将保留在队列中,直到有空闲工作线程可用。

您还可以通过增加运行服务的实例数量来水平扩展 Nameko。这被称为集群,这也是名称Nameko的来源,因为 Nameko 蘑菇是集群生长的。

Nameko 还可以响应来自 HTTP 和 WebSocket 等其他协议的请求。

兔子

RabbitMQ 用作 Nameko 的消息代理,并允许它使用 AMQP。在我们开始之前,您需要将其安装到您的机器上;为此,我们将使用 Docker,它在所有主要操作系统上都可用。

对于 Docker 的新手来说,它允许我们在称为容器的独立、自包含的环境中运行代码。容器中包含代码独立于任何其他内容运行所需的所有内容。您还可以下载并运行预构建的容器,这就是我们运行 RabbitMQ 的方式。这使我们不必在本地计算机上安装它,并将在不同平台(如 macOS 或 Windows)上运行 RabbitMQ 可能产生的问题降至最低。

如果您尚未安装 Docker,请访问http://url.marcuspen.com/docker-install 其中有所有平台的详细安装说明。本章其余部分将假定您已经安装了 Docker。

启动 RabbitMQ 容器

在终端中,执行以下操作:

$ docker run -d -p 5672:5672 -p 15672:15672 --name rabbitmq rabbitmq 

这将使用以下设置启动 RabbitMQ 容器:

  • -d:指定要在守护进程模式(后台进程)下运行容器。
  • -p:允许我们将容器上的端口567215672暴露给本地机器。这些是 Nameko 与 RabbitMQ 通信所必需的。
  • --name:将容器名称设置为rabbitmq

您可以通过执行以下命令来检查新 RabbitMQ 容器是否正在运行:

$ docker ps

安装 Python 需求

对于这个项目,我将使用 Python 3.6,在编写本文时,它是 Python 的最新稳定版本。我建议始终使用 Python 的最新稳定版本,这不仅适用于新功能,还可以确保始终将最新的安全更新应用于您的环境。

Pyenv is a really simple way to install and switch between different versions of Python: http://url.marcuspen.com/pyenv.

我还强烈建议使用 virtualenv 创建一个隔离的环境来安装我们的 Python 需求。在没有虚拟环境的情况下安装 Python 需求可能会对其他 Python 应用或更糟糕的操作系统造成意外的副作用!

To learn more about virtualenv and how to install it visit: http://url.marcuspen.com/virtualenv

通常,在处理 Python 软件包时,您会创建一个requirements.txt文件,用您的需求填充它,然后安装它。我想向您展示一种不同的方式,使您能够轻松跟踪 Python 包的版本。

首先,让我们在您的 VirtualNV 中安装pip-tools

pip install pip-tools 

现在创建一个名为requirements的新文件夹,并创建两个新文件:

base.in 
test.in 

base.in文件将包含运行我们服务核心所需的需求,而test.in文件将包含运行我们测试所需的需求。将这些需求分开很重要,尤其是在微服务体系结构中部署代码时。本地机器可以安装测试包,但部署的代码版本应该尽可能地精简和轻量级。

base.in文件中,放入以下行:

nameko 

test.in文件中,放入以下行:

pytest 

如果您位于包含requirements文件夹的目录中,请运行以下操作:

pip-compile requirements/base.in 
pip-compile requirements/test.in 

这将生成两个文件,base.txttest.txt。以下是base.txt的一个小样本:

... 
nameko==2.8.3 
path.py==10.5             # via nameko 
pbr==3.1.1                # via mock 
pyyaml==3.12              # via nameko 
redis==2.10.6 
requests==2.18.4          # via nameko 
six==1.11.0               # via mock, nameko 
urllib3==1.22             # via requests 
... 

请注意,我们现在有一个文件,其中包含 Nameko 的所有最新依赖项和子依赖项。它指定需要哪些版本,以及是什么导致安装每个子依赖项。例如,sixnamekomock所必需的。

这使得将来解决升级问题变得非常容易,因为它能够轻松地跟踪代码的每个版本之间的版本更改。

在撰写本文时,Nameko 目前的版本是 2.8.3,Pytest 是 3.4.0。如果可用,请随意使用这些软件包的更新版本,但如果您在本书中有任何问题,请在您的base.intest.in文件中添加版本号,以恢复到这些版本,如下所示:

nameko==2.8.3 

要安装需求,只需运行:

$ pip-sync requirements/base.txt requirements/test.txt 

pip-sync命令安装文件中指定的所有要求,同时删除环境中未指定的任何包。这是保持你的虚拟世界干净的好方法。或者,您也可以使用:

$ pip install -r requirements/base.txt -r requirements/test.txt 

创建您的第一个 Nameko 微服务

首先,我们创建一个名为temp_messenger的新文件夹,并在其中放置一个名为service.py的新文件,其代码如下:

from nameko.rpc import rpc 

class KonnichiwaService: 

    name = 'konnichiwa_service' 

    @rpc 
    def konnichiwa(self): 
        return 'Konnichiwa!' 

我们首先从nameko.rpc导入rpc。这将允许我们使用rpc装饰器装饰我们的方法,并将它们作为入口点暴露到我们的服务中。入口点是 Nameko 服务中充当服务网关的任何方法。

为了创建 Nameko 服务,我们只需创建一个新类KonnichiwaService,并为其分配一个name属性。name属性给它一个名称空间;这将在稍后尝试远程调用服务时使用。

我们已经在服务上编写了一个方法,它只返回单词Konnichiwa!。注意这个方法是如何用rpc修饰的。konnichiwa方法现在将通过 RPC 公开。

在测试这段代码之前,我们需要创建一个小的config文件,它将告诉 Nameko 在哪里访问 RabbitMQ 以及使用什么 RPC 交换。创建一个新文件,config.yaml

AMQP_URI: 'pyamqp://guest:guest@localhost' 
rpc_exchange: 'nameko-rpc' 

The AMQP_URI configuration here is correct for users who have started the RabbitMQ container using the instructions given earlier. If you have adjusted the username, password or location, ensure that your changes are reflected here.

现在,您应该有一个类似于以下内容的目录结构:

. 
├── config.yaml 
├── requirements 
│   ├── base.in 
│   ├── base.txt 
│   ├── test.in 
│   └── test.txt 
├── temp_messenger 
    └── service.py 

现在,在终端的项目目录根目录中,执行以下操作:

$ nameko run temp_messenger.service --config config.yaml 

您应该具有以下输出:

starting services: konnichiwa_service 
Connected to amqp://guest:**@127.0.0.1:5672// 

给我们的服务打电话

我们的微服务正在运行!为了进行我们自己的调用,我们可以启动一个集成了 Nameko 的 pythonshell,以允许我们调用入口点。要访问它,请打开一个新的终端窗口并执行以下操作:

$ nameko shell 

这将使您能够访问 Python shell,并能够进行远程过程调用。让我们尝试一下:

>>> n.rpc.konnichiwa_service.konnichiwa() 
'Konnichiwa!' 

成功了!我们已成功致电 Konnichiwa 服务,并收到了一些反馈。当我们在 Nameko shell 中执行此代码时,我们在队列上放置了一条消息,然后由我们的KonnichiwaService接收。然后,它产生了一个新的工人来执行konnichiwaRPC 的工作。

单元测试 Nameko 微服务

根据文件,http://url.marcuspen.com/nameko ,Nameko 是:

"A microservices framework for Python that lets service developers concentrate on application logic and encourages testability."

现在我们将重点讨论 Nameko 的可测试性部分;它提供了一些非常有用的工具来隔离和测试它的服务。

创建一个新文件夹tests,并在其中放置两个新文件__init__.py(可以留空)和test_service.py

from nameko.testing.services import worker_factory 
from temp_messenger.service import KonnichiwaService 

def test_konnichiwa(): 
    service = worker_factory(KonnichiwaService) 
    result = service.konnichiwa() 
    assert result == 'Konnichiwa!' 

当在测试环境之外运行时,Nameko 会为每个被调用的入口点生成一个新的 worker。早些时候,当我们测试我们的konnichiwaRPC 时,Konnichiwa 服务将一直在侦听兔子队列上的新消息。一旦它收到konnichiwa入口点的新消息,就会产生一个执行该方法的新工人,然后死亡。

To learn more about the anatomy of Nameko services, see: http://url.marcuspen.com/nam-key.

对于我们的测试,Nameko 提供了一种通过woker_factory模拟的方法。如您所见,我们的测试使用了worker_factory,我们通过了服务类KonnichiwaService。这将允许我们调用该服务上的任何入口点并访问结果。

要从代码目录的根目录运行测试,只需执行:

pytest 

就这样。测试套件现在应该通过了。玩一玩,试着打破它。

公开 HTTP 入口点

我们现在将创建一个新的微服务,负责处理 HTTP 请求。首先,让我们修改service.py文件中的进口:

from nameko.rpc import rpc, RpcProxy 
from nameko.web.handlers import http 

在我们之前制作的KonnichiwaService下方,插入以下内容:

class WebServer: 

    name = 'web_server' 
    konnichiwa_service = RpcProxy('konnichiwa_service') 

    @http('GET', '/') 
    def home(self, request): 
        return self.konnichiwa_service.konnichiwa() 

请注意,如何遵循与KonnichiwaService类似的模式。它有一个name属性和一个修饰的方法,以便将其公开为入口点。在本例中,它用http入口点装饰。我们在http装饰器中指定它是GET请求以及该请求的位置——在本例中,是我们网站的根。

还有一个更重要的区别:此服务通过RpcProxy对象持有对 Konnichiwa 服务的引用。RpcProxy允许我们通过 RPC 调用另一个 Nameko 服务。我们用前面在KonnichiwaService中指定的name属性实例化它。

让我们试一试-只需使用前面的命令重新启动 Nameko(需要考虑代码的任何更改),然后在您选择的浏览器中转到http://localhost:8000/

成功了!我们现在已经成功地创建了两个微服务,一个负责显示消息,另一个负责服务 web 请求。

集成测试 Nameko 微服务

早些时候,我们研究了通过生成单个工作线程来孤立地测试服务。这对于单元测试来说非常好,但是对于集成测试来说,这不是一个可行的选择。

Nameko 使我们能够在一次测试中测试多个串联工作的服务。请看以下内容:

def test_root_http(web_session, web_config, container_factory): 
    web_config['AMQP_URI'] = 'pyamqp://guest:guest@localhost' 

    web_server = container_factory(WebServer, web_config) 
    konnichiwa = container_factory(KonnichiwaService, web_config) 
    web_server.start() 
    konnichiwa.start() 

    result = web_session.get('/') 

    assert result.text == 'Konnichiwa!' 

正如您在前面的代码中所看到的,Nameko 还允许我们访问以下测试夹具:

  • web_session:为我们提供一个会话,在此会话中向服务发出 HTTP 请求
  • web_config:允许我们访问服务的配置(测试之外,相当于config.yaml文件)
  • container_factory:这允许我们将服务作为一个整体来模拟,而不仅仅是一个工作者的实例,这在集成测试时是必要的

因为这是在运行实际的服务,所以我们需要通过将 AMQP 代理注入到web_config中来指定 AMQP 代理的位置。使用container_factory,我们创建了两个容器:web_serverkonnichiwa。然后我们启动两个容器。

这是一个简单的例子,使用web_session向我们站点的根目录发出GET请求,并检查结果是否符合我们的预期。

在我们阅读本章的其余部分时,我鼓励您为代码编写自己的测试,因为它不仅可以防止 bug,而且有助于巩固您在这方面的知识。这也是一个很好的方法来试验你自己的想法和对代码的修改,因为如果你破坏了任何东西,它们会很快告诉你。

For more information on testing Nameko services, see: http://url.marcuspen.com/nam-test.

存储消息

我们希望应用显示的消息需要是临时的。我们可以为此使用关系数据库,比如 PostgreSQL,但这意味着必须为像文本这样简单的内容设计和维护一个数据库。

Redis 简介

Redis 是内存中的数据存储。整个数据集可以存储在内存中,使得读写速度比关系数据库快得多,这对于不需要持久性的数据非常有用。此外,我们可以存储数据而无需创建模式,如果我们不需要复杂的查询,这很好。在我们的例子中,我们只需要一个数据存储,它允许我们存储消息、获取消息和使消息过期。Redis 非常适合我们的用例!

启动 Redis 容器

在终端中,执行以下操作:

$ docker run -d -p 6379:6379 --name redis redis

这将使用以下设置启动 Redis 容器:

  • -d:指定要在守护进程模式(后台进程)下运行容器。
  • -p:允许我们将容器上的端口6379暴露给本地机器。这是 Nameko 与 Redis 通信所必需的。
  • --name:将容器名称设置为redis

您可以通过执行以下命令来检查新 Redis 容器是否正在运行:

$ docker ps

安装 Python Redis 客户端

您还需要安装 Python Redis 客户端,以便通过 Python 与 Redis 交互。为此,我建议将您的base.in文件从前面修改为包含redis并重新编译以生成新的base.txt文件。或者,您可以运行pip install redis

使用 Redis

让我们简要介绍一下对我们的 TempMessenger 有用的 Redis 命令类型:

  • SET:设置一个给定的键来保存给定的字符串。它还允许我们以秒或毫秒为单位设置过期时间。
  • GET:获取使用给定密钥存储的数据的值。
  • TTL:获取给定密钥的生存时间(秒)。
  • PTTL:获取给定密钥的生存时间(毫秒)。
  • KEYS:返回数据存储中所有密钥的列表。

为了试用它们,我们可以使用redis-cli,这是我们的 Redis 容器附带的一个程序。要访问它,请首先通过在终端中执行以下操作登录到容器:

docker exec -it redis /bin/bash

然后在同一终端窗口中通过简单运行访问redis-cli

redis-cli 

关于如何使用redis-cli,下面给出了一些示例:;如果您不熟悉 Redis,那么我鼓励您自己尝试使用这些命令。

将一些数据hello设置为msg1键:

127.0.0.1:6379> SET msg1 hello
OK

获取存储在键msg1处的数据:

127.0.0.1:6379> GET msg1
"hello"

msg2键设置更多数据hi there并检索:

127.0.0.1:6379> SET msg2 "hi there"
OK
127.0.0.1:6379> GET msg2
"hi there"

检索当前存储在 Redis 中的所有密钥:

127.0.0.1:6379> KEYS *
1) "msg2"
2) "msg1"

msg3保存数据,有效期为 15 秒:

127.0.0.1:6379> SET msg3 "this message will die soon" EX 15
OK

以秒为单位获得msg3的生存时间:

127.0.0.1:6379> TTL msg3
(integer) 10

获取msg3的生存时间(毫秒):

127.0.0.1:6379> PTTL msg3
(integer) 6080

msg3到期前取回:

127.0.0.1:6379> GET msg3
"this message will die soon"

msg3到期后取回:

127.0.0.1:6379> GET msg3
(nil)

Nameko 依赖提供程序

在构建微服务时,Nameko 鼓励使用依赖提供者与外部资源(如数据库、服务器或我们的应用所依赖的任何东西)进行通信。通过使用依赖项提供程序,您可以隐藏仅特定于该依赖项的逻辑,从而保持服务级别代码的干净性,并且不知道与该外部资源接口的细节。

通过这样构造我们的微服务,我们能够轻松地交换或重用其他服务中的依赖提供者。

Nameko provides a list of open source dependency providers that are ready to use: http://url.marcuspen.com/nam-ext.

添加 Redis 依赖项提供程序

因为 Redis 是我们应用的外部资源,所以我们将为它创建一个依赖项提供程序。

设计客户端

首先,让我们在temp_messenger文件夹中创建一个名为dependencies的新文件夹。在里面,放置一个新文件,redis.py。现在,我们将使用一个简单的方法创建一个 Redis 客户端,该方法将在给定密钥的情况下获取消息:

from redis import StrictRedis 

class RedisClient: 

    def __init__(self, url): 
        self.redis = StrictRedis.from_url( 
            url, decode_responses=True 
        ) 

我们通过实现__init__方法开始我们的代码,该方法创建我们的 Redis 客户端并将其分配给self.redisStrictRedis可以接受许多可选参数,但是,我们只指定了以下参数:

  • url:不需要单独指定主机、端口和数据库号,我们可以使用StrictRedis``from_url,这将允许我们用一个字符串来指定所有三个,如 so-redis://localhost:6379/0。这对于以后在我们的config.yaml中存储它要方便得多。
  • decode_responses:这将自动将我们从 Redis 获得的数据转换为 Unicode 字符串。默认情况下,以字节为单位检索数据。

现在,在同一个类中,让我们实现一个新方法:

def get_message(self, message_id): 
    message = self.redis.get(message_id) 

    if message is None: 
        raise RedisError( 
            'Message not found: {}'.format(message_id) 
        ) 

    return message 

在新类之外,让我们还实现一个新的错误类:

class RedisError(Exception): 
    pass 

这里我们有一个方法,get_message,它使用一个message_id作为我们的 Redis 密钥。我们在 Redis 客户端上使用get方法来检索具有给定密钥的消息。从 Redis 检索值时,如果该键不存在,只返回None。因为这个方法期望有一条消息,所以我们应该自己处理引发错误的问题。在本例中,我们做了一个简单的例外,RedisError

创建依赖项提供程序

到目前为止,我们已经用一个方法创建了一个 Redis 客户端。我们现在需要创建一个 Nameko 依赖提供程序来利用这个客户机与我们的服务一起使用。在同一redis.py文件中,更新您的导入以包括以下内容:

from nameko.extensions import DependencyProvider 

现在,让我们实现以下代码:

class MessageStore(DependencyProvider): 

    def setup(self): 
        redis_url = self.container.config['REDIS_URL'] 
        self.client = RedisClient(redis_url) 

    def stop(self): 
        del self.client 

    def get_dependency(self, worker_ctx): 
        return self.client 

在前面的代码中,您可以看到我们新的MessageStore类继承自DependencyProvider类。我们在新 MessageStore 类中指定的方法将在微服务生命周期的某些时刻被调用:

  • setup:这将在我们的 Nameko 服务开始之前调用。在这里,我们从config.yaml中获取 Redis URL,并使用前面的代码创建一个新的RedisClient
  • stop:当我们的 Nameko 服务开始关闭时,这将被调用。
  • get_dependency:所有依赖提供程序都需要实现此方法。当一个入口点触发时,Nameko 创建一个 worker,并将服务中指定的每个依赖项的get_dependency结果注入 worker。在我们的例子中,这意味着我们的工作人员都可以访问RedisClient的实例。

Nameko offers more methods to control how your dependency providers function at different moments of the service lifecycle: http://url.marcuspen.com/nam-writ.

创建我们的消息服务

在我们的service.py中,我们现在可以使用我们新的 Redis 依赖提供程序。让我们从创建一个新服务开始,它将取代以前的 Konnichiwa 服务。首先,我们需要更新文件顶部的导入:

from .dependencies.redis import MessageStore 

现在,我们可以创建我们的新服务:

class MessageService: 

    name = 'message_service' 
    message_store = MessageStore() 

    @rpc 
    def get_message(self, message_id): 
        return self.message_store.get_message(message_id) 

这与我们以前的服务类似;但是,这次我们指定了一个新的类属性message_store。我们的 RPC 入口点get_message现在可以利用它在RedisClient中调用get_message并简单地返回结果。

我们可以通过在 RPC 入口点中创建一个新的 Redis 客户端并实现一个 RedisGET来完成所有这些。然而,通过创建依赖提供程序,我们提高了可重用性,并隐藏了当密钥不存在时 Redis 返回None的不必要行为。这只是一个很小的例子,说明了为什么依赖项提供者非常擅长将我们的服务与外部依赖项分离。

把它们放在一起

让我们试试我们刚刚创建的代码。首先使用redis-cli将新的键值对保存到 Redis:

127.0.0.1:6379> set msg1 "this is a test"
OK

现在开始我们的 Nameko 服务:

$ nameko run temp_messenger.service --config config.yaml

我们现在可以使用nameko shell远程呼叫我们的新MessageService

>>> n.rpc.message_service.get_message('msg1') 
'this is a test' 

正如预期的那样,我们能够通过我们的MessageService入口点检索我们先前使用redis-cli设置的消息。

现在,让我们尝试获取一条不存在的消息:

    >>> n.rpc.message_service.get_message('i_dont_exist')
    Traceback (most recent call last):
      File "<console>", line 1, in <module>
      File "/Users/marcuspen/.virtualenvs/temp_messenger/lib/python3.6/site-packages/nameko/rpc.py", line 393, in __call__
        return reply.result()
      File "/Users/marcuspen/.virtualenvs/temp_messenger/lib/python3.6/site-packages/nameko/rpc.py", line 379, in result
        raise deserialize(error)
    nameko.exceptions.RemoteError: RedisError Message not found: i_dont_exist

这不是最漂亮的错误,我们可以做一些事情来减少回溯,但最后一行说明了我们前面定义的异常,并清楚地向我们展示了请求失败的原因。

我们现在将继续保存消息。

保存消息

早些时候,我介绍了 RedisSET方法。这将允许我们将消息保存到 Redis,但首先,我们需要在依赖项提供程序中创建一个新方法来处理此问题。

我们可以简单地创建一个名为redis.set(message_id, message)的新方法,但是我们如何处理新的消息 ID 呢?如果我们希望用户为他们想要发送的每条消息输入一个新的消息 ID,这会有点麻烦,对吗?另一种方法是让消息服务在调用依赖项提供程序之前生成一个新的随机消息 ID,但这会使我们的服务与依赖项本身可以处理的逻辑混乱。

我们将通过让依赖项创建一个随机字符串作为消息 ID 来解决这个问题。

向 Redis 客户端添加保存消息方法

redis.py中,我们修改我们的进口,将uuid4包括在内:

from uuid import uuid4 

uuid4为我们生成一个唯一的随机字符串,我们可以将其用于我们的消息。

我们现在可以将新的save_message方法添加到RedisClient中:

    def save_message(self, message): 
        message_id = uuid4().hex 
        self.redis.set(message_id, message) 

        return message_id 

首先,我们使用uuid4().hex生成一个新的消息 ID。hex属性将 UUID 作为 32 个字符的十六进制字符串提供给我们。然后,我们将其用作保存消息并返回消息的键。

添加保存消息 RPC

现在让我们创建一个 RPC 方法,它将调用我们的新客户机方法。在我们的MessageService中,增加以下方法:

    @rpc 
    def save_message(self, message): 
        message_id = self.message_store.save_message(message) 
        return message_id 

这里没有什么特别之处,但请注意,为我们的服务添加新功能变得多么容易。我们将依赖项中的逻辑与入口点分离,同时使代码可重用。如果我们将来创建的另一个 RPC 方法需要将消息保存到 Redis,我们可以轻松地这样做,而无需再次重新创建相同的代码。

让我们仅通过使用nameko shell来测试这一点-记住重新启动您的 Nameko 服务以使更改生效!

>>> n.rpc.message_service.save_message('Nameko is awesome!')
    'd18e3d8226cd458db2731af8b3b000d9'

The ID returned here is random and will differ from the one you get from your session.

>>>n.rpc.message_service.get_message
   ('d18e3d8226cd458db2731af8b3b000d9')
    'Nameko is awesome!'

如您所见,我们已成功保存了一条消息,并使用返回的 UUID 来检索消息。

这一切都很好,但就我们的应用而言,我们不希望用户必须提供消息 UUID 才能读取消息。让我们更实际一点,看看如何在 Redis 商店中获取所有消息。

检索所有邮件

与前面的步骤类似,我们需要向 Redis 依赖项添加一个新方法,以便添加更多功能。这一次,我们将创建一个方法,该方法将遍历 Redis 中的所有键,并在列表中返回相应的消息。

向 Redis 客户端添加 get all messages 方法

让我们在RedisClient中添加以下内容:

def get_all_messages(self): 
    return [ 
        { 
            'id': message_id, 
            'message': self.redis.get(message_id) 
        } 
        for message_id in self.redis.keys() 
    ] 

我们首先使用self.redis.keys()收集存储在 Redis 中的所有密钥,在我们的例子中,这些密钥就是消息 ID。然后,我们有一个列表理解,它将遍历所有消息 ID,并为每个 ID 创建一个字典,其中包含消息 ID 本身以及使用self.redis.get(message_id)存储在 Redis 中的消息。

For large scale applications in a production environment, it is not recommended to use the Redis KEYS method, since this will block the server until it has finished completing its operation. For more information, see: http://url.marcuspen.com/rediskeys.

就个人而言,我更喜欢在这里使用列表理解来构建消息列表,但是如果您很难理解此方法,我建议将其作为循环的标准编写。

对于本例,请参见以下代码,了解作为 For 循环构建的相同方法:

def get_all_messages(self): 
    message_ids = self.redis.keys() 
    messages = [] 

    for message_id in message_ids: 
        message = self.redis.get(message_id) 
        messages.append( 
            {'id': message_id, 'message': message} 
        ) 

    return messages 

这两种方法的作用完全相同。你喜欢哪一种?我会把选择权留给你。。。

每当我写一个列表或字典时,我总是从检查函数或方法输出的测试开始。然后,我以理解的方式编写代码,并对其进行测试,以确保输出是正确的。然后,我将代码更改为 for 循环,并确保测试仍然通过。之后,我查看两个版本的代码,并决定哪一个看起来最可读和干净。除非代码需要超级高效,否则我总是选择可读性好的代码,即使这意味着还要多写几行。从长远来看,这种方法在以后读回和维护代码时是值得的!

我们现在有了一种在 Redis 中获取所有消息的方法。在前面的代码中,我可以简单地返回一个消息列表,而不涉及字典,只返回消息的字符串值。但是,如果我们想在以后为每条消息添加更多数据,该怎么办?例如,一些元数据表示消息是何时创建的,或者消息到过期还有多长时间。。。我们稍后再谈那部分!在这里为每条消息使用一个字典将允许我们以后轻松地发展我们的数据结构。

我们现在可以考虑在MessageService中添加一个新的 RPC,它将允许我们获取所有消息。

添加“获取所有消息”RPC

在我们的MessageService课程中,只需添加:

@rpc 
def get_all_messages(self): 
    messages = self.message_store.get_all_messages() 
    return messages 

我相信到现在为止,我可能不需要解释这里发生了什么!我们只是调用我们在 Redis 依赖项中前面创建的方法并返回结果。

把它们放在一起

在您的 VirtualNV 中,使用nameko shell,我们现在可以测试这一点:

>>> n.rpc.message_service.save_message('Nameko is awesome!')
'bf87d4b3fefc49f39b7dd50e6d693ae8'
>>> n.rpc.message_service.save_message('Python is cool!')
'd996274c503b4b57ad5ee201fbcca1bd'
>>> n.rpc.message_service.save_message('To the foo bar!')
'69f99e5863604eedaf39cd45bfe8ef99'
>>> n.rpc.message_service.get_all_messages()
[{'id': 'd996274...', 'message': 'Python is cool!'},
{'id': 'bf87d4b...', 'message': 'Nameko is awesome!'},
{'id': '69f99e5...', 'message': 'To the foo bar!'}]

好了!我们现在可以检索数据存储中的所有消息。(为了空间和可读性,我截断了消息 ID。)

这里返回的消息有一个问题-你能发现它是什么吗?我们将消息放入 Redis 的顺序与再次取出消息时收到的顺序不同。稍后我们将回到这里,但现在,让我们继续在 web 浏览器中显示这些消息。

在 web 浏览器中显示消息

早些时候,我们添加了WebServer微服务来处理 HTTP 请求;我们现在将对此进行修改,以便当用户登录根主页时,他们将显示数据存储中的所有消息。

一种方法是使用一个模板引擎,比如 Jinja2。

添加 Jinja2 依赖项提供程序

Jinja2 是 Python 的模板引擎,与 Django 中的模板引擎非常相似。对于那些熟悉 Django 的人来说,使用它你应该感到宾至如归。

在我们开始之前,您应该修改您的base.in文件以包括jinja2,重新编译您的需求并安装它们。或者,只需运行pip install jinja2

创建模板渲染器

在 Jinja2 中生成简单 HTML 模板时,需要执行以下三个步骤:

  • 创建模板环境
  • 指定模板
  • 呈现模板

通过这三个步骤,重要的是确定在应用运行时哪些部分永远不会更改(或至少不太可能更改)。。。哪些是。在我通过下面的代码进行解释时,请记住这一点。

在 dependencies 目录中,添加一个新文件jinja2.py并从以下代码开始:

from jinja2 import Environment, PackageLoader, select_autoescape 

class TemplateRenderer: 

    def __init__(self, package_name, template_dir): 
        self.template_env = Environment( 
            loader=PackageLoader(package_name, template_dir), 
            autoescape=select_autoescape(['html']) 
        ) 

    def render_home(self, messages): 
        template = self.template_env.get_template('home.html') 
        return template.render(messages=messages) 

在我们的__init__方法中,我们需要一个包名和一个模板目录。有了这些,我们就可以创建模板环境了。环境需要一个加载器,这只是一种能够从给定的包和目录加载模板文件的方法。为了安全起见,我们还指定要在 HTML 文件上启用自动转义。

然后我们制作了一个render_home方法,它允许我们在制作home.html模板后渲染它。请注意我们如何使用messages呈现模板。。。你以后会明白原因的!

你能理解我为什么这样构造代码吗?因为__init__方法总是被执行,所以我将模板环境的创建放在那里,因为在应用运行时,这不太可能改变。

但是,我们要呈现的模板以及我们为该模板提供的变量总是会发生变化,这取决于用户试图访问的页面以及在给定时刻可用的数据。在前面的结构中,为应用的每个网页添加一个新方法变得很简单。

创建我们的主页模板

现在让我们看一下模板所需的 HTML。让我们首先在依赖项旁边创建一个名为templates的新目录。

在我们的新目录中,创建以下home.html文件:

<!DOCTYPE html> 

<body> 
    {% if messages %} 
        {% for message in messages %} 
            <p>{{ message['message'] }}</p> 
        {% endfor %} 
    {% else %} 
        <p>No messages!</p> 
    {% endif %} 
</body> 

这个 HTML 不是什么花哨的东西,模板逻辑也不是!若您不熟悉 Jinja2 或 Django 模板,那个么您可能会认为这个 HTML 看起来很奇怪,到处都是花括号。Jinja2 使用这些工具允许我们将类似 Python 的语法输入到模板中。

在前面的例子中,我们从一个if语句开始,看看我们是否有任何消息(messages的格式和结构将与我们前面制作的get_all_messagesRPC 返回的消息相同)。如果我们这样做,那么我们就有了更多的逻辑,包括一个 for 循环,它将迭代并显示messages列表中每个字典的'message'值。

如果没有消息,那么我们只显示No messages!文本。

To learn more about Jinja2, visit: http://url.marcuspen.com/jinja2.

创建依赖项提供程序

我们现在需要将我们的TemplateRenderer公开为 Nameko 依赖项提供者。在我们之前制作的jinja2.py文件中,更新我们的导入,以包括以下内容:

from nameko.extensions import DependencyProvider 

然后添加以下代码:

class Jinja2(DependencyProvider): 

    def setup(self): 
        self.template_renderer = TemplateRenderer( 
            'temp_messenger', 'templates' 
        ) 

    def get_dependency(self, worker_ctx): 
        return self.template_renderer 

这与我们以前的 Redis 依赖关系非常相似。我们指定一个setup方法来创建TemplateRenderer的实例,并指定一个get_dependency方法将其注入到 worker 中。

现在可以供我们的WebServer使用了。

生成 HTML 响应

我们现在可以在WebServer中开始使用新的 Jinja2 依赖项。首先,我们需要将其包括在我们进口的service.py中:

from .dependencies.jinja2 import Jinja2 

现在我们将WebServer类修改为:

class WebServer: 

    name = 'web_server' 
    message_service = RpcProxy('message_service') 
    templates = Jinja2() 

    @http('GET', '/') 
    def home(self, request): 
        messages = self.message_service.get_all_messages() 
        rendered_template = self.templates.render_home(messages) 

        return rendered_template 

请注意,我们是如何分配一个新属性templates,就像我们在前面的MessageServicemessage_store中所做的那样。我们的 HTTP 入口点现在与我们的MessageService对话,检索 Redis 中的所有消息,并使用它们使用新的 Jinja2 依赖项创建呈现模板。然后返回结果。

把它们放在一起

重新启动 Nameko 服务,让我们在浏览器中尝试:

成功了。。。某种程度上!我们之前在 Redis 中存储的消息是存在的,这意味着我们模板中的逻辑功能正常,但我们也有来自home.html的所有 HTML 标记和缩进。

这是因为我们还没有为 HTTP 响应指定任何头来表示它是 HTML。为此,让我们在WebServer类之外创建一个小的 helper 函数,该函数将把呈现的模板转换为具有适当标题和状态代码的响应。

在我们的service.py中,修改我们的进口,包括:

from werkzeug.wrappers import Response 

然后在类之外添加以下函数:

def create_html_response(content): 
    headers = {'Content-Type': 'text/html'} 
    return Response(content, status=200, headers=headers) 

此函数用于创建标题字典,其中包含正确的内容类型 HTML。然后,我们创建并返回一个Response对象,该对象的 HTTP 状态代码为200、标题和内容,在我们的例子中,这些内容将是呈现的模板。

现在,我们可以修改 HTTP 入口点以使用新的帮助函数:

@http('GET', '/') 
def home(self, request): 
    messages = self.message_service.get_all_messages() 
    rendered_template = self.templates.render_home(messages) 
    html_response = create_html_response(rendered_template) 

    return html_response 

我们的homeHTTP 入口点现在使用create_html_reponse,为其提供呈现模板,然后返回所做的响应。让我们在浏览器中再试一次:

正如您现在看到的,我们的消息现在显示为我们所期望的,没有找到 HTML 标记!尝试使用redis-cli使用flushall命令删除 Redis 中的所有数据并重新加载网页。会发生什么?

我们现在将继续发送消息。

通过 POST 请求发送消息

到目前为止,我们取得了良好的进展;我们有一个站点,它能够通过两个微服务显示数据存储中的所有消息。一个微服务处理信息的存储和检索,另一个充当用户的 web 服务器。我们的MessageService已经具备了保存信息的能力;让我们通过POST请求在WebServer中公开这一点。

添加发送消息 POST 请求

在我们的service.py中,添加以下导入:

import json 

现在将以下内容添加到我们的WebServer类中:

@http('POST', '/messages') 
def post_message(self, request): 
    data_as_text = request.get_data(as_text=True) 

    try: 
        data = json.loads(data_as_text) 
    except json.JSONDecodeError: 
        return 400, 'JSON payload expected' 

    try: 
        message = data['message'] 
    except KeyError: 
        return 400, 'No message given' 

    self.message_service.save_message(message) 

    return 204, '' 

使用新的POST入口点,我们首先从请求中提取数据。我们指定参数as_text=True,因为我们将以字节的形式返回数据。

一旦有了这些数据,我们就可以尝试将其从 JSON 加载到 Python 字典中。如果数据不是有效的 JSON,那么这可能会在我们的服务中导致一个JSONDecodeError,因此最好妥善处理,并返回一个错误的请求状态代码400。如果没有此异常处理,我们的服务将返回一个内部服务器错误,其状态代码为500

既然数据是字典格式的,我们就可以获得其中的消息。同样,我们有一些防御性代码,可以处理任何缺少'message'键的情况,并返回另一个400

然后,我们继续使用前面在MessageService中创建的save_messageRPC 保存消息。

有了它,TempMessenger 现在可以通过 HTTPPOST请求保存新消息了!如果愿意,可以使用 curl 或其他 API 客户端进行测试,如下所示:

$ curl -d '{"message": "foo"}' -H "Content-Type: application/json" -X POST http://localhost:8000/messages

我们现在将更新我们的home.html模板,以包括使用此新POST请求的能力。

在 jQuery 中添加 AJAX POST 请求

在开始之前,我要说,在撰写本文时,我绝不是 JavaScript 专家。我的专长更多地在于后端编程,而不是前端编程。这就是说,如果您从事 web 开发超过 10 分钟,那么您就知道尝试避免使用 JavaScript 几乎是不可能的。在某个时候,我们可能需要涉猎一些东西,只是为了交付一件作品。

记住这一点,请不要害怕!

您将要阅读的代码是我通过阅读 jQuery 文档学到的,因此非常简单。如果您对前端代码感到满意,那么我相信在 JavaScript 中可能有一百万种不同而且可能更好的方法来实现这一点,所以请根据您的需要进行修改。

您首先需要在<!DOCTYPE html>之后添加以下内容:

<head> 
  <script src="https://code.jquery.com/jquery-latest.js"></script> 
</head> 

这将在浏览器中下载并运行最新版本的 jQuery。

在我们的home.html中,在结束</body>标记之前,添加以下内容:

<form action="/messages" id="postMessage"> 
  <input type="text" name="message" placeholder="Post message"> 
  <input type="submit" value="Post"> 
</form> 

我们从一些简单的 HTML 开始,添加一个基本表单。它只有一个文本输入和一个提交按钮。它本身会呈现一个文本框和一个提交按钮,但它不会做任何事情。

现在,让我们使用一些 jQuery JavaScript 来了解这些代码:

<script> 

$( "#postMessage" ).submit(function(event) { # ① 
  event.preventDefault(); # ② 

  var $form = $(this), 
    message = $form.find( "input[name='message']" ).val(), 
    url = $form.attr("action"); # ③ 

  $.ajax({ # ④ 
    type: 'POST', 
    url: url, 
    data: JSON.stringify({message: message}), # ⑤ 
    contentType: "application/json", # ⑥ 
    dataType: 'json', # ⑦ 
    success: function() {location.reload();} # ⑧ 
  }); 
}); 
</script> 

这将为我们的提交按钮添加一些功能。让我们简要介绍一下这里发生的事情:

  1. 这将为我们的页面创建一个侦听postMessage事件的事件侦听器。
  2. 我们还使用event.preventDefault();防止提交按钮的默认行为。在本例中,它将提交我们的表单并尝试在/messages?message=I%27m+a+new+message上执行GET
  3. 一旦触发,我们就会在表单中找到消息和 URL。
  4. 有了这些,我们就可以构造 AJAX 请求,这是一个 POST 请求。
  5. 我们使用JSON.stringify将有效负载转换为有效的 JSON 数据。
  6. 还记得之前,我们必须构造一个响应并提供标题信息来表示我们的内容类型是text/html吗?我们在 AJAX 请求中也做了同样的事情,但这次,我们的内容类型是application/json
  7. 我们将datatype设置为json。这会告诉浏览器我们期望从服务器返回的数据类型。
  8. 我们还注册了一个回调,如果请求成功,它将重新加载网页。这将允许我们在页面上看到我们的新消息(以及任何其他新消息),因为它将再次获得所有消息。这种强制重新加载页面的方式并不是最优雅的处理方式,但现在就可以了。

让我们重新启动 Nameko 并在浏览器中尝试以下操作:

如果您没有从 Redis 中清除数据(可以通过手动删除数据或简单地重新启动计算机来完成),您应该仍然可以看到以前的消息。

键入邮件后,单击“发布”按钮提交新邮件:

看来成功了!我们的应用现在能够发送新消息。现在我们将讨论应用的最后一个要求,即在给定时间段后使消息过期。

Redis 中正在过期的消息

我们现在讨论应用的最后一个要求,过期消息。因为我们使用 Redis 来存储消息,所以这就成了一项微不足道的任务。

让我们回顾一下 Redis 依赖项中的save_message方法。Redis'SET有一些可选参数;我们最感兴趣的两个是expx。两者都允许我们设置要保存的数据的到期时间,但有一个区别:ex以秒为单位,px以毫秒为单位:

def save_message(self, message): 
    message_id = uuid4().hex 
    self.redis.set(message_id, message, ex=10) 

    return message_id 

在前面的代码中,您可以看到我对代码所做的唯一修改是在redis.set方法中添加ex=10;这将导致我们所有的邮件在 10 秒内过期。立即重新启动 Nameko 服务并尝试此操作。当您发送新消息时,请等待 10 秒并刷新页面,该消息应消失。

Please note that if there were any messages in Redis before you made this change, they will still be present, since they were saved without an expiry. To remove them, delete all data in Redis with the flushall command using the redis-cli.

您可以随意使用到期时间,通过expx参数将到期时间设置为您想要的任何值。一种更好的方法是将到期时间常量移动到配置文件中,然后在启动 Nameko 时加载该文件,但现在,这就足够了。

邮件排序

你会很快注意到我们应用的当前状态是,消息根本没有任何顺序。当你发送一条新消息时,它可以被插入到消息线程的任何位置,至少可以说,这让我们的应用非常不方便!

为了解决这个问题,我们将按照消息过期前的剩余时间对消息进行排序。首先,我们必须修改 Redis 依赖项中的get_all_messages方法,以获得每条消息的生存时间:

def get_all_messages(self): 
    return [ 
        { 
            'id': message_id, 
            'message': self.redis.get(message_id), 
            'expires_in': self.redis.pttl(message_id), 
        } 
        for message_id in self.redis.keys() 
    ] 

正如您在前面的代码中所看到的,我们为每条消息添加了一个新的expires_in值。这使用 redispttl 命令,该命令返回给定密钥的生存时间(以毫秒为单位)。或者,我们也可以使用 Redis TTL 命令,该命令以秒为单位返回生存时间,但我们希望它尽可能精确,以使排序更准确。

现在,当我们的MessageService调用get_all_messages时,它还将知道每条消息的生存时间。有了它,我们可以创建一个新的助手函数来对消息进行排序。

首先,在我们的进口产品中添加以下内容:

from operator import itemgetter 

MessageService类之外,创建以下函数:

def sort_messages_by_expiry(messages, reverse=False): 
    return sorted( 
        messages, 
        key=itemgetter('expires_in'), 
        reverse=reverse 
    ) 

这使用 Python 内置的sorted函数,该函数能够从给定的 iterable 返回排序列表;在我们的例子中,iterable 是messages。我们使用key指定我们希望messages按什么排序。因为我们希望messagesexpires_in排序,所以我们使用itemgetter提取它作为比较。我们给了sort_messages_by_expiry函数一个可选参数reverse,如果设置为True,将使sorted以相反的顺序返回已排序的列表。

有了这个新的帮助函数,我们现在可以修改我们的MessageService中的get_all_messagesRPC:

@rpc 
def get_all_messages(self): 
    messages = self.message_store.get_all_messages() 
    sorted_messages = sort_messages_by_expiry(messages) 
    return sorted_messages 

我们的应用现在将返回我们的消息,并在底部按最新消息排序。如果您想在顶部显示最新消息,只需将sorted_messages更改为:

sorted_messages = sort_messages_by_expiry(messages, reverse=True) 

我们的应用现在符合我们之前指定的所有验收标准。我们能够发送消息和获取现有消息,并且它们都会在一段可配置的时间后过期。一件不太理想的事情是,我们依靠浏览器刷新来获取消息的最新状态。我们可以用多种方法解决这个问题,但我将演示解决这个问题的最简单方法之一;通过投票。

通过使用轮询,浏览器可以不断向服务器发出请求以获取最新消息,而无需强制刷新页面。我们将不得不引入更多的 JavaScript 来实现这一点,但任何其他方法都是如此。

浏览器轮询消息

当浏览器进行轮询以获取最新消息时,我们的服务器应该以 JSON 格式返回消息。为了实现这一点,我们需要创建一个新的 HTTP 端点,以 JSON 的形式返回消息,而不使用 Jinja2 模板。我们将首先构造一个新的 helper 函数来创建 JSON 响应,并设置正确的头。

在我们的 Web 服务器之外,创建以下函数:

def create_json_response(content): 
    headers = {'Content-Type': 'application/json'} 
    json_data = json.dumps(content) 
    return Response(json_data, status=200, headers=headers) 

这与前面的create_html_response类似,但这里它将内容类型设置为'application/json',并将数据转换为有效的 JSON 对象。

现在,在 Web 服务器中,创建以下 HTTP 入口点:

@http('GET', '/messages') 
def get_messages(self, request): 
    messages = self.message_service.get_all_messages() 
    return create_json_response(messages) 

这将调用我们的get_all_messagesRPC,并将结果作为 JSON 响应返回给浏览器。注意我们是如何使用相同的 URL,/messages,就像我们在端点中所做的那样,在这里发送一条新消息。这是一个安静的好例子。我们使用对/messages的 POST 请求来创建新消息,并使用对/messages的 GET 请求来获取所有消息。

使用 JavaScript 进行轮询

为了使我们的消息能够在不刷新浏览器的情况下自动更新,我们将创建两个 JavaScript 函数-messagePoll,用于获取最新消息,以及updateMessages,用于使用这些新消息更新 HTML。

首先用以下行替换我们的home.html中的 Jinja2if块,它在我们的消息列表中进行迭代:

<div id="messageContainer"></div> 

这将在稍后用于保存 jQuery 函数生成的新消息列表。

在我们的home.html中的<script>标签内,写下以下代码:

function messagePoll() { 
  $.ajax({ 
    type: "GET", # ① 
    url: "/messages", 
    dataType: "json", 
    success: function(data) { # ② 
      updateMessages(data); 
    }, 
    timeout: 500, # ③ 
    complete: setTimeout(messagePoll, 1000), # ④ 
  }) 
} 

这是另一个 AJAX 请求,类似于我们之前发送新消息的请求,但有一些区别:

  1. 这里,我们对我们在WebServer中提出的新端点执行GET请求,而不是POST请求。
  2. 如果成功,我们将使用success回调调用稍后创建的updateMessages函数。
  3. timeout设置为 500 毫秒—这是我们在放弃之前应该期望服务器响应的时间量。
  4. 使用complete,它允许我们定义successerror回调完成后会发生什么-在这种情况下,我们使用setTimeout函数将其设置为在 1000 毫秒后再次调用poll

现在我们将创建updateMessages函数:

function updateMessages(messages) { 
  var $messageContainer = $('#messageContainer'); # ① 
  var messageList = []; # ② 
  var emptyMessages = '<p>No messages!</p>'; # ③ 

  if (messages.length === 0) { # ④ 
    $messageContainer.html(emptyMessages); # 
  } else { 
    $.each(messages, function(index, value) { 
      var message = $(value.message).text() || value.message; 
      messageList.push('<p>' + message + '</p>'); # 
    }); 
    $messageContainer.html(messageList); # ⑤ 
  } 
} 

通过使用此函数,我们可以替换 HTML 模板中生成 Jinja2 模板中消息列表的所有代码。让我们一步一步地看一下:

  1. 首先,我们在 HTML 中获取messageContainer,以便更新它。
  2. 我们生成一个空的messageList数组。
  3. 我们生成emptyMessages文本。
  4. 我们检查消息量是否等于 0:
    1. 如果是,我们使用.html()messageContainerHTML 替换为"No messages!"
    2. 否则,对于messages中的每条消息,我们首先使用 jQuery 的内置.text()函数剥离可能存在的任何 HTML 标记。然后我们将消息包装在<p>标记中,并使用.push()将它们附加到messageList中。
  5. 最后,我们使用.html()messageContainerHTML 替换为messagesList

In point 4b, it's important to escape any HTML tags that could be present in the message, as a malicious user could send a nasty script as a message, which would be executed by everyone using the app!

这绝不是解决必须强制刷新浏览器以更新消息的问题的最佳方法,但这是我在本书中演示的最简单的方法之一。可能有更优雅的方式来实现轮询,如果您真的想正确地实现这一点,那么 WebSocket 是目前为止您最好的选择。

总结

现在,我们将结束编写 TempMessenger 应用指南的学习。如果您以前从未使用过 Nameko 或编写过微服务,我希望我能为您提供一个良好的基础,以便在保持服务小型化和切中要害的问题上有所建树。

我们首先使用单个 RPC 方法创建一个服务,然后通过 HTTP 在另一个服务中使用该方法。然后,我们研究了使用设备测试 Nameko 服务的方法,这些设备允许我们产生工作人员,甚至服务本身。

我们引入了依赖提供程序,并创建了一个能够获取单个消息的 Redis 客户端。有了这些,我们扩展了 Redis 依赖关系,使用了一些方法,可以保存新消息、使消息过期并在列表中返回所有消息。

我们研究了如何使用 Jinja2 将 HTML 返回到浏览器,以及如何创建依赖项提供程序。我们甚至研究了一些 JavaScript 和 JQuery,使我们能够从浏览器发出请求。

您可能已经注意到的一个主要主题是需要将依赖关系逻辑与服务代码分开。通过这样做,我们可以使我们的服务不受特定于该依赖项的工作的影响。如果我们决定把 Redis 换成 MySQL 数据库呢?在我们的代码中,这只是为 MySQL 创建一个新的依赖提供程序和映射到我们MessageService所期望的新客户机方法的一个例子。然后,我们将在MessageService中对 Redis 与 MySQL 进行最小的更改。如果我们不以这种方式编写代码,那么我们将不得不投入更多的时间和精力来更改我们的服务。我们还将引入更多漏洞出现的空间。

如果您熟悉其他 Python 框架,那么现在您应该了解 Nameko 是如何让我们轻松创建可伸缩的微服务的,同时与 Django 之类的东西相比,它还为我们提供了一种更多的电池,而不是方法。当涉及到编写小型服务,这些服务只用于后端任务时,Nameko 可能是一个完美的选择。

在下一章中,我们将研究使用 PostgreSQL 数据库通过用户认证微服务扩展 TempMessenger。