Masonite 权威指南(全)
一、入门指南
通过写这本书,我们希望教你如何使用 Masonite 框架( https://github.com/masoniteframework/masonite
)构建伟大的应用。Masonite 是一个现代的 Python 框架,它包含了旨在简化该任务的工具和约定。
如果你是 Python 的新手,这是可以的。如果你是 Masonite 的新用户,没关系。写这本书是为了让你仍能从中获得最大收益。在这个过程中,可能会有一些更高级的主题,但我们会尽最大努力使它们成为额外的、对您的体验不重要的内容。
如果你从未使用过 Python,我们推荐这个短期课程让你熟悉基础知识: https://teamtreehouse.com/library/python-basics-3
。
你需要的是一台安装 Python 的计算机,一些阅读时间和一些实验时间。它不需要一台花哨的计算机,也不需要大量的时间。
我们已经安排好了章节,这样你就可以了解框架的核心概念,并构建一个功能应用。你可以阅读这本书作为参考指南。你可以把它作为一系列教程来阅读。你可以一次读一章,或者一起读。
感谢您迈出成为 Masonite pro 的第一步。
“我从哪里开始?”
有许多不同种类的编程。你可能最熟悉的是应用编程,这是在手机、平板电脑或电脑上安装应用(或使用已安装的应用)的地方。
仔细想想,你可能还熟悉另一种类型:网站编程。这类程序是通过 Chrome、Safari 或 Firefox 等网络浏览器使用的。
Masonite 介于这两种程序之间。我来解释一下原因。
Python 最初被设计为一种系统编程语言。这意味着它旨在用于简短的服务器管理脚本,以配置其他软件和执行批处理操作。
随着时间的推移,它已经成为一种强大的多范例编程语言。一些编程语言主要用于 web 编程,它们是通过 web 服务器使用的,像 Apache 和 Nginx。其他语言对 web 服务器的行为有更全面的控制。Python 是后一种语言之一。
Python web 应用,尤其是 Masonite 应用,通常负责从监听端口到解释 HTTP 请求到发送 HTTP 响应的所有事情。如果 Masonite 应用有问题,那么整个服务器也有问题。随着风险的增加,灵活性也随之增加。
此外,控制整个服务器使我们能够做更高级的事情,如服务 web 套接字和与外部设备交互(如打印机和装配线)。
Masonite 如何处理版本
在我们看代码之前,讨论一下 Masonite 如何处理发布是很重要的。像 Masonite 这样的大框架变化很快。你可能在 2.1 版本上开始一个项目,但是几个星期后 2.2 版本就发布了。这可能会导致一些重要的问题:
-
我应该升级吗?
-
升级需要什么?
-
这种情况多久发生一次?
我们来回答这些,一个一个来。
我应该升级吗?
升级是好事,但有时也有取舍。
功能可能被否决,这意味着它被标记为将来删除。您可能需要进行多项更改,以便您的应用能够在新版本中工作。你可能会发现一些你没有测试过的错误或东西。
尽管如此,升级也可以带来新的功能和安全修复。光是安全方面的好处就足以让我们认真对待任何升级。
最好的做法是查看升级指南,并确定升级的成本是否值得它带来的好处。保留几个主要版本没有害处,只要您仍然使用可以接收安全更新的框架的次要版本(并且只要您使用的版本没有明显的安全问题)。
您可以在文档网站上找到最新的升级指南: https://docs.masoniteproject.com/upgrade-guide
。
升级需要什么?
这个问题很容易通过阅读升级指南来回答。如果您落后几个版本,您可能需要阅读多份升级指南以获得最新版本。
Masonite 使用一个三部分版本方案:PARADIGM.MAJOR.MINOR
。
这意味着当从2.1.1
升级到2.1.2
时,你应该几乎没有问题。从2.1
升级到2.2
有点复杂,但是我发现它们通常只需要 10 分钟或者更少,假设我没有偏离框架的惯例太远。
与这种版本化方案相反,每个 Masonite 库都使用语义版本化( https://semver.org
)。如果你使用的是单个的 Masonite 库,而不是整个框架,那么从2.1
升级到2.2
是相当安全的,不会破坏变更。
这种情况多久发生一次?
Masonite 遵循 6 个月的发布周期。这意味着你可以每 6 个月期待一个新的MAJOR
版本。这些版本旨在要求不到 30 分钟的升级。
如果他们被期望接受更多,他们会被转移到一个新的版本。
安装依赖项
Masonite 需要一些依赖项才能正常工作。当我们安装它们的时候,我们可能还会讨论一下如何最好地编写 Python 代码。首先,让我们安装 Python。
在 macOS 上安装 Python 和 MySQL
我在苹果电脑上工作,所以我想从这里开始。macOS 没有命令行包管理器(就像你在 Linux 中期望的那样),所以我们需要安装一个。
在本节中,我们假设您安装了最新版本的 macOS,并且可以访问互联网。
打开 Safari,进入 https://brew.sh
。这是家酿啤酒的故乡。这是一个很棒的包管理器,它将为我们提供安装 Python 3 和数据库的方法。
有一个命令,前面和中心。它应该看起来像这样
/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/ install/master/install)"
它说从那个 URL 下载一个 Ruby 脚本,用大多数 macOS 系统已经安装的 Ruby 解释器执行它。如果你觉得特别可疑,请随意在 Safari 中打开该 URL 并检查其内容。
对那些告诉你盲目执行来自互联网的脚本的网站保持怀疑是有好处的。在这种情况下,家酿啤酒在安全性和实用性方面享有盛誉。他们正在权衡安装的便利性和潜在的怀疑。
如果您仍然认为这样做风险太大,请查看本节的末尾,在那里我推荐了一个关于设置新 Python 环境的更深入的参考资料。
在终端中运行此命令,将开始安装 Homebrew。这需要一点时间,并且会在过程中提出问题。其中一个问题是你是否想安装 Xcode 命令行工具。
如果你想使用自制软件,你没有选择的余地。这些实用程序包括用于代码自制下载的编译器,所以没有它们就无法安装太多。
当安装完成时,您应该能够开始通过 Homebrew 安装依赖项。我们感兴趣的是 Python 3 和 MySQL 5.7。让我们安装它们:
$ brew install python3
$ brew install [email protected]
安装 MySQL 后,您会得到一些启动服务器的说明。我建议您遵循这些,否则您将无法登录或更改数据库。
您可以通过运行以下命令来验证 Python 和 MySQL 的版本
$ python --version
$ mysql --version
您应该看到安装了Python 3.x.x
和mysql ... 5.7.x
。
如果这个命令告诉你你还在使用Python 2.x
,那么你可能需要把路径添加到你的PATH
变量中,这个路径是在python3
安装结束时建议的。我的看起来像这样:
export PATH="/usr/local/opt/python/libexec/bin:$PATH"
这是从~/.zshrc
开始的,但是你应该把它放在~/.profile
或者~/.bashrc
,这取决于你的系统是如何设置的。
在新的终端窗口中运行--version
命令,您应该会看到Python 3.x.x
作为版本。
在 Linux 上安装 Python 和 MySQL
接下来,我们将看看如何在 Debian/Ubuntu Linux 上安装这些依赖项。在这里,我们可以访问命令行包管理器,称为 aptitude。
您可以使用以下命令来安装 Python 和 MySQL:
$ sudo apt update
$ sudo apt install python-dev libssl-dev
$ sudo apt install mysql-server-5.7
如果apt
命令不存在,您可能使用的是稍微旧一点的 Linux 版本,您应该使用apt-get
来代替。
最好启动 MySQL 服务器,否则您将无法登录或更改它:
$ systemctl start mysql
$ systemctl enable mysql
您可以通过运行以下命令来验证 Python 和 MySQL 的版本
$ python --version
$ mysql --version
您应该看到安装了Python 3.x.x
和mysql ... 5.7.x
。
编辑代码
您应该使用您最熟悉的代码编辑器或集成开发环境。我们建议您使用类似 Visual Studio 代码的东西,因为它包含足够有用的自动化工具,但仍然快速且免费。
可以在 https://code.visualstudio.com
下载。
当您打开 Masonite 文件时,您会看到安装代码扩展的提示。这些将为您提供方便的提示,并在您的代码中出现错误时告诉您。我们建议您在出现提示时安装这些软件。
在其他环境中设置
如果您使用的是 macOS 或 Linux,这些说明应该适合您。如果您使用不同版本的 Linux 或 Windows,您可能需要遵循不同的指南在您的系统上安装 Python。
一个很好的地方是查看 Masonite 官方文档: https://docs.masoniteproject.com
。
如果你想重温一下 Python 语言,可以看看 www.apress.com/la/book/9781484200292
。
创建新的 Masonite 应用
Masonite 提供的工具之一是一个全局命令行实用程序,用于帮助创建和维护项目。除了 Python,前面的指令还应该安装了一个名为 Pip 的依赖性管理工具。我们可以使用 Pip 安装 Masonite 的命令行实用程序:
pip install --user masonite-cli
根据您的系统设置,您可能需要使用一个名为pip3
的二进制文件。如果你不确定使用哪个,运行which pip
和which pip3
。这些将提示您二进制文件的安装位置,您可以选择看起来更好的二进制文件。
该命令执行完毕后,您应该可以访问 Masonite 的命令行实用程序craft
。您可以通过检查其版本来验证这一点:
craft --version
现在,是时候创建新的 Masonite 项目了。导航到您希望项目的代码文件夹所在的位置,并运行new
命令:
craft new friday-server
您可以在看到friday-server
的地方替换自己的项目名称。我之所以这样称呼我的名字,是因为一会儿就会明白的原因。
然后应该会提示您导航到新创建的文件夹并运行一个install
命令。让我们这样做:
cd friday-server
craft install
该命令安装 Masonite 需要运行的依赖项。运行这段代码后,您应该会看到一些文本,告诉您“key added to your。env 文件”。
为了确保一切正常,让我们运行serve
命令:
craft serve
这将告诉您应用将在“http://127.0.0.1:8000
”或类似的时间提供服务。在您的浏览器中打开该 URL,您应该会看到如图 1-1 所示的 Masonite 2.1 登录页面。
图 1-1
Masonite 2.1 登录页面
探索 Masonite 文件夹结构
我们将在这个代码库中花费大量时间,因此对文件夹结构的基本理解(如图 1-2 所示)将有助于我们知道在哪里创建新文件和更改现有文件。
图 1-2
Masonite 2.1 文件夹结构
让我们看看这些文件和文件夹的用途,不要涉及太多细节:
-
app
–该文件夹开始保存应用中响应单个 HTTP 请求的部分,并对所有请求和响应应用一揽子规则和限制。当我们为应用添加响应请求的方式时,我们将向这个文件夹添加很多。 -
bootstrap
–此文件夹保存用于启动应用的脚本,并缓存应用运行期间生成的文件。 -
config
–该文件夹保存配置文件,这些文件告诉 Masonite 应用在运行时使用哪些设置。 -
databases
–该文件夹保存数据库配置脚本。与config
文件夹的脚本不同,这些脚本旨在修改现有的数据库,创建和修改表和记录。 -
这个文件夹保存静态文件,比如 HTML 模板。
-
routes
–该文件夹保存将 HTTP 请求映射到app
文件夹中用于处理这些请求的部分的文件。它是我们告诉应用如何从浏览器 URL 获取应用文件的地方。 -
这个文件夹存放更多的静态文件,但通常是我们自己放进去的那种。比如文件上传、Sass 文件和可公开访问的文件(比如
favicon.ico
和robots.txt
)。 -
这个文件夹包含测试脚本,我们将编写这些脚本来确保我们的应用按预期运行。
-
.env
–该文件存储环境变量。这些变量可能会在不同的环境中发生变化,并且通常是秘密值(如服务密钥)。这个文件不应该提交到共享代码存储位置,比如 GitHub。这就是为什么默认的.gitignore
文件特别忽略了.env
。Python 应用中还有其他常见的文件。在适当的时候,我们会谈论这些文件。
当我们构建示例应用时,我们将开始添加文件并更改这些现有的文件。当您看到文件的路径时,您可以假设我们是在谈论相对于基本文件夹的路径。
规划示例应用
有些人发现当他们使用一种工具来构建东西时,学习这种工具更容易。
因此,在本书中,我们将构建一个示例应用。
您不一定要跟着示例走,因为这本书的主要焦点是 Masonite 库的理论和技术用法。这仅仅是对你将要学习的 Masonite 知识的补充,是巩固你所学知识的一种手段。
我喜欢尝试电子产品,这种兴奋感只是在看了《钢铁侠》这样的电影后才有所增长。在《?? 钢铁侠》中,观众认识了一个名叫托尼·斯塔克的人,他建造了充满科技的豪宅,让生活的方方面面实现了自动化。
看完那些电影后,我有一种强烈的冲动想做同样的事情。
当我们计划这本书时,我们试图为一个示例项目想出有趣的主题,于是这个想法出现了。因此,我们将构建我们的示例项目,目标是自动化我们生活的一部分。
我们将从简单的任务开始,如实现播客和媒体中心管理,然后继续更大的事情,如获得最新的天气预报和自动回复电子邮件。
如果我们有时间,我们甚至会钻研电子世界,将设备连接到我们的代码,并让它们为我们执行物理任务。
跟随电影的潮流,我想把我的家庭自动化和个人助理称为星期五。我对这个示例应用的潜力感到兴奋不已,我希望它能像我们希望的那样激励您的学习。
摘要
在本章中,我们迈出了学习 Masonite 的第一步。我们安装了一些工具并创建了一个新的应用。在本书的其余部分,我们将继续构建这个应用。
我们还讨论了这个示例应用的主题。随着我们的继续,您可以随意在示例中添加自己的设计和风格。它旨在让您对使用 Masonite 保持兴趣,并希望在本书结束时成为对您有用的东西。
二、路由
在前一章中,我们已经做好了开始构建 Masonite 应用的准备。我们安装了 Python、数据库和 craft 命令行实用程序。
在这一章中,我们将学习将浏览器 URL 连接到处理每个请求的应用代码的过程。我们将了解 Masonite 可以解释的不同类型的请求,并且我们将开始为我们的示例应用构建功能。
" Masonite 如何处理请求?"
互联网是围绕着请求/响应循环的思想而建立的。每当您打开浏览器并输入 URL 时,都会发生同样的事情:
-
你的浏览器将你输入的地址(如
www.apress.com
)与一个 IP 地址连接起来。IP 地址有两种形式:IPv4 和 IPv6。这两种类型的地址都是为了将不同的机器连接在一起,但人类不容易处理。称为域名服务器(或简称 DNS)的东西有查找表,它接受人类可读的域名,并将 IPv4 或 IPv6 地址返回给浏览器。
-
浏览器在 IPv4 或 IPv6 地址的末尾(通常在端口 80 或端口 443)向服务器发出请求。在 DNS 解析后,对
www.apress.com
的请求将导致浏览器向151.101.172.250:443
发送请求(当您尝试时,地址可能会有所不同,因为服务器可以更改它们的 IP 地址)。 -
然后,服务器有机会解释请求并做出相应的响应。大多数情况下,响应是一个文本体(可以包含 HTML)和一些描述服务器和响应体的头。
这是 Masonite 接管的第三步。Masonite 应用监听端口 80 和端口 443,除非另外配置,并被给予 HTTP 请求来解释。
HTTP 是超文本传输协议的意思,它描述了发出请求和发送响应的格式。我忽略了大量的细节,因为这对我们学习 Masonite 来说并不重要。如果你想看完整的规格,你可以在 https://tools.ietf.org/html/rfc2616
找到。
Masonite 接收一个 HTTP 请求,对其执行一些初始格式化,并将该请求传递给路由处理程序。为了让我们响应特定的请求,我们需要创建路由处理程序和相应的控制器。
创建控制器和路由
此代码可在 https://github.com/assertchris/friday-server/tree/chapter-2
找到。
在 Masonite 中,我们认为路由是应用的第一个入口点,但是在创建新路由之前,我们必须创建新的控制器。
craft 命令具有内置功能,可以帮助我们轻松创建新的控制器。在我们的项目文件夹中,我们可以使用以下命令:
craft controller Home
这将在app/http/controllers
文件夹中创建一个名为HomeController.py
的文件。控制器是 HTTP 请求和响应之间的粘合剂。我们刚刚做的这个看起来像这样:
"""A HomeController Module."""
from masonite.request import Request
from masonite.view import View
class HomeController:
"""HomeController Controller Class."""
def __init__ (self, request: Request):
"""HomeController Initializer
Arguments:
request {masonite.request.Request}...
"""
self.request = request
def show(self, view: View):
pass
这是来自app/http/controllers/HomeController.py
。
控制器是普通的 Python 类。它们的强大之处在于它们是使用依赖注入容器创建和调用的。我们将在第三章深入探讨这意味着什么。
现在,你需要知道的是你看到的Request
和View
对象,会自动提供。我们不需要创建这个控制器的新实例,也不需要用这些对象来填充它,就可以让它正常工作。
大多数控制器代码都是文档。为了简洁起见,我们将尽可能多地省略这类文档。您将在您的文件中看到它(它仍然在我们的文件中),但是我们不会在代码清单中重复它。
现在我们已经制作了一个控制器,我们可以将它连接到一个路由。如果你打开routes/web.py
,你会注意到它已经有了一个定义好的路由。您可能还注意到了现有的控制器。暂时忘掉这些吧。让我们添加自己的路由,以响应在/home
的GET
请求:
from masonite.routes import Get, Post
ROUTES = [
# ...
Get().route('/home', 'HomeController@show').name('home'),
]
这是来自routes/web.py
。
这应该够了吧?让我们启动服务器:
craft serve
旧版本的 Masonite 需要一个-r
标志,以使服务器在每次看到文件更改时重新启动。如果您的更新没有显示在浏览器中,请检查控制台,确保服务器在每次文件更改时都重新加载。如果你没有看到任何活动,你可能需要这个标志。
当我们在浏览器中打开服务器时(在http://127.0.0.1:8000/home
,我们看到如图 2-1 所示的屏幕。
图 2-1
哎呀!一个错误
那不可能是正常的,不是吗?好吧,让我们回到控制器代码:
from masonite.request import Request
from masonite.view import View
class HomeController:
def __init__ (self, request: Request):
self.request = request
def show(self, view: View):
pass
这是来自app/http/controllers/HomeController.py
。
我们的路由告诉 Masonite 使用show
方法,但是show
方法刚好通过。为了让路由工作,它们需要返回一些东西。错误消息告诉我们这一点,尽管是以一种迂回的方式:“响应的类型不能是:None。”
修复出奇的简单。我们只需要从show
方法中返回一些东西。简单的字符串就可以了:
def show(self, view: View):
return 'hello world'
这是来自app/http/controllers/HomeController.py
。
图 2-2
从show
返回一个字符串
成功!这可能看起来不多,但这是构建功能性应用的第一步。
让我们回顾一下到目前为止发生了什么:
-
我们打开了一个浏览器
http://127.0.0.1:8000/home
。浏览器创建了一个 HTTP 请求并将其发送到该地址。 -
Masonite 服务器从
craft serve -r
开始,监听端口 80,接收 HTTP 请求。 -
Masonite 服务器使用
GET
请求方法寻找匹配/home
的路由。它找到一个匹配,并查看使用哪个控制器和方法。 -
Masonite 服务器获取主要的请求和视图对象,实例化控制器,并将这些对象发送给控制器和
show
方法。 -
我们告诉控制器为该类型的请求返回一个字符串,它确实这样做了。该字符串被格式化为 HTTP 响应并发送回浏览器。
-
浏览器显示了 HTTP 响应。
您创建的每条路由都将与控制器中的一个方法相连接,或者直接连接到一个响应文件。你需要经常遵循这个过程,所以现在掌握它是很重要的。
这只是一条普通的GET
路由,但是我们可以使用许多不同种类的路由和变体。
创建不同种类的路由
我们已经掩盖了这一点,但是 HTTP 请求可以有不同的方面来区分它们。我们已经看到了GET
请求的样子——当你在浏览器中输入地址时发生的那种请求。
不同的方法
还有其他一些方法:
-
当您在浏览器中提交表单时,通常会出现这种请求。它们用于表示正在传送的对象应该在服务器上创建。
-
PATCH
、PUT
——这类请求通常不会出现在浏览器中,但它们有特殊的含义,操作类似于POST
请求。它们分别用于表示被传送的对象应该被部分改变或覆盖。 -
DELETE
–这些类型的请求通常也不会在浏览器中发生,但是它们的操作类似于GET
请求。它们用于表示正在传送的对象应该从服务器上移除。 -
HEAD
–这类请求确实发生在浏览器中,但它们更多的是关于被传送对象的元数据,而不是对象本身。HEAD
请求是检查有问题的对象以及浏览器是否有权限对其进行操作的方法。
使用这些请求方法,对同一路径的请求(如/room
)可能意味着不同的事情。一个GET
请求可能意味着浏览器或使用它的人想要查看关于一个特定房间的信息。
一个POST
、PATCH
或PUT
请求可以指示用户想要创建或改变一个房间,指定创建或改变它的属性。
DELETE
请求可以指示用户想要从系统中移除房间。
不同参数
路由(和请求)也可以有不同种类的参数。第一个,也是最容易想到的,是作为 URL 一部分的那种参数。
你知道当你看到博客文章,有类似 https://assertchris.io/post/2019-02-11-making-a-development-app
的网址..?URL 的最后一部分是一个参数,可以是硬编码的,也可以是动态的,这取决于应用。
我们可以通过改变路由的外观来定义这些类型的参数:
from masonite.routes import Get, Post
ROUTES = [
# ...
Get().route('/home/@name', 'HomeController@show')
.name('home'),
]
这是来自routes/web.py
。
注意我们是如何将/@name
添加到路由中的?这意味着我们可以使用像/home/ chris
这样的 URL,并且chris
将被映射到@id
。我们可以在控制器中访问这些参数:
def __init__ (self, request: Request):
self.request = request
def show(self, view: View):
return 'hello ' + self.request.param('name')
这是来自app/http/controllers/HomeController.py
。
__ init__
方法(或构造函数)接受一个Request
对象,我们可以在show
方法中访问它。我们可以调用param
方法来获取命名的 URL 参数,这是我们在路由中定义的。
因为我们只有show
方法,而所有 __ init__
所做的就是存储Request
对象,我们可以缩短这段代码:
from masonite.request import Request
from masonite.view import View
class HomeController:
def show(self, view: View, request: Request):
return 'hello ' + request.param('name')
这是来自app/http/controllers/HomeController.py
。
和以前一样,这是可行的,因为控制器方法是在从依赖注入容器中解析了它们的依赖关系之后被调用的。
如果你在一个方法中使用一个依赖项,你应该在同一个方法中接受那个参数。如果您多次重用它,那么在构造函数中接受依赖关系会更快一些。
参数化请求的另一种方法是允许查询字符串参数。这是当一个 URL 被请求时,但是以类似于?name=chris
的语法结束。让我们使路由的@name
部分可选,并允许它作为查询字符串参数给出:
from masonite.routes import Get, Post
ROUTES = [
# ...
Get().route('/home/@name', 'HomeController@show')
.name('home-with-name'), Get().route('/home', 'HomeController@show')
.name('home-without-name'),
]
这是来自routes/web.py
。
使参数成为可选参数的最快、最简单的方法是定义不需要提供参数的第二条路径。然后,我们必须修改控制器,使其同时适用于这两种情况:
from masonite.request import Request
from masonite.view import View
class HomeController:
def show(self, view: View, request: Request):
return 'hello ' + (
request.param('name') or request.input('name')
)
这是来自app/http/controllers/HomeController.py
。
我们可以在Request
对象上使用input
方法访问查询字符串参数。想知道这段代码最棒的部分吗?如果我们想响应POST
、PATCH
或PUT
的请求,我们不需要修改任何控制器代码。
我们可以修改/home
路由以接受GET
和POST
请求:
from masonite.routes import Get, Post, Match
ROUTES = [
# ...
Match(['GET', 'POST'], '/home/@name',
'HomeController@show').name('home-with-name'),
Match(['GET', 'POST'], '/home',
'HomeController@show').name('home-without-name'),
]
这是来自routes/web.py
。
在 CSRF 中间件中,我们必须允许对这些 URL 的不安全 POST 请求:
from masonite.middleware import CsrfMiddleware as Middleware
class CsrfMiddleware(Middleware):
exempt = [
'/home',
'/home/@name',
]
every_request = False
token_length = 30
这是来自app/http/middlware/CsrfMiddleware.py
。
我们将在第八章中学习中间件,在第四章中学习 CSRF 保护。现在,知道POST
请求来自应用外部时通常会被阻止就足够了。
浏览器请求应该继续工作,但是现在我们也可以向这些端点发出POST
请求。最简单的测试方法是安装一个名为 Postman 的应用。以下是测试的步骤:
-
前往
www.getpostman.com
下载安装 app。当你打开应用时,你需要创建一个免费帐户,除非你以前使用过 Postman。 -
将方法下拉菜单从
Get
更改为Post
,并输入网址httsp:// 127.0.0.1:8000/home
。 -
将数据选项卡从
Params
更改为Body
,并输入name
(键)=chris
(值)。 -
Click
Send
.图 2-3
向服务器发送 POST 请求
如果GET
或POST
请求给你一个错误,比如“只能连接 str(不是“bool”)到 str”,这可能是因为你既没有提供路由参数,也没有提供查询字符串/post 主体名称。
路由组
有时,您希望将多条路由配置为相似的名称,或者以相似的方式运行。我们可以通过将/home
路由组合在一起来简化它们:
from masonite.routes import Get, Match, RouteGroup
ROUTES = [
# ...
RouteGroup(
[
Match(['GET', 'POST'], '/@name',
'HomeController@show').name('with-name'),
Match(['GET', 'POST'], '/',
'HomeController@show').name('without-name'),
],
prefix='/home',
name='home-',
)
]
这是来自routes/web.py
。
如果我们使用RouteGroup
而不是Match
或Get
,我们可以定义公共路径和名称前缀。这节省了大量的输入,并且更容易看到有共同点的路由。
RouteGroup
还有一些更高级的方面,但它们最好留在适当解释它们的章节中。注意第八章的中的中间件和第十三章的中的域(部署)。
探索请求和响应
当我们在控制器中时,让我们更详细地看一下请求和响应类。我们已经使用了几个请求方法,但是还有更多的要看。
我们已经看到了如何请求单个指定的输入,但是还有一种方法可以获得请求的所有输入:
request.all()
这将返回一个输入字典。对于HEAD
、GET
和DELETE
方法,这可能意味着查询字符串值。对于POST
、PATCH
和PUT
方法,这可能意味着请求主体数据。
后一种方法可以将它们的主体数据作为 URL 编码的值发送,甚至作为 JSON 数据发送。
我说这“可能意味着”是因为后面的方法也可能有查询字符串值。虽然这在大多数设置中是允许的,但它违反了 HTTP 规范。当您设计应用使用后一种方法时,您应该尽量避免混合查询字符串和主体数据。
request.all()
非常有用,在你不确定你到底想要什么数据的情况下。这种方法有多种变体,变得更加具体:
request.only('name')
request.without('surname')
这些方法分别限制返回的字典项和排除指定的字典项。
如果您不确定您期望的输入是什么,但是您想知道某些键是否存在,那么您可以使用另一种方法:
request.has('name')
request.has()
根据指定的键是否被定义,返回True
或False
。例如,您可以根据某些数据位的存在来改变 route 方法的行为。如果您检测到特定于某个用户的数据,您可能希望更新该用户的帐户详细信息。或者,如果您在他们提交的表单中找到相关数据,您可能需要重置他们的密码。由你决定。
读写 Cookies
我们记住用户并存储与其会话相关的数据的方法之一是通过设置 cookies。这些可以在浏览器中设置和读取,因此认识到 Masonite 默认设置可以防止这种情况的发生是很重要的。
可以使用以下方法设置 Cookies:
request.cookie('accepts-cookies', 'yes')
除非我们也禁用仅 HTTP 和服务器端加密,否则我们将无法使用 JavaScript 读取这些内容:
request.cookie(
'accepts-cookies',
'yes',
encrypt=False,
http_only=False,
expires='5 minutes',
)
这段代码还演示了如何设置 cookies 的过期时间。默认情况下,它们将在用户关闭浏览器时过期,因此任何长期或持久数据都必须设置该值。
可以用几种方式阅读 Cookies。第一种是通过指定一个键:
request.get_cookie('accepts-cookies', decrypt = False)
如果你设置Encrypt
为False
,那么你需要设置Decrypt
为False
。否则Decrypt
的论点可能会被省略。如果 Masonite 试图解密一个 cookie,但失败了,那么它将删除该 cookie。这是针对 cookie 篡改的安全预防措施。
如果您想手动删除 cookie,可以使用以下方法:
request.delete_cookie('accepts-cookies')
发送其他类型的响应
到目前为止,我们只向浏览器发回了普通字符串。我们可以发送无数的其他响应,从 JSON 响应开始:
return response.json({'name': 'chris'})
这种响应将在响应后附加适当的内容类型和长度头。通过返回字典,我们可以使它更短:
return {'name': 'chris'}
正是这种魔力让我如此享受 Masonite!当我们返回普通字符串时,也有类似的事情发生,但这是我们第一次深入了解发生了什么。
现在,假设我们想要重定向用户,而不是向浏览器返回一些可呈现的响应。为此,我们可以使用redirect
方法:
return response.redirect('/home/joe')
这本身并不太灵活。然而,我们可以使用一个类似命名的Request
方法,重定向到一个命名的路由:
return request.redirect_to(
'home-with-name',
{'name': 'chris'},
)
这是我建议你总是给你的路由命名的主要原因之一。如果您稍后想要更改路由的路径,则引用命名路由的所有代码将继续运行,无需修改。使用一个命名的路径通常比重建或硬编码你需要的 URL 更快。
创建视图
我想谈的最后一种回应是涉及 HTML 的那种。如果我们对构建丰富的 UI 感兴趣,普通的字符串是不够的。我们需要一种方法来构造更复杂的模板,这样我们就可以显示动态的和风格化的界面元素。
让我们看看如果让/home
路由显示动态 HTML 会是什么样子。第一步是创建布局文件:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
</head>
<body>
@block content
<!-- template content will be put here-->
@endblock
</body>
</html>
这是来自resources/templates/layout.html
。
构建适合布局的模板是个好主意,这样就可以在一个地方应用全局更改。我们很快就会看到这一点。现在,让我们创建一个主页模板:
@extends 'layout.html'
@block content
<h1>hello {{ name }}</h1>
@endblock
这是来自resources/templates/home.html
。
注意这个模板需要重复的地方有多少,因为我们扩展了layout.html
模板。此路径相对于 templates 文件夹。“外部”模板中定义的块可以被“内部”模板覆盖。这意味着我们可以定义默认内容,“内部”模板可以用更具体的内容来替换。
Masonite 视图使用 Jinja2 模板语法的超集,可以在 http://jinja.pocoo.org/docs
找到。一个重要的区别是 Masonite 模板可以使用@extends
语法来代替{%extends ...%}
语法。
为了使用这些模板,我们需要在控制器中做一些改动。首先,我们使用动态数据,以{{ name }}
的形式。这些数据需要传递到视图中。其次,我们需要指定加载哪个视图模板。
下面是这段代码的样子:
def show(self, view: View, request: Request):
return view.render('home', {
'name': request.param('name') or request.input('name')
})
这是来自app/http/controllers/HomeController.py
。
我们通过定义一个动态数据字典将name
数据传递给视图。
关于 Jinja2 语法以及 Masonite 如何扩展它,还有很多东西需要学习。在我们构建示例应用时,我们将进一步探索它。
启动示例应用
在开始之前,我想强调的是,示例应用对于您的学习来说是完全可选的。每一章的示例代码都可以在 GitHub 上找到,所以你不用重新输入任何东西。
也就是说,我们强烈建议您至少跟随示例应用的开发。我们相信,如果你看到你所学的东西融入到真实的东西中,你会更容易记住它。如果你自己建造一些真实的东西,就更是如此。
此代码可在 https://github.com/assertchris/friday-server/tree/between-chapters-2-and-3
找到。
我听很多播客,所以我想让 Friday(我的个人助理和家庭自动化软件)按需组织和播放播客。星期五将开始她作为一个美化的播客应用的生活。
让我们从创建一个搜索新播客的页面开始。我们需要一个新的控制器和模板:
craft controller Podcast
craft view podcasts/search
这个新的控制器和我们创建的HomeController
一模一样,除了名字。我们应该重命名show
方法,以便它更准确地反映我们想要显示的内容:
from masonite.view import View
class PodcastController:
def show_search(self, view: View):
return view.render('podcasts.search')
这是来自app/http/controllers/PodcastController.py
。
这个新视图只是一个空文件,但是它位于正确的位置。让我们给它一些标记,这样我们就可以知道它是否被正确地呈现了:
@extends 'layout.html'
@block content
<h1>Podcast search</h1>
@endblock
这是来自resources/templates/podcasts/search.html
。
在此之前,我们需要添加一条路由。我们可以从一个RouteGroup
开始,因为我们希望添加更多具有相似名称和前缀的路由。
from masonite.routes import Get, Match, RouteGroup
ROUTES = [
# ...
RouteGroup(
[
Get().route('/', 'PodcastController@show_search')
.name('-show-search')
],
prefix='/podcasts',
name='podcasts',
),
]
这是来自routes/web.py
。
如果你正在运行craft serve -r
命令,你只需要在浏览器中进入/podcasts
就可以看到这个新页面。看起来有点丑,所以我觉得应该开始应用一些风格了。让我们使用一个叫做顺风( https://tailwindcss.com
)的工具,因为它很容易上手:
npm init -y
npm install tailwindcss --save-dev
这将添加两个新文件和一个新文件夹。您可以将文件提交给 Git,但是我建议将文件夹(即node_modules
)添加到您的.gitignore
文件中。你可以通过运行npm install
来重新创建它。
Masonite 为我们的应用提供了一种构建 Sass ( https://sass-lang.com
)的简单方法。我们可以将以下链接添加到布局文件中:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<link href="/static/style.css" rel="stylesheet" type="text/css">
</head>
<body>
@block content
<!-- template content will be put here-->
@endblock
</body>
</html>
这是来自resources/templates/layout.html
。
这个/static/style.css
文件不存在,但那是因为它被重定向到了storage/compiled/style.css
。这个文件是由我们放入storage/static/sass/style.css
的内容生成的。我们可以向该文件添加新的样式,并看到它们在我们的应用中得到反映:
@import "node_modules/tailwindcss/dist/base";
@import "node_modules/tailwindcss/dist/components";
@import "node_modules/tailwindcss/dist/utilities";
h1 {
@extend .text-xl;
@extend .font-normal;
@extend .text-red-500;
}
input {
@extend .outline-none;
@extend .focus\:shadow-md;
@extend .px-2;
@extend .py-1;
@extend .border-b-2;
@extend .border-red-500;
@extend .bg-transparent;
&[type="button"], &[type="submit"] {
@extend .bg-red-500;
@extend .text-white;
}
}
这是来自storage/static/sass/style.scss
。
这只有在我们使用pip install libsass
或pip3 install libsass
安装了 Sass 库的情况下才有效。您也可能看不到仅通过刷新页面所做的更改。如果您看不到更改,请重新启动服务器并清除浏览器缓存。
关于 Tailwind 我不想说太多细节,除了说它是一个基于实用工具的 CSS 框架。这意味着样式是通过给元素类(内联)来应用的,或者像我们用这些h1
和input
选择器所做的那样提取类。
让我们重新定位内容,使其位于页面中间:
<!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 py-4">
@block content
<!-- template content will be put here-->
@endblock
</div>
</body>
</html>
这是来自resources/templates/layout.html
。
让我们添加一个搜索表单和一些虚拟结果:
@extends 'layout.html'
@block content
<h1 class="pb-2">Podcast search</h1>
<form class="pb-2">
<label for="terms" class="hidden">Terms:</label>
<input type="search" name="terms" id="terms" />
<input type="submit" value="search" />
</form>
<div class="flex flex-row flex-wrap">
<div class="w-full md:w-2/5 mr-2 flex flex-row pb-2">
<div class="min-h-full w-48 bg-red-300"></div>
<div class="p-4 flex flex-col flex-grow">
<div class="mb-8 flex flex-col flex-grow">
<div class="text-xl mb-2">Title</div>
<p class="text-base">Description</p>
</div>
<div class="flex flex-grow items-center">
<div class="w-10 h-10 bg-red-300"></div>
<div class="text-sm ml-4">
<p class="leading-none">Author</p>
<p class="">date</p>
</div>
</div>
</div>
</div>
<div class="w-full md:w-2/5 mr-2 flex flex-row pb-2">
<div class="min-h-full w-48 bg-red-300"></div>
<div class="p-4 flex flex-col flex-grow">
<div class="mb-8 flex flex-col flex-grow">
<div class="text-xl mb-2">Title</div>
<p class="text-base">Description</p>
</div>
<div class="flex flex-grow items-center">
<div class="w-10 h-10 bg-red-300"></div>
<div class="text-sm ml-4">
<p class="leading-none">Author</p>
<p class="">date</p>
</div>
</div>
</div>
</div>
<div class="w-full md:w-2/5 mr-2 flex flex-row pb-2">
<div class="min-h-full w-48 bg-red-300"></div>
<div class="p-4 flex flex-col flex-grow">
<div class="mb-8 flex flex-col flex-grow">
<div class="text-xl mb-2">Title</div>
<p class="text-base">Description</p>
</div>
<div class="flex flex-grow items-center">
<div class="w-10 h-10 bg-red-300"></div>
<div class="text-sm ml-4">
<p class="leading-none">Author</p>
<p class="">date</p>
</div>
</div>
</div>
</div>
</div>
@endblock
这是来自resources/templates/podcasts/search.html
。
图 2-4
播客搜索表单和结果
摘要
在本章中,我们学习了控制器、路由和视图。我们在应用中创建了多个入口点,接受了多种请求方法,并用简单和复杂的响应进行响应。
我们还开始开发我们个人助理应用,启动并运行 Sass,并开始将样式应用于定制标记。
在下一章,我们将学习 Masonite 提供的一些更高级的工具,从依赖注入容器开始。
三、服务容器
Masonite 是围绕所谓的“服务容器”构建的但是不要让这种措辞迷惑你。一个服务容器就是一组…服务,确切地说!这个上下文中的服务只是功能。把服务容器想象成一个工具箱,把服务想象成你的工具,把 Masonite 想象成你的工作室。一个服务可以小到一个用于发送邮件的Mail
类,或者一个用于向 RabbitMQ 这样的消息代理发送作业的Queue
类。服务甚至可以变得更高级,比如路由引擎将 URL 映射到给定的控制器。
所有这些服务都被加载(绑定)到服务容器中,然后我们在以后获取服务。稍后我会详细解释为什么这很重要。
服务容器的真正好处是它为您处理应用依赖性。举个例子,你不得不:
-
导入对象。
-
初始化对象。
-
将一些数据传递给 setter 方法。
-
最后调用对象方法。
-
在几个文件中做同样的事情。
Masonite 的服务容器也称为 IoC 容器。服务容器和 IoC 容器将互换使用。IoC 代表控制反转。控制反转仅仅意味着对象的常规控制被翻转。正常情况下,一个对象负责
-
寻找对象
-
实例化对象
使用 Masonite 的 IoC 容器,所有对象都
-
实例化
-
传递给对象
看到控制反转了吗?构建包装在服务容器周围的应用的好处实际上非常简单。容器有两个主要的好处。
第一个好处是,它允许您在应用启动(如启动服务器)之初将所有服务(对象)加载到容器中,然后在整个应用中使用。这样就不需要在多个地方实例化一个类。它还允许您在以后将该类与任何其他类交换。也许你不喜欢你正在使用的日志类,所以你把它换成另一个实现。
第二个好处是,它允许您将大多数相互依赖的类连接在一起。例如,如果一个Logger
类需要Request
和Mail
类,Masonite 会把它们连接在一起,给你一个已经完成并初始化好的类供你使用。没有必要将所有的应用依赖项连接在一起。这对可维护的代码库来说是无价的。
让我们开始更多地了解容器。
我们正在解决的问题
以此为例。我们有两节非常简单的课。
第一个类从请求对象发送一封简单的电子邮件,并记录一条消息,说明邮件已发送:
from some.package import SMTPMail, BaseMail
class Mail(BaseMail):
def __init__ (self, request, logger):
self.request = request
self.logger = logger
def send(self, message):
self.to(self.request.input('email')).smtp(message)
很简单,对吧?我们可以像这样在控制器方法中使用它:
from masonite.request import Request
from app.mail import Mail
from app.logger import Logger
class MailController:
def show(self, request: Request):
logger = Logger(level='warning', dir='/logs')
mail = Mail(request, logger)
mail.send('Email has been sent!')
这段代码看起来不错,但是请注意,我们必须设置一个名为 logger 的新对象,以便将信息传递给 mail 类。想象一下,我们必须在十个不同的文件中使用这个类。可能有 20 个其他对象不得不使用这个Logger
类。我们真的要每次都把它导入到文件中,初始化它,然后传入吗?
类型提示
现在请注意,我们在前面的方法签名中有一行,如下所示:
def show(self, request: Request):
这被称为“类型提示”,它是我们主要如何与服务容器交互的基础。
类型提示是告诉参数它应该是哪种类型的艺术。我们可以告诉一个参数是一个Request
类还是一个Logger
类。
在 Masonite 方面,Masonite 会说“哦,这个参数想要成为一个 Logger 类。我已经知道了那个 logger 类,所以我将强制那个参数成为我已经知道的同一个对象。”
类型提示在语义上是这样写的:
from some.package import Logger
def function(logger: Logger):
pass
语法是{variable}: Class
。
变量可以用你喜欢的名字命名。例如,签名可以用以下任何一种方式书写:
def function(log: Logger):
def function(logging: Logger):
def function(l: Logger):
变量仅仅是一个变量。随便你怎么命名。
在调用对象之前,Masonite 会在代码库中的几个地方检查对象。这些地方包括控制器、中间件和队列作业方法。这些只是 Masonite 为您解析的地方,但是您也可以总是解析您自己的类。
服务提供商
现在你可能想知道 Masonite 是怎么知道提供哪个类的?我申请了Logger
班,我得到了Logger
班。那么 Masonite 如何知道提供哪个类呢?
这都是由 Masonite 所谓的“服务提供商”来完成的
服务提供者是用于将服务注入容器的简单类。它们是构成 Masonite 应用的构建块。Masonite 检查它的服务提供者列表,并使用它来引导应用。Masonite 实际上主要由这些服务提供商组成。
这是服务提供商列表的一个示例:
from masonite.providers import AppProvider, SessionProvider, ...
PROVIDERS = [
# Framework Providers
AppProvider,
SessionProvider,
RouteProvider,
StatusCodeProvider,
WhitenoiseProvider,
ViewProvider, HelpersProvider,
]
这是 Masonite 应用核心的简单流程:
-
WSGI 服务器(像 Gunicorn 一样)首先启动。
-
Masonite 遍历服务提供者列表,并对所有服务提供者运行
register
方法。 -
Masonite 在所有服务提供者上运行所有的方法。这个
wsgi = False
属性只是告诉 Masonite,我们不需要运行 WSGI 服务器来引导应用的这一部分。如果是wsgi = True
,那么 Masonite 将对每个请求运行boot
方法。如果我们有一个服务提供者在容器中加载了一个Mail
服务,那么它不需要对每个请求都运行。 -
然后,Masonite 将监听特定端口上的任何请求。
-
当请求到达服务器时(比如主页),Masonite 将只在不存在
wsgi = True
或属性的服务提供者上运行boot
方法(默认情况下是True
)。这些是需要运行的提供者,比如将请求 URL 映射到路由和控制器,或者将 WSGI 环境加载到请求对象中。
通过前面的要点可以看出,Masonite 完全依赖于这个服务容器。如果您需要交换 Masonite 的功能,那么您可以交换服务容器。
在流程中,您将构建执行特定服务(如日志记录)的类,然后使用服务提供者将其放入任何 Masonite 应用中。
一个简单的服务提供者应该是这样的:
from masonite.providers import ServiceProvider
class SomeServiceProvider(ServiceProvider):
def register(self):
pass
def boot(self):
pass
注册方法
让我们进一步分解服务提供者,因为如果您知道这是如何工作的,那么您就可以在您的 Masonite 应用中编写非常易于维护的代码。
register
方法首先在所有服务提供者上运行,并且是将您的类bind
到容器中的最佳位置(稍后将详细介绍绑定)。永远不要试图从 register 方法内部的容器中获取任何东西。这应该只用于将类和对象放入。
我们可以使用bind
方法注册类和对象。绑定是将对象放入容器的概念。
from some.package import Logger
..
def register(self):
self.app.bind('Logger', Logger(level='warning', dir='/logs'))
我们还希望 Masonite 为我们的新Mail
类管理应用依赖性:
from some.package import Logger, Mail
..
def register(self):
self.app.bind('Logger', Logger(level='warning', dir='/logs'))
self.app.bind('Mail', Mail)
这些类现在已经被放到容器中了。现在我们可以将它“类型提示”到我们的邮件类中。
from some.package import Logger
class Mail:
def __init__ (self, logger: Logger):
self.logger = logger
现在,当 Masonite 试图构造这个类时,它会初始化这个类,但是会说“嘿,我看到你想要一个Logger
类。嗯,我已经有了那个日志类。让我给你一个我知道的,已经在我的容器里面设置好的。”
现在,当我们解析这个Mail
类时,它会像这样:
from some.place import Mail
mail = container.resolve(Mail)
mail.logger #== <some.package.Logger x82092jsa>
我们稍后将更多地讨论解决问题,所以不要让这一部分迷惑了你。现在请注意,Masonite 知道的Logger
类被传递到了Mail
类中,因为我们对它进行了类型暗示。
该引导方法
boot 方法是您与容器进行大部分交互的地方。在这里,您可以做一些事情,比如构造类,调整容器中已经存在的类,以及指定容器挂钩。
添加邮件功能的典型服务提供商如下所示:
from some.place import MailSmtpDriver, Mail
class MailProvider(ServiceProvider):
wsgi = False
def register(self):
self.app.bind('MailSmtpDriver', MailSmtpDriver)
self.app.bind('Mail', Mail)
def boot(self, mail: Mail):
self.app.bind('Mail', mail.driver('smtp'))
所以我们在这里做的是,当容器被注册时,我们将一个MailSmtpDriver
以及完整的Mail
类绑定到容器中。然后在所有的提供者都注册之后,我们将Mail
类解析出容器,然后将它绑定回容器,但是将驱动程序设置为smtp
。
这是因为可能会有其他服务提供者将额外的邮件驱动程序注册到容器中,所以我们希望只有在所有东西都被注册之后才与容器进行交互。
WSGI 属性
您会注意到有一个 wsgi 属性被设置为True
或False
。只显示了类的前半部分,看起来像这样:
class MailProvider(ServiceProvider):
wsgi = False
def register(self):
如果这个属性缺失或者设置为True
(默认情况下是True
,那么它将在每个请求上运行。但是我们在这里看到,我们只是添加了一个新的邮件功能,所以我们真的不需要它在每个请求上运行。
几乎所有的服务提供商都不需要对每个请求都运行。需要在每个请求上运行的服务提供者主要是那些对框架本身至关重要的服务提供者,比如“RouteProvider
”,它接收传入的请求并将其映射到正确的路径。
在这些提供程序上,您可能会看到一个重要的参数“wsgi = True
”。此属性将用于指示特定的提供程序应该在每个请求上运行。如果您需要基于用户的 CSRF 令牌运行代码,这可能会在请求之间发生变化,因此您需要将属性设置为True
。您应该会发现,大多数应用级服务提供者只需要将更多的类绑定到服务容器中,因此该属性通常被设置为 False。
另一个在每个请求上运行的提供者是StatusCodeProvider
,它将接受一个错误的请求(例如404
或500
),并在生产过程中显示一个通用视图。
但是现在我们有了一个提供者,它简单地将一些类绑定到容器,我们不需要任何与请求相关的东西,我们可以确保wsgi
是False
。
不这样做的唯一缺点是,它将在请求上花费一些额外的时间来执行实际上不需要执行的代码。
关于绑定的更多信息
谈到bind
方法,有一些重要的事情需要注意。基本上有两种类型的对象可以绑定到容器中,它们是类和初始化的对象。让我们来看看这两种类型的对象是什么。
类与对象行为
类是简单的未初始化对象,所以回顾我们之前的Mail
类,我们有这个例子:
from some.place import Mail
mail = Mail # this is a class
mail = Mail() # this is an uninitialized object
这很重要,因为你可以有几个不同的对象。如果修改一个对象,不会修改另一个对象。以此为例:
from some.place import Mail
mail1 = Mail()
mail2 = Mail()
mail1.to = '[email protected]'
mail2.to #== '' empty
mail2.to = '[email protected]'
因此,由于这种行为,我们可以将一个类绑定到容器中,而不是一个已初始化的对象:
from some.place import Mail
container.bind('Mail', Mail)
现在每次我们解决它,它都会不同,因为它每次都是被构造的:
from some.place import Mail
container.bind('Mail', Mail)
mail1 = container.resolve(Mail)
mail2 = container.resolve(Mail)
mail1.to = '[email protected]'
mail2.to #== '' empty
mail2.to = '[email protected]'
既然我们知道了这种行为,我们也可以将初始化的对象绑定到容器中。不管我们分解多少次,这都是同一个物体。现在请看这个例子:
from some.place import Mail
container.bind('Mail', Mail())
mail1 = container.resolve(Mail)
mail2 = container.resolve(Mail)
mail1.to = '[email protected]'
mail2.to #== '[email protected]'
以这种方式绑定类很有用,因为您可以添加新的服务提供者来为您操作对象。因此,添加服务提供者可能会为您的请求类添加一个完整的基于会话的特性。因为它是同一个对象,所以当我们稍后解析它时,与容器中初始化的类的任何交互都将具有相同的功能。
根据您的用例,这样做的缺点或优点是需要您手动设置您的类,因为我们需要在将它绑定到容器之前构建完整的对象。
所以回到我们的Logger
和Mail
的例子,我们必须这样做:
from some.place import Mail, Logger
container.bind('Mail', Mail(Logger()))
mail1 = container.resolve(Mail)
mail2 = container.resolve(Mail)
mail1.to = '[email protected]'
mail2.to #== '[email protected]'
没什么大不了的,但这只是一个简单的例子。
在这种情况下,我们将调用单例模式。
绑定单线态
单体是一个非常简单的概念。这只是意味着,每当我们需要这个类的时候,我们每次都想要相同的类。当我们绑定类时,Masonite 通过解析类简单地实现了这一点。因此实例化的对象被放入容器中,在服务器的整个生命周期中,它始终是同一个对象。
我们可以通过以下方法将单例绑定到容器中
from some.package import Logger, Mail
..
def register(self):
self.app.bind('Logger', Logger)
self.app.singleton('Mail', Mail)
那么每当我们解析它的时候,每次都会得到和Logger
对象一样的对象。我们可以通过获取它并检查内存位置来证明这一点:
mail1 = container.make('Mail')
id(mail1) #== 163527
id(mail1.logger) #== 123456
mail2 = container.make('Mail')
id(mail2) #== 163527
id(mail2.logger) #== 098765
注意到Mail
类是相同的,但是Logger
类是不同的。
简单装订
我们已经注意到,当我们绑定到容器中时,有时我们在复制自己。我们的大多数绑定键只是我们的类的名字。为了解决这个问题,我们可以使用simple
绑定。
# Instead of:
def register(self):
container.bind('Mail', Mail)
# We can do:
def register(self):
container.simple(Mail)
这两行代码完全相同,我们现在可以像通常使用类名作为键一样make
它:
container.make('Mail')
解析类别
因此,我们已经简单地讨论了如何用容器中已经存在的对象来解析一个对象,但是让我们更详细地讨论解析实际上做了什么。
解析仅仅意味着我们将获取 objects 参数列表,提取哪些对象是类型提示的,在我们的容器中找到它们,然后将它们注入参数列表并返回新的对象。
需要注意的是,我们解析的对象不需要在容器中,,但是所有的参数都需要在容器中。因此,如果我们正在解析我们一直在处理的Mail
类,我们不需要将Mail
类绑定到容器中,但是Mail
类初始化器中的Logger
类需要。如果不是,那么 Masonite 将抛出一个异常,因为它不能正确地构建对象。
因此,代码示例如下所示:
from some.place import Mail, Logger
container.bind('Logger', Logger)
mail = container.resolve(Mail)
mail.logger #== <some.place.Logger x098765>
注意Mail
类不在容器中,但是它的依赖项在容器中。这样我们就能正确地为你构建这个对象。
钩住
钩子是另一个有趣的概念。当你想拦截一个对象的解析、生成或绑定时,钩子是很有用的。
我们可以使用三种方法之一来注册带有钩子的可调用函数:on_make
、on_bind
、on_resolve
。
正在制作
一个例子是这样的:
def change_name(obj):
obj.name = 'John'
return obj
...
def register(self):
self.app.on_make('Mail', change_name)
def boot(self):
mail = self.app.make('Mail')
mail.name #== 'John'
注意,当我们使用make
方法时,它触发了这个钩子。这是因为make
方法触发了on_make
钩子,寻找任何注册的钩子,并在返回之前将对象传递给它。
受约束
按照前面的例子,我们可以在将对象绑定到容器时做同样的事情:
def change_name(obj):
obj.name = 'John'
return obj
...
def register(self):
self.app.on_bind('Mail', change_name)
mail = self.app.make('Mail')
mail.name #== 'John'
注意它和前面的例子做的是一样的,但是在后端,当我们绑定对象的时候钩子是运行的。
决心已定
每次解析对象时都会执行最后一次挂钩:
from some.place import Mail, TerminalLogger
def change_logger(obj):
obj.logger = TerminalLogger()
return obj
...
def register(self):
self.app.bind('Mail', Mail)
mail = self.app.make('Mail')
mail.logger #== <some.place.Logger x098765>
def boot(self, mail: Mail):
mail.logger #== '<some.place.TerminalLogger x098765>'
注意,当我们使用bind
和make
方法时,我们的钩子从未运行过。直到我们解决了这个问题,记录器才被更换。当您希望在类遇到测试用例之前修改类的一些属性时,这对于测试来说是非常有用的。
交换
服务容器的另一个令人惊叹的特性是能够将类替换为其他类。当你想简化你的类型提示或者想编码抽象而不是具体化时,这是很有用的。
下面是一个代码片段示例:
from some.place import ComplexLogger, TerminalLogger, LogAdapter
container.swap(
TerminalLogger,
ComplexLogger(LogAdapater).driver('terminal')
)
现在,每当我们解析这个TerminalLogger
类时,我们将返回更复杂的记录器:
from some.place import TerminalLogger
def get_logger(logger: TerminalLogger)
return logger
logger = container.resolve(get_logger)
logger #== '<some.place.ComplexLogger x098765>'
这对于构建抽象类而不是具体类来说非常好。我们可以用一种简单的方式用更复杂的实现替换复杂的实现。
设计模式知识
为了成为服务容器方面的绝对专家,我认为先掌握一些知识再继续很重要。这应该给你足够全面的知识来完全掌握一切。如果你仍然不确定任何事情,你甚至可能需要重读几遍这一章,以使一切都更好。
抽象编码
软件设计中一个常见的设计模式是依赖倒置的概念。依赖倒置是一个定义。简单来说,它的意思就是你想依赖一个抽象类,而不必担心直接类本身。当你需要改变底层的类来做一些不同的事情时,这是很有用的。
例如,如果你正在使用一个TerminalLogger
,那么你实际上不想使用TerminalLogger
类本身,而是想使用它的一些抽象,比如一个新的LoggerConnection
类。这个类被称为抽象类,因为LoggerConnection
可以是任何东西。它可能是一个终端记录器,一个哨兵记录器,一个文件记录器等。它是抽象的,因为类LoggerConnection
实际上并不清楚它在使用什么,因此它的实现可以在以后的任何时候被换出。
依赖注入
我们已经讨论过依赖注入,你可能还没有意识到。这是另一个定义的 10 个短语。依赖注入所做的只是将一个依赖传递给一个对象。
这很简单,只需将一个变量传递给一个函数,如下所示:
def take(dependency):
return dependency
inject_this = 1
x = take(inject_this)
就这样!我们刚刚完成了依赖注入。我们获得一个依赖项(“inject_this
”变量),并将其赋予(或注入)到take
函数中。
控制反转(IoC)
好,这是简单的一个。这与前面的依赖注入是一样的,但是它只是依赖于逻辑发生在的。如果依赖注入来自你,那只是正常的依赖注入,但如果来自容器或框架,那就是控制反转。
这就是为什么 Masonite 的服务容器有时被称为 IoC 容器。
这是任何新的信息,但只是背景知识,这将使您更好地理解容器到底试图实现什么。
实现抽象
建立我们的抽象概念
让我们从制作我们的LoggerConnection
类开始。我们可以通过创建一个简单的基类来编码抽象,我们的具体类将从这个基类继承:
class LoggerConnecton:
pass
然后我们可以构建我们的终端记录器,并继承我们的新LoggerConnection
类:
from some.place import LoggerConnection
class TerminalLogger(LoggerConnection):
def log(self, message):
print(message)
现在最后一步是将TerminalLogger
绑定到我们的容器中。我们将在我们的一个服务提供商的register
方法中实现这一点:
from some.place import TerminalLogger
class LoggerServiceProvider:
def register(self):
self.app.bind('Logger', TerminalLogger)
太好了。现在,我们已经准备好对抽象进行编码了。记住我们的抽象是LoggerConnection
类,所以现在如果我们键入提示那个类,我们将实际得到我们的TerminalLogger
:
from some.place import LoggerConnection
class SomeController:
def show(self, logger: LoggerConnection):
logger #== <some.place.TerminalLogger x09876>
所以你可能想知道它是怎么做到的。其工作原理是,当 Masonite 试图查找LoggerConnection
类时,它会跟踪任何属于LoggerConnection
子类的类。Masonite 将知道它的容器中没有LoggerConnection
,并将返回它的第一个实例。在这种情况下,它就是TerminalLogger
。
换出记录器
以这种方式编码的最大好处是,将来你可以为一个不同的日志记录器切换日志记录器,而不必接触应用的任何其他部分。这就是如何构建可维护的代码库。
以此为例。我们现在想把我们的TerminalLogger
换成新的改进的FileLogger
。
首先,我们构造这个类:
from some.place import LoggerConnection
class FileLogger(LoggerConnection):
def log(self, message):
# Write to log file
with open('logs/log.log', 'a') as fp:
fp.write(message)
然后,我们再次将它绑定到容器,但删除之前的绑定:
from some.place import FileLogger
class LoggerServiceProvider:
def register(self):
# self.app.bind('Logger', TerminalLogger)
self.app.bind('Logger', FileLogger)
就这样!现在当我们解决它时,我们得到了FileLogger
:
from some.place import LoggerConnection
class SomeController:
def show(self, logger: LoggerConnection):
logger #== <some.place.FileLogger x09876>
除了容器绑定,我们没有改变应用的任何其他部分,它改变了代码库中其他地方的日志类型。
记住
如您所知,resolve
方法需要做很多事情。它需要检查对象以了解它是什么,它需要提取出参数列表,然后需要逐个检查每个参数,它需要遍历容器中的所有对象,实际上找到正确的对象,然后构建列表并将其注入到对象中。
可以想象,这是极其昂贵的。它不仅昂贵,而且有时每个请求需要运行几十次。
幸运的是,Masonite 的容器会记住每个对象需要解析的内容并缓存它们。下次该对象再次需要它的依赖项时,比如在下一次请求时,它将从它构建的特殊字典中获取它,并为您注入它们。
这可以使您的应用至少提升近 10 倍。测试表明,当 Masonite 记住对象签名时,解析一个类可以从每次解析 55ns 降低到每次解析 3.2ns。
收集
Collecting 是一个非常棒的特性,您可以从容器中指定想要的对象,它会返回一个包含所有对象的新字典。
可以用两种不同的方式收集:按键和按对象。
按键收集
您可以通过在键名之前、期间或之后指定通配符来按键收集。
以此为例,如果您想获得所有以Command
结尾的键:
container.bind('Request', Request())
container.bind('MigrateCommand', MigrateCommand)
container.collect('∗Command')
#== {'MigrateCommand': MigrateCommand}
注意,我们让容器中的所有对象通过通配符∗Command
绑定了键。这将得到以Command
结尾的所有内容。
您也可以用另一种方式获得以特定键开始的所有内容:
container.bind('Request', Request())
container.bind('MigrateCommand', MigrateCommand)
container.collect('Migrate∗')
#== {'MigrateCommand': MigrateCommand}
注意这些都是一样的,因为之前我们得到的都是以Command
键开始的,现在我们得到的都是以Migrate
键开始的。您也可以在键中间指定通配符:
container.bind('Request', Request())
container.bind('SessionCookieDriver', SessionCookieDriver)
container.collect('Session∗Driver')
#== {'SessionCookieDriver': SessionCookieDriver}
收集Session∗Driver
将获得SessionCookieDriver
、SessionMemoryDriver
或SessionRedisDriver
等钥匙。
当您想用特定的格式绑定时,这真的很有用,这样您以后可以很容易地再次检索它们。
收集对象
您还可以收集对象和对象的子类。也许您有一个基类,并希望收集该基类的所有实例。Masonite 将其用于其调度任务包,其中所有任务都继承了一个基本的Task
类,然后我们可以收集容器中的所有任务:
class Task1(BaseTask):
pass
class Task2(BaseTask):
pass
container.simple(Task1)
container.simple(Task2)
container.collect(BaseTask)
#== {'Task1': Task1, 'Task2', Task2}
如果您想将对象绑定到容器中,然后使用父类将它们取回,这是非常有用的。如果你正在开发一个包,那么这是一个非常有用的特性。
应用
好了,现在你已经是服务容器方面的专家了,让我们看看如何利用我们目前所掌握的知识来为我们的 Friday 应用添加一个 RSS 提要。我们将:
-
在我们的容器中添加一个 RSS 提要类
-
将代码添加到抽象中,而不是具体化中
-
从容器中解析该类,并将其用于我们的控制器方法
包裹
一个很好的包就是feedparser
包。因此,在我们的应用和虚拟环境中,让我们安装这个包:
$ pip install feedparser
让它安装好,现在我们开始构建我们的抽象类和具体化类。
抽象类
我们的抽象类将非常简单。它基本上是一个基类,我们的具体类将继承它。
让我们称这个类为RSSParser
类。在这个类中,我们将创建一个parse
方法,它将返回我们将在具体类中定义的经过解析的 RSS 提要。
让我们也在一个app/helpers/RSSParser.py
类中手动创建这个类:
# app/helpers/RSSParser.py
class RSSParser:
def parse(self, url):
pass
我们称之为RSSParser
,是因为我们将在将来与其他 RSS 解析器交换这个实现,所以我们需要给它一个足够抽象的名字,这样我们就可以这么做了。
混凝土类
因为我们使用的是feedparser
库,所以让我们称这个具体的类为FeedRSSParser
类:
# app/helpers/FeedRSSParser.py
import feedparser
from .RSSParser import RSSParser
class FeedRSSParser(RSSParser):
def parse(self, url):
return feedparser.parse(url)
如果前面代码中第二个导入令人困惑,它只是意味着从当前目录开始导入文件。由于两个文件都在app/helpers
目录中,我们可以像这样导入它。
服务提供商
让我们创建一个新的服务提供商,它将只负责处理我们的 RSS 提要类。
我们想称它为RSSProvider
,因为它为我们的应用提供了 RSS 类。我们可以用手艺来做这件事:
$ craft provider RSSProvider
一旦我们这样做了,我们就可以像这样开始将类绑定到容器:
from masonite.provider import ServiceProvider
from app.helpers.FeedRSSParser import FeedRSSParser
class RSSProvider(ServiceProvider):
wsgi = False
def register(self):
self.app.bind('FeedRSSParser', FeedRSSParser())
def boot(self):
"""Boots services required by the container """
pass
最后,我们需要告诉 Masonite 我们的提供商,让我们将其导入到config/providers.py
中,并将其添加到底部的列表中:
我们将首先在顶部导入我们的提供者,并将其添加到列表底部的# Application Providers
注释旁边:
from app.providers.RSSProvider import RSSProvider
...
CsrfProvider,
HelpersProvider,
# Third Party Providers
# Application Providers
RSSProvider,
]
控制器
好了,现在是最后一步,我们需要在控制器方法中使用这个新的抽象。
创建控制器
让我们首先创建一个专门用于解析 RSS 提要的控制器,名为FeedController
。
craft 命令将用Controller
作为控制器的后缀,所以我们只需要运行这个命令:
craft controller Feed
设置控制器
这一部分非常简单,但是第一次做的时候可能会有点棘手。我们将首先导入我们创建的抽象类,而不是导入和使用我们之前创建的具体类。
这意味着不是导入和使用FeedRSSParser
,而是导入和使用我们创建的RSSParser
抽象类。
现在让我们导入这个类,并在我们的控制器show
方法中返回它。我们现在将在 https://rss.itunes.apple.com/api/v1/us/podcasts/top-podcasts/all/10/explicit.rss
使用 iTunes 播客 RSS 源。以下是完整控制器的示例:
from app.helpers.RSSParser import RSSParser
class FeedController:
"""FeedController Controller Class."""
def __init__ (self, request: Request):
self.request = request
def show(self, parser: RSSParser):
return
parser.parse('https://rss.itunes.apple.com/api/v1/us/podcasts/top-podcasts/all/10/explicit.rss')
抽象和具体化的复习
记住抽象和具体,以及容器如何将它们匹配起来。这是一个需要把握的重要概念。你不需要这样做,但是编写抽象类而不是具体类的代码通常是一个好的设计模式。当一个库被废弃或者你需要换一个更好或更快的库时,它将使代码在未来的 2 到 3 年内更易维护。
在切换实现的情况下,您只需在服务提供者中切换出绑定,就大功告成了。
路由
最后一步是设置路由,这样我们就可以点击这个控制器方法。这是一个非常简单的步骤,我们已经学过了。
Get().route('/feed', 'FeedController@show').name('feeds'),
太好了!现在,当我们转到/feed
路由时,我们会看到 iTunes podcast 提要,如图 3-1 所示。
图 3-1
RSS 源响应
四、使用表单接受数据
在前几章中,我们学习了一些 Masonite 用来组织应用的模式。我们从容器中学习了绑定和解析。我们还看到了如何使用管理器、驱动程序和工厂来创建高度可定制的系统。
在这一章中,我们将使用这些新的技术和工具回到构建应用的实际方面。
“我如何存储数据?”
使用浏览器向 web 应用发送数据有多种方式。有一些显而易见的方法,比如当我们在浏览器的地址栏中输入一个网站地址时。我们告诉浏览器我们想去哪里,这个请求最终到达了 web 应用的门口。
在第二章中,我们看到了向 Masonite 应用发出请求的许多方式。就本章而言,我更感兴趣的是我们发送和接收数据的其他方式。
你以前听说过“Ajax”这个术语吗?这个名字最初是一组特定技术的缩写( A 同步JavaScriptAndXML),但现在已经成为描述多种部分页面加载的术语。
从本质上讲,Ajax 是指我们通常发送的 GET 或 POST 请求在幕后悄悄发生,通常是为了保持某种状态或用新内容重新加载页面的一部分。
然后是网络插座。这些是我们到目前为止看到的 HTTP 请求的演变。与对新内容的全部或部分请求不同,web 套接字是一个连续的开放连接,通过它服务器可以将新内容推送到浏览器。
还有更多方法,但这些有助于说明我希望我们解决的一个问题。当我们构建 web 应用时,我们需要能够沿着这些通道发送数据。在处理数据之前,我们还需要验证数据是否有序。通常,表单数据被存储起来,但是它也可以被发送到其他服务,这些服务需要特定格式的特定内容。
因此,在这一章中,我们将研究如何创建表单以及如何将表单数据安全地发送到服务器。我们将探索我们拥有的选项,以确保数据被正确格式化,并且不会试图在服务器上做恶意的事情。
构建安全表单
此代码可在 https://github.com/assertchris/friday-server/tree/chapter-5
找到。
让我们从第二章结束时我们停下的地方继续。我们构建了几个页面,包括一个列出播客搜索结果的页面,如图 4-1 所示。
图 4-1
到目前为止,我们所拥有的
我们将从动态页面开始。当有人第一次访问它时,我们可以显示一个空的搜索结果。我们通过向模板发送一个空的播客列表,并使用所谓的条件:
from masonite.controllers import Controller
from masonite.view import View
class PodcastController(Controller):
def show_search(self, view: View):
return view.render('podcasts.search', {
'podcasts': self.get_podcasts()
})
def get_podcasts(self, query=“):
return []
这是来自app/http/controllers/PodcastController.py
。
@extends 'layout.html'
@block content
<h1 class="pb-2">Podcast search</h1>
<form class="pb-2" method="POST">
{{ csrf_field }}
<label for="terms" class="hidden">Terms:</label>
<input type="search" name="terms" id="terms" />
<input type="submit" value="search" />
</form>
<div class="flex flex-row flex-wrap">
@if podcasts|length > 0
@for podcast in podcasts
@include 'podcasts/_podcast.html'
@endfor
@else
No podcasts matching the search terms
@endif
</div>
@endblock
这是来自resources/templates/podcasts/search.html
。
因为我们要动态制作播客列表,所以我们创建了一个PodcastController
方法来返回该列表。目前,它返回一个空数组,但我们会随着时间的推移扩展它。
通过向view.render
提供一个字典,该数组被传递给podcasts/search.html
模板。然后,在模板中,我们用一些动态代码替换预览静态内容。我们检查是否有播客,如果没有,我们会提供一些有用的文本。
如果有播客,我们循环播放。这里有很多事情要做,所以我们要花一些时间来看看这个模板在做什么,以及模板一般能做什么。系好安全带。
模板条件句
Masonite 模板是 Jinja2 模板的超集。这意味着,在普通的 Jinja2 模板中可以做的任何事情在 Masonite 中都可以做。Masonite 包括一些额外的好东西,比如交替块语法。
您可以通过以下几种方式与控制器中的数据进行交互:
-
If 语句
这些是我们在模板中可以做的最简单的检查。它们接受一个不一定是布尔值的变量或表达式。变量或表达式的值被解释为
True
或False
。如果True
,嵌套块将被显示。当我们说
@if podcasts|length > 0
时,我们是说“如果播客的数量大于零,显示下一个嵌套层次的内容。”我们还可以定义一个@else
块和多个@elif
块。我个人不喜欢使用
@elif
块的想法,因为它们会很快使模板变得混乱。定义多个模板并在控制器内部尽可能多地执行条件逻辑要清楚得多。 -
循环语句
这些帮助我们为列表中的每一项呈现内容/标记块。在我们的示例应用中,我们可以使用它们来呈现播客列表,就像我们在前面的示例中所做的那样。
注意
@endfor
和@endif
的区别。这些帮助编译器知道哪种条件块被关闭,所以使用合适的关闭块是很重要的。这是一件需要习惯的事情,尤其是因为 Python 没有像这样的块终结符。 -
包含报表
这对于将其他模板包含到当前模板中很有用。例如,我们可以将为每个播客呈现的块放入另一个模板中,并将其包含在循环中。
包含的模板可以访问包含它的模板中定义的所有变量。我们不需要“传下去”什么的。我们可以直接开始使用它们。
-
扩展/阻塞语句
正如我们在第三章中了解到的,这些对于扩展现有布局非常有用。随着我们向应用中添加更多的 JavaScript,我们将了解更多关于块的知识。
你可以在官方文档中看到更多的细节:Views - Masonite 文档。
模板过滤器
除了可以使用 Masonite 块语法之外,Jinja2 还附带了一系列过滤器:
-
值| '默认'
当我们开始显示播客细节时,我们会看到这个过滤器被更多地使用。它说,“如果
value
不是假的,显示它。否则,显示值'default'
。它非常适合填补没有内容可展示的空白。 -
项|第一项
此过滤器显示项目列表中的第一个项目。如果您有一个事物列表,但您只想显示第一个,这很有用。当然,您总是可以在控制器中从列表中取出第一个项目,然后只将它发送给视图。
-
‘你好% s’|格式(名称)
该过滤器的工作方式类似于 Python 字符串插值法。如果您想要在模板中使用模板字符串,并且您可以访问想要用其替换占位符的变量,这将非常有用。
-
项|联接(',')
该过滤器有助于将一系列项目组合成一个字符串,在每个项目之间使用另一个字符串。如果列表只有一个条目,则根本不会添加“join”字符串。
-
项|最后一项
类似于
first
,但它返回最后一项。 -
项目|长度
该过滤器返回项目列表的长度。这对于在搜索结果中分页和汇总列表内容是必不可少的。
-
items|map(attribute='value ')或 items|map('lower')|join(',')
map
是一个极其强大的过滤器。有了它,我们可以从列表中的每个对象中提取属性,或者为列表中的每个项目提供另一个过滤器。然后,甚至可以通过提取一个属性,然后对每个提取的值应用另一个过滤器来合并它。 -
物品|随机
从较长的项目列表中随机返回一个项目。
-
值|反转
反转 n 对象(如字符串),或返回一个迭代器,该迭代器反向遍历列表中的项目。
-
项目|排序或项目|排序(attribute='name ',reverse=True)
此过滤器对项目列表进行排序。如果条目是字符串,只使用`|sort`就足够了,尽管您可能还想更改`reverse`参数使其降序排序。如果项目是字典,您可以选择按哪个属性排序。
- 值|修剪
修剪字符串前后的空格。
有相当多的过滤器没有包括在这个列表中。我认为其中一些很简单,但没有那么有用,而另一些则更深入一些,我希望我们在这一点上继续下去。如果你正在寻找一个你在这里看不到的过滤器,查看 Jinja2 过滤器文档: [`https://jinja.palletsprojects.com/en/2.10.x/templates/#list-of-builtin-filters`](https://jinja.palletsprojects.com/en/2.10.x/templates/%2523list-of-builtin-filters) 。
CSRF 保护
在我们看如何在后端使用这个表单之前,我想提一件事,就是{{ csrf_field }}
字段。CSRF(或称 C 罗斯-SiteRequestForg ery)是当您开始在站点上使用表单时出现的一个安全问题。
需要用户登录才能执行敏感操作的 Web 应用可能会在浏览器中存储一些凭据。这样,当您从一个页面导航到另一个页面时(或者当您过一会儿返回站点时),您仍然处于登录状态。
这样做的问题是,恶意的人可以伪造一个从您到 web 应用的请求,要求进行身份验证。假设您在浏览器中登录到脸书。当你浏览一个不相关的网站时,该网站使用 Ajax 请求将你的浏览器导航到脸书 URL,使你的帐户跟随他们的帐户。
这不可能发生,因为脸书正在使用一种叫做 CSRF 保护的东西。它在页面上添加了一个特殊的标记,这样你的帐户就可以自然地跟随另一个帐户。然后,当你的浏览器启动请求跟随另一个帐户时,脸书会将它为你记住的令牌与 HTTP 请求传递的令牌进行比较。
如果它们匹配,您的浏览器一定已经通过一个自然的路径开始了跟随操作。
我不想过多地讨论这个问题的细节,只想说 Masonite 提供了一个简单的机制来使用脸书使用的安全性。创建一个隐藏字段,保存这个 CSRF 令牌。如果您的表单不使用{{ csrf_field }}
,默认情况下,您可能无法将其内容提交到另一个 Masonite URL。
在较小的程度上,CSRF 保护也使自动化脚本(或机器人)更难使用您的 web 应用。他们必须将请求的数量加倍,并适应找到初始标记的页面标记的变化。
CSRF 可以影响通过 HTTP GET 请求执行破坏性或敏感操作的 web 应用。只是好的应用很少通过 GET 请求执行这类操作,因为这违背了 HTTP 规范的最初设计。你也应该这样做。
在第二章中,当我们给一些中间件添加异常时,我们瞥见了 CSRF。重要的是要记住,虽然我们不应该养成这种习惯,但我们肯定可以绕过这种内置的 CSRF 保护。如果有我们想要“开放”给其他服务的 HTTP 端点,我们可以通过将它们添加到 CSRF 中间件例外列表来实现:
"""CSRF Middleware."""
from masonite.middleware import CsrfMiddleware as Middleware
class CsrfMiddleware(Middleware):
"""Verify CSRF Token Middleware."""
exempt = [
'/home',
'/home/@name',
'/paypal/notify',
]
very_request = False
token_length = 30
这是来自app/http/middleware/CsrfMiddleware.py
。
一个很好的例子是,在我们的要求下,PayPal 和 Stripe 等服务将向我们发送客户支付的详细信息。在我们的家庭自动化中,我们不会使用它们,但是随着你构建的越来越多,你可能会遇到类似的事情。
像这样的服务需要一种方法来向我们发送 HTTP POST 请求,而不需要通过 CSRF 环。他们不会首先在浏览器中打开一个表单并找到 CSRF 令牌。
诀窍在于明确哪些端点被允许绕过内置保护,并确保它们是防弹的。
当人们在浏览器中用有效的用户会话调用这些端点时会发生什么?当他们用恶意数据呼叫端点时怎么办?端点被机器人锤了怎么办?
在允许终端绕过保护之前,您应该问这些问题。
验证表单数据
表单提交后,我们需要检查它提供的数据是否有效。您可以在用于显示搜索页面的同一个控制器动作中实现这一点,但是我建议您将这些动作分开一点。
当您没有将多个 HTTP 请求方法和路径组合到同一个动作中时,更容易发现哪里需要进行更改。
from masonite.request import Request
from masonite.validation import Validator
# ...snip
def get_podcasts(self, query=“):
if query:
dd(query)
return []
def do_search(self, view: View, request: Request,
validate: Validator):
errors = request.validate(
validate.required('terms')
)
if errors:
request.session.flash('errors', errors)
return request.back()
return view.render('podcast.search', {
'podcasts': self.get_podcasts(request.input('terms'))
})
这是来自app/http/controllers/PodcastController.py
。
Masonite 附带了一个强大的验证类,我们无疑会在本书中重用它。这是最简单的使用方法:
-
我们在搜索动作中输入提示参数
Request
和Validator
。Masonite 的容器,我们在第三章中了解到,反映了参数,以查看它应该将哪些对象注入到函数调用中。 -
我们使用
Request
类的validate
方法,以及我们想要执行的验证列表。Validator
类提供了不同的规则生成方法,我们可以用它们来定义有效数据的样子。 -
如果有错误,我们会找到一种合理的方式来通知用户这些错误。我们在第二章中了解到,在会话中闪现它们可以让我们暂时记住它们。然后,在重定向之后,我们可以向用户显示它们。
@if session().has('errors')
<div class="bg-red-100 px-2 pt-2 pb-1 mb-2">
@for field in session().get('errors')
<div class="mb-1">
{{ session().get('errors')[field]|join('. ') }}
</div>
@endfor
</div>
@endif
这是来自resources/templates/podcasts/search.html
。
如果有验证错误,我们希望能够在搜索模板中显示它们。这里,我们可以访问一个session()
函数,它是我们在控制器中看到的同一个request.session
对象的快捷方式。
如果会话有一个errors
值,我们显示它包含错误的字段的枚举。在一个简单的数组中,@for item in items
将返回我们可以直接放入标记中的值。对于字典来说,它变成了@for key in items
。每个键都是验证失败的字段的名称。
然后,我们取消对这些错误的引用(其中每个字段名或键都有一组错误消息),并用我们刚刚学习过的join
过滤器将它们连接起来。
有许多内置的验证方法。事实上,太多了,以至于我更希望我们在阅读本书的过程中发现它们,而不是一下子全部发现。如果你迫不及待,请阅读官方文档了解更多信息: https://docs.masoniteproject.com/advanced/validation
。
图 4-2
在模板中呈现错误消息
提取远程数据
既然我们已经获得并验证了搜索词,是时候获取匹配播客的列表了。我们将接入 iTunes 来寻找新的播客并解析它们的数据。
首先,我们需要一个库来发出远程请求:
pip install requests
在第三章中,我们学习了如何创建服务提供商。让我们回顾一下我们所学的内容。
首先,我们使用一个craft
命令创建了一个新类:
craft provider RssParserProvider
我们在配置中注册了这个:
# ...snip
from app.providers.RssParserProvider import RssParserProvider
PROVIDERS = [
# ...snip
RssParserProvider,
]
这是来自config/providers.py
。
这个新的提供者将一个解析器类绑定到 IoC 容器:
from masonite.provider import ServiceProvider
from app.helpers.RssParser import RssParser
class RssParserProvider(ServiceProvider):
wsgi = False
def register(self):
self.app.bind('RssParser', RssParser())
def boot(self):
pass
这是来自app/providers/RssParserProvider.py
。
这个RssParser
类使用了一个名为 feedparser ( https://pythonhosted.org/feedparser/index.html
)的第三方库来解析一个提要 URL:
import feedparser
class RssParser:
def parse(url):
return feedparser.parse(url)
这是来自app/helpers/RssParser.py
。
当我们将 HTTP 请求库绑定到 IoC 容器时,我们将重复这个过程。我们将使用一个名为 Requests ( https://2.python-requests.org/en/master
)的库,从新的提供者开始:
craft provider HttpClientProvider
然后,我们需要在提供者内部绑定一个HttpClient
:
from masonite.provider import ServiceProvider
from app.helpers.HttpClient import HttpClient
class HttpClientProvider(ServiceProvider):
wsgi = False
def register(self):
self.app.bind('HttpClient', HttpClient())
def boot(self):
pass
这是来自app/providers/HttpClientProvider.py
。
我们还需要将此提供程序添加到配置:
# ...snip
from app.providers import (
HttpClientProvider,
RssParserProvider
)
PROVIDERS = [
# ...snip
HttpClientProvider,
RssParserProvider,
]
这是来自config/providers.py
。
只有当我们也创建一个init__
文件时,这种导入速记才是可能的:
from .HttpClientProvider import HttpClientProvider
from .RssParserProvider import RssParserProvider
这是来自app/providers/init.py
。
HttpClient
类只是请求库的一个代理:
import requests
class HttpClient:
def get(*args):
return requests.get(*args)
这是来自app/helpers/HttpClient.py
。
从容器中解析依赖关系
现在我们有了这些工具,我们需要把它们从容器中取出来,这样我们就可以用它们来搜索新的播客:
from masonite.controllers import Controller
from masonite.request import Request
from masonite.validation import Validator
from masonite.view import View
class PodcastController(Controller):
def __init__ (self, request: Request):
self.client = request.app().make('HttpClient')
self.parser = request.app().make('RssParser')
def show_search(self, view: View):
return view.render('podcasts.search', {
'podcasts': self.get_podcasts()
})
def get_podcasts(self, query=“):
if query:
dd([query, self.client, self.parser])
return []
def do_search(self, view: View, request: Request,
validate: Validator):
errors = request.validate(
validate.required('terms')
)
if errors:
request.session.flash('errors', errors)
return request.back()
return view.render('podcasts.search', {
'podcasts': self.get_podcasts(
request.input('terms')
)
})
这是来自app/http/controllers/PodcastController.py
。
我们添加了一个init__
方法,它将HttpClient
和RssParser
解析出容器。
这不是解决这些依赖性的唯一方法,替代方法肯定值得考虑。我们很快就会回到他们身边。
现在,剩下的工作就是发出 iTunes 请求并解析搜索结果:
def get_podcasts(self, query="):
if query:
response = self.client.get(
'https://itunes.apple.com/search?media=podcast&term=' + query)
return response.json()['results']
return []
这是来自app/http/controllers/PodcastController.py
。
iTunes 提供了一个简洁、开放的 HTTP 端点,通过它我们可以搜索新的播客。我们唯一要做的就是格式化从这个端点返回的数据:
<div class="w-full md:w-2/5 mr-2 flex flex-row pb-2">
<div class="min-h-full w-48"
style="background-image: url('{{podcast.artworkUrl600}}');
background-size: 100% auto; background-repeat: no-repeat;
background-position: center center; "></div>
<div class="p-4 flex flex-col flex-grow">
<div class="mb-8 flex flex-col flex-grow">
<div class="text-xl mb-2">
{{ podcast.collectionName }}</div>
<!-- <p class="text-base">description</p> -->
</div>
<div class="flex flex-grow items-center">
<!-- <div class="w-10 h-10 bg-red-300"></div> -->
<div class="text-sm">
<p class="leading-none">
{{ podcast.artistName }}</p>
<!-- <p class="">date</p> -->
</div>
</div>
</div>
</div>
这是来自resources/templates/podcasts/_podcast.html
。
我已经注释掉了一些字段,因为我们需要解析每个播客的 RSS 提要来找到这些信息。既然我们可以从 IoC 容器中提取 RSS 提要解析器,这肯定是可能的,但是我觉得我们已经为这一章取得了足够的成就。
图 4-3
在我们开发的应用中寻找新的播客!
摘要
我们在这一章里讲了很多东西。我们还可以添加很多东西。想想看,找到更多关于每个播客的信息,并填写更多的_podcast.html
模板是一个挑战。
除了学习所有关于表单和模板的知识,我们还有机会进一步巩固我们对 IoC 容器的了解,以及如何向它添加我们自己的服务。
在下一章中,我们将探索如何将这种数据保存到数据库中,以及所有需要做的事情。
五、使用数据库
在前一章中,我们学习了所有关于表格的知识。我们创建了一些,甚至让它们从远程数据源获取数据。这很有用,但除非我们还可以从自己的数据源中存储和检索数据,否则用处不大。因此,在这一章中,我们将学习如何建立一个数据库,如何在其中存储数据,以及如何从数据库中取出相同的数据。
我如何存储数据?
我已经暗示了一种方法,但实际上还有许多其他方法来存储和检索数据。我们可以走“老路”,使用 XML 或 JSON 数据的平面文件。这当然是最简单的方法之一,但是它也有一些问题,比如文件锁和有限的分发。
我们可以使用类似 Firebase 的东西,它仍然是一个数据库,但我们不必管理和控制它。它的成本也比在同一台服务器上使用一个数据库要高。管理起来有点困难,也没有它可能达到的速度快。
相反,我们将使用一个本地 MySQL 数据库(和一些 SQL 引导)来存储我们的数据。Masonite 对 MySQL 数据库有很大的支持,甚至有一些工具可以帮助我们构建数据库。这会很有趣的!
用代码保存数据库
此代码可在 https://github.com/assertchris/friday-server/tree/chapter-6
找到。
通常,在一本书的这一点上,作者可能会要求你跳出到另一个应用。他们可能会要求您直接开始规划和构建您的数据库,并完全断开与代码编辑器的连接。出于几个原因,我不会要求您这样做:
-
我相信数据库能够并且应该在你的应用代码中表现出来,因为那是它们被测试的地方,也是你需要理解它们的首要地方。
-
Masonite 提供了实现这一目标的工具。我喜欢使用的所有框架都提供了这些工具。这是一个已经解决的问题!
假设我们想开始存储播客(通过我们现有的 UI“订阅”它们的结果)。我们可能会决定将这些播客 URL 一起存储在 users 表中。可能在文本字段中,用逗号分隔。
或者,我们可能希望创建一个新表,并将其命名为 subscriptions。对我来说,第二种方法感觉更干净,因为有些用户可能根本就不想订阅播客。相反,他们可能想听音乐!
首先,我们需要使用 craft 创建一个叫做迁移的东西:
craft migration create_subscriptions_table
这将创建一个新的空迁移:
from orator.migrations import Migration
class CreateSubscriptionsTable(Migration):
def up(self):
"""Run the migrations."""
pass
def down(self):
"""Revert the migrations."""
pass
这是来自database/migrations/x_create_subscriptions_table.py
。
迁移分为两个部分:
-
up
–对现有数据库结构进行新的添加/更改 -
down
–这些新增/变更可以回滚,以防出现问题或迁移发生得太快
让我们定义一个新表:
from orator.migrations import Migration
class CreateSubscriptionsTable(Migration):
def up(self):
with self.schema.create('subscriptions') as table:
table.increments('id')
table.string('url')
table.string('title')
table.timestamps()
def down(self):
self.schema.drop('subscriptions')
这是来自database/migrations/x_create_subscriptions_table.py
。
首先,我们的订阅表有点小而且简单。我们将存储播客的标题和可以找到播客详细信息的 URL。我们通过调用schema.create
方法创建一个表。这将返回一个新的 table 对象,我们可以对它调用各种方法,以便在表中创建字段。
有几个领域非常普遍和重要:
-
increments
–一个自动编号的整数字段,是表格的主键 -
timestamps
–几个时间戳字段,用于记住特定事件发生的时间(比如记录创建到最后更新的时间)
还有许多其他字段类型:
-
string
–长度受限的字符串字段 -
text
–可变长度字符串字段 -
integer
–一个整数字段 -
float
–十进制字段 -
timestamp
–时间戳字段
字段上还可能有修饰符,这会影响字段的元数据。例如,我们可以应用其中的一个:
-
nullable
–当字段允许包含值NULL
时 -
default(value)
–不可空字段应具有的默认值 -
unsigned
–用于任何数值字段,因此它们可以存储两倍的非负数这里有很多我没有提到的字段类型。如果你在寻找缺失的东西,你可以参考演说家的文档。astorar 是底层数据库库的名字,它使得所有这一切成为可能。
创建新表是进行迁移的一个原因,但是您可能还想改变表的结构。在这种情况下,您可以使用schema.table
方法:
from orator.migrations import Migration
class ChangeSubscriptionsTable(Migration):
def up(self):
with self.schema.table('subscriptions') as table:
table.string('title', 200).change()
def down(self):
with self.schema.table('subscriptions') as table:
table.string('title').change()
这是来自database/migrations/x_change_subscriptions_table.py
。
除了改变一个字段,这也是一个如何使用down
方法的好例子。这个想法是,您在数据库中添加或更改的任何内容都在down
方法中被“还原”。我们将 title 字段更改为有长度限制,因此这一回滚将删除 200 个字符的限制。
类似地,我们也可以调用一个table.dropColumn(name)
方法来删除一个字段,或者调用一个schema.drop(name)
方法来完全删除这个表。
以这种方式思考数据库表需要一点时间。我鼓励你通读一下关于管理迁移的 astral 文档,这样你就可以熟悉在迁移中你可以做的所有不同的事情。
在运行这些迁移之前,我们可能需要确保一切都已设置好。您应该安装了 MySQL 数据库。如果您在 macOS 上(并且安装了 Homebrew),您可以这样做:
brew install mysql
对于其他系统和配置,请查看 astorar 配置文档。
您还需要安装一个数据库依赖项:
pip install mysqlclient
最后,您需要确保您的.env
数据库凭证与您已经创建的数据库相匹配:
DB_DATABASE=Friday
DB_USERNAME=<username>
DB_PASSWORD=<password>
家酿默认使用用户名【root】和密码。这些不是我所说的安全凭证,但是如果这是你第一次在你的系统上使用 MySQL,了解它们是有好处的。当然,您可以根据自己的需要进行更改。即使有了这些凭证,您仍然需要确保 MySQL 正在运行,并且您已经创建了一个与您配置的数据库相匹配的数据库。
用虚拟数据填充数据库
有些人用空数据库测试他们的应用,或者通过使用站点手动插入数据。这可能有点像陷阱,因为这意味着他们插入的数据符合他们对网站使用方式的预期,并且很少涵盖应用特定部分可能处于的所有重要状态。让我们考虑一下我们的应用可能处于的不同状态:
-
空的搜索屏幕,在我们搜索播客之前
-
当没有找到结果时,空的搜索屏幕
-
“细节”屏幕,显示播客的细节
-
“订阅”屏幕,显示某人订阅的所有播客
-
当用户没有订阅任何播客时,一个空的“订阅”屏幕
更不用说订阅和取消订阅播客的所有确认屏幕了。
而且,这只是可能成为一个巨大应用的一种数据类型!想象一下,尝试手动测试所有这些东西。您可能会忘记大约一半的页面,并且手工测试会花费很长时间(或者根本不会发生)。
除了这些问题,想象一下你在应用中拥有的数据类型:
-
你会迎合大标题的播客吗?
-
你会满足数百个搜索结果吗?
-
您的应用可以处理播客标题中的 Unicode 字符吗?
用测试数据填充数据库(或者通常所说的播种)是一个重要的设计步骤,因为它帮助你记住所有你需要考虑的边缘情况和状态。当与测试结合时(我们将在第十五章中讲到),种子数据迫使设计变得健壮。
问题变成了:我们如何播种数据库数据?有一个工艺指令:
craft seed subscriptions
这将创建一个新的种子(er)文件,如下所示:
from orator.seeds import Seeder
class SubscriptionsTableSeeder(Seeder):
def run(self):
pass
这是来自database/seeds/subscriptions_table_seeder.py
。
我们可以稍微改变一下,这样我们就可以确定它正在运行:
from orator.seeds import Seeder
class SubscriptionsTableSeeder(Seeder):
def run(self):
print('in the seeder')
这是来自database/seeds/subscriptions_table_seeder.py
。
在运行之前,我们需要将它添加到“基础”种子:
from orator.seeds import Seeder
# from .user_table_seeder import UserTableSeeder
from .subscriptions_table_seeder import SubscriptionsTableSeeder
class DatabaseSeeder(Seeder):
def run(self):
# self.call(UserTableSeeder)
self.call(SubscriptionsTableSeeder)
这是来自database/seeds/database_seeder.py
。
这个播种机是 craft 运行所有其他播种机的入口点。我已经注释掉了用户的东西,因为在第八章之前我们不需要它。我还添加了订阅种子,并使用self.call
方法调用它。
让我们播种数据库,看看订阅播种程序是否正在运行:
craft seed:run
> in the seeder
> Seeded: SubscriptionsTableSeeder
> Database seeded!
如果您还看到“在种子中”文本,则订阅种子正在工作。让我们了解一下如何读写数据库。
写入数据库
运行一个数据库 UI 应用会很有帮助,这样您就可以看到我们将要对数据库做的事情。强烈推荐 TablePlus 或者 Navicat。如果你正在寻找更便宜的东西,请查看 HeidiSQL。
我们将学习如何与数据库交互,而 astorar 将生成并使用 SQL 来完成这一任务。不需要懂 SQL,但无疑会有帮助。在 www.apress.com/us/databases/mysql
查看阿普瑞斯关于这个主题的书籍。
让我们通过假装订阅开始向数据库写入数据。我们的订阅表有几个需要填写的材料字段:
from config.database import DB
from orator.seeds import Seeder
class SubscriptionsTableSeeder(Seeder):
def run(self):
DB.table('subscriptions').insert({
'url': 'http://thepodcast.com',
'title': 'The podcast you need to listen to',
})
这是来自database/seeds/subscriptions_table_seeder.py
。
数据库连接是在应用的 config 部分定义的,我们可以从那里提取连接实例,并写入其中。如果您已经打开了数据库 GUI,现在您应该在 subscriptions 表中看到一个订阅。您还应该在控制台中看到相应的 SQL 语句。
有用的是,我们不需要写出完整的 SQL 语句来执行它。这是 astorar 试图构建能在它支持的任何引擎中工作的 SQL 语句的副作用。这个想法是,我们应该能够转移到一个不同的(受支持的)引擎,并且我们所有抽象的 SQL 语句应该继续工作。
我们还可以做其他类型的操作,但是我们一会儿会讲到这些操作的例子。
这段代码只是第一步。如果我们希望我们的播种者真的有用(并且我们的设计是健壮的),我们需要在播种阶段使用随机数据。演说家自动安装了一个名为 Faker 的软件包。这是一个随机的假数据发生器,我们可以用在我们的播种机上:
from config.database import DB
from faker import Faker
from orator.seeds import Seeder
class SubscriptionsTableSeeder(Seeder):
def run(self):
fake = Faker()
DB.table('subscriptions').insert({
'url': fake.uri(),
'title': fake.sentence(),
})
这是来自database/seeds/subscriptions_table_seeder.py
。
现在,我们可以为设计中不同种类和数量的数据做好准备,因为我们无法准确控制要输入哪些数据。我们不仅仅是按照我们期望的数据填充它们。Faker 提供了很多有用的数据类型,所以我不打算一一介绍。足以说明,Faker 文档是惊人的,你一定要去看看: https://faker.readthedocs.io/en/stable/providers.html
。
从数据库中读取
插入数据很酷,但是我们如何将数据从数据库中取出,以便在应用需要它的部分显示它呢?让我们做一个页面来列出我们的订阅。
from config.database import DB
# ...snip
class PodcastController(Controller):
# ...snip
def show_subscriptions(self, view: View):
subscriptions = DB.table('subscriptions').get()
return view.render('podcasts.subscriptions', {
'subscriptions': subscriptions,
})
这是来自app/http/controllers/PodcastController.py
。
@extends 'layout.html'
@block content
<h1 class="pb-2">Subscriptions</h1>
<div class="flex flex-row flex-wrap">
@if subscriptions|length > 0
@for subscription in subscriptions
@include 'podcasts/_subscription.html'
@endfor
@else
No subscriptions
@endif
</div>
@endblock
这是来自resources/templates/podcasts/subscriptions.html
。
<div class="w-full flex flex-col pb-2">
<div class="text-grey-darker">{{ subscription.title }}</div>
<div class="text-sm text-grey">{{ subscription.url }}</div>
</div>
这是来自resources/templates/podcasts/_subscription.html
。
RouteGroup(
[
# ...snip
Get('/subscriptions',
'PodcastController@show_subscriptions').name('-show-subscriptions')
],
prefix='/podcasts',
name='podcasts',
),
这是来自routes/web.py
。
这四个文件你现在应该比较熟悉了。第一个是附加的控制器动作,它响应我们在第四个中创建的路由。第二个和第三个文件是显示订阅列表的标记(视图)。在浏览器中,它应该类似于图 5-1 。
图 5-1
列出存储在数据库中的订阅
隐藏在新控制器动作中的是从数据库中提取订阅的数据库代码:DB.table('subscriptions').get()
。
过滤数据库数据
如果我们想过滤那个列表呢?首先,我们需要添加筛选依据字段。最有用的是添加“收藏”订阅的功能,这样它就会出现在列表的顶部。为此,我们需要创建另一个迁移:
from orator.migrations import Migration
class AddFavoriteToSubscriptionsTable(Migration):
def up(self):
with self.schema.table('subscriptions') as table:
table.boolean('favorite').index()
def down(self):
with self.schema.table('subscriptions') as table:
table.drop_column('favorite')
这是来自database/migrations/x_add_favorite_to_subscriptions_table.py
。
在这个新的迁移中,我们添加了一个名为favorite
的boolean
字段,并为它创建了一个索引。notes 迁移是反向的;我们还会删除这个专栏,这样它就像从未存在过一样。知道您可以使用 craft 回滚所有迁移并再次运行它们可能是有用的:
craft migrate:refresh --seed
我们可能还需要更新种子来考虑这个新字段,因为我们不允许该字段为空,并且我们也没有指定默认值:
from config.database import DB
from faker import Faker
from orator.seeds import Seeder
class SubscriptionsTableSeeder(Seeder):
def run(self):
fake = Faker()
DB.table('subscriptions').insert({
'url': fake.uri(),
'title': fake.sentence(),
'favorite': fake.boolean(),
})
这是来自database/seeds/subscriptions_table_seeder.py
。
现在我们有了一个新的可过滤字段,我们可以将订阅分成“普通订阅”和“收藏订阅”列表:
@extends 'layout.html'
@block content
<h1 class="pb-2">Favorites</h1>
<div class="flex flex-row flex-wrap">
@if favorites|length > 0
@for subscription in favorites
@include 'podcasts/_subscription.html'
@endfor
@else
No subscriptions
@endif
</div>
<h1 class="pb-2">Subscriptions</h1>
<div class="flex flex-row flex-wrap">
@if subscriptions|length > 0
@for subscription in subscriptions
@include 'podcasts/_subscription.html'
@endfor
@else
No subscriptions
@endif
</div>
@endblock
这是来自resources/templates/podcasts/subscriptions.html
。
我们可以复制基于订阅的代码块(也许稍后,我们可以包含另一个),这样我们就可以使用不同的订阅项目源。我们可以称之为收藏夹,但这也意味着我们需要从控制器提供:
def show_subscriptions(self, view: View):
favorites = DB.table('subscriptions').where('favorite', True).get()
subscriptions = DB.table('subscriptions').where(
'favorite', '!=', True).get()
return view.render('podcasts.subscriptions', {
'favorites': favorites,
'subscriptions': subscriptions,
})
这是来自app/http/controllers/PodcastController.py
。
在这里,我们使用where
方法根据他们最喜欢的字段是否有真值来过滤订阅。这是许多有用的查询方法之一,包括
-
where
有两个参数,第一个是字段,第二个是值 -
where
有三个参数,其中中间的参数是比较运算符(就像我们如何使用!=
来表示“不等于”) -
where_exists
使用单个查询对象,以便外部查询仅在内部查询返回结果时返回结果(类似于左连接) -
where_raw
带有一个原始的 where 子句字符串(如subscriptions.favorite = 1
)
这些有一些小标题,你可以通过阅读 https://orator-orm.com/docs/0.9/query_builder.html#advanced-where
的文档找到。记住确切的语法并不重要,重要的是要知道这些方法的存在,这样你就知道在文档的什么地方可以学到更多关于它们的知识。
如果我们要让 favorite 字段为空,那么第二个查询将捕获 favorite 没有设置为True
的所有记录,包括 favorite 为False
和Null
的记录。我们可以说得更明确一点,比如说where('favorite', False)
,但是如果我们曾经将 favorite 字段设置为可空,我们就必须记住修改它。
更新数据库数据
让我们添加喜欢(和不喜欢)数据库记录的功能。我们需要几个新的控制器动作和路由:
def do_favorite(self, request: Request):
DB.table('subscriptions').where('id', request.param('id')).update({
'favorite': True,
})
return request.redirect_to('podcasts-show-subscriptions')
def do_unfavorite(self, request: Request):
DB.table('subscriptions').where('id', request.param('id')).update({
'favorite': False,
})
return request.redirect_to('podcasts-show-subscriptions')
这是来自app/http/controllers/PodcastController.py
。
除了一个insert
方法,我们还可以使用一个update
方法来影响数据库记录。这两个动作非常相似,但是我认为最好不要将它们抽象成一个方法,因为对于哪个动作做什么,这是不可否认的清楚。
更新订阅后,我们还将重定向回订阅页面。我们需要设置路由并更改订阅,包括:
from masonite.routes import Get, Patch, Post, Match, RouteGroup
ROUTES = [
# ...snip
RouteGroup(
[
# ...snip
Patch('/subscriptions/@id/favorite', 'PodcastController@do_favorite').name('-favorite- subscription'),
Patch('/subscriptions/@id/unfavorite', 'PodcastController@do_unfavorite').name('-unfavorite- subscription'),
],
prefix='/podcasts',
name='podcasts',
),
]
这是来自routes/web.py
。
<div class="w-full flex flex-col pb-2">
<div class="text-grey-darker">{{ subscription.title }}</div>
<div class="text-sm text-grey">{{ subscription.url }}</div>
<div class="text-sm text-grey">
<form class="inline-flex" action="{{ route('podcasts-favorite- subscription', {'id': subscription.id}) }}" method="POST">
{{ csrf_field }}
{{ request_method('PATCH') }}
<button onclick="event.preventDefault(); this.form.submit()">favorite</button>
</form>
<form class="inline-flex" action="{{ route('podcasts-unfavorite- subscription', {'id': subscription.id}) }}" method="POST">
{{ csrf_field }}
{{ request_method('PATCH') }}
<button onclick="event.preventDefault(); this.form.submit()">unfavorite</button>
</form>
</div>
</div>
这是来自resources/templates/podcasts/_subscription.html
。
因为我们使用了非 GET 和非 POST 请求方法(用于路由),所以我们需要使用表单来启动喜欢/不喜欢的操作。我们使用request_method
视图助手告诉 Masonite 这些是PATCH
请求。我们应该能够使用按钮在我们创建的列表之间切换订阅。
删除数据库数据
我希望我们添加的最后一点功能是取消订阅播客的能力。
这需要的代码比我们已经制作和学习的代码多一点:
<form class="inline-flex" action="{{ route('podcasts-unsubscribe', {'id': subscription.id}) }}" method="POST">
{{ csrf_field }}
{{ request_method('DELETE') }}
<button onclick="event.preventDefault(); this.form.submit()">unsubscribe</button>
</form>
这是来自resources/templates/podcasts/_subscription.html
。
这类似于我们的补丁路由,但是我们需要的适当方法(对于“取消订阅”)是 DELETE。同样,我们需要使用Delete
路由方法,在定义路由时:
from masonite.routes import Delete, Get, Patch, Post, Match, RouteGroup
ROUTES = [
# ...snip
RouteGroup(
[
# ...snip
Delete('/subscriptions/@id/unsubscribe', 'PodcastController@do_unsubscribe').name('-unsubscribe'),
],
prefix='/podcasts',
name='podcasts',
),
]
这是来自routes/web.py
。
而且,我们可以使用delete
方法从 subscriptions 表中删除记录:
def do_unsubscribe(self, request: Request):
DB.table('subscriptions').where('id', request.param('id')).delete()
return request.redirect_to('podcasts-show-subscriptions')
这是来自app/http/controllers/PodcastController.py
。
《梅森尼特》的这一部分有如此多的深度,以至于没有哪一章能够做到公正。这是一种尝试,但你要掌握所有的演讲人必须提供的,这里的唯一方法是深入挖掘文档,并实际使用演讲人做不同的和复杂的事情。
您可以在 https://orator-orm.com/docs/0.9/query_builder.html#introduction
找到这些 DB 语句的详细文档。
通过模型简化代码
既然我们已经掌握了如何编写抽象的数据库查询,我想让我们看看如何通过明智地使用模型来简化这些查询。模型就是我们所说的遵循活动记录数据库模式的对象。起初,这是一个有点棘手的概念。基本思想是我们将数据库表定义为类,用静态方法引用表级动作,用实例方法引用行级动作。
我们可以定义一个新的模型,使用 craft:
craft model Subscription
这会产生一个新的类,如下所示:
from config.database import Model
class Subscription(Model):
"""Subscription Model."""
pass
这是来自app/Subscription.py
。
这个Subscription
类扩展了演说家Model
类,这意味着它已经有了很多减少我们已经编写的代码的魔法。我们可以通过直接引用模型来简化我们的初始检索查询集:
from app.Subscription import Subscription
# ...later
def show_subscriptions(self, view: View):
# favorites = DB.table('subscriptions').where('favorite', True).get()
favorites = Subscription.where('favorite', True).get()
# subscriptions = DB.table('subscriptions').where(
# 'favorite', '!=', True).get()
subscriptions = Subscription.where(
'favorite', '!=', True).get()
return view.render('podcasts.subscriptions', {
'favorites': favorites,
'subscriptions': subscriptions,
})
这是来自app/http/controllers/PodcastController.py
。
类似地,我们可以通过直接引用模型来简化播种、更新和删除:
from app.Subscription import Subscription
# from config.database import DB
from faker import Faker
from orator.seeds import Seeder
class SubscriptionsTableSeeder(Seeder):
def run(self):
fake = Faker()
# DB.table('subscriptions').insert({
# 'url': fake.uri(),
# 'title': fake.sentence(),
# 'favorite': fake.boolean(),
# })
Subscription.create(
url=fake.uri(),
title=fake.sentence(),
favorite=fake.boolean(),
)
# ...or
Subscription.create({
'url': fake.uri(),
'title': fake.sentence(),
'favorite': fake.boolean(),
})
这是来自database/seeds/subscriptions_table_seeder.py
。
第一次运行时,您可能会遇到一个MassAssignmentError
。这是因为 Masonite 可以防止意外的记录批量更新。我们可以通过向模型添加一个特殊属性来绕过这一点:
class Subscription(Model):
__fillable__ = ['title', 'url', 'favorite']
这是来自app/Subscription.py
。
def do_favorite(self, request: Request):
# DB.table('subscriptions').where('id', request.param('id')).update({
# 'favorite': True,
# })
subscription = Subscription.find(request.param('id'))
subscription.favorite = True
subscription.save()
return request.redirect_to('podcasts-show-subscriptions')
def do_unfavorite(self, request: Request):
# DB.table('subscriptions').where('id', request.param('id')).update({
# 'favorite': False,
# })
subscription = Subscription.find(request.param('id'))
subscription.favorite = False
subscription.save()
return request.redirect_to('podcasts-show-subscriptions')
def do_unsubscribe(self, request: Request):
# DB.table('subscriptions').where('id', request.param('id')).delete()
subscription = Subscription.find(request.param('id'))
subscription.delete()
return request.redirect_to('podcasts-show-subscriptions')
这是来自app/http/controllers/PodcastController.py
。
我把前面的 DB 调用留在这里,但是注释掉了,所以我们可以把它们与基于模型的代码进行比较。在某些情况下,使用模型的代码会稍微多一点,但是结果会清晰得多。随着我们继续阅读本书的其余部分,您将会看到更多的模型代码和更少的低级查询代码。
摘要
在这一章中,我们首先了解了如何使用数据库。我们从定义数据库结构一直到以模型的形式表示表和行。这有点像旋风之旅,但也是本书其余部分的基础。
花些时间试验不同的数据库查询和操作,看看它们如何在模型形式中使用。尝试创建一个“订阅”操作,这样在搜索结果中返回的播客会保存到数据库中。如果你能做到这一点,根据你在本章中学到的,那么你就在掌握 Masonite 的火箭船上了!
六、安全
Masonite 的开发考虑到了应用的安全性。当一个版本准备好了,维护者会检查它是否有安全漏洞。Masonite 还利用了诸如 DeepSource 之类的服务,这些服务将扫描每个 pull 请求,查找可能的安全漏洞、代码气味、可能的 bug 和其他代码问题。
然而,认为所有的安全漏洞永远不会进入代码库是愚蠢的,特别是因为攻击应用的新方法可以被发现或发明。还有其他方法来处理这种情况,我们将在本章后面讨论。
另一个重要的提醒是,当我们谈论 Masonite 和安全性时,我们真正谈论的是应用安全性。Masonite 是应用,我们只能保护应用免受漏洞。还有许多其他类型的漏洞是 Masonite 无法控制的。例如,您托管 Masonite 的特定操作系统版本上可能存在漏洞,从而导致攻击。
因此,一定要注意,仅仅因为您的应用是安全的,并不意味着您不容易受到攻击。您将需要了解许多不同的攻击途径,并确保您受到保护。
CSRF 保护
CSRF 代表跨站点请求伪造。最简单地说,它在两个方面有所帮助:
-
防止不良行为者代表用户发出请求。
-
防止恶意代码潜入您的站点,使其看起来像是来自您的站点的按钮或警报,但它实际上是去往另一个站点。
让我们以登录表单为例。用户输入电子邮件和密码,然后点击提交。提交到一个端点,在这里我们检查用户名和密码是否正确,然后让用户登录。但是,是什么阻止了人们简单地向该页面发送 POST 请求呢?现在,任何人都可以通过 Postman 或 cURL 一遍又一遍地点击端点来强行通过。
另一种保护是防止恶意代码被保存到数据库中,然后显示在您的站点上。如果有人可以保存一个 JavaScript <script>
标签,简单地隐藏你的登录按钮并显示他们自己的登录按钮,除非你的输入被发送到他们的服务器,那么这将是对安全性的毁灭性打击。Masonite,以及,通过扩展,演说家和 Jinja,通过清除输入的每一步来防止这种攻击。
让我们更多地讨论 Masonite 如何防范这些攻击。
清洗请求输入
默认情况下,Masonite 会为您清除请求输入,因此您不必担心用户提交恶意的 JavaScript 代码。Masonite 会清理掉<
、>
和&
角色。这都是在幕后完成的。例如,如果我们有这段代码
"""
POST {
'bad': '<script>alert('Give me your bank information')</script>'
}
"""
def show(self, request: Request):
request.input('bad')
那么我们实际上会得到这样一个值
<script>alert('Give me your bank information')</script>
HTML 实体现在被转义了,如果它出现在您的网页上,您的浏览器会简单地将它显示为文本,而不是执行脚本。
你可以通过传入clean=False
来选择不清理某个东西,但是如果你选择这样做的话,风险自负。通过阅读本章的其余部分,您应该成为 Masonite 应用安全方面的专家。
CSRF 代币
我们将在本节中多次讨论 CSRF 代币,所以让我们花几段时间来讨论它实际上是什么以及它来自哪里。
CSRF 令牌背后的技巧非常简单:我们给用户一个令牌,用户将令牌发送回来。令牌只是给客户机的一个不可预测的秘密字符串,服务器也知道它。这允许客户端接收令牌并发回令牌。如果我们给客户端的令牌与我们得到的令牌不同,那么我们将拒绝请求。
我们怎么知道它来自我们的网站?因为 CSRF 令牌只是注册给来自我们网站的用户的一个 cookie。在用户会话开始时(比如当用户第一次访问我们的网站时),会创建一个名为csrf_token
的 cookie,它只是生成一个完全随机的字符串,这个字符串是我们的常量,我们可以稍后检查。
当用户提交表单时,他们也将提交这个 CSRF 令牌。我们将获取该令牌,并将其与我们保存在 cookie 中的令牌进行核对。如果 cookie 值和他们提交的令牌值都匹配,那么我们可以非常安全地假设注册了 cookie 的用户和提交表单的用户是同一个人。
表单提交
Masonite 保护表单提交免受 CSRF 攻击。前面描述的 CSRF 流程正是 CSRF 表单提交保护的工作方式。
<form action=".." method="..">
{{ csrf_field }}
<input ..>
</form>
你在表格中看到的实际上就是
<input type="hidden" name=" token" value="906b697ba9dbc5675739b6fced6394">
当页面完全呈现时。让我们解释一下这是怎么回事,因为它很重要。
首先,它创建了一个隐藏的输入;这意味着它实际上不会显示在表单上,但是它会在那里,除了用户提交的输入之外,它还会提交这个输入。
它做的第二件事实际上是提交名为 __ token
的 CSRF 令牌,这样我们就可以从后端获取它来进行验证。
最后,它将值设置为等于 CSRF 令牌。默认情况下,每个用户会话生成一次 CSRF 令牌(您可以在本章后面的每个请求中添加这个令牌)。
当用户提交表单时,它将检查 cookie 和用户提交的令牌,并验证它们是否匹配。如果匹配,它会让请求通过;如果不匹配,那么它将阻塞请求,因为如果值不匹配,那么用户要么恶意提交表单,要么恶意操纵令牌值。不管怎样,我们都会阻止这个请求。
AJAX 调用
AJAX 调用与表单提交略有不同。AJAX 调用不利用表单提交的相同类型的请求。AJAX 调用通常是通过 JavaScript 完成的,执行所谓的“异步请求”,并在重定向或在 JavaScript 中执行其他逻辑之前等待响应。
因此,前面发送隐藏输入的方法不再有效。但是不要担心,因为您仍然可以在发送请求的同时发送令牌,但是您只是在 Ajax 调用中发送它。
类似于如何使用{{ csrf_field }}
内置变量,您可以使用{{ csrf_token }}
变量来获取令牌。然后,我们可以将该令牌放入 meta 标记中,并将其放入 HTML 的 head 部分:
<head>
<title>Page Title</title>
<meta name="csrf-token" content="{{ csrf_token }}">
</head>
现在,为了让令牌与我们的调用一起传递,我们可以简单地从这个元字段中获取令牌:
let token = document.head.querySelector('meta[name="csrf-token"]').content;
剩下的取决于您使用的 JavaScript 框架。如果您使用的是 jQuery,它可能看起来像这样:
let token = document.head.querySelector('meta[name="csrf-token"]').content;
$.ajax({
type: "POST",
data: {
'email': '[email protected]',
'password': 'secret',
'__token': token
},
success: success,
dataType: dataType
});
请注意,我们传递 __ token
的方式与我们传递表单请求的方式非常相似,但是现在我们需要更加手动地传递它。
有些人错误地试图在服务器之间或应用之间使用 CSRF 令牌。请记住,CSRF 令牌只能在同一应用中使用。如果您需要在应用之间或服务器之间发出请求,您应该考虑使用 JWT 令牌和 API 认证方法或类似的方法,而不是 CSRF 令牌。
密码重置
如果您是第一次开始一个项目,建议运行craft auth
命令,它将为您搭建几个视图、路径和控制器,处理登录、注册、认证和密码重置。您不需要这样做,但这将防止您需要自己构建它,并且它是 100%可定制的。
应用最容易受到攻击的部分之一是密码重置功能。由于应用的这一部分处理的是像密码这样的敏感数据,所以它很容易成为恶意参与者的攻击目标。保护您的密码重置是至关重要的。
重置密码有几种不同的最佳方法,但是让我们解释一下 Masonite 是如何做的,并且我们会解释一些关键点。
表单
密码重置过程的第一部分是进入密码重置表单。默认情况下,如果你使用默认脚手架,这是在/password
路由下。该表单是一个用于输入电子邮件的提交输入和一个简单的提交按钮。
当用户输入他们的电子邮件并点击提交时,我们在数据库中查找该电子邮件,如果它存在,我们使用我们数据库中的电子邮件地址向他们发送一封电子邮件,并附上一个带有令牌的 URL。他们将收到一个成功通知,告知他们一封电子邮件已经发送给他们,并按照那里的指示进行操作。
你可能错过的第二个重要部分是,我们向从我们的数据库中获得的用户的电子邮件发送电子邮件,而我们不会向用户****提交的电子邮件发送电子邮件。换句话说,我们使用用户提交的电子邮件地址在数据库中查找用户,但是我们向数据库中的用户发送电子邮件。乍一看,这似乎很奇怪;我的意思是,他们是相同的电子邮件地址,对不对?不完全是。Unicode 字符和非 Unicode 字符之间存在差异。对盲人来说,它们可能看起来是一样的。
Unicode 攻击
让我们来谈谈 Unicode 攻击,这是密码重置方面最危险的攻击之一。
首先,让我们解释一下什么是 Unicode。Unicode 中的每个字符都有一个数字。这些数字以类似于U+0348
的格式存储,然后可以在所有支持 Unicode 的系统(几乎是所有系统)上解码。它基本上是一个字符编码标准。问题是一些 Unicode 字符与其他 Unicode 字符非常相似。
让我们看看这两个电子邮件地址:
John@Gıthub.com
[email protected]
看起来有点相似,对吧?如果你仔细想想,你可能会发现GitHub
中的i
有些奇怪;上面没有点。
现在让我们试着做一个比较:
>>> 'ı' == 'i'
False
进行比较会返回 False,但现在让我们将它们都转换为 upper:
>>> 'ı'.upper() == 'i'.upper()
True
这是因为一些 Unicode 字符有“大小写冲突”,这意味着当i
被转换为大写,而被转换为大写时,它们都是I
。现在它们匹配了。更可怕的是,我们可以用之前开始的原始电子邮件地址进行同样的比较:
>>> 'John@Gıthub.com'.upper() == '[email protected]'.upper()
True
这些电子邮件地址看起来不同,但实际上是真实的。这就是攻击的来源。
用户可以提交电子邮件地址John@G
来重置密码;我们可能会在数据库中找到[email protected]
的匹配项,然后发送一个电子邮件地址到不正确的John@G``thub.com
地址,从而利用我们的密码重置表单。
这就是为什么我们确保发送电子邮件到我们的数据库中的地址,因为它确保我们将重置指令发送到正确的电子邮件地址。
SQL 注入
既然我们已经谈到了攻击的话题,我们还应该谈谈另一种非常常见的攻击,叫做 SQL 注入。这可能是最常见的攻击,如果你从事软件开发超过 5 分钟,你就会听说过这种攻击。
SQL 注入其实很简单。当您没有正确整理传入的用户数据,然后使用这些数据进行查询时,就会发生 SQL 注入。让我们来看一个简单的代码片段,它可能看起来像 Masonite 和 astorar:
def show(self, request: Request):
User.where_raw(f"email = {request.input('email')}").first()
在正常情况下,这可能会生成如下查询:
SELECT * FROM `users` WHERE `email` = '[email protected]'
看起来很天真,但这是假设请求输入等于[email protected]
。如果它等于更恶意的东西,比如说,[email protected]; drop table users;
?
现在这个查询看起来像这样
SELECT * FROM `users` WHERE `email` = [email protected]; drop table users;
这实际上是两个查询。SQL 将尝试运行第一个查询(可能会抛出语法错误),然后尝试运行第二个有效的查询,并实际删除 users 表。
用户现在将查询“注入”到我们的数据库中,因为我们有一个代码漏洞。
查询绑定
注意,在前面的代码示例中,我们使用了一个原始查询。我演示这个示例是因为代码示例可能如下所示:
def show(self, request: Request):
User.where('email', request.input('email')).first()
在这个例子中,查询实际上有点不同。演说家现在将生成这样一个查询:
SELECT * FROM `users` WHERE `email` = ?
然后,作为第二步的一部分,它会将输入内容发送到数据库。然后,底层数据库包将负责净化输入,以确保在发送到数据库之前没有恶意行为。
批量分配
astorar 有一种非常特殊的与类和数据库交互的方式。你会注意到,演说家模型非常简单,因为演说家为你处理所有的重担。
首先,让我们谈谈两种方法,它们实际上是对演说家的大规模分配。批量赋值是指从大量输入中更新一个表。
例如,这两行代码是批量赋值的:
def show(self, request: Request):
User.create(request.all())
User.find(1).update(request.all())
这段代码是而不是批量赋值:
def show(self, request: Request):
user = User.find(1)
user.admin = request.input('is_admin')
user.save()
演说家的设计模式为一种称为大规模任务攻击的攻击打开了大门。
让我们先来看看这段代码,然后我们再来浏览一下:
"""
POST {
'email': '[email protected]',
'name': 'Joe Mancuso',
'password': 'secret'
}
"""
def show(self, request: Request):
User.create(request.all())
如果我们有一个简单的请求输入,这个查询可能如下所示:
INSERT INTO `users` (`email`, `name`, `password`)
VALUES ('[email protected]', 'Joe Mancuso', 'secret')
这看起来很天真,但是它为用户传递任何信息打开了大门。例如,如果他们是管理员,他们可能会传入,他们所要做的就是传入这些值:
"""
POST {
'email': '[email protected]',
'name': 'Joe Mancuso',
'password': 'secret',
'admin': 1
}
"""
def show(self, request: Request):
User.create(request.all())
这将生成如下查询:
INSERT INTO `users` (`email`, `name`, `password`, `admin`)
VALUES ('[email protected]', 'Joe Mancuso', 'secret', '1')
现在,用户只需非常简单地让自己成为管理员。
可填充
为了防止这种攻击,演说家制作了一个 __ fillable__
属性,你可以把它放在你的模型上。现在我们可以这样做:
class User:
___fillable__ = ['email', 'name', 'password']
现在,它将忽略任何试图进行质量分配的字段。回到易受攻击的代码片段:
"""
POST {
'email': '[email protected]',
'name': 'Joe Mancuso',
'password': 'secret',
'admin': 1
}
"""
def show(self, request: Request):
User.create(request.all())
它现在将正确地生成如下查询:
INSERT INTO `users` (`email`, `name`, `password`)
VALUES ('[email protected]', 'Joe Mancuso', 'secret')
它将忽略不在 __ fillable__
属性内的所有内容。
克-奥二氏分级量表
大多数人与 CORS 的互动都是试图访问实现 CORS 的服务器,然后试图绕过 CORS,因为人们不太理解它。
CORS 代表跨源资源共享,它允许服务器通过 HTTP 头告诉浏览器允许访问哪些特定资源以及应该如何访问这些资源。例如,如果请求来自site.com
,服务器可能会告诉浏览器只向example.com
发送请求。也许我们有某种微服务,我们想确保只有来自我们在site.com
的应用。
浏览器处理这种情况的方式是,它们做一些称为预检请求的事情,这是一个简单的 HTTP 请求,它们在发送有效载荷之前发送。该飞行前请求实质上用于“侦察”服务器,并检查 CORS 指令是否与它们将要发送的内容相匹配。如果它们不匹配,那么浏览器将抛出一个与 CORS 指令无效相关的错误。
这不是保护应用的可靠方法,但它确实增加了一层安全性和请求验证。
CORS 提供商
Masonite 允许您的应用返回 CORS 标头,因此我们可以帮助保护我们的应用。这样做很简单。我们只需将该提供商添加到您的AppProvider
下方的提供商配置列表中:
# config/providers.py
# ...
from masonite.providers import CorsProvider
PROVIDERS = [
AppProvider
CorsProvider,
# ...
]
现在,您的服务器将开始返回以下 CORS 标题,浏览器将开始执行您的规则。
最后,你可以在config/middleware.py
文件的底部添加一些合理的默认值:
# config/middleware.py
# ...
CORS = {
'Access-Control-Allow-Origin': "*",
"Access-Control-Allow-Methods": "DELETE, GET, HEAD, OPTIONS, PATCH, POST, PUT",
"Access-Control-Allow-Headers": "Content-Type, Accept, X-Requested-With",
"Access-Control-Max-Age": "3600",
"Access-Control-Allow-Credentials": "true"
}
这将在人们访问您的应用时设置这些标题:
Access-Control-Allow-Origin: *,
Access-Control-Allow-Methods: DELETE, GET, HEAD, OPTIONS, PATCH, POST, PUT,
Access-Control-Allow-Headers: Content-Type, Accept, X-Requested-With,
Access-Control-Max-Age: 3600,
Access-Control-Allow-Credentials: true
您可以通过修改中间件文件中的CORS
变量的键和值来修改头。如果看不到它,您需要创建它:
# config/middleware.py
# ...
CORS = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "DELETE, GET, HEAD, OPTIONS, PATCH, POST, PUT",
"Access-Control-Allow-Headers": "Content-Type, Accept, X- Requested-With",
"Access-Control-Max-Age": "3600",
"Access-Control-Allow-Credentials": "true"
}
请随意修改这些标题,甚至查找您可以添加的其他标题。
安全标题
类似于 CORS 头为源之间的 HTTP 请求设置规则,安全头为所有 HTTP 请求设置规则。
Masonite 在使用SecureHeadersMiddleware
中间件时内置的一些头文件包括
-
严格的运输安全
-
x-框架-选项
-
X-XSS 保护
-
x-内容-类型-选项
-
推荐人-策略
-
缓存控制
-
杂注
这些都很神秘,所以让我们试着逐一解释它们的用途。因为有很多值,所以我不会重复每个值的含义。我会解释每个选项的用途,你可以研究一下你需要为你的情况设置什么值。
标题的含义
Strict-Transport-Security
头告诉浏览器应该通过 HTTPS 而不是 HTTP 发出请求。这也被称为 HSTS 头球攻门(“??”HTTPStrictTtransportSsecurity”)。
X-Frame-Options
头告诉浏览器它是否应该呈现<iframe>
、<frame>
、<embded>
或<object>
中的页面。这些选项存在已知的漏洞,因为用户可以将自己的网站注入这些 iFrames。如果用户在其中一个被劫持的框架中输入信息,这可能会导致用户的凭据被盗。只要你的网站不容易受到 CSRF 的攻击,那么你应该没问题。
如果浏览器本身检测到任何跨站脚本攻击的迹象,那么X-XSS-Protection
头告诉浏览器阻止任何请求。
X-Content-Type-Options
头防止嗅探 MIME 类型(比如图像)。一些 MIME 类型包含可执行代码,有助于防止这种情况。
Referrer-Policy
标题详细说明了当一个请求从你的网页转到另一个网页时,Referrer 标题中应该有多少信息。通常网站可以通过阅读这个标题来判断用户来自哪里。
Cache-Control
为浏览器提供了关于应该为请求和响应缓存多少信息和什么类型的信息的指令。
最后,Pragma
头与Cache-Control
头本质上是一样的,但用于向后兼容。
使用安全中间件
您可以通过将这个中间件导入到您的config/middleware.py
文件中来轻松使用它:
from masonite.middleware import SecureHeadersMiddleware
HTTP_MIDDLEWARE = [
# ...
SecureHeadersMiddleware,
]
还可以通过在中间件配置文件的底部添加一个SECURE_HEADERS
变量来随意覆盖任何默认值:
from masonite.middleware import SecureHeadersMiddleware
HTTP_MIDDLEWARE = [
# ...
SecureHeadersMiddleware,
]
SECURE_HEADERS = {
'X-Frame-Options' : 'deny'
}
放
当在社区中发现安全漏洞时,维护人员会意识到这一点,并创建一个安全版本。这些可能是次要版本中的突破性变化,这通常违反了 Masonite 的发布策略,但应用突破比用户数据被盗或服务器、应用或数据库被破坏要好。
这就是为什么在社区中很重要,这样每个人都可以知道发生了什么,我们都可以在必要的时候尽可能地保持透明。
还会有关于安全版本是什么、我们如何到达这里以及如何前进的文档。一些安全版本可以在 Masonite 端通过简单地创建一个新版本来完成,而一些可能需要在应用本身中打补丁。
当您使用 Masonite 进行开发时,只需确保您使用的是最新的次要版本。例如,如果您使用 Masonite 2.2 进行开发,那么请确保第三个数字始终是最新的,比如从 2.2.10 升级到 2.2.11。这些对保持最新很重要。次要版本有时每隔几天或几周发布一次,没有固定的时间表。您应该经常检查 Masonite 是否创建了一个版本,以及那个版本是什么,并且您应该在将更改转移到生产服务器之前进行升级和测试。
CVE 警报
既然我们在讨论发布和警报,那么就有必要提一下 CVE 警报。CVE 代表共同的弱点和暴露。它基本上是在软件中发现的许多暴露的巨大档案。如果一个备受关注的 Python 包发现了漏洞,他们将创建一个 CVE,并为其分配一个每个人都可以引用的标识号。
如果你在 GitHub 上托管你的代码,那么当你的任何包符合最近发布的 CVE 时,他们会给你发送通知,并推荐解决方案。这些都不应该被忽略,如果您使用的软件包有漏洞,您应该尽快升级或修复问题。
大多数解决方案都是简单的升级,但是您应该阅读附加文档的链接,以了解您的应用是否存在风险,以及是否有任何数据可能已经暴露。
如果你想关注他们,他们甚至有一个推特页面。
加密
加密是一个有趣的话题,因为它是一种屏蔽值的艺术,只允许特定的应用、用户或服务器看到该值。
Masonite 使用著名的加密软件包进行加密。这个包在 Python 社区中非常有名,有大量的包需要它。
Masonite 使用这个包中的 Fernet 加密算法来进行大多数加密。下面是他们的文档中关于 Fernet 是什么的一段:
Fernet 保证使用它加密的消息在没有密钥的情况下不能被操纵或读取。Fernet 是对称(也称为“密钥”)认证加密的实现。
你可能已经意识到梅索尼特有一把秘密钥匙。您可能在第一次安装 Masonite 时就已经看到了,或者您可能需要使用craft key
命令生成一个。
这个秘密密钥不应该公开。如果这个密钥是公开的,您应该立即生成一个新的密钥。Masonite 中有许多面向公众并使用该密钥加密的内容,如果恶意用户获得了该密钥,他们就能够解密一些敏感信息。
Masonite 本身没有像 cookie 一样公开的内容,但是如果您作为开发人员创建了一个包含敏感信息的 cookie(您可能不应该这样做),那么这些信息可能会有风险。
您可以在您的.env
文件中找到您的密钥,它看起来像这样:
-4gZFiEz_RQClTjr9CQMvgZfv0-YnTuO485GG_BMecs=
事实上,如果不会对用户造成太大伤害,您应该定期轮换您的密钥。默认情况下,最糟糕的情况是您的用户将被注销,因为 Masonite 将删除它加密但无法解密的任何 cookies。有了新的密钥,它将无法解密用不同密钥加密的值。
如果这对你来说没问题,那么你应该尽可能每周轮换你的钥匙。
编码与加密
这是一个非常重要的话题,因为许多人不知道这两者的区别。编码的内容通常使用某种标准编码算法,如 Base64。
任何人都可以使用几乎任何程序对编码值进行解码。
不应编码敏感信息;应该是加密的。
加密通常使用通过秘密密钥的加密。这意味着一个值被转换(加密)成一个散列,并且不能被解密,除非使用相同的加密密钥将其解密。
一些加密被称为单向加密,只能转换成散列,而永远不能转换回来。所有的查找都需要进行相同的单向转换,转换成相同的散列,并检查相似性。一些单向散列是像 MD5 和 SHA1 这样的算法,它们已经不再受更好的可用加密算法的青睐。
因此,如果我说的是加密或编码,那么如果你不知道我的意思,请务必参考这里的这一部分。
密码加密
Masonite 使用 Bcrypt 进行密码加密。这是密码常用的最强加密方法之一。
要在 Masonite 中使用 Bcrypt,您可以使用一个非常有用的助手:
>>> from masonite.helpers import password
>>> password('secret')
'$2b$12$DFBQYaF6SFZZAcAp.0tN3uWHc5ngCtqZsov3TkIt30Q91Utcf9RAW'
现在,您可以自由地将该密码存储在数据库中,并使用 Masonite 的身份验证类来验证它。
饼干
Cookies 也是另一个加密点。默认情况下,Masonite 甚至会加密应用生成的所有 cookies。这可以防止任何恶意用户看到您的 cookies 的值。如果您下载的一个 JavaScipt 包碰巧可以访问您的 cookie(它不能,稍后将详细介绍),我们仍然可以,因为他们将获得您的 cookie 的加密版本,并且不能对它们做任何事情。
JavaScript 无法访问我们的 cookie 的原因是因为我们在 cookie 上设置了一个HTTPOnly
标志,这意味着我们的 cookie 只能通过 HTTP 请求读取。换句话说,应该只有我们的应用和服务器能够读取它们。这是另一个安全点。仅仅因为您的代码没有任何漏洞并不意味着您可能从节点包管理器下载的 1000 个包中的任何一个没有漏洞。您引入到应用中的任何 JavaScript 代码(或者实际上是任何代码)都可能成为恶意参与者的攻击点。
自己签名
如果你想完全像 Masonite 那样做,你甚至可以签署你自己的代码。
默认情况下,KEY
环境变量用于签名:
from masonite.auth import Sign
sign = Sign()
signed = sign.sign('value') # PSJDUudbs87SB....
sign.unsign(signed) # 'value'
在幕后,Masonite 调用这个类。如果你需要自己签名,比如用户提供的信息,你可以。如果你可以说连你自己都看不到用户的信息,这可能是一个很好的广告标志。
一旦某个值被加密,就必须使用加密它的同一个密钥对它进行解密。如果某些值是用已撤销的密钥签名的,那么您可能无法使用新的密钥解密这些值。该值将永远被加密,直到用来加密它的密钥被用来解密它。
在对值进行签名时请记住这一点,否则您的密钥就会泄露。您将需要更改您的密钥,但是所有已签名的字符串将永远无法被签名。
七、认证
在前几章中,我们学习了很多关于创建表单和使用它们填充数据库的知识。对于 web 应用来说,很常见的一件事是尝试将这种数据发送给用户帐户。
对于你所使用的服务,你可能有很多不同的“用户账户”。也许你在推特或脸书上。这些将你发布的数据和你喜欢看的东西与个性化的“身份”联系起来。
Masonite 在这方面提供了很多工具,这样我们就不必每次构建这些用户注册系统时都做繁重的工作。因此,在这一章中,我们将学习如何使用这个工具。
我如何认证用户?
您可能会惊讶地发现,我们已经使用了认证用户所需的所有不同位。需要明确的是,有许多不同种类的身份验证。
我们将要学习的这种方法将用户凭据与数据库记录进行比较,并记住有效凭据的存在,这样用户就可以在未经身份验证的用户无法到达的地方移动。
为此,我们将了解 Masonite 如何帮助我们建立接受这些凭证的安全表单。我们将查看一些代码,这些代码将凭据与数据库中的凭据进行比较,并告诉我们用户凭据是否有效。
最后,我们将看到成功的登录是如何被“记住”的,并且还可以与其他数据库记录结合使用。
使用提供的工具
此代码可在 https://github.com/assertchris/friday-server/tree/chapter-8
找到。
我们需要做的第一件事是使用 craft 命令生成一些新的与 auth 相关的文件:
craft auth
这做了很多,所以让我们来看一下每一部分。首先,它创建了一系列新路由。我们可以在已经定义的路由之后看到它们:
ROUTES = ROUTES + [
Get().route('/login', 'LoginController@show').name('login'),
Get().route('/logout', 'LoginController@logout').name('logout'),
Post().route('/login', 'LoginController@store'),
Get().route('/register', 'RegisterController@show').name('register'),
Post().route('/register', 'RegisterController@store'),
# Get().route('/home', 'HomeController@show').name('home'),
Get().route('/email/verify',
'ConfirmController@verify_show').name('verify'),
Get().route('/email/verify/send', 'ConfirmController@send_verify_email'),
Get().route('/email/verify/@id:signed', 'ConfirmController@confirm_email'),
Get().route('/password', 'PasswordController@forget').name('forgot.password'),
Post().route('/password', 'PasswordController@send'),
Get().route('/password/@token/reset', 'PasswordController@reset').name('password.reset'),
Post().route('/password/@token/reset', 'PasswordController@update'),
]
这是来自routes/web.py
。
使用ROUTES = ROUTES + [...]
插入代码的方式意味着它不会覆盖现有的路由。您可能希望更改这些内容的添加格式。
例如,您可能更喜欢将它们放在一个组中。没关系。只要确保它们指向相同的控制器,并相应地调整您调用的 URL。
auth
命令做的下一件事是生成这些控制器。主要的是RegisterController
和LoginController
,主要是因为大部分“认证”工作都是在其中完成的。RegisterController
提供显示注册表单和保存用户帐户的操作。
LoginController
提供显示登录表单和根据数据库记录检查凭证的操作。
奇怪的是“检查这些凭证”方法被命名为store
,但是这可能是为了与 POST 请求使用的其他动作保持一致。
其他控制器用于密码重置和电子邮件验证——内置的强大功能,但完全是可选的。
请随意重命名这些控制器及其动作,使它们更容易被您理解。只要记得更新相关的路由,所以一切都保持工作。
第三个变化是auth
添加的视图。它们都放在resources/templates/auth
文件夹中,并与新控制器中的动作相对应。花些时间浏览一下控制器和模板,感受一下它们是如何组合在一起的。
等等,我不是已经看过“用户”的东西了吗?
您可能已经注意到了代码库中一些与用户相关的文件,特别是User
模型和CreateUsersTable
迁移。这些在所有新的 Masonite 安装中都存在,部分原因是 auth 配置也存在(并且依赖于模型,而模型依赖于迁移的存在)。
这是一个奇怪的依赖链,但它意味着新鲜的应用包括拼图的一小部分,其余部分来自craft auth
。
在使用注册和登录表单之前,我们需要确保所有内容都已迁移:
craft migrate:refresh --seed
如果您在这里遇到错误,请记住您需要安装一个 MySQL 驱动程序(使用pip install mysqlclient
)并在.env
中配置正确的数据库细节。
您现在应该会看到一个users
表,这是User
模型获取和放置数据的地方。让我们创建一个新账户,运行craft serve
,进入注册页面,如图 7-1 所示。
图 7-1
创建新用户
这应该会让我们自动登录,但是万一没有登录(或者你想稍后登录),我们可以转到/login
页面(图 7-2 )并在那里使用相同的凭证。
图 7-2
登录
注意在users
表中有一条新记录。那是你!
我如何使用不同的字段?
默认情况下,email
字段用于识别用户。这是将在登录方法中使用的字段名称:
auth.login(request.input('email'), request.input('password'))
这是来自app/http/controllers/LoginController.py
。
根据您使用的 Masonite 版本,您的模型可能为此定义了一个常数:
__auth__ = 'email'
这是来自app/User.py
。
同样,根据您的版本,各种新控制器可能会使用此常量作为字段名称:
auth.login(
request.input(auth_config.AUTH['model']. __auth__),
request.input('password')
)
这是来自app/http/controllers/RegisterController.py
。
我更喜欢所有的引用都使用相同的值,所以如果我看到多种变化,我会把它们都改为使用常量或硬编码的值。我建议您也这样做,这样代码更容易理解。
在任何情况下,这都是 Masonite 将登录凭证与现有数据库记录进行比较的方式。这也意味着我们可以把“电子邮件”换成另一个领域。核心身份验证代码引用常量,因此我们可以更改它:
__auth__ = 'name'
这是来自app/User.py
。
if auth.login(
request.input(auth_config.AUTH['model']. __auth__),
request.input('password')
):
return request.redirect('/home')
这是来自app/http/controllers/LoginController.py
。
如果您要更改这个字段,我建议您将任何字段切换为唯一字段。您可以在迁移级别和验证级别做到这一点。
如何让用户自动登录?
RegisterController
是让用户自动登录的一个很好的例子,但这不是唯一的方法。假设我们知道用户是谁,但是手头没有他们的登录凭证。在这种情况下,我们可以通过他们的 ID 登录他们:
auth.login_by_id(user.id)
原来,可能有这样的情况,您拥有用户(和他们的 ID),他们需要做一些需要认证的事情,但是您不希望他们保持登录状态。
auth
对象也有一个once
方法,它让用户登录,而不“记得”他们已经登录,在后续请求中:
auth.once().login_by_id(user.id)
# ...or
auth.once().login(
request.input('email'),
request.input('password')
)
注销用户
虽然我们的主题是让用户登录,但是知道用户可以用另一种方法注销也是有用的:
def logout(self, request: Request, auth: Auth):
auth.logout()
return request.redirect('/login')
这是来自app/http/controllers/LoginController.py
。
使用令牌(JWT)而不是凭据
Masonite 提供不同的认证方式。我们的应用实际上只需要凭证身份验证,但是知道 JWT(基于令牌)身份验证也受支持是很有用的。
查看官方文档了解更多关于配置它的信息: https://docs.masoniteproject.com/security/authentication#jwt-driver
。
如何保护我的部分应用?
让用户登录到您的应用与保护部分内容不被未登录的用户看到和/或使用密切相关。让我们来看看几种“保护”应用的方法。
第一种方法是在我们运行craft auth
命令时介绍给我们的。再看HomeController
:
def show(self, request: Request, view: View, auth: Auth):
if not auth.user():
request.redirect('/login')
return view.render('home', {
'name':
request.param('name') or request.input('name'),
})
这是来自app/http/controllers/HomeController.py
。
当我们对用户是否登录感到好奇时,我们可以将auth: Auth
对象引入到我们的操作中。auth().user()
要么为空,要么有一个与当前登录用户相关联的User
模型对象。
这是有效的,但我猜它会导致大量的重复。此外,我们可能会忘记将它添加到所有需要它的操作中。当我定义路由时,我发现决定一个动作是否应该被“保护”要容易得多:
RouteGroup(
[
Match(['GET', 'POST'], '/@name', 'HomeController@show').name('with-name'),
Match(['GET', 'POST'], '/', 'HomeController@show').name('without-name'),
],
prefix='/home',
name='home-',
middleware=['auth'],
),
这是来自routes/web.py
。
我们可以在路由组中组织受保护的路由,并为整个组定义一个middleware
参数。auth
中间件内置在 Masonite 中,所以我们不必自己定义。
我们没有花太多时间学习中间件,但这是下一章的主题,所以我们将深入探讨。
或者,我们可以保护个别路由:
Match(['GET', 'POST'], '/home', 'HomeController@show')
.name('without-name')
.middleware('auth'),
这是来自routes/web.py
。
这两种方法(命名参数和middleware
方法)都接受一个中间件列表或一个单独的中间件字符串。这取决于你是否想要采取声明性的方法来保护路由,或者命令性的方法来保护动作本身。
我如何确保有效的电子邮件?
我希望我们以简单了解一下电子邮件验证来结束本章。我不想深究其中的机制,因为我觉得 Masonite 自动生成的代码对于大多数情况来说都是惊人的。
当我们从用户那里寻找“有效”的电子邮件地址时,只有一个有效的解决方案。当然,我们可以使用表单验证来判断一个电子邮件地址看起来是否有效,但是知道它是否有效的唯一有效方法是向它发送电子邮件。
这就是电子邮件验证的目的。这是一种确保用户的电子邮件地址是他们所说的那个地址的方法,这样我们就可以与他们进行有效的沟通。
为此,CreateUsersTable
迁移包含一个名为verified_at
的时间戳,auth
命令生成路由、控制器和视图,以允许用户验证他们的电子邮件地址。
要启用它,我们需要更改默认的User
模型:
from config.database import Model
from masonite.auth import MustVerifyEmail
class User(Model, MustVerifyEmail):
__fillable__ = ['name', 'email', 'password']
__auth__ = 'email'
这是来自app/User.py
。
此外,我们需要引入一种新的中间件,它将促使用户验证他们的电子邮件地址:
RouteGroup(
[
Match(['GET', 'POST'], '/@name', 'HomeController@show').name('with- name'),
],
prefix='/home',
name='home-',
middleware=['auth', 'verified'],
),
Match(['GET', 'POST'], '/home', 'HomeController@show')
.name('without-name')
.middleware('auth', 'verified'),
这是来自routes/web.py
。
现在,当用户的verified_at
字段为空时,他们将被提示验证他们的电子邮件地址,即使他们已经登录,如图 7-3 所示。
图 7-3
提示电子邮件验证
您可能需要配置 Masonite 发送电子邮件的方式,然后才能看到这些验证电子邮件: https://docs.masoniteproject.com/useful-features/mail#configuring-drivers
。在第十一章中,当我们在应用中添加通知时,我们会深入研究这个配置。
摘要
在这一章中,我们学习了 Masonite 提供的认证工具,如何使用它,以及如何定制体验以适应我们的应用。
这是很强大的东西,毫无疑问,您将需要在您可能要构建的一些应用中使用它。最好现在就抓住它!
在下一章,我们将更深入地研究中间件。我们将了解它是如何工作的,有多少是新应用自带的,以及如何制作自己的应用。
八、创建中间件
现在我们知道了什么是中间件,让我们看看中间件是如何创建的。
让我们创建一个中间件,然后讨论中间件的每个部分。我们暂时保持简单,只创建一个简单的 Hello World 中间件,并通过它进行讨论。然后,我们将进入更复杂的特定应用的中间件。
如果你还不明白,我们将为此使用一个 craft 命令:
$ craft middleware HelloWorld
这将在app/http/middleware
目录中为您创建一个HelloWorldMiddleware.py
文件。
这个目录没有什么特别的,所以如果您愿意,可以将您的中间件移出这个目录。只需确保您的config/middleware.py
文件中的任何导入都指向新位置。这是更多的背景信息,所以不要觉得你需要移动它们;这个目录很好。
构建中间件
如果我们看这个类,你会发现中间件是一个非常简单的类,有三个部分。让我们看一下每个部分,这样我们就知道每个部分在做什么以及可以用它做什么。
值得注意的是,HTTP 中间件和路由中间件的构造完全相同。使它成为 HTTP 或路由中间件的唯一因素是我们如何向 Masonite 注册它,这将在下一节中讨论。
初始化程序
class HelloWorldMiddleware:
def __init__(self, request: Request):
self.request = request
# ...
初始化器是一个简单的__init__
方法,就像任何其他类一样。唯一特别的是它是由容器来解析的。因此,您可以在您的__init__
方法中键入提示应用依赖关系,这将像您的控制器方法一样解析类。
由于许多中间件需要请求类,Masonite 将为您键入提示请求类。如果您的特定中间件不需要它,那么您可以毫无问题地移除它。
before 方法
class HelloWorldMiddleware:
#...
def before(self):
print("Hello World")
before 方法是另一个简单的方法。此方法中的任何代码都将负责在调用控制器方法之前运行。在内置的auth
中间件中,这是用来检查用户是否被认证并告诉请求类重定向回来的方法。
这个方法也可以接受我们从 routes 文件传入的变量。我们将在本章的后面讨论这一点。
after
法
class HelloWorldMiddleware:
#...
def after(self):
print('Goodbye World')
除了代码是在控制器方法被调用后运行之外,after
方法与before
方法非常相似。例如,如果我们想缩小 HTML 响应,这就是逻辑的走向。
这个方法也可以接受我们从 routes 文件传入的变量。我们将在本章的后面讨论这一点。
注册中间件
既然我们已经创建了中间件类,我们可以向 Masonite 注册它。我们可以将它导入到我们的config/middleware.py
文件中,并放入两个列表中的一个。我们可以把它放在HTTP_MIDDLEWARE
列表或者ROUTE_MIDDLEWARE
字典里。
HTTP 中间件
还记得之前我们说过两个中间件的构造是一样的,所以如果你想让中间件在每个请求上运行,就把它放在HTTP_MIDDLEWARE
类中。
这看起来会像
from app.http.middleware.HelloWorldMiddleware import
HelloWorldMiddleware
HTTP_MIDDLEWARE = [
LoadUserMiddleware,
CsrfMiddleware,
ResponseMiddleware,
MaintenanceModeMiddleware,
HelloWorldMiddleware, # New Middleware
]
注意 HTTP 中间件只是一个列表,所以您可以将它添加到列表中。您的中间件的顺序可能并不重要,但实际上可能很重要。
中间件的运行顺序与您将它放入列表的顺序相同。因此LoadUserMiddleware
将首先运行,然后HelloWorldMiddleware
将最后运行。因为我们的HelloWorldMiddleware
只是打印一些文本到终端,我们可以把它添加到列表的底部,因为它实际上不依赖于任何东西。
另一方面,如果中间件依赖于用户,那么我们应该确保我们的中间件在LoadUserMiddleware
之后。这样,用户被加载到请求类中,然后我们的中间件可以访问它。如你所知,LoadUserMiddleware
正是因为这个原因而成为第一。
现在,HTTP 中间件已经完全注册到 Masonite,它现在可以在每个请求上运行。稍后,我们将看到输出是什么样子的。在此之前,我们将讨论如何注册路由中间件。
路由中间件
现在,路由中间件再次与 HTTP 中间件相同,但是注册它有点不同。我们可以马上注意到路由中间件是一个字典。这意味着我们需要将它绑定到某个键上。
这个密钥是我们将用来把中间件附加到我们的路由上的。我们想给这个中间件起一个简短而甜蜜的名字。我们可以使用键helloworld
作为键,并使中间件成为字典中的值。这将看起来像
from app.http.middleware.HelloWorldMiddleware import
HelloWorldMiddleware
ROUTE_MIDDLEWARE = {
'auth': AuthenticationMiddleware,
'verified': VerifyEmailMiddleware,
'helloworld': HelloWorldMiddleware,
}
命名惯例由你决定,但我喜欢尽量用一个词来命名。如果你需要拆分成一个以上的单词,我们可以将其命名为hello.world
或hello-world
之类的东西。只是一定不要使用:
字符,因为 Masonite 将拼接我们的 routes 文件中的那个键。稍后,您将在“路由”部分看到更多这方面的内容。
使用中间件
我们已经讨论了中间件的用途,我们可以创建的不同类型的中间件,如何创建这两种中间件,以及如何向 Masonite 注册这两种中间件。
现在我们将最终了解如何使用我们创建的中间件。现在 HTTP 中间件,也就是我们放在列表中的那个,已经准备好了。我们实际上不需要做任何进一步的工作。
如果我们开始导航我们的应用并打开我们的终端,那么我们可能会看到类似于
hello world
INFO:root:"GET /login HTTP/1.1" 200 10931
goodbye world
hello world
INFO:root:"GET /register HTTP/1.1" 200 12541
goodbye world
hello world
INFO:root:"GET /dashboard HTTP/1.1" 200 4728
goodbye world
请注意,在我们的控制器方法被点击之前和之后,我们开始看到hello world
和goodbye world
打印语句。
另一方面,路由中间件有点不同。我们需要通过在 routes 文件中指定密钥来使用这个中间件。
例如,如果我们想要使用我们之前制作的helloworld
中间件,我们可以将它添加到一个看起来像这样的路由中
Get('/dashboard', 'YourController@show').middleware('helloworld')
这将只为这个路由运行中间件,而不为任何其他路由运行。
回顾我们以前的终端输出,我们的新应用将类似于这样:
INFO:root:"GET /login HTTP/1.1" 200 10931
INFO:root:"GET /register HTTP/1.1" 200 12541
hello world
INFO:root:"GET /dashboard HTTP/1.1" 200 4728
goodbye world
请注意,我们只将中间件放在了/dashboard
路由上,因此它将只为该特定路由执行:
Get('/dashboard',
'YourController@show').middleware('helloworld:Hello,Joe')
还记得我们之前说过要确保你的中间件别名中没有一个:
吗,因为 Masonite 会拼接在那个上面?这就是它的意思。Masonite 将拼接在:
字符上,并将其后的所有变量传递给中间件。
既然我们已经说过要将这些值传递给中间件,那么让我们看看中间件将会是什么样子:
class HelloWorldMiddleware:
#...
def before(*self*, *greeting*, *name*):
pass
def before(*self*, *greeting*, *name*):
pass
无论我们传递给路由什么,before
和after
中间件都需要这两个参数。
正如您可能已经猜到的,参数的顺序与您在路由中指定的顺序相同。所以greeting
将会是Hello
,name
将会是Joe
。
中间件堆栈
中间件堆栈是另一个简单的概念。有时候,您的一些路由看起来非常重复,一遍又一遍地使用同一个中间件。我们可以将中间件分组到中间件“栈”或中间件列表中,以便在一个别名下运行所有这些中间件。
例如,假设我们有一些中间件,我们想在一个别名下运行。我们可以用一个更好的例子,我们可能会看到自己一遍又一遍地使用非常相似的中间件:
ROUTES = [
(Get('/dashboard', 'YourController@show')
.middleware('auth', 'trim', 'admin')),
(Get('/dashboard/user', 'YourController@show')
.middleware('auth', 'trim', 'admin')),
]
注意中间件似乎有点重复。我们在这里可以做的是创建一个中间件堆栈来对它们进行分组。这看起来像
ROUTE_MIDDLEWARE = {
'auth': AuthenticationMiddleware,
'verified': VerifyEmailMiddleware,
'dashboard': [
AuthenticationMiddleware,
TrimStringsMiddleware,
AdminMiddleware,
]
}
然后,我们可以稍微重构一下我们的路由,以使用这个堆栈:
ROUTES = [
(Get('/dashboard', 'YourController@show')
.middleware('dashboard')),
(Get('/dashboard/user', 'YourController@show')
.middleware('dashboard')),
]
九、使用助手
我们已经谈了很多,所以我们要换个话题,谈谈助手。简而言之,助手是我们可以在任何地方使用的功能,它比我们原本可以做的更快或更有效地为我们做事。不看代码很难解释它们的用法,所以这就是我们要做的。
这些助手在全球范围内都是可用的,因此您不需要导入其中的大部分。重要的时候,我会告诉你例外是什么。
请求和验证助手
此代码可在 https://github.com/assertchris/friday-server/tree/chapter-10
找到。
我们对请求类并不陌生。我们通常将它注入到控制器动作中:
def show(self, request: Request, view: View):
return view.render('home', {
'name': request.param('name'),
})
如果我们想使用来自其他地方的请求呢?我不是在说我们是否应该,而是在说,“我们可以吗?”
我们可能想用它的一个明显的地方是视图:
@extends 'layout.html'
@block content
hello {{ request().param('name') }}
@endblock
如果您喜欢这种风格,您可能也喜欢在操作中使用请求帮助器:
from masonite.auth import Auth
from masonite.view import View
class HomeController:
def show(self, view: View, auth: Auth):
return view.render('home', {
'name': request().param('name') or request().input('name'),
})
类似地,我们可以通过使用 Auth helper 来缩短授权代码:
from masonite.view import View
class HomeController:
def show(self, view: View):
return view.render('home', {
'name': request().param('name') or auth().email,
})
这是来自app/http/controllers/HomeController.py
。
auth()
功能非常有用,但是要小心使用。如果用户没有登录,那么auth()
将返回False
。您的代码应该考虑到这一点。它在视图层也很棒:
<!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">
@if (auth())
<a href="{{ route('logout') }}">log out</a>
@else
<a href="{{ route('login') }}">log in</a>
@endif
@block content
<!-- template content will be put here-->
@endblock
</div>
</body>
</html>
这是来自resources/templates/layout.html
。
如果用户没有登录,auth()
函数将返回None
,可以根据用户会话的存在重用该函数来切换 UI。这也是另一个帮手的例子。
路由助手
路径助手对于较大的应用是必不可少的,但是您必须命名您的路径才能让它工作:
Get('/profile/@id', 'ProfileController@show').name('profile')
我们可以使用route()
函数构建任何命名的路由,包括我们已经为其定义了参数的路由:
<a href="{{ route('profile', { 'id': auth().id }) }}">public profile</a>
容器和解析助手
有时候,我们可能想在服务容器中添加我们自己的类和函数,我们在第四章中已经了解到了。您可能并不总是处于可以访问“应用”的操作中,但是您可以访问容器助手:
from app.Service import Service
container().bind('Service', Service)
# ...later
container().make('Service')
Service
就是一个例子,这里。可以把它看作是下一个应用可能需要的自定义类的占位符。
这对于扩展容器中已经存在的内容以及从其他上下文(如视图和模型)访问存储在容器中的内容非常有用。同样,解析函数的依赖关系也很有用,就像 Masonite 自动解析动作一样。
下面是一个如何自动解析控制器动作的request
参数的示例:
from masonite.helpers import compact
# ...later
def respond(request: Request):
action = request.param('action')
name = request.param('name')
if (action == 'read'):
return view('read.html', compact(name))
if (action == 'write'):
return view('write.html', compact(name))
def handle(self):
return resolve(respond)
resolve helper 使用另一个函数,从容器中解析出它需要的参数。这也是我们的第一个非全局助手(compact()
函数)的例子,它接受一个变量列表并返回一个字典,其中每个键是字符串变量名,每个值是变量值。
非全局助手只是你仍然需要在每个使用它们的文件中导入的助手。在这里,我们正在导入 compact helper。您可以通过以下类似模式使非全局助手全局可用: https://docs.masoniteproject.com/the-basics/helper-functions#introduction
。
环境和配置助手
env()
和config()
是密切相关的助手,它们分别从环境(或.env
文件)和config
文件夹中的文件中提取配置变量。除了它们在不同的文件中查找之外,它们之间的主要区别是从env()
返回的数据被主动缓存,而从config()
返回的数据可以被更改和延迟加载。
当使用这些时,最好只在配置文件中使用env()
,在其他地方只使用config()
:
FROM = {
'address': env('MAIL_FROM_ADDRESS', '[email protected]'),
'name': env('MAIL_FROM_NAME', 'Masonite')
}
这是来自config/mail.py
。
如前所述,env()
函数的第二个参数是默认值。如果或当环境变量不能保证存在时,这是很好的。一旦配置变量在一个配置文件中,我们可以用config()
函数把它们取出来:
from config import mail as mail_config
# ...later
print(mail_config.FROM['address'])
弃尸助手
就像许多梅森奈特人一样,这个抛弃并死去的帮手也受到了《拉韦尔》中相同帮手的启发: https://laravel.com/docs/6.x/helpers#method-dd
。这是一种快速停止正在发生的事情的方法,因此您可以检查多个变量的内容:
dd(User.find(1).email, request().params('email'))
它不是一个步骤调试器,但在紧要关头它很棒!
摘要
在这一章中,我们已经了解了最流行的帮助函数。它们肯定不是唯一的帮助函数,所以我鼓励你看一看文档来了解更多: https://docs.masoniteproject.com/the-basics/helper-functions
。有可能你会在那里找到你喜欢的东西。
在下一章,我们将学习从应用发送通知的所有不同方式。
十、在后台工作
在前一章中,我们看了各种各样的方法来制作和发送通知。对于大型应用来说,这是一个非常有价值的工具,对于需要在后台执行任务的应用来说更是如此。
想想你经常使用的网站,比如 YouTube、脸书、Twitter 和 Google。你知道他们有什么共同点吗(除了很多钱)?他们都需要在请求/响应周期之外做一些无聊的工作。
YouTube 将视频从一种格式转换成许多不同的格式。脸书和 Twitter 处理用户数据,并向你的朋友和家人发送通知,即使他们不在线。谷歌用一群饥饿的机器人爬遍了整个互联网。
如果这些网站在没人注意的时候突然失去了工作能力,它们就会完全停止工作。而且,您可能会构建一些需要在后台做类似工作的东西。
我如何用队列加速我的应用?
队列是 Masonite 提供的主要工具,用于将工作推到后台。队列是运行在服务器上的独立程序。它可以与 Masonite 应用在同一个服务器上,但这不是必需的。
这是应用与队列交互的方式:
-
应用需要做一些工作。在我们的例子中,它是应该在请求/响应周期之外完成的工作。
-
应用连接到队列,并将需要完成的工作摘要发送到队列。
-
同一个应用(或者另一个,这并不重要)连接到同一个队列,并检查是否有任何新的“作业”添加到其中。
-
如果“worker”脚本获得了新的作业,它会从队列中取出这些作业,执行它们,然后从队列中删除它们。
-
如果发生错误,作业可能会保留在队列中,也可能会被删除或过期。全靠配置。
因此,本质上,我们可以通过获取不需要立即完成的工作并将其发送给队列工作器来处理,从而加速我们的应用。
以 YouTube 为例:
-
一个创作者上传一个视频到 YouTube。有一些后台处理,但是用户看到的只是一个细节表单和一个进度指示器。他们仍然可以使用该表单并在网站上做其他事情。一旦原始视频被上传,他们甚至不需要留在网站上进行其余的处理。
-
YouTube 将原始视频发送到一个队列中,第一项“工作”是快速创建一个低质量版本。这确保了观众可以尽快看到一些东西,同时创建更高质量的版本。
-
当低质量版本制作完成并可以观看时,YouTube 会给创作者发电子邮件。这发生在“作业”过程结束时,不管创建者是否仍然打开 YouTube。
-
原始视频的更高质量版本被创建,并且当它们变得可用时,新的质量选项出现在视频播放器中。
有许多排队的操作在起作用。没有它们,创作者将需要让 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
请求是最好的请求方法。我们正在创建一个全新的记录,而不是更新或删除现有的记录。
因此,我们将title
和url
值放在隐藏字段中,并用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
路由上)时,我们应该会看到一些新的事情发生:
-
浏览器应该显示一个大部分空白的页面,带有“完成”
-
终端窗口(运行
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=1024∗1024):
if chunk:
file.write(chunk)
这是来自app/jobs/DownloadEpisode.py
。
首先,我们为文件生成一个安全的名称。一种方法是对我们下载文件的 URL 进行 base64 编码。然后,我们向 URL 发出一个请求(使用请求库),并得到一个流响应。
流式响应非常好(尤其是对于较大的文件),因为它们不必完全在内存中。如果我们读取许多大的音频文件,超过许多请求,服务器可能会耗尽内存来服务新的请求。取而代之的是,流响应被分解,这样我们每次下载的文件只有一小部分在内存中。
当我们获得每个文件块时,我们将其写入目标文件。此时,您可能需要做几件事情:
-
创建
storage/episodes
文件夹。如果您在作业执行中看到一个FileNotFoundError
或NotADirectoryError
(检查终端),那么这可能就是原因。 -
如果您还没有安装请求库,请安装它。
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 所示。
图 11-1
推杆仪表板
接下来,单击“创建新应用”按钮,您应该会看到一个弹出窗口,询问应用的详细信息。我选择 Vanilla JS 作为前端技术,Python 作为后端技术。
如图 11-2 所示,我还输入了“星期五”作为应用名称,并选择了一个离我最近的地区。
图 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 所示。
图 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 所示。记住在布局和命令中使用你自己的按键,否则将不起作用。
图 11-4
在 pusher 中接收消息
这太酷了。我们应该在 JavaScript 控制台中看到相同的事件,从我们应用的任何页面,如图 11-5 所示。
图 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">✗</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': '[email protected]',
'password': 'pass123'
})
self.assertDatabaseHas('users.email', '[email protected]')
注意,我们可以很容易地断言 users 表中的 email 列包含值 [email protected].
测试环境
当 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.7
的masoniteproject.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 的屏幕。
图 13-1
Vultr.com 仪表板
需要注意的是,本章的其余部分需要一种支付方式来设置服务器和部署应用。
如果你是在 Masonite Slack 频道上做的,你会看到我们有一个服务器用于那个网站,我们也有一个服务器用于我们需要做的随机测试,比如浏览教程或模仿人们遇到的 Linux 问题。
在右上角,我们会看到一个+
图标。当我们点击它时,我们会看到一个屏幕,上面有许多不同的选项可供选择。
选项
我们需要做的第一件事是选择服务器的类型。我们目前有四种选择,如图 13-2 所示。
图 13-2
选择所需的服务器类型
我们可以选择最符合我们需要的选项,但在大多数情况下,第一个选项是好的。这是一个非常标准的盒子,我们可以在云中使用,完全符合我们的需求。
接下来,我们会看到一个我们希望服务器所在位置的列表,如图 13-3 所示。需要注意的是,你应该选择离你的观众最近的服务器。服务器离您的受众越近,服务器响应时间就越快。
图 13-3
选择服务器位置
如图 13-4 所示,下一步我们需要选择服务器类型。最受欢迎的选项之一是 Ubuntu。
图 13-4
选择服务器类型
最后一步是选择服务器的大小。我发现我的大多数 Masonite 应用都运行在大约 150MB 的内存上,你应该有一些缓冲空间,因为你的应用的某些部分可能比其他部分需要更多的内存,而且你也很可能会安装一个数据库,所以你可能会有峰值。我建议留出大约 20%的空闲内存,以免降低应用的速度。
因此,如果您选择 512MB 的服务器并留出 20%的空闲空间,那么您将有大约 409MB 的空间可以使用。其中一些空间将专用于其他应用,因此一台 512MB 的服务器可以运行两到三个 Masonite 应用。这个规则不是通用的,而是非常特定于应用的,所以在添加其他应用之前,您应该监视您的服务器性能。
在这个页面的底部有几个选项,比如设置 SSH 密钥和启动脚本,但是现在可以跳过这些选项。
连接到服务器
一旦服务器完成配置(安装所有必要的操作系统软件),我们现在就可以连接到它,并开始安装运行 Python 应用所需的所有东西。
当你点击刚刚构建的新服务器时,你会看到一些连接凭证,如图 13-5 所示。在左侧,您会看到“IP 地址”、“用户名”和“密码”
图 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 所示的错误页面。
图 13-6
重定向错误消息
因此,让我们安装 NGINX,这样我们就可以让这个功能正常工作。
在我们安装任何东西之前,我们需要更新我们的 Ubuntu 包目录:
$ apt-get update
接下来,我们可以简单地安装 NGINX。只需运行以下命令:
$ apt-get install nginx
让安装步骤运行。如果你被提示一个是或否的问题来确认你是否想安装 NGINX,只需输入 Y 并按回车键。
安装完成后,我们可以回到网络浏览器,再次输入我们的 IP 地址。我们现在将看到一个 NGINX 加载页面:
请记住,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 所示。
图 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 替换username
和repo
。如果你没有,你可以使用masoniteframework
和cookie-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
既然应用已经死了,就不能再访问它了。我们现在可以重新启动应用。
-
激活应用的虚拟环境:
source /venvs/example/bin/activate
-
转到目录并获取新的代码更改(这取决于您想要部署的分支或提交):
$ cd /srv/sites/example $ git pull -f https://github.com/username/repo.git master
-
安装任何新要求:
$ pip install -r requirements.txt
-
运行
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 上执行提交或剪切发行版,这将为您完成本章中的大部分步骤。链接可以在主要的在线文档中找到。
标签:指南,权威,request,self,def,import,我们,Masonite From: https://www.cnblogs.com/apachecn/p/18443267