Masonite-权威指南-三-

168 阅读33分钟

Masonite 权威指南(三)

原文:The Definitive Guide to Masonite

协议:CC BY-NC-SA 4.0

十、在后台工作

在前一章中,我们看了各种各样的方法来制作和发送通知。对于大型应用来说,这是一个非常有价值的工具,对于需要在后台执行任务的应用来说更是如此。

想想你经常使用的网站,比如 YouTube、脸书、Twitter 和 Google。你知道他们有什么共同点吗(除了很多钱)?他们都需要在请求/响应周期之外做一些无聊的工作。

YouTube 将视频从一种格式转换成许多不同的格式。脸书和 Twitter 处理用户数据,并向你的朋友和家人发送通知,即使他们不在线。谷歌用一群饥饿的机器人爬遍了整个互联网。

如果这些网站在没人注意的时候突然失去了工作能力,它们就会完全停止工作。而且,您可能会构建一些需要在后台做类似工作的东西。

我如何用队列加速我的应用?

队列是 Masonite 提供的主要工具,用于将工作推到后台。队列是运行在服务器上的独立程序。它可以与 Masonite 应用在同一个服务器上,但这不是必需的。

这是应用与队列交互的方式:

  1. 应用需要做一些工作。在我们的例子中,它是应该在请求/响应周期之外完成的工作。

  2. 应用连接到队列,并将需要完成的工作摘要发送到队列。

  3. 同一个应用(或者另一个,这并不重要)连接到同一个队列,并检查是否有任何新的“作业”添加到其中。

  4. 如果“worker”脚本获得了新的作业,它会从队列中取出这些作业,执行它们,然后从队列中删除它们。

  5. 如果发生错误,作业可能会保留在队列中,也可能会被删除或过期。全靠配置。

因此,本质上,我们可以通过获取不需要立即完成的工作并将其发送给队列工作器来处理,从而加速我们的应用。

以 YouTube 为例:

  1. 一个创作者上传一个视频到 YouTube。有一些后台处理,但是用户看到的只是一个细节表单和一个进度指示器。他们仍然可以使用该表单并在网站上做其他事情。一旦原始视频被上传,他们甚至不需要留在网站上进行其余的处理。

  2. YouTube 将原始视频发送到一个队列中,第一项“工作”是快速创建一个低质量版本。这确保了观众可以尽快看到一些东西,同时创建更高质量的版本。

  3. 当低质量版本制作完成并可以观看时,YouTube 会给创作者发电子邮件。这发生在“作业”过程结束时,不管创建者是否仍然打开 YouTube。

  4. 原始视频的更高质量版本被创建,并且当它们变得可用时,新的质量选项出现在视频播放器中。

有许多排队的操作在起作用。没有它们,创作者将需要让 YouTube 标签打开几个小时,或者冒着视频不能正确上传和处理的风险。观众可能需要等待更长的时间。电子邮件不会被发送。

何时何地使用排队?

只要需要处理大型任务,就应该使用作业队列。如果您需要调整请求超时配置,那是一个应该在队列中完成的任务。如果关闭选项卡会导致可避免的数据和/或处理丢失,那么这是一个应该在队列中完成的任务。

如果任务很小,但是不重要或者不是即时的,考虑把它们放到一个队列中。如果你需要发送电子邮件或在服务器上存档文件或导出用户数据,这些都可以在队列中完成。

查找要缓存的文件

此代码可在 https://github.com/assertchris/friday-server/tree/chapter-12 找到。

让我们继续以我们的家庭个人助理为例。我们已经有了一种在家里搜索我们喜欢听的播客的方法。现在,让我们看看如何与他们合作。

首先,我们需要将播客搜索结果与我们所做的订阅工作联系起来:

<form action="{{ route('podcasts-subscribe') }}" method="POST">
    {{ csrf_field }}
    {{ request_method('POST') }}
    <input type="hidden" name="url" value="{{ podcast.feedUrl }}">
    <input type="hidden" name="title" value="{{ podcast.collectionName }}">
    <button onclick="event.preventDefault(); this.form.submit()">subscribe</button>
</form>

这是来自resources/templates/podcasts/_podcast.html

这个按钮类似于我们在订阅列表页面上添加的按钮。我们还没有那个控制器动作,所以我们需要添加它和一个新的路由:

def do_subscribe(self, request: Request):
    Subscription.create({
        'url': request.input('url'),
        'title': request.input('title'),
        'favorite': False,
    })

    return request.redirect_to('podcasts-show-subscriptions')

这是来自app/http/controllers/PodcastController.py

Post('/subscribe', 'PodcastController@do_subscribe').name('-subscribe'),

这是来自routes/web.py

从语义上来说,Post请求是最好的请求方法。我们正在创建一个全新的记录,而不是更新或删除现有的记录。

因此,我们将titleurl值放在隐藏字段中,并用request.input从请求中提取它们。成功订阅后,我们可以重定向到订阅列表。

让我们展开订阅页面,显示每个播客最近的五集:

def show_subscriptions(self, view: View):
    favorites = Subscription.where('favorite', True).get()
    subscriptions = Subscription.where('favorite', '!=', True).get()

    self.get_episodes(favorites)
    self.get_episodes(subscriptions)

    return view.render('podcasts.subscriptions', {
        'favorites': favorites,
        'subscriptions': subscriptions,
    })

def get_episodes(self, podcasts):
    for podcast in podcasts:
        podcast.episodes = []

        for entry in feedparser.parse(podcast.url).entries:
            enclosure = next(
                link for link in entry.links if link.rel == 'enclosure'
            )

            if (enclosure):
                podcast.episodes.append({
                    'title': entry.title,
                    'enclosure': enclosure,
                })

这是来自app/http/controllers/PodcastController.py

前段时间我们添加了feedparser库。现在,我们将使用它来查找每一集播客的媒体文件。我们通过定义一个get_episodes方法来做到这一点,该方法遍历播客列表中的条目。

在每个条目中,我们寻找类型为enclosure的链接,并将其添加回

<ol class="list-decimal">
    @for episode in subscription.episodes[:5]
        <li>{{ episode.title }}</li>
    @endfor
</ol>

这是来自resources/templates/podcasts/_subscription.html

创造就业机会

现在我们有文件要下载,是时候创建我们的第一个Job类了:

craft job DownloadEpisode

这将创建一个新文件,类似于

from masonite.queues import Queueable

class DownloadEpisode(Queueable):
    def __init__(self):
        pass

    def handle(self):
        pass

这是来自app/jobs/DownloadEpisode.py

作业只是通过队列传递的类。让我们在这里打印一些东西,并通过一个新的控制器动作来触发它。

def __init__(self):
    print("in __init__ method")

def handle(self):
    print("in handle method")

这是来自app/jobs/DownloadEpisode.py

Post('/download', 'PodcastController@do_download').name('-download'),

这是来自routes/web.py

from app.jobs.DownloadEpisode import DownloadEpisode
from masonite import Queue

# ...later

def do_download(self, request: Request, queue: Queue):
    queue.push(DownloadEpisode)
    return "done"

这是来自app/http/controllers/PodcastController.py

<ol class="list-decimal">
    @for episode in subscription.episodes[:5]
        <li>
            {{ episode.title }}
            <form action="{{ route('podcasts-download') }}" method="POST">
                {{ csrf_field }}
                {{ request_method('POST') }}
                <input type="hidden" name="url" value="{{ episode.enclosure.href }}">
                <button onclick="event.preventDefault(); this.form.submit()">download</button>
            </form>
        </li>
    @endfor
</ol>

这是来自resources/templates/podcasts/_subscription.html

