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 mysql@5.7
安装 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 = 'user@email.com'
mail2.to #== '' empty
mail2.to = 'admin@email.com'
因此,由于这种行为,我们可以将一个类绑定到容器中,而不是一个已初始化的对象:
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 = 'user@email.com'
mail2.to #== '' empty
mail2.to = 'admin@email.com'
既然我们知道了这种行为,我们也可以将初始化的对象绑定到容器中。不管我们分解多少次,这都是同一个物体。现在请看这个例子:
from some.place import Mail
container.bind('Mail', Mail())
mail1 = container.resolve(Mail)
mail2 = container.resolve(Mail)
mail1.to = 'user@email.com'
mail2.to #== 'user@email.com'
以这种方式绑定类很有用,因为您可以添加新的服务提供者来为您操作对象。因此,添加服务提供者可能会为您的请求类添加一个完整的基于会话的特性。因为它是同一个对象,所以当我们稍后解析它时,与容器中初始化的类的任何交互都将具有相同的功能。
根据您的用例,这样做的缺点或优点是需要您手动设置您的类,因为我们需要在将它绑定到容器之前构建完整的对象。
所以回到我们的Logger和Mail的例子,我们必须这样做:
from some.place import Mail, Logger
container.bind('Mail', Mail(Logger()))
mail1 = container.resolve(Mail)
mail2 = container.resolve(Mail)
mail1.to = 'user@email.com'
mail2.to #== 'user@email.com'
没什么大不了的,但这只是一个简单的例子。
在这种情况下,我们将调用单例模式。
绑定单线态
单体是一个非常简单的概念。这只是意味着,每当我们需要这个类的时候,我们每次都想要相同的类。当我们绑定类时,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 容器的了解,以及如何向它添加我们自己的服务。
在下一章中,我们将探索如何将这种数据保存到数据库中,以及所有需要做的事情。