当我们点击这个“下载”按钮(在/podcasts路由上)时,我们应该会看到一些新的事情发生:

  1. 浏览器应该显示一个大部分空白的页面,带有“完成”

  2. 终端窗口(运行craft serve的窗口)应该显示in _init_method

这意味着我们成功地将DownloadEpisode作业放入队列,但是缺少的in handle method方法告诉我们该作业还没有被拾取。

这是因为默认队列配置使作业在后台运行:

# ...
DRIVERS = {
    'async': {
        'mode': 'threading'
    },
    # ...
}

这是来自config/queue.py

作业正在执行,但是它们是在其他线程中执行的,所以我们看不到它们正在被处理或打印。我们可以配置异步队列来阻止作业的执行,因此它们将被立即执行:

# ...

DRIVERS = {
    'async': {
        'mode': 'threading',
        'blocking': env('APP_DEBUG')
    },
    # ...
}

这是来自config/queue.py

这里,我们告诉 Masonite 让作业立即执行,但只有在应用处于调试模式时。如果我们再次单击“download”按钮,我们应该会看到作业正在执行。我们仍然没有看到打印输出,但至少我们知道它正在发生,现在。

下载文件

让我们开始下载播客片段。我们可以在 handle 方法中做到这一点,但是我们需要 URL:

def do_download(self, request: Request, queue: Queue):
    url = request.input('url')
    folder = 'storage/episodes'

    queue.push(DownloadEpisode(url, folder))

    return request.redirect_to('podcasts-show-subscriptions')

这是来自app/http/controllers/PodcastController.py

作业接受构造函数参数,就像任何其他 Python 类一样。在这种情况下,我们可以将 URL 传递给播客片段和我们想要存储音频文件的文件夹。

重定向回订阅页面可能是个好主意——这比只打印字符串“done”要好得多

现在,我们可以使用一些 file-fu 将剧集的音频文件存储在storage/episodes文件夹中。

from masonite.queues import Queueable
import base64
import requests

class DownloadEpisode(Queueable):

    def __init__(self, url, folder):
        self.url = url
        self.folder = folder

    def handle(self):
        encodedBytes = base64.b64encode(self.url.encode("utf-8"))
        name = str(encodedBytes, "utf-8")

        response = requests.get(self.url, stream=True)
        file = open(self.folder + '/' + name + '.mp3', 'wb')

        for chunk in response.iter_content(chunk_size=10241024):
            if chunk:
                file.write(chunk)

这是来自app/jobs/DownloadEpisode.py

首先,我们为文件生成一个安全的名称。一种方法是对我们下载文件的 URL 进行 base64 编码。然后,我们向 URL 发出一个请求(使用请求库),并得到一个流响应。

流式响应非常好(尤其是对于较大的文件),因为它们不必完全在内存中。如果我们读取许多大的音频文件,超过许多请求,服务器可能会耗尽内存来服务新的请求。取而代之的是,流响应被分解,这样我们每次下载的文件只有一小部分在内存中。

当我们获得每个文件块时,我们将其写入目标文件。此时,您可能需要做几件事情:

  1. 创建storage/episodes文件夹。如果您在作业执行中看到一个FileNotFoundErrorNotADirectoryError(检查终端),那么这可能就是原因。

  2. 如果您还没有安装请求库,请安装它。pip install requests应该能行。

    为了保持事情简单,我没有尽我所能处理好下载。例如,我们应该检查该文件是否是一个有效的音频文件,以及我们是否用它已有的扩展名保存它。此外,我们可以为这些下载创建一个新的模型,这样我们就可以保存细节供以后使用。

如果一切设置正确,单击“下载”按钮应该会将我们重定向到do_download动作,该动作应该会执行作业并将我们重定向回来。如果我们以阻塞模式运行队列,这意味着当我们回到订阅页面时,我们已经下载了音频。

显示下载的文件

现在我们正在下载文件,我们可以显示哪些剧集已经下载:

def get_episodes(self, podcasts):
    for podcast in podcasts:
        podcast.episodes = []

        for entry in feedparser.parse(podcast.url).entries:
            enclosure = next(
                link for link in entry.links if link.rel == 'enclosure'
            )

            if (enclosure):
                encodedBytes = base64.b64encode(
                    enclosure.href.encode("utf-8"))
                name = str(encodedBytes, "utf-8")

                is_downloaded = False

                if path.exists('storage/episodes/' + name + '.mp3'):
                    is_downloaded = True

                podcast.episodes.append({
                    'title': entry.title,
                    'enclosure': enclosure,
                    'is_downloaded': is_downloaded,
                })

这是来自app/http/controllers/PodcastController.py

当我们找到剧集的附件时,我们可以通过对照storage/ episodes文件夹中的文件检查 base64 编码的名称来查看文件是否已经下载。

然后,我们可以用这个来有选择地隐藏“下载”按钮,在视图中:

@for episode in subscription.episodes[:5]
    <li>
        {{ episode.title }}
        @if episode.is_downloaded != True
            <form action="{{ route('podcasts-download') }}" method="POST">
                {{ csrf_field }}
                {{ request_method('POST') }}
                <input type="hidden" name="url" value="{{ episode.enclosure.href }}">
                <button onclick="event.preventDefault(); this.form.submit()">download</button>
            </form>
        @endif
    </li>
@endfor

这是来自resources/templates/podcasts/_subscription.html

我们甚至可以用一个音频播放器来代替下载表单,因为我们已经有了自己的文件。

使用不同的队列提供程序

我们只使用了异步提供者,但是我们还可以尝试其他一些方法。您应该使用适合您的服务器设置和您想要推入其中的作业类型的提供程序。

ampq/rabbitq

RabbitMQ(通过 AMPQ 提供者)是一个队列应用,它与 Masonite 服务器一起运行。它也可以运行在单独的服务器上,因为 Masonite 通过 IP 地址和端口连接到它。

你看过这些配置设置了吗?

QUEUE_DRIVER=async
QUEUE_USERNAME=
QUEUE_VHOST=
QUEUE_PASSWORD=
QUEUE_HOST=
QUEUE_PORT=
QUEUE_CHANNEL=

这是来自.env

这些控制使用哪个队列提供者,对于 RabbitMQ,还控制 Masonite 如何连接到它们。您不能像我们使用异步提供程序那样使用 RabbitMQ。

我建议您使用阻塞驱动程序进行所有的本地开发,并将 RabbitMQ 留给生产。

数据库ˌ资料库

如果您希望更好地控制如何处理失败的作业,最好使用数据库提供程序。只要您创建了适当的表,该提供程序就会将失败作业的详细信息放入数据库:

craft queue:table –jobs
craft queue:table –failed
craft migrate

第一个表是存储准备处理的作业的地方。第二个是记录失败作业的位置。这意味着您可以密切关注失败的作业表并调查任何失败。

您甚至可以构建这些表的 HTML 视图,以便更好地跟踪队列中正在处理和已经处理的内容。

如果你想使用其他的提供商,一定要查看官方文档: https://docs.masoniteproject.com/v/v2.2/useful-features/queues-and-jobs

摘要

在本章中,我们学习了为什么我们应该使用队列以及如何设置它们。队列非常有用,如果没有类似的东西,任何大型应用都不会存在。

花些时间在你的应用中放一个音频播放器,这样你就可以开始听你的播客了。

在下一章,我们将会看到另一种形式的进程间通信,这次是在服务器和浏览器之间。没错,我们要解决网络套接字问题!

十一、使用 Pusher 添加 Websockets

我们已经做了很多服务器端的工作,但现在是时候在浏览器中做一些工作了。具体来说,我希望我们看看如何将新信息“推”到浏览器,而不需要用户发起操作(通过单击按钮或键入消息)。

“那不是阿贾克斯吗?”

在第五章中,我们谈到了创建表单,我们谈到了 Ajax 和 Websockets。概括地说,Ajax 是一种向服务器发送请求的方法,无需在浏览器中加载新的 URL,并在请求完成时更新页面的一小部分。

这是 Ajax 的简单定义,但也是它最常见的用例。当请求完成时,页面没有需要更新。

Ajax 和“普通”表单请求通常是用户发起的动作。有时,web 应用可以启动这些操作,但结果越激烈,原因越不可能是自动的。

有时候,不必等待用户操作是很有用的。想象一下,你希望收到新邮件或推文的通知,但你不想点击按钮。

可以在浏览器中建立一种循环,发出 Ajax 请求,但是大多数时候它们不会显示任何新数据。这将是浪费工作,这将减缓其他类似的行动,零收益。

另一方面,Websockets 是到服务器的开放连接。服务器可以在任何时候通过 Websocket 推送新数据,而不需要用户发起动作或不必要的 HTTP 请求。

安装推杆

该代码可在 https://github.com/assertchris/friday-server/tree/chapter-13 找到。

建立 Websockets 的方法有很多,但我最喜欢的是通过一种叫做 Pusher 的服务。这是一个托管的 Websocket 服务,允许从服务器向浏览器推送新事件,而无需服务器直接支持 Websockets。

我们开始吧!进入 https://pusher.com ,点击“报名”我更喜欢使用我的 GitHub 帐户,所以我需要密码保护的帐户较少。一旦你注册了,你应该被带到仪表板,如图 11-1 所示。

img/484280_1_En_11_Fig1_HTML.jpg

图 11-1

推杆仪表板

接下来,单击“创建新应用”按钮,您应该会看到一个弹出窗口,询问应用的详细信息。我选择 Vanilla JS 作为前端技术,Python 作为后端技术。

如图 11-2 所示,我还输入了“星期五”作为应用名称,并选择了一个离我最近的地区。

img/484280_1_En_11_Fig2_HTML.jpg

图 11-2

设置新的推送应用

前端集成推动器

应用页面显示了打开 pusher 连接所需的代码。我们先来添加前端代码:

<!doctype html>
<html lang="en">
    <head>
        <meta charset="utf-8">
        <link href="/static/style.css" rel="stylesheet" type="text/css">
    </head>
    <body>
        <div class="container mx-auto p-4">
            @block content
                <!-- template content will be put here-->
            @endblock
        </div>
        <script src="https://js.pusher.com/5.0/pusher.min.js"></script>
        <script>
            Pusher.logToConsole = true;

            var pusher = new Pusher('c158052fb78ff1c7b4b2', {
                cluster: 'eu',
                forceTLS: true
            });

            var channel = pusher.subscribe('my-channel');

            channel.bind('my-event', function(data) {
                console.log(data);
            });
        </script>
    </body>
</html>

这是来自resources/templates/layout.html

这是一个公钥,所以它可以直接出现在视图中,但是您可能想考虑将它移到.env文件中。通常,最好将与服务相关的键(和秘密)放在.env中,而不要将该文件提交给 Git。

确保用您的推动器应用密钥替换c158052fb78ff1c7b4b2。我已经包括了我的,所以你可以看到它的确切位置,但它不会为你的应用工作。

Pusher 使用通道的概念。浏览器(或者手机 app 等。)连接到他们感兴趣的通道,pusher 在这些通道内发送事件。

在这里,我们连接到my-channel频道并监听my-event事件。当我们添加服务器端代码时,我们将再次看到这些值。

创建命令

我们可以在很多地方触发推送事件,但我认为重新审视控制台命令是有意义的。让我们安装 Pusher 库并创建一个新的控制台命令,向所有在线浏览器发送消息:

pip install pusher
craft command SendMessageToOnlineBrowsers

这将创建一个类似于以下内容的文件:

from cleo import Command

class SendMessageToOnlineBrowsersCommand(Command):
    """
    Description of command

    command:name
        {argument : description}
    """

     def handle(self):
        pass

这是来自app/commands/SendMessageToOnlineBrowsersCommand.py

让我们定制该命令以反映其目的,并打印一些内容:

from cleo import Command

class SendMessageToOnlineBrowsersCommand(Command):
    """
    Sends a message to all currently online browsers

    send-messages-to-online-browsers
        {message : The text to send}
    """

    def handle(self):
        print("in the command")

这是来自app/commands/SendMessageToOnlineBrowsersCommand.py

每个新的控制台命令都有一个handle方法,该方法在控制台命令被调用时被调用。我们需要在craft中注册这个命令,所以让我们在一个新的服务提供者中这样做:

craft provider CraftProvider

这将创建一个类似于以下内容的文件:

from masonite.provider import ServiceProvider

class CraftProvider(ServiceProvider):
    wsgi = False

    def register(self):
        pass

    def boot(self):
        pass

这是来自app/providers/CraftProvider.py

提供者的register方法在应用启动时被调用,而boot方法仅在应用完全启动后被调用。区别很重要,因为放置其他提供商可能想要的服务的最佳位置在register,而放置使用其他服务的最佳位置在boot

在这种情况下,我们想让我们的新craft命令对其他提供者和服务可用:

from masonite.provider import ServiceProvider
from app.commands.SendMessageToOnlineBrowsers import
SendMessageToOnlineBrowsers

class CraftProvider(ServiceProvider):
    wsgi = False

    def register(self):
        self.app.bind(
            'SendMessageToOnlineBrowsers',
            SendMessageToOnlineBrowsers()
        )

    def boot(self):
        pass

这是来自app/providers/CraftProvider.py

反过来,这个提供者需要在应用配置中注册:

from .HttpClientProvider import HttpClientProvider
from .RssParserProvider import RssParserProvider
from .CraftProvider import CraftProvider

这是来自app/providers/__init__.py

# ...snip

from app.providers import (
    HttpClientProvider,
    RssParserProvider,
    CraftProvider, )

PROVIDERS = [
    # ...snip

    HttpClientProvider,
    RssParserProvider,
    CraftProvider,
]

这是来自config/providers.py

现在,当我们运行craft命令时,在一个新的终端窗口中,我们应该看到我们添加的新命令,如图 11-3 所示。

img/484280_1_En_11_Fig3_HTML.jpg

图 11-3

列出新命令

并且,我们可以运行命令:

craft send-messages-to-online-browsers "hello world"

后端集成推动器

好,我们有一个可以使用的命令,但是它没有使用我们发送的消息。让我们安装 Pusher 库,并在命令中使用它:

pip install pusher

from cleo import Command
from pusher import Pusher

class SendMessageToOnlineBrowsersCommand(Command):
    """
    Sends a message to all currently online browsers

    send-messages-to-online-browsers
        {message : The text to send}
    """

    def handle(self):
        message = self.argument('message')

        pusher = Pusher(
            app_id='935879',
            key='c158052fb78ff1c7b4b2',
            secret='ab37b95e1648ba5c67cc',
            cluster='eu',
            ssl=True
        )

        pusher.trigger('my-channel', 'my-event', {'message': message})

这是来自app/commands/SendMessageToOnlineBrowsers.py

如果我们再次运行该命令,我们应该会在 Pusher app 窗口中看到该消息,如图 11-4 所示。记住在布局和命令中使用你自己的按键,否则将不起作用。

img/484280_1_En_11_Fig4_HTML.jpg

图 11-4

在 pusher 中接收消息

这太酷了。我们应该在 JavaScript 控制台中看到相同的事件,从我们应用的任何页面,如图 11-5 所示。

img/484280_1_En_11_Fig5_HTML.jpg

图 11-5

在控制台中查看事件

根据收到的消息采取行动

让我们添加一个弹出窗口,它显示这些消息几秒钟。我们可以在 include 中添加标记,并从已经添加的 JavaScript 中引用它:

<div
    class="
        message
        hidden flex-row items-center justify-center
        absolute top-0 left-0 p-2 m-8
        bg-blue-100 border-2 border-blue-200
    "
>
    <div class="text-blue-700">message text here</div>
    <button class="text-blue-500 ml-2">&cross;</button>
</div>

这是来自resources/templates/_message.html

@include '_message.html' <script src="https://js.pusher.com/5.0/pusher.min.js"></script>
<script>
    Pusher.logToConsole = true;

    var pusher = new Pusher('c158052fb78ff1c7b4b2', {
        cluster: 'eu',
        forceTLS: true
    });

    var channel = pusher.subscribe('my-channel');
    var message = document.querySelector('.message');
    var messageClose = message.querySelector('button');
    var messageText = message.querySelector('div');

    messageClose.addEventListener('click', function() {

        message.classList.remove('flex');
        message.classList.add('hidden');
    });

    channel.bind('my-event', function(data) {
        messageText.innerHTML = data.message;

        message.classList.remove('hidden');
        message.classList.add('flex');

        setTimeout(function() {
            message.classList.remove('flex');
            message.classList.add('hidden');
        }, 1000 * 5 /* 5 seconds */)
    });
</script>

这是从resources/templates/layout.html开始的。

我们定制了 JavaScript 来查找消息 HTML 元素,并在新消息到来时使它们可见。用户可以选择通过点击“关闭”按钮来关闭消息,或者消息会自动消失。

摘要

在本章中,我们学习了如何安装和使用 Pusher。还有一些其他有趣的推送功能,如频道存在和私人频道,但它们更复杂一些。也许这是进一步学习 Websocket 的好地方!

Websockets 是一个强大的工具,也是我经常使用的工具。想想你能告诉用户的所有事情,通过一个永久开放的直接连接到他们的浏览器。

十二、测试

单元测试是一门艺术,它提取代码的一小部分(称为单元)并测试其功能以确保其正常工作。例如,您可以获取一个小的代码单元,比如一个控制器方法,并断言它返回一个视图类。

单元测试对于任何应用都是至关重要的。有很多框架让单元测试成为事后的想法,但是有了 Masonite,我们希望确保能够测试您的应用对我们来说是绝对重要的。

什么是集成测试?

集成测试只是一个比单元测试更宽泛的概念。典型地,它是一个测试,涉及更大的代码片段,或者运行一个类似于用户所做的过程。例如,您可以测试当用户点击一个端点时

  • 一封电子邮件被发送

  • 数据库中会添加一条记录

  • 用户被重定向到仪表板

现在单元测试很棒,但是有时候在小单元中测试代码不允许你在更大的范围内测试,你可能会错过一些未测试的关键方面。因此,在单元测试做不到的地方,你可以做集成测试之类的事情。

我们将在这一章中讨论如何做这两件事。

为什么首先要测试?

您应该测试您的应用的原因之一是,确保当您不断添加新功能时,旧功能不会中断。我无法告诉你有多少次我在 Masonite 的一个 requests 类中修改了一小段代码,在一些随机的类中修改了一些东西。

在 Masonite 发布之前,它将在 Python 的最后四个主要版本上运行所有测试。拥有自动化测试有助于确保 Masonite 能够在所有受支持的 Python 版本上完美运行。

现在,人们不测试他们的应用的一个原因是,编写测试需要花费大量的初始时间,而且除了测试之外,有时还需要更长的时间来编写整个功能。建立一个基本的测试来测试应用的简单部分确实要花很多时间。我们通常在不同的路由上主张同样的事情。也许我们断言某个路由上存在中间件,或者没有登录的用户不能访问文件。

对于 Masonite,我们考虑到了这一点,并希望确保尽可能快地设置这些测试。您甚至会看到为什么 Masonite 的自动解析依赖注入方面实际上有助于测试,因为您正在类型提示的所有那些类都可以被注入到您的测试中。因此,如果您的控制器接受请求类,您可以模拟请求,然后您可以在测试中构建一个新的类,并将其直接注入到您的控制器方法中。

对您的应用进行单元测试的另一个原因是,可能有一个极其复杂的业务逻辑规则需要一直工作。比如有特定物品需要有物品限制,物品销售限制。这两件事情中的一件失败可能意味着业务诉讼,这是你真的不希望失败的事情。

最后,单元测试的原因是为了重构。当重构你的代码时,如果你有一个测试,你可以确保代码在重构前和重构后的工作方式是一样的。这些都是追求单元测试的原因,在我们开始创建我们的测试之前,我们将谈论 Masonite 如何处理所有这些。

我们的测试在哪里?

因此,在我们开始实际创建测试之前,最好知道我们实际上要把测试放在哪里,或者甚至知道我们要如何运行它们。所有的测试都在tests目录中,这个目录被分成几个不同的目录。

第一个导演叫做“tests/unit”。这是你放置所有单元测试的地方。

下一个是“tests/framework”。每当你需要扩展框架的时候,你可以在这里放置所有与框架相关的测试。

另一件需要了解的重要事情是我们将要使用的库。因此,您可能会看到奇怪的语法,因为 Masonite 使用 PEP 8 编码标准来解释方法应该是下划线,但 Masonite 测试套件使用内置的unittest测试库来编写测试,然后推荐pytest来实际运行测试。

我们使用内置的unittest库的原因是因为它实际上比pytest在概念上更容易理解,我们推荐 pytest 实际运行测试套件的原因是它有一个更容易使用的命令行工具。因此,通过将unittest的优点与pytest的优点结合起来,我们能够让 Masonite 的测试更加完美。

单元测试库是在 PEP 8 标准存在之前创建的,所以我们的单元测试将主要使用camelCase来创建。因为我们不希望开发人员在创建测试时必须在标准之间切换,所以所有的测试方法和断言都使用camelCase。这样你就可以在心理上准备好使用(并持续使用)camelCase

创建测试

既然我们已经了解了将要看到的内容,那么让我们开始创建我们的第一个测试用例,看看它是什么样子的。

因此,为了创建您的测试,您将运行一个简单的 craft 命令:

$ craft test Home

这将在tests/test_home.py中创建一个基本的样板测试。我们可以把它留在这里,但是让我们把它拖到unit目录中,这样我们就有了一个tests/unit/test_home.py文件。

如果我们打开这个文件,我们会看到我们现在要讨论的三个基本部分。

以下是您应该看到的示例:

"""TestHomee Testcase."""

from masonite.testing import TestCase

class TestHome(TestCase):

    """..."""
    transactions = True

    def setUp(self):
        """..."""
        super().setUp()

    def setUpFactories(self):
        """..."""
        pass

让我们从上到下浏览一下代码。

我们拥有的第一行只是一个普通的TestCase类的导入。这里有我们将用于创建和运行测试的所有方法、定制断言和设置逻辑。

接下来您将看到的是类名。所有的测试都需要以Test开始,这样测试库就知道把它作为一个测试而不是一个普通的类来运行。

继续前进,你会看到一个transaction = True属性。这将让 Masonite 知道它是否应该运行事务内部的所有测试。在事务内部运行测试非常有用,因此您可以用数据库的相同状态反复运行测试。

接下来你会看到这里有一个setUp方法。setUp方法将在测试创建之前运行。因此,如果您需要修改容器和覆盖一些默认行为或默认值,您可以在 setup 方法中这样做。

最后,您将看到一个setUpFactories方法。这种方法类似于 setup 方法,但是在这里您将做一些事情,比如播种您的数据库,运行您拥有的任何工厂,创建用户,以及在数据库级别设置您的测试所需的所有其他事情。例如,如果您有一个端点需要测试是否有 50 个用户,那么在运行该测试之前创建 50 个用户可能比较好。

最后需要注意的是,默认情况下,所有测试都必须在 SQLite 数据库内部运行。这样做的原因是,如果您使用 MySQL 或 Postgres 之类的东西,有时您会不小心搞砸您的生产数据库,甚至您的正常开发数据库。如果你愿意,你可以通过在你的测试用例上设置sqlite = False来禁用它。这样,您可以为任何数据库运行测试。

我们的第一个测试

我们的测试方法都应该是下划线,它们将以test_underscore开始。现在让我们构建我们的第一个测试。

我们将基于我们在上一节中创建的TestHome测试:

出于篇幅原因,我们将只关注我们的方法,但是请确保您将它附加到我们的测试用例中。

def test_can_visit_homepage(self):
    self.get('/').assertContains('Masonite')

如果您是从 Masonite 的基础安装编写这个测试,那么这个测试应该可以工作,因为首页显示了 Masonite 启动页面。如果您已经修改了应用,那么将Masonite更改为您可以在主页上看到的任何文本(您的/路由)。

同样,任何以assert开头的方法都可以链接在一起。因此,我们还要检查状态是否为200:

def test_can_visit_homepage(self):
    self.get('/').assertContains('Masonite').assertIsStatus(200)

现在我们可以用pytest运行这个测试。如果您还没有安装,现在就可以安装:

$ pip install pytest

现在,我们可以直接进入我们的终端,运行:

$ python -m pytest

我们使用python -m pytest而不仅仅是pytest的原因是前者会将当前工作目录添加到系统路径中。这意味着我们的测试将能够在我们的应用中找到路由、模型和其他任何东西。

什么时候给的

尝试找出你将如何构建测试的一个好的技巧是使用一个简单的“给定”..当...的时候..然后..”格式。例如,您可以将一个测试用例分解成"假设我是一个访客用户,我去了家的路由,我然后将被重定向。"

这个测试可以这样分解:

def test_guest_will_be_redirected(self):
    # Given .. I am a guest user
    # When .. I go to the home route
    response = self.get('/home')

    # Then I will be redirected
    response.assertIsStatus(302)

下面是另一个例子:“假设我是一个认证用户,我去了家的路由,然后我应该看到主页”:

def test_user_sees_home_page(self):
    # Given .. I am an authenticated user
    response = self.actingAs(User.find(1))

    # When .. I go to the home route
    response = self.get('/home')

    # Then .. I should get redirected
    response.assertIsStatus(302)

有时候测试真的很复杂,所以像这样把它们分解成简单的步骤会让测试变得非常清晰。

测试驱动开发

如果我不触及测试驱动的开发,那将是极其不负责任的。既然我们已经对什么是测试以及如何创建测试有了一些基本的了解,那么让我们来谈谈测试驱动开发,或者简称为 TDD。TDD 是先写测试,然后再写代码的艺术。

我们可以以最后一次测试为例。在那里我们断言"给定..我是认证用户”和“..我走回家的路由。”只要我们运行测试,这两个步骤就会失败。所以我们能做的就是继续运行我们的测试,直到我们能通过它们。

在这种情况下,我们首先需要有用户。所以我们可以从创建一些用户开始。我们将遇到的下一个错误是回家的路由。我们将得到一个错误,因为一个主路由不存在。

现在我们已经有了用户和一个本地路由,我们可以将用户传递到路由中并到达那个端点。一旦完成,我们最终可以做出我们的断言。在这种情况下,一个很好的断言可能是用户是否真的可以访问主页,或者他们是否被重定向。

TDD 优于其他测试方法的好处是,它使测试变得更容易,比如事后编写测试。如果你从测试开始,你的代码需要本质上是可测试的。如果你在之后编写测试,可能很难测试你的应用的某些部分,因为你可能没有考虑过以后如何测试它。

例如,你可能在你的控制器中有某种逻辑,它一直在执行。如果您不想在测试中运行这段代码,那么您可以在控制器上设置某种选项,甚至在控制器上设置一个类似withoutComplexLogicOption的 setter 方法,特别是,这样您就可以在测试中避开这个挑战。就更大的领域逻辑而言,该方法可能没有任何实际用途,但它是一段可测试的代码,您将能够安全地进行重构。

与其他选项相比,我个人更喜欢测试驱动开发,并且在构建 Masonite 时经常使用它。事实上,如果有人打开一个特性或问题的拉请求,我不会告诉他们如何修复他们可能错过的用例,而是给他们写一个快速测试,告诉他们确保在拉请求合并之前通过测试。这允许他们将测试插入到他们的代码中,并一直工作到代码运行为止。非常强大的东西。

工厂

工厂是非常有用的代码,允许您快速生成模拟数据。无论您只是想要一个模拟用户,还是需要实现一个复杂的 When 子句,工厂都是在运行测试之前将数据导入数据库的方法。

创建工厂

工厂都存储在config/factories.py文件中。这需要一个函数来返回一个通常是随机的数据集,这个数据集可以运行一次或多次来模拟我们以后可以使用的数据。例如,Masonite 带有用于创建用户的默认工厂。我们能够在种子和测试中使用这些工厂。

为了创建工厂,您必须将工厂注册到模型中。我们将使用我们的Subscription模型,这是我们在数据库章节的前几章中制作的。

我们可以通过在config/factories.py中创建一个新函数来轻松创建一个工厂,并返回一个简单的列值字典。工厂的格式如下所示:

from app.Subscription import Subscription
# ...

def subscription_factory(faker):
    return {
      'url': faker.uri(),
      'title': faker.sentence()
    }

factory.register(Subscription, systems_factory)

请注意,我们只是导入了模型,然后将模型映射到工厂函数。工厂函数采用了一个faker实例,这是一个流行的 Python 库,能够非常快速地生成模拟数据。

使用工厂

我们现在可以在应用的任何部分使用这些工厂。我们可以导入工厂和模型,然后使用它:

from config.factories import factory
from app.Subscription import Subscription
# ...

systems = factory(Subscription, 50).create()

systems变量现在保存了 50 个系统的集合。我们现在可以在测试中做任何我们需要做的事情。在接下来的几节中,我们将使用这个工厂来设置我们的测试。

如果你只想创建一个单一的模型,我们可以很容易地得到一个单一的系统:

system = factory(Subscription).create()

断言数据库值

大多数情况下,您会断言您有特定的数据库值。也许您创建了一个新用户,然后需要确保该用户被持久化到您的数据库中。假设我们有一条POST路由来创建新用户。我们的测试可能看起来像这样:

def test_create_users(self):
  self.post('/users', {
    'username': 'user123',
    'email': 'user@example.com',
    'password': 'pass123'
  })

  self.assertDatabaseHas('users.email', 'user@example.com')

注意,我们可以很容易地断言 users 表中的 email 列包含值 user@example.com.

测试环境

当 Masonite 检测到一个测试正在运行时(由于测试运行时设置的特定环境变量),它将另外加载一个.env.testing文件,如果存在的话。这个文件可以包含不同于标准.env文件的环境变量。

例如,在开发和生产过程中,您可以使用 RabbitMQ 驱动程序来处理队列作业,但也可以选择使用更基本的async驱动程序来进行测试。这样,我们就不需要仅仅为了测试而运行队列服务器。

要更改测试的环境变量,您可以创建一个.env.testing文件并将变量放入其中,如下所示:

DEBUG=True
DB_CONNECTION=sqlite
DB_DATABASE=testing.db
# ...

这些将覆盖任何同名的现有环境变量。

十三、部署 Masonite

有许多服务使应用的生命周期变得极其简单。这些服务包括像 Heroku 或 Python anywhere 这样的东西。在这一章中,我们将主要关注手动部署您的应用,从底层开始,通过配置服务器、安装所需的软件,以及安装和运行我们的应用。如果你知道如何做这些低级的任务,并了解更大的画面,那么你可以很容易地弄清楚如何使用像 Heroku 这样的点击系统。

需要注意的是,本章的某些部分需要一种支付方式来设置服务器和部署应用。

请求生命周期

让我们来谈谈当你在网络浏览器中输入一个域名并点击回车会发生什么。一旦我们做到这一点,你应该有足够的背景信息来开始适应我们的生命周期。

当你在网络浏览器(如 Chrome 或 Firefox)中输入masoniteproject.com并按回车键时,网络浏览器将建立一个请求。该请求包含一组标头形式的元信息。此时,我们的请求继续执行一项任务,将masoniteproject.com转换成 IP 地址,这样互联网就知道如何将该请求定向到服务器。

我们可以通过查找 DNS(域名系统)来完成这种转换。在我们的本地计算机上有一个域系统(想想你的主机文件),在我们的内部网络上有一个域系统(想想公司是如何屏蔽某些网站或者让某些网站只能从办公室内部访问),然后在互联网层面上有一个域系统(想想 Cloudflare 或者 Namecheap DNS)。

假设我们在本地或内部网络级别没有特殊指令,该请求(仍在搜索 IP 地址)将发送到 Cloudflare。Cloudflare 是一家 DNS 提供商,由于其慷慨的免费计划而非常受欢迎。Cloudflare 收到请求,查看他们自己的系统,然后说“好的,我这里有一个 IP 地址为17.154.195.7masoniteproject.com的记录”。

此时,请求就知道该去哪里了。然后,互联网将该请求路由到 Vultr 上的服务器。该请求连接到服务器,然后将所有 web 流量定向到一个端口,通常是端口 80。有一个名为 NGINX 的应用监听端口 80 上的所有流量。NGINX 收到请求,说“好的,当前的域名是masoniteproject.com,我有一个 Python 应用来监听这个域的所有重定向请求。”

NGINX 将该域重定向到另一个端口,称为端口 8001 或套接字,稍后将详细介绍。

现在,这一部分很重要,并且特定于 Python 应用。在请求到达我们的 Masonite 应用之前,还需要进行另一次转换。我们需要将传入的请求转换成 Python 字典,并通过我们的应用发送字典。

这种中间人转换被称为 WSGI 服务器。因为请求的转换相当简单,所以已经为 Python 构建了几个。最常见的有 Gunicorn 和 uWSGI。这些也可以随时换出。Gunicorn 非常容易启动,但是 uWSGI 更容易配置,并且有许多不同的选项可以用来调整设置。

一旦完成了从请求到字典的转换,它就将字典传递给 Masonite 框架,Masonite 框架调用 Masonite 应用的所有相关部分,并以字节为单位返回响应。然后,WSGI 服务器将这些 Python 字节转换成 NGINX 能够理解的响应。

这个请求现在附带了一个来自 Masonite 的响应,它可以一路返回到整个流程中,但是现在反过来了。最终,请求和响应会一路返回到您的 web 浏览器,而您的 web 浏览器会将该响应转换为您所看到的内容。

既然我们知道了整个生命周期是如何工作的,我们就可以着手做我们需要做的事情,以使我们自己和我们的新应用适应这个生命周期。

我们需要做的主要事情如下:

  • 建立网络服务器(数字海洋、Vultr 等)。).

  • 在我们的网络服务器上安装特殊软件(NGINX 和其他软件包)。

  • 将我们的 Masonite 应用放到我们的 web 服务器上(git 克隆)。

  • 运行我们的 Masonite 应用(以便 NGINX 可以将响应定向到我们的应用)。

网络服务器

第一部分是网络服务器。传统上,这是一些公司仓库中的物理服务器,但我们已经在服务器的工作方式方面取得了很大进展,因此实际上它可能只是物理机的一个孤立部分。然而,出于解释的目的,该服务器将是物理服务器。

web 服务器实际上只是一台安装了特殊软件的普通计算机,它可以接受传入的请求,并产生一个响应发送回您的 web 浏览器。请记住这个“特殊软件”,因为我们稍后将对此进行更详细的解释。

事实上,任何计算机都可以成为 web 服务器。我个人曾经在地下室的一台台式电脑上托管我所有的网站,后来我才知道这是多么不安全,或者我离一次可能使我的互联网瘫痪或暴露一些敏感信息的攻击有多近。那是我早期编程的日子。我必须确保我的电脑一直开着。我记得我收到消息说我的网站关闭了,却发现我的台式电脑进入了睡眠状态,或者我的电源暂时中断了,我的电脑从来没有正常重启过。

现在有许多公司能以相对低廉的价格向你提供这些网络服务器。你可以每月花 5 美元左右买一台基本服务器,它可以为你托管几个网站。

个人选择的最大公司包括

  • 数字海洋

  • 填妥了吗

  • 利诺德

许多企业选择 AWS(亚马逊网络服务)和微软这样的公司。出于本书的目的,我们将使用我个人最喜欢的:Vultr。

同样重要的是要注意,这些 web 服务器通常不包含 GUI,并且是严格基于终端的(想想最初的微软 DOS 系统或者只通过终端使用你的计算机)。这是因为 web 服务器应该只做一件事,那就是处理 web 流量。在 web 服务器上运行的任何不促进这个目标的东西都是不必要的开销,所以当你设置你的服务器时,如果你只看到一个黑色的终端屏幕,不要感到惊讶。

设置服务器

我们来谈谈如何设置服务器。我们将在本书中使用 Vultr。如果我们去 Vultr.com,创建一个帐户,然后去仪表板,我们会看到一个类似图 13-1 的屏幕。

img/484280_1_En_13_Fig1_HTML.jpg

图 13-1

Vultr.com 仪表板

需要注意的是,本章的其余部分需要一种支付方式来设置服务器和部署应用。

如果你是在 Masonite Slack 频道上做的,你会看到我们有一个服务器用于那个网站,我们也有一个服务器用于我们需要做的随机测试,比如浏览教程或模仿人们遇到的 Linux 问题。

在右上角,我们会看到一个+图标。当我们点击它时,我们会看到一个屏幕,上面有许多不同的选项可供选择。

选项

我们需要做的第一件事是选择服务器的类型。我们目前有四种选择,如图 13-2 所示。

img/484280_1_En_13_Fig2_HTML.jpg

图 13-2

选择所需的服务器类型

我们可以选择最符合我们需要的选项,但在大多数情况下,第一个选项是好的。这是一个非常标准的盒子,我们可以在云中使用,完全符合我们的需求。

接下来,我们会看到一个我们希望服务器所在位置的列表,如图 13-3 所示。**需要注意的是,你应该选择离你的观众最近的服务器。**服务器离您的受众越近,服务器响应时间就越快。

img/484280_1_En_13_Fig3_HTML.jpg

图 13-3

选择服务器位置

如图 13-4 所示,下一步我们需要选择服务器类型。最受欢迎的选项之一是 Ubuntu。

img/484280_1_En_13_Fig4_HTML.jpg

图 13-4

选择服务器类型

最后一步是选择服务器的大小。我发现我的大多数 Masonite 应用都运行在大约 150MB 的内存上,你应该有一些缓冲空间,因为你的应用的某些部分可能比其他部分需要更多的内存,而且你也很可能会安装一个数据库,所以你可能会有峰值。我建议留出大约 20%的空闲内存,以免降低应用的速度。

因此,如果您选择 512MB 的服务器并留出 20%的空闲空间,那么您将有大约 409MB 的空间可以使用。其中一些空间将专用于其他应用,因此一台 512MB 的服务器可以运行两到三个 Masonite 应用。这个规则不是通用的,而是非常特定于应用的,所以在添加其他应用之前,您应该监视您的服务器性能。

在这个页面的底部有几个选项,比如设置 SSH 密钥和启动脚本,但是现在可以跳过这些选项。

连接到服务器

一旦服务器完成配置(安装所有必要的操作系统软件),我们现在就可以连接到它,并开始安装运行 Python 应用所需的所有东西。

当你点击刚刚构建的新服务器时,你会看到一些连接凭证,如图 13-5 所示。在左侧,您会看到“IP 地址”、“用户名”和“密码”

img/484280_1_En_13_Fig5_HTML.jpg

图 13-5

您的服务器的连接凭据

您将使用这三个设置连接到服务器并开始运行命令。如果您使用的是 Mac 或 Linux,可以使用终端自带的内置ssh命令。如果你用的是 Windows,你就需要用油灰之类的东西。

我发现大多数开发人员使用 Mac 和 Linux,所以我们将演示连接到服务器的这条路由。

首先,在 Mac 或 Linux 机器上打开终端并运行以下命令:

$ ssh {username}@{host}

用您在控制面板中看到的用户名和 IP 地址替换用户名和主机。一旦运行,您将看到另一个提示,要求您输入密码。回到您的控制面板,单击眼睛显示您的密码,或者单击复制图标复制密码。将密码粘贴到提示符中,您的终端将变成服务器的终端。您应该会看到类似这样的内容:

Last login: Sun Feb 16 13:39:21 2020 from 69.119.199.3
root@{server name}:~#

恭喜你!您已经连接到服务器,我们现在可以开始安装您需要启动和运行的一切。让我们继续安装任何需要的软件。

网络服务器软件

在上一节中,我们说过将更详细地解释“特殊软件”。web 服务器需要安装一些软件来告诉它应该如何处理进入它的 web 流量。web 服务器软件有两个主要参与者。

首先是 NGINX 。根据我的经验,这是目前最流行的网络服务器软件。在过去的十年里,它真的在 web 服务器领域占据了主导地位。NGINX 的设置非常简单,并且根据其配置文件的风格,具有极强的可扩展性和可插拔性。我们将使用这个选项来部署我们的 Masonite 应用。

第二个是阿帕奇。这是一个有点老的标准软件,人们曾经使用过,现在许多公司还在使用。它当然不再受 NGINX 的青睐,但仍然是一个可行的选择。与 NGINX 相比,它的设置时间要长一些,配置起来也要困难一些。

这个软件为我们的用例工作的方式是,它简单地接受传入的请求,并将其重定向到服务器上的特定应用。例如,我们可能有一个 Laravel PHP 应用和一个 Python Masonite 应用;NGINX 会将请求发送到每个服务器。

Web 服务器软件还可以做许多其他事情,如负载平衡、电子邮件服务器代理,以及在许多其他协议上执行通信,但就我们的目的而言,它只是将请求重定向到我们的应用中。

为了这本书,我们将与 NGINX 合作。

安装 NGINX

我们需要做的第一件事是安装 NGINX。请记住,这个软件负责接收一个传入的请求,将其路由到正确的应用以获得响应,然后将其发送出去,最终返回到您的浏览器。

因此,如果我们测试这个流程,现在我们可以看到有一个断开。只要进入你的浏览器,输入你从 Vultr 仪表板上得到的 IP 地址。您将看到如图 13-6 所示的错误页面。

img/484280_1_En_13_Fig6_HTML.jpg

图 13-6

重定向错误消息

因此,让我们安装 NGINX,这样我们就可以让这个功能正常工作。

在我们安装任何东西之前,我们需要更新我们的 Ubuntu 包目录:

$ apt-get update

接下来,我们可以简单地安装 NGINX。只需运行以下命令:

$ apt-get install nginx

让安装步骤运行。如果你被提示一个是或否的问题来确认你是否想安装 NGINX,只需输入 Y 并按回车键。

安装完成后,我们可以回到网络浏览器,再次输入我们的 IP 地址。我们现在将看到一个 NGINX 加载页面:

img/484280_1_En_13_Figa_HTML.jpg

请记住,NGINX 必须将请求重定向到某个地方才能得到响应,所以在 NGINX 安装的同时,NGINX 还会向服务器发送一些静态网页,以确认它安装正确。

NGINX 需要一些额外的配置,但是我们会在我们的新服务器上安装我们的应用时进行配置。

设置 Python 软件

如果您以前安装过 Masonite,您可能会阅读安装 Masonite 的文档。

您需要的 Linux 包有

  • python3-dev

  • python3-pip

  • libssl-dev

  • 构建-基本

  • python3-venv

  • 饭桶

当使用之前的apt-get install命令时,您可以通过在每个包之间使用一个空格来同时安装它们:

$ apt-get install python3-dev python3-pip libssl-dev build-essential
python3-venv git

安装完成后,您就拥有了启动和运行 Masonite 所需的一切。

配置 NGINX

我们需要做的下一步是告诉 NGINX 将请求重定向到哪里。为此,我们需要稍微配置 NGINX。幸运的是,我们只需要添加几行配置。

为了找出我们需要在哪里添加这个配置,我们需要检查我们的主 NGINX 配置文件。我们可以通过运行找到这个位置

$ nginx -t

这个-t标志将测试配置文件,但也方便地输出其位置。我们应该会看到这样的结果:

root@{server name}:~# nginx -t nginx: the configuration file
/etc/nginx/nginx.conf syntax is ok nginx: configuration file
/etc/nginx/nginx.conf test is successful

这个/etc/nginx/nginx.conf位置是我们的 NGINX 配置所在的位置,所以我们可以打开它来找到放置我们的几行应用配置的位置。

让我们通过运行以下命令来查看该文件的内容

cat/etc/nginx/nginx.conf

cat 命令将显示文件的内容。如果我们向上滚动一点,我们会看到这样一个部分:

  ##
  # Virtual Host Configs
  ##

  include /etc/nginx/conf.d/∗.conf;
  include /etc/nginx/sites-enabled/∗;

除了所有的配置设置,还有这两行。这些将只是在这些位置附加任何配置文件。因此,我们可以将我们的配置添加到这些目录中,而不是将所有内容添加到这个文件中并拥有一个巨大的配置文件,它们将自动添加到这里,以便 NGINX 读取配置。

现在我们可以转到这个目录,开始构建我们的应用配置文件:

$ cd /etc/nginx/sites-enabled
$ nano example.com.conf

nano命令将为您提供一个基于终端的编辑器,您可以使用它来创建文件。我通常每个域名都有一个 web 应用,所以无论您希望您的域名被称为什么,您都可以输入它而不是“example.com ”,但是您可以随意命名这个文件。

如果我们想关闭编辑器,我们可以按“Ctrl+X”,输入 Y,然后按 Enter。

现在您应该看到一个空白编辑器。让我们开始构建我们的配置文件。我将展示完整的文件,我们将逐行查看:

server {
    listen 80;
    server_name {ip address};

    location / {
        include uwsgi_params;
        uwsgi_pass unix:///srv/sockets/{example.com}.sock;
        proxy_request_buffering off;
        proxy_buffering off;
        proxy_redirect off;
    }
}

为了避免部署过程中的额外步骤,我们将使用套接字。套接字只是一些文件,NGINX 和我们的 WSGI 服务器都可以通过这些文件来获取它们需要的必要信息。这样,我们就不用监听端口,也不需要在部署之间重启应用。这将改善停机时间。

因此,从上到下,我们有所谓的“服务器块”这只是一组配置。请记住,这基本上会包含在主 NGINX 配置文件中,因此需要将它封装在一个块中,以隔离这些设置。

接下来我们看到的是一首port来听。这很可能始终是端口 80,因为默认情况下,所有 web 流量都将在端口 80 上传输。

下一行是一个server_name。如果你有域名,你可以在这里输入。如果没有,你可以简单地把服务器的 IP 地址放在这里。

接下来,我们有另一个块,但这次是一个location块。我们希望所有的流量都指向我们的应用,所以我们将放置一个基本位置/

在这个块中,我们将设置特殊的头,我们的 WSGI 服务器将需要这些头来构建我们的 Python 字典,并将其传递给 Masonite 框架。

接下来是一条uwsgi_pass线。这将把流量流式传输到一个套接字文件,我们的 WSGI 服务器也将从该文件流式传输。所以这是 NGINX 和我们的 WSGI 服务器之间的通信点。这一行实际上以unix://开始,其后的所有内容都是一个目录路径。我们将/srv/sockets/设置为保存所有套接字的目录。

最后,我们有一些代理设置,用于配置连接的一些行为。

配置完成后,我们可以点击“Ctrl+X”关闭编辑器,输入 Y,然后点击 Enter。

重新安置 nginx

您可能还需要重新加载 NGINX,这样我们就可以运行另一个简单的命令:

$ nginx -s reload

测试一切正常

现在,您可以在浏览器中返回到您的 IP 地址,以确保一切正常。如果你把你的 IP 地址放入你的应用配置中,你应该会看到一个网关错误,如图 13-7 所示。

img/484280_1_En_13_Fig7_HTML.jpg

图 13-7

错误的网关错误消息

这是一件好事。这意味着 NGINX 正在尝试与我们的 Masonite 应用(没有正确安装)正确通信。

设置任务

我们放入配置文件中的一些东西还不存在,比如/srv/sockets目录。所以我们现在就能做到:

$ mkdir -p /srv/sockets

我们还需要确保 NGINX 和我们的应用都有权限访问这个目录,这样我们就可以运行另一个简单的命令:

$ chmod 0777 /srv/sockets

设置我们的应用

好了,现在我们终于可以进入正题了——设置我们的 Masonite 应用。

我个人喜欢把所有东西都放在一个目录里,以保持整洁。现在让我们创建目录:

$ mkdir -p /srv/sites

现在我们可以将我们的存储库克隆到这个目录。该示例回购将在 GitHub 上托管,因此我们的链接将如下所示:

$ git clone https://github.com/username/repo.git example
$ cd example

用你的 GitHub 项目的用户名和 repo 替换usernamerepo。如果你没有,你可以使用masoniteframeworkcookie-cutter分别作为用户名和回购。

这将把我们的应用放在一个/srv/sites/example目录中。

运行应用

我们要做的下一件事是安装并运行我们的应用。这部分你已经习惯了,在你的机器上开发和这个服务器之间没有太多的变化。我们只需要再次创建一个虚拟环境,并安装我们的 Python 包:

$ python3 -m venv /venvs/example
$ source /venvs/example/bin/activate
$ pip install -r requirements.txt

我们的 Masonite 应用现在应该完全安装好了,我们现在可以运行它了。

为了运行我们的应用,我们将使用uWSGI。我们现在可以安装uWSGI并运行一个简单的命令来开始:

$ pip install uwsgi $ uwsgi
--socket /srv/sockets/example.com.sock --wsgi-file wsgi.py \
    --chmod-socket=777 --pidfile /srv/sockets/example.com.pid &>
/dev/null &

只要确保这个套接字的位置与您在应用配置文件中放置的套接字的位置相同。我们还使用了一个--chmod-socket命令,它将给予 uWSGI 正确的权限。权限的事情有点棘手,没有它,你会遇到奇怪的问题,看起来好像应用没有运行。您将继续得到 502 错误。

我们有一个--wsgi-file wsgi.py行,它简单地运行所有 Masonite 应用根目录中的wsgi.py文件。这是需要通过 WSGI 服务器运行的 Masonite 应用的入口点。

您还会注意到结尾有一个奇怪的&> /dev/null &语法。这告诉 uwsgi 在后台运行这个命令。这样,我们可以退出服务器或执行其他服务器操作,但应用仍在运行。

你还会注意到我们放了一个--pidfile标志。它的作用是将文件连接到应用的这个实例。问题在未来;我们可以随时通过简单地杀死 PID 文件来杀死它。

如果前面的步骤由于某种原因不起作用,您可以检查 NGINX 的错误日志。错误日志很可能位于/var/log/nginx/error.log处,可以通过运行

$ cat /var/log/nginx/error.log

使用文件的内容开始调试任何问题。

如果路径不存在,您可以在主 nginx 配置文件中找到该路径,如下所示

error_log /var/log/nginx/error.log;

现在,您终于可以在 web 浏览器中最后一次访问服务器了,您将看到您的 Masonite 应用正在运行!

这里需要注意的是,运行服务器并不像将 web 应用放在服务器上并运行它们那么简单。维护服务器包括安全更新、管理部署和文件权限,以及保持第三方服务(如数据库、管理程序等)的运行。您必须管理您的应用的正常运行时间。如果出现任何问题,您将需要 SSH 回到服务器并调试问题所在。

让像 Heroku 这样的第三方服务为你管理这一切可能更明智。启动计划开始于每月几美元的低费用。

部署

在上一节中,我们解释了如何设置服务器和执行部署。您将使用两种类型的部署:手动部署和自动部署。

手动部署是当您 SSH 回到服务器时,终止您的应用的以前运行的实例,然后启动新的实例。

自动部署是指服务在您启动并运行新版本的应用之前执行所有这些步骤。这些操作可能是当一个新的提交被提交到你的主分支或者当你删除一个新的发布时。

手动部署

如果您想要执行手动部署,您将 SSH 回到您的服务器,终止 PID 文件,更新代码库,然后重新运行 uWSGI serve 命令。

例如,当我们第一次启动我们的应用时,我们有一个这样的标志:

--pidfile /srv/sockets/example.com.pid

这将我们正在运行的应用的生命线连接到这个 PID 文件的生命线。杀死 PID 文件也就杀死了应用。您可以通过运行以下命令来终止 PID 文件

uwsgi --stop /srv/sockets/example.com.pid

既然应用已经死了,就不能再访问它了。我们现在可以重新启动应用。

  1. 激活应用的虚拟环境:

    source /venvs/example/bin/activate
    
    
  2. 转到目录并获取新的代码更改(这取决于您想要部署的分支或提交):

    $ cd /srv/sites/example
    $ git pull -f https://github.com/username/repo.git master
    
    
  3. 安装任何新要求:

    $ pip install -r requirements.txt
    
    
  4. 运行uwsgi命令启动“运行应用”一节中描述的应用:

    $ uwsgi --socket /srv/sockets/example.com.sock --wsgi-file wsgi.py \
        --chmod-socket=777 --pidfile /srv/sockets/example.com.pid &> /dev/null &
    
    

自动部署

如果您愿意,也可以使用许多自动部署。有许多服务可以为你做到这一点,这些服务在网上有很好的记录,试图在这本书里复制这些记录是不明智的,但我最喜欢的是 Heroku。这是一个非常简单的服务,通常只需在本地点击很少的终端命令就可以让你的服务启动并运行。

您也可以查看 Masonite 文档,了解获取其他形式的自动部署的其他链接,例如在 GitHub 上执行提交或剪切发行版,这将为您完成本章中的大部分步骤。链接可以在主要的在线文档中找到。