在我们“现在的世界”中,人们几乎无法开发新的应用程序而不将许多技术粘合在一起,无论是新趋势数据库,消息系统还是各种语言。谈到Web开发,事情可能会变得稍微复杂,因为你不仅必须将许多技术混合在一起,而且它们还必须与访问它们的应用程序(也称为Web浏览器)良好地配合。它们还应与您的部署服务器兼容,这本身又是另一回事!
Flask定位自己不是像Django和Web2py那样的开箱即用的解决方案,而是一个最简解决方案,你只得到最基本的东西,然后选择其他所有的东西。当你想要对应用程序进行细粒度控制时,当你想要精确选择你的组件时,或者当你的解决方案很简单时(不是简化的,好吗?)。
来学习如何创建出色的Flask应用程序,为您的项目和客户提供价值!
第一章《FlaskinaFlask,IMean,Book》向你介绍了Flask,解释了它是什么,它不是什么,以及它在Web框架世界中的定位。
第二章《FirstApp,HowHardCoulditBe》涵盖了通往Flask开发的第一步,包括环境设置,你自己的“HelloWorld”应用程序,以及模板如何进入这个方程式。这是一个轻松的章节!
第三章《Man,DoILikeTemplates!》介绍了面部标签和过滤器在Jinja2模板引擎中的进展,以及它如何与Flask集成。从这里开始事情开始变得有点严肃!
第四章《PleaseFillinThisForm,Madam》讨论了如何处理表单(因为表单是Web开发生活中的一个事实),并使用WTForms以其全部荣耀来对待它们!
第五章《WhereDoYouStoreYourStuff》介绍了关系型和非关系型数据库的概念,涵盖了如何处理这两种情况,以及何时处理。
第六章《ButIWannaRESTMom,Now!》是关于创建REST服务的一章(因为REST的热情必须得到满足),手动创建和使用令人惊叹的Flask-Restless。
第七章,“如果没有经过测试,就不是游戏,兄弟!”,是我们以质量为中心的章节,您将学习通过适当的测试、TDD和BDD方式提供质量!
第八章,“技巧和窍门或Flask魔法101”,是一个密集的章节,涵盖了良好的实践、架构、蓝图、调试和会话管理。
第九章,“扩展,我是如何爱你”,涵盖了到目前为止尚未涉及的所有伟大的Flask扩展,这些扩展将帮助您实现现实世界对您的生产力要求。
第十章,“现在怎么办?”,结束了我们的开发之旅,涵盖了健康部署的所有基础知识,并指引您在Flask世界中迈出下一步。
本书面向Python开发人员,无论是有一些或没有Web开发经验的人,都希望创建简约的Web应用程序。它专注于那些希望成为Web开发人员的人,因为所有基础知识都在一定程度上得到了涵盖,也专注于那些已经熟悉使用其他框架进行Web开发的人,无论是基于Python的框架,如Django、Bottle或Pyramid,还是其他语言的框架。
尽管如此,请放心,如果您对Python有基本的了解,您完全有能力理解示例和章节;在本书结束时,您将创建出表现良好且易于维护的令人惊叹的Web应用程序。
在本书中,您将找到一些区分不同信息类型的文本样式。以下是一些这些样式的示例,以及它们的含义解释。
文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟URL、用户输入和Twitter句柄显示如下:“进入新项目文件夹并创建main.py文件”。
代码块设置如下:
#coding:utf-8fromflaskimportFlaskapp=Flask(__name__)@app.route("/")defhello():return"HelloWorld!"if__name__=="__main__":app.run()任何命令行输入或输出都以以下形式编写:
sudopipinstallvirtualenvwrapper新术语和重要单词以粗体显示。您在屏幕上看到的单词,比如菜单或对话框中的单词,会以这样的形式出现在文本中:“您有没有想象过在网站上填写表单并在末尾点击那个花哨的发送按钮时会发生什么?”。
警告或重要说明会以这样的方式出现在框中。
提示和技巧会以这样的方式出现。
Flask是什么?这是一个人类几千年来一直在思考的问题……嗯,实际上是自2010年ArminRonacher首次承诺该项目以来。Flask是一个Web框架,与大多数人习惯使用的方式非常不同。它对你的应用程序应该是什么样子或者你应该使用什么来使其可用并不那么自以为是。这个BSD许可的软件包就是这样!
Flask框架实际上是一个非常好的胶水,它将令人惊叹的Werkzeug和Jinja2框架粘合在一起,负责响应请求和呈现输出(HTML,也许)。在MVC架构中,也被称为模型-视图-控制器,Flask涵盖了C和V。但M在哪里?Flask并没有为你提供集成的模型层,因为对于Web应用程序实际上并不需要。如果你确实需要使用数据库,只需从众多可用的数据库解决方案中选择一个并创建自己的模型层,这并不难,而且会让你很开心!微框架的概念,怀着良好的意图,专门为Flask而设计,就是要给你提供你所需要的最小(但也是最有用的)功能集,而且不会妨碍你。
框架中必须具备哪些特性?
开发服务器和调试器(健全友好)
Unicode支持(拉丁语言友好)
WSGI兼容(uWsgi友好)
单元测试客户端(质量代码)
URL路由(它让我感动得流泪,它太美了!)
请求分发
安全的cookies
会话
Jinja2模板(标签、过滤器、宏等)
有了这些,你可以处理Ajax请求、浏览器请求和请求之间的用户会话;将HTTP请求路由到你的控制器;评估表单数据;响应HTML和JSON等等。
这很好,但Flask不是一个MVC框架吗?嗯,这是值得讨论的。如果一个Web框架不实现MVC反模式,比如在视图中处理请求或混合模型和控制器,它可能有助于实现MVC,这在我看来是最好的,因为它不会强制你的应用程序结构。
Flask不是一个MVC框架,因为它没有实现模型层,尽管如果你希望创建自己的模型层,它不会限制你。
如果你需要一个简单的、单文件的Web应用程序,接收一个表单并返回一个答案,无论是HTML还是其他,Flask都可以帮助你轻松实现。如果你需要一个多层、高深度模块化的Facebook克隆,Flask也可以为你提供帮助。
那么,我们到目前为止学到了什么?
Flask诞生于2010年
Flask是一个基于Jinja2和Werkzeug的极简主义Web框架
Flask不强制执行特定的项目架构
现在,你可能想知道你的哪些好点子可以用Flask来实现。这就对了!我们一起来想想这个问题吧?
让我们再深入思考一下。如果你需要一组特定的库在你的项目中一起工作,而你又不希望Web框架妨碍你;这对于Flask来说是另一个非常好的场景,因为它给你提供了最基本的东西,让你自己组合其他你可能需要的一切。一些框架对它们自己的组件有如此高的耦合(读作依赖性),以至于如果你想使用特定的替代方案,你可能会遇到严重的问题。
例如,你可能想在项目中使用NoSQL数据库;然而,如果这样做,你的项目的一些组件可能会停止工作(例如:管理组件)。
现在,让我们谈谈令人敬畏的事情,在读完这本书后,你将能够处理HTTP和Ajax请求;创建具有数据库集成(SQL和NoSQL)和REST服务的完整功能的Web应用程序;使用Flask扩展(表单、缓存、日志、调试、认证、权限等);以及模块化和对应用程序进行单元和功能测试。
希望你喜欢这本书,并能用所学的知识做出很棒的东西
在一个完整的章节中没有一行代码,你需要这个,对吧?在这一章中,我们将逐行解释我们的第一个应用程序;我们还将介绍如何设置我们的环境,开发时使用什么工具,以及如何在我们的应用程序中使用HTML。
当学习新技术时,人们通常会写一个HelloWorld应用程序,这个应用程序包含启动一个简单应用程序并显示文本"HelloWorld!"所需的最小可能代码。让我们使用Flask来做到这一点。
本书针对Python2.x进行了优化,所以我建议你从现在开始使用这个版本。所有的示例和代码都针对这个Python版本,这也是大多数Linux发行版的默认版本。
让我们从以下方式开始安装所需的Debian软件包:
sudoapt-getinstallpython-devpython-pip这将安装Python开发工具和编译Python包所需的库,以及pip:一个方便的工具,你可以用它来从命令行安装Python包。继续吧!让我们安装我们的虚拟环境管理工具:
sudopipinstallvirtualenvwrapperecho"source/usr/local/bin/virtualenvwrapper.sh">>~/.bashrc解释一下我们刚刚做的事情:sudo告诉我们的操作系统,我们想要以管理员权限运行下一个命令,pip是默认的Python包管理工具,帮助我们安装virtualenvwrapper包。第二个命令语句添加了一个命令,将virtualenvwrapper.sh脚本与控制台一起加载,以便命令在你的shell内工作(顺便说一下,我们将使用它)。
虚拟环境是Python将完整的包环境与其他环境隔离开来的方式。这意味着你可以轻松地管理依赖关系。想象一下,你想为一个项目定义最小必需的包;虚拟环境将非常适合让你测试和导出所需包的列表。我们稍后会讨论这个问题。现在,按下键盘上的Ctrl+Shift+T创建一个新的终端,并像这样创建我们的helloworld环境:
mkvirtualenvhellopipinstallflask第一行创建了一个名为"hello"的环境。你也可以通过输入deactivate来停用你的虚拟环境,然后可以使用以下命令再次加载它:
workonhello#substitutehellowiththedesiredenvironmentnameifneeded第二行告诉pip在当前虚拟环境hello中安装Flask包。
创建一个文件夹来保存你的项目文件(你不需要,但如果你这样做,人们会更喜欢你),如下所示:
mkdirhello_world进入新的项目文件夹并创建main.py文件:
cdhello_worldtouchmain.pymain.py文件将包含整个"HelloWorld"应用程序。我们的main.py内容应该像这样:
#coding:utf-8fromflaskimportFlaskapp=Flask(__name__)@app.route("/")defhello():return"HelloWorld!"if__name__=="__main__":app.run()提示下载示例代码
哇!那需要一些打字,对吧?不是吗?是的,我知道。那么,我们刚刚做了什么?
第一行说明我们的main.py文件应该使用utf-8编码。所有酷孩子都这样做,所以不要对您的非英语朋友不友好,并在所有Python文件中使用它(这样做可能有助于您避免在大型项目中出现一些讨厌的错误)。
为了创建我们的“HelloWorld”,我们需要告诉我们的Flask实例在用户尝试访问我们的Web应用程序(使用浏览器或其他方式)时如何响应。为此,Flask有路由。
路由是Flask读取请求头并决定哪个视图应该响应该请求的方式。它通过分析请求的URL的路径部分,并找到注册了该路径的路由来实现这一点。
在helloworld示例中,在第5行,我们使用路由装饰器将hello函数注册到"/"路径。每当应用程序接收到路径为"/"的请求时,hello都会响应该请求。以下代码片段显示了如何检查URL的路径部分:
@app.route("/")@app.route("/index")defhello():return"HelloWorld!"在这种情况下,"/"和"/index"路径都将映射到hello。
在第6和第7行,我们有一个将响应请求的函数。请注意,它不接收任何参数并且以熟悉的字符串作出响应。它不接收任何参数,因为请求数据(如提交的表单)是通过一个名为request的线程安全变量访问的,我们将在接下来的章节中更多地了解它。
关于响应,Flask可以以多种格式响应请求。在我们的示例中,我们以纯字符串作出响应,但我们也可以以JSON或HTML字符串作出响应。
第9和第10行很简单。它们检查main.py是作为脚本还是作为模块被调用。如果是作为脚本,它将运行与Flask捆绑在一起的内置开发服务器。让我们试试看:
pythonmain.py您的终端控制台将输出类似以下内容:
将main.py作为脚本运行通常是一个非常简单和方便的设置。通常,您可以使用Flask-Script来处理为您调用开发服务器和其他设置。
如果您将main.py作为模块使用,只需按以下方式导入它:
frommainimportwhat_I_want通常,您会在测试代码中执行类似以下操作来导入应用工厂函数。
这基本上就是关于我们的“HelloWorld”应用程序的所有内容。我们的世界应用程序缺少的一件事是乐趣因素。所以让我们添加一些;让我们让您的应用程序有趣!也许一些HTML、CSS和JavaScript可以在这里起作用。让我们试试看!
首先,要使我们的hello函数以HTML响应,我们只需将其更改为以下内容:
defhello():return"
如果您使用F5刷新浏览器,您会注意到没有任何变化。这就是为什么当源代码更改时,Flask开发服务器不会重新加载。只有在调试模式下运行应用程序时才会发生这种情况。所以让我们这样做:
app=Flask(__name__)app.debug=True现在去你的应用程序正在运行的终端,输入Ctrl+C然后重新启动服务器。你会注意到除了你的服务器正在运行的URL之外有一个新的输出——关于“stat”的内容。这表示你的服务器将在源代码修改时重新加载代码。这很好,但你注意到我们刚刚犯下的罪行了吗:在处理响应的函数内部定义我们的模板?小心,MVC之神可能在看着你。让我们把我们定义视图的地方和定义控制器的地方分开。创建一个名为templates的文件夹,并在其中创建一个名为index.html的文件。index.html文件的内容应该像这样:
fromflaskimportFlask,render_response@app.route("/")defhello():returnrender_template("index.html")你看到我们做了什么了吗?render_response能够从templates/文件夹(Flask的默认文件夹)中加载模板,并且你可以通过返回输出来渲染它。
现在让我们添加一些JavaScript和CSS样式。默认情况下,Flask内置的开发服务器会提供project文件夹中名为static的子文件夹中的所有文件。让我们创建我们自己的文件夹并向其中添加一些文件。你的项目树应该是这样的:
尝试用一些漂亮的CSS样式来改进“helloworld”示例!
建立开发环境是一项非常重要的任务,我们刚刚完成了这个任务!创建一个“HelloWorld”应用程序是向某人介绍新技术的好方法。我们也做到了。最后,我们学会了如何提供HTML页面和静态文件,这基本上是大多数Web应用程序所做的。你在本章中掌握了所有这些技能,我希望这个过程既简单又充实!
在下一章中,我们将通过更加冒险的模板来为我们的挑战增添一些调味。我们将学习如何使用Jinja2组件来创建强大的模板,从而让我们在输入更少的情况下做更多的事情。到时见!
如前所述,Flask为您提供了MVC中的VC。在本章中,我们将讨论Jinja2是什么,以及Flask如何使用Jinja2来实现视图层并让您感到敬畏。做好准备!
fromjinja2importTemplatex="""
UncleScroogenephews
- {%foriinmy_list%}
- {{i}} {%endfor%}
请注意,您可以在模板实例中调用render多次,并使用不同的键值参数,也称为模板上下文。上下文变量可以有任何有效的Python变量名——也就是说,任何符合正则表达式[a-zA-Z_][a-zA-Z0-9_]格式的内容。
一个更复杂的例子将使用环境类实例,这是一个中央、可配置、可扩展的类,可以以更有组织的方式加载模板。
您明白我们要说什么了吗?这是Jinja2和Flask背后的基本原理:它为您准备了一个环境,具有一些响应式默认设置,并让您的轮子转起来。
Jinja2非常灵活。您可以将其与模板文件或字符串一起使用;您可以使用它来创建格式化文本,例如HTML、XML、Markdown和电子邮件内容;您可以组合模板、重用模板和扩展模板;甚至可以使用扩展。可能性是无穷无尽的,并且结合了良好的调试功能、自动转义和完整的Unicode支持。
自动转义是Jinja2的一种配置,其中模板中打印的所有内容都被解释为纯文本,除非另有明确要求。想象一个变量x的值设置为b。如果启用了自动转义,模板中的{{x}}将打印给定的字符串。如果关闭了自动转义,这是Jinja2的默认设置(Flask的默认设置是开启的),则生成的文本将是b。
在介绍Jinja2允许我们进行编码之前,让我们先了解一些概念。
首先,我们有前面提到的大括号。双大括号是一个分隔符,允许您从提供的上下文中评估变量或函数,并将其打印到模板中:
fromjinja2importTemplate#createthetemplatet=Template("{{variable}}")#–Built-inTypes–t.render(variable='helloyou')>>u"helloyou"t.render(variable=100)>>u"100"#youcanevaluatecustomclassesinstancesclassA(object):def__str__(self):return"__str__"def__unicode__(self):returnu"__unicode__"def__repr__(self):returnu"__repr__"#–CustomObjectsEvaluation–#__unicode__hasthehighestprecedenceinevaluation#followedby__str__and__repr__t.render(variable=A())>>u"__unicode__"在上面的例子中,我们看到如何使用大括号来评估模板中的变量。首先,我们评估一个字符串,然后是一个整数。两者都会产生Unicode字符串。如果我们评估我们自己的类,我们必须确保定义了__unicode__方法,因为在评估过程中会调用它。如果没有定义__unicode__方法,则评估将退回到__str__和__repr__,依次进行。这很简单。此外,如果我们想评估一个函数怎么办?好吧,只需调用它:
fromjinja2importTemplate#createthetemplatet=Template("{{fnc()}}")t.render(fnc=lambda:10)>>u"10"#evaluatingafunctionwithargumentt=Template("{{fnc(x)}}")t.render(fnc=lambdav:v,x='20')>>u"20"t=Template("{{fnc(v=30)}}")t.render(fnc=lambdav:v)>>u"30"要在模板中输出函数的结果,只需像调用任何常规Python函数一样调用该函数。函数返回值将被正常评估。如果您熟悉Django,您可能会注意到这里有一点不同。在Django中,您不需要使用括号来调用函数,甚至不需要向其传递参数。在Flask中,如果要对函数返回值进行评估,则始终需要使用括号。
以下两个示例展示了Jinja2和Django在模板中函数调用之间的区别:
{#flasksyntax#}{{some_function()}}{#djangosyntax#}{{some_function}}您还可以评估Python数学运算。看一下:
fromjinja2importTemplate#nocontextprovided/neededTemplate("{{3+3}}").render()>>u"6"Template("{{3-3}}").render()>>u"0"Template("{{3*3}}").render()>>u"9"Template("{{3/3}}").render()>>u"1"其他数学运算符也可以使用。您可以使用花括号分隔符来访问和评估列表和字典:
fromjinja2importTemplateTemplate("{{my_list[0]}}").render(my_list=[1,2,3])>>u'1'Template("{{my_list['foo']}}").render(my_list={'foo':'bar'})>>u'bar'#andhere'ssomemagicTemplate("{{my_list.foo}}").render(my_list={'foo':'bar'})>>u'bar'要访问列表或字典值,只需使用普通的Python表示法。对于字典,您还可以使用变量访问表示法访问键值,这非常方便。
除了花括号分隔符,Jinja2还有花括号/百分比分隔符,它使用{%stmt%}的表示法,用于执行语句,这可能是控制语句,也可能不是。它的使用取决于语句,其中控制语句具有以下表示法:
{%stmt%}{%endstmt%}第一个标签具有语句名称,而第二个是闭合标签,其名称在开头附加了end。您必须意识到非控制语句可能没有闭合标签。让我们看一些例子:
{%blockcontent%}{%foriinitems%}{{i}}-{{i.price}}{%endfor%}{%endblock%}前面的例子比我们之前看到的要复杂一些。它在块语句中使用了控制语句for循环(您可以在另一个语句中有一个语句),这不是控制语句,因为它不控制模板中的执行流程。在for循环中,您可以看到i变量与关联的价格(在其他地方定义)一起打印出来。
{#firstexample#}{#secondexample#}两种注释分隔符都隐藏了{#和#}之间的内容。可以看到,这个分隔符适用于单行注释和多行注释,非常方便。
Jinja2中默认定义了一组不错的内置控制结构。让我们从if语句开始学习它。
{%iftrue%}Tooeasy{%endif%}{%iftrue==true==True%}Trueandtruearethesame{%endif%}{%iffalse==false==False%}Falseandfalsealsoarethesame{%endif%}{%ifnone==none==None%}There'salsoalowercaseNone{%endif%}{%if1>=1%}Compareobjectslikeinplainpython{%endif%}{%if1==2%}Thiswon'tbeprinted{%else%}Thiswill{%endif%}{%if"apples"!="oranges"%}Allcomparisonoperatorswork=]{%endif%}{%ifsomething%}elifisalsosupported{%elifsomething_else%}^_^{%endif%}if控制语句很美妙!它的行为就像pythonif语句一样。如前面的代码所示,您可以使用它以非常简单的方式比较对象。"else"和"elif"也得到了充分支持。
您可能还注意到了true和false,非大写,与普通的Python布尔值True和False一起使用。为了避免混淆的设计决策,所有Jinja2模板都有True、False和None的小写别名。顺便说一句,小写语法是首选的方式。
如果需要的话,您应该避免这种情况,可以将比较组合在一起以改变优先级评估。请参阅以下示例:
{%if5<10<15%}true{%else%}false{%endif%}{%if(5<10)<15%}true{%else%}false{%endif%}{%if5<(10<15)%}true{%else%}false{%endif%}前面示例的预期输出是true、true和false。前两行非常直接。在第三行中,首先,(10<15)被评估为True,它是int的子类,其中True==1。然后评估5 for语句非常重要。几乎无法想象一个严肃的Web应用程序不必在某个时候显示某种列表。for语句可以迭代任何可迭代实例,并且具有非常简单的、类似Python的语法: {%foriteminmy_list%}{{item}}{#printevaluateitem#}{%endfor%}{#or#}{%forkey,valueinmy_dictionary.items()%}{{key}}:{{value}}{%endfor%}在第一个语句中,我们有一个开放标签,指示我们将遍历my_list项,每个项将被名称item引用。名称item仅在for循环上下文中可用。 在第二个语句中,我们对形成my_dictionary的键值元组进行迭代,这应该是一个字典(如果变量名不够具有启发性的话)。相当简单,对吧?for循环也为您准备了一些技巧。 在构建HTML列表时,通常需要以交替颜色标记每个列表项,以改善可读性,或者使用一些特殊标记标记第一个和/或最后一个项目。这些行为可以通过在Jinja2for循环中访问块上下文中可用的循环变量来实现。让我们看一些例子: {%foriin['a','b','c','d']%}{%ifloop.first%}Thisisthefirstiteration{%endif%}{%ifloop.last%}Thisisthelastiteration{%endif%}{{loop.cycle('red','blue')}}{#printredorbluealternating#}{{loop.index}}-{{loop.index0}}{#1indexedindex–0indexedindex#}{#reverse1indexedindex–reverse0indexedindex#}{{loop.revindex}}-{{loop.revindex0}}{%endfor%}for循环语句,就像Python一样,也允许使用else,但意义略有不同。在Python中,当您在for中使用else时,只有在没有通过break命令到达else块时才会执行else块,就像这样: foriin[1,2,3]:passelse:print"thiswillbeprinted"foriin[1,2,3]:ifi==3:breakelse:print"thiswillnevernotbeprinted"如前面的代码片段所示,else块只有在for循环中从未被break命令中断执行时才会执行。使用Jinja2时,当for可迭代对象为空时,将执行else块。例如: {%foriin[]%}{{i}}{%else%}I'llbeprinted{%endfor%}{%foriin['a']%}{{i}}{%else%}Iwon't{%endfor%}由于我们正在讨论循环和中断,有两件重要的事情要知道:Jinja2的for循环不支持break或continue。相反,为了实现预期的行为,您应该使用循环过滤,如下所示: {%foriin[1,2,3,4,5]ifi>2%}value:{{i}};loop.index:{{loop.index}}{%-endfor%}在第一个标签中,您会看到一个普通的for循环和一个if条件。您应该将该条件视为一个真正的列表过滤器,因为索引本身只是在每次迭代中计数。运行前面的示例,输出将如下所示: value:3;index:1value:4;index:2value:5;index:3看看前面示例中的最后一个观察——在第二个标签中,您看到{%-中的破折号吗?它告诉渲染器在每次迭代之前不应该有空的新行。尝试我们之前的示例,不带破折号,并比较结果以查看有何变化。 现在我们将看看用于从不同文件构建模板的三个非常重要的语句:block、extends和include。 block和extends总是一起使用。第一个用于定义模板中的“可覆盖”块,而第二个定义了具有块的当前模板的父模板。让我们看一个例子: 例如,使用other.txt,我们扩展child.txt模板并仅覆盖命名为block的模板。您可以从直接父模板或任何父模板覆盖块。 如果您正在定义一个index.txt页面,您可以在其中有默认块,需要时进行覆盖,从而节省大量输入。 解释最后一个示例,就Python而言,非常简单。首先,我们创建了一个Jinja2环境(我们之前谈到过这个),并告诉它如何加载我们的模板,然后直接加载所需的模板。我们不必费心告诉环境如何找到父模板,也不必预加载它们。 include语句可能是迄今为止最简单的语句。它允许您以非常简单的方式在另一个模板中呈现模板。让我们看一个例子: withopen('base.txt','w')asfile:file.write("""{{myvar}}Youwannahearadirtyjoke{%include'joke.txt'%}""".strip())withopen('joke.txt','w')asfile:file.write("""Aboyfellinamudpuddle.{{myvar}}""".strip())fromjinja2importEnvironment,FileSystemLoaderenv=Environment()#telltheenvironmenthowtoloadtemplatesenv.loader=FileSystemLoader('.')printenv.get_template('base.txt').render(myvar='Haha!')在前面的示例中,我们在base.txt中呈现joke.txt模板。由于joke.txt在base.txt中呈现,它也可以完全访问base.txt上下文,因此myvar会正常打印。 最后,我们有set语句。它允许您在模板上下文中定义变量。它的使用非常简单: {%setx=10%}{{x}}{%setx,y,z=10,5+5,"home"%}{{x}}-{{y}}-{{z}}在前面的示例中,如果x是通过复杂计算或数据库查询给出的,如果要在模板中重复使用它,将其缓存在一个变量中会更有意义。如示例中所示,您还可以一次为多个变量分配一个值。 宏是您在Jinja2模板中最接近编码的地方。宏的定义和使用类似于普通的Python函数,因此非常容易。让我们尝试一个例子: withopen('formfield.html','w')asfile:file.write('''{%macroinput(name,value='',label='')%}{%iflabel%} {%from'formfield.html'importinputasfield_input%}您还可以将formfield作为模块导入并按以下方式使用它: {%import'formfield.html'asformfield%}在使用宏时,有一种特殊情况,您希望允许任何命名参数传递到宏中,就像在Python函数中一样(例如,**kwargs)。使用Jinja2宏,默认情况下,这些值在kwargs字典中可用,不需要在宏签名中显式定义。例如: #coding:utf-8withopen('formfield.html','w')asfile:file.write('''{%macroinput(name)-%} 宏在纯模板上具有一些明显的优势,您可以通过include语句注意到: 使用宏时,您不必担心模板中的变量名称 您可以通过宏签名定义宏块的确切所需上下文 您可以在模板中定义一个宏库,并仅导入所需的内容 Web应用程序中常用的宏包括用于呈现分页的宏,用于呈现字段的宏,以及用于呈现表单的宏。您可能还有其他用例,但这些是相当常见的用例。 关于我们之前的例子,使用HTTPS(也称为安全HTTP)发送敏感信息,如密码,通过互联网是一个良好的做法。要小心! 扩展是Jinja2允许您扩展其词汇的方式。扩展默认情况下未启用,因此只有在需要时才能启用扩展,并且可以在不太麻烦的情况下开始使用它: env=Environment(extensions=['jinja2.ext.do','jinja2.ext.with_'])在前面的代码中,我们有一个示例,其中您创建了一个启用了两个扩展的环境:do和with。这些是我们将在本章中学习的扩展。 {%setx={1:'home','2':'boat'}%}{%dox.update({3:'bar'})%}{%-forkey,valueinx.items()%}{{key}}-{{value}}{%-endfor%}在前面的例子中,我们使用一个字典创建了x变量,然后用{3:'bar'}更新了它。通常情况下,您不需要使用do扩展,但是当您需要时,可以节省大量编码。 {%withage=user.get_age()%}Myage:{{age}}{%endwith%}Myage:{{age}}{#novaluehere#}如示例所示,age仅存在于with块内。此外,在with块内设置的变量将仅在其中存在。例如: {%with%}{%setcount=query.count()%}CurrentStock:{{count}}Diff:{{prev_count-count}}{%endwith%}{{count}}{#emptyvalue#}过滤器过滤器是Jinja2的一个奇妙之处!这个工具允许您在将常量或变量打印到模板之前对其进行处理。目标是在模板中严格实现您想要的格式。 要使用过滤器,只需使用管道运算符调用它,就像这样: {%setname='junior'%}{{name|capitalize}}{#outputisJunior#}它的名称被传递给capitalize过滤器进行处理,并返回大写的值。要将参数传递给过滤器,只需像调用函数一样调用它,就像这样: {{['Adam','West']|join('')}}{#outputisAdamWest#}join过滤器将连接传递的可迭代值,将提供的参数放在它们之间。 Jinja2默认提供了大量可用的过滤器。这意味着我们无法在这里覆盖它们所有,但我们当然可以覆盖一些。capitalize和lower已经看到了。让我们看一些进一步的例子: {#printsdefaultvalueifinputisundefined#}{{x|default('noopinion')}}{#printsdefaultvalueifinputevaluatestofalse#}{{none|default('noopinion',true)}}{#printsinputasitwasprovided#}{{'someopinion'|default('noopinion')}}{#youcanuseafilterinsideacontrolstatement#}{#sortbykeycase-insensitive#}{%forkeyin{'A':3,'b':2,'C':1}|dictsort%}{{key}}{%endfor%}{#sortbykeycase-sensitive#}{%forkeyin{'A':3,'b':2,'C':1}|dictsort(true)%}{{key}}{%endfor%}{#sortbyvalue#}{%forkeyin{'A':3,'b':2,'C':1}|dictsort(false,'value')%}{{key}}{%endfor%}{{[3,2,1]|first}}-{{[3,2,1]|last}}{{[3,2,1]|length}}{#printsinputlength#}{#sameasinpython#}{{'%s,=D'|format("I'mJohn")}}{{"Hehastwodaughters"|replace('two','three')}}{#safeprintstheinputwithoutescapingitfirst#}{{' 阅读了这么多关于Jinja2的内容,您可能会想:“Jinja2很酷,但这是一本关于Flask的书。给我看看Flask的东西!”好的,好的,我可以做到! 根据我们迄今所见,几乎一切都可以在Flask中使用而无需修改。由于Flask为您管理Jinja2环境,因此您不必担心创建文件加载程序之类的事情。但是,您应该知道的一件事是,由于您不是自己实例化Jinja2环境,因此您实际上无法将要激活的扩展传递给类构造函数。 要激活扩展程序,请在应用程序设置期间将其添加到Flask中,如下所示: fromflaskimportFlaskapp=Flask(__name__)app.jinja_env.add_extension('jinja2.ext.do')#orjinja2.ext.with_if__name__=='__main__':app.run()搞乱模板上下文在第二章中所见,第一个应用,有多难?,您可以使用render_template方法从templates文件夹加载模板,然后将其呈现为响应。 fromflaskimportFlask,render_templateapp=Flask(__name__)@app.route("/")defhello():returnrender_template("index.html")如果您想向模板上下文添加值,就像本章中的一些示例中所示,您将不得不向render_template添加非位置参数: fromflaskimportFlask,render_templateapp=Flask(__name__)@app.route("/")defhello():returnrender_template("index.html",my_age=28)在上面的示例中,my_age将在index.html上下文中可用,其中{{my_age}}将被翻译为28。my_age实际上可以具有您想要展示的任何值。 现在,如果您希望所有视图在其上下文中具有特定值,例如版本值-一些特殊代码或函数;您该怎么做?Flask为您提供了context_processor装饰器来实现这一点。您只需注释一个返回字典的函数,然后就可以开始了。例如: fromflaskimportFlask,render_responseapp=Flask(__name__)@app.context_processordefluck_processor():fromrandomimportrandintdeflucky_number():returnrandint(1,10)returndict(lucky_number=lucky_number)@app.route("/")defhello():#lucky_numberwillbeavailableintheindex.htmlcontextbydefaultreturnrender_template("index.html")总结在本章中,我们看到了如何仅使用Jinja2呈现模板,控制语句的外观以及如何使用它们,如何编写注释,如何在模板中打印变量,如何编写和使用宏,如何加载和使用扩展,以及如何注册上下文处理器。我不知道您怎么看,但这一章节感觉像是大量的信息!我强烈建议您运行示例进行实验。熟悉Jinja2将为您节省大量麻烦。 下一章,我们将学习使用Flask的表单。期待许多示例和补充代码,因为表单是您从Web应用程序打开到Web的大门。大多数问题都来自Web,您的大多数数据也是如此。 无论如何,没有什么可害怕的!Flask可以帮助你完成这些步骤,但也有专门为此目的设计的工具。在本章中,我们将学习: 如何使用Flask编写和处理表单 如何验证表单数据 如何使用WTForms验证Flask中的表单 如何实现跨站点请求伪造保护 这实际上将是一个相当顺利的章节,有很多新信息,但没有复杂的东西。希望你喜欢! 虽然我们不会详细介绍HTML,但我们会专门介绍HTML;我指的是
然后,前面的URL将保存到浏览器历史记录中,任何有权访问它的人都可以看到上一个用户在搜索什么。对于敏感数据来说,这是不好的。
无论如何,回到示例1。第二个属性action对于告诉浏览器应该接收和响应表单数据的URL非常有用。我们使用'.'作为它的值,因为我们希望表单数据被发送到当前URL。
接下来的两行是我们的输入字段。输入字段用于收集用户数据,与名称可能暗示的相反,输入字段可以是input、textarea或select元素。在使用输入字段时,始终记得使用属性name对它们进行命名,因为这有助于在Web应用程序中处理它们。
在第三行,我们有一个特殊的输入字段,它不一定有任何要发送的数据,即提交输入按钮。默认情况下,如果在input元素具有焦点时按下Enter,或者按下提交按钮,表单将被发送。我们的示例1是后者。
现在让我们看看如何将示例1中的表单与应用程序集成:
#coding:utf-8fromflaskimportFlask,render_template,requestapp=Flask(__name__)@app.route('/',methods=['get','post'])deflogin_view():#themethodsthathandlerequestsarecalledviews,inflaskmsg=''#formisadictionarylikeattributethatholdstheformdataifrequest.method=='POST':username=request.form["username"]passwd=request.form["passwd"]#staticuselessvalidationifusername=='you'andpasswd=='flask':msg='Usernameandpasswordarecorrect'else:msg='Usernameorpasswordareincorrect'returnrender_template('form.html',message=msg)if__name__=='__main__':app.run()在前面的例子中,我们定义了一个名为login_view的视图,该视图接受get或post请求;当请求为post时(如果是由get请求发送的表单,则我们忽略该表单),我们获取username和passwd的值;然后我们运行一个非常简单的验证,并相应地更改msg的值。
注意:在Flask中,视图不同于MVC中的视图。在Flask中,视图是接收请求并返回响应的组件,可以是函数或类。
您看到我们在示例中处理的request变量了吗?这是当前活动request上下文的代理。这就是为什么request.form指向发送的表单数据。
到目前为止,我们用内联验证处理表单验证非常糟糕。不再这样做了!让我们从现在开始尝试一些更花哨的东西。
首先,要安装WTForms库,请使用以下命令:
fromwtformsimportForm,StringField,PasswordFieldclassLoginForm(Form):username=StringField(u'Username:')passwd=PasswordField(u'Password:')在前面的代码中,我们有一个带有两个字段username和passwd的表单,没有验证。只需在模板中构建一个表单就足够了,就像这样:
WTForms使用标志系统,以允许您检查何时对字段应用了一些验证。如果字段有一个required验证规则,fields.flags属性中的required标志将设置为true。但是WTForms验证是如何工作的呢?
每个验证器都接收表单和字段作为参数,并必须使用它们进行验证。如validate_password所示,它是因为命名约定而为字段password调用的。field.data保存字段输入,因此您通常可以只验证它。
让我们了解每个验证器:
Length:验证输入值的长度是否在给定范围内(最小、最大)。
InputRequired:验证字段是否接收到值,任何值。
is_proper_username:验证字段值是否与给定的正则表达式匹配。(还有一个内置验证器,用于将正则表达式与给定值匹配,称为Regexp。您应该尝试一下。)
validate_password:验证字段值是否符合给定的正则表达式规则组。
在我们的示例测试中,您可能已经注意到了使用werkzeug库中称为MultiDict的特殊类似字典的类。它被使用是因为formdata参数,它可能接收您的request.form或request.args,必须是multidict-type。这基本上意味着您不能在这里使用普通字典。
调用form.validate时,将调用所有验证器。首先是字段验证器,然后是class方法字段验证器;form.errors是一个字典,其中包含在调用validate后找到的所有字段错误。然后您可以对其进行迭代,以在模板、控制台等中显示您找到的内容。
Flask使用扩展以便与第三方库透明集成。WTForms与Flask-WTF是这样的一个很好的例子,我们很快就会看到。顺便说一句,Flask扩展是一段代码,以可预测的方式与Flask集成其配置、上下文和使用。这意味着扩展的使用方式非常相似。现在确保在继续之前在您的虚拟环境中安装了Flask-WTF:
与WTForms集成
使用CSRF令牌保护表单
与Flask-Uploads一起工作的文件上传
全局CSRF保护
Recaptcha支持
国际化集成
我们将在本章中看到前两个功能,而第三个将在第十章中讨论,现在怎么办?。最后三个功能将不在本书中涵盖。我们建议您将它们作为作业进行探索。
Flask-WTF在集成时使用了关于request的小技巧。由于request实现了对当前请求和请求数据的代理,并且在request上下文中可用,扩展Form默认会获取request.form数据,节省了一些输入。
我们的login_view示例可以根据迄今为止讨论的内容进行重写,如下所示:
#makesureyou'reimportingFormfromflask_wtfandnotwtformsfromflask_wtfimportForm#--//--@app.route('/',methods=['get','post'])deflogin_view():#themethodsthathandlerequestsarecalledviews,inflaskmsg=''#request.formispassedimplicitly;impliesPOSTform=LoginForm()#iftheformshouldalsodealwithform.args,doitlikethis:#form=LoginForm(request.formorrequest.args)#checksthatthesubmitmethodisPOSTandformisvalidifform.validate_on_submit():msg='Usernameandpasswordarecorrect'else:msg='Usernameorpasswordareincorrect'returnrender_template('form.html',message=msg)我们甚至可以更进一步,因为我们显然是完美主义者:
#flashallowsustosendmessagestotheusertemplatewithout#alteringthereturnedcontextfromflaskimportflashfromflaskimportredirect@app.route('/',methods=['get','post'])deflogin_view():#msgisnolongernecessary.Wewilluseflash,insteadform=LoginForm()ifform.validate_on_submit():flash(request,'Usernameandpasswordarecorrect')#it'sgoodpracticetoredirectafterasuccessfulformsubmitreturnredirect('/')returnrender_template('form.html',form=form)在模板中,将{{message}}替换为:
闪现消息在重定向时特别有用,因为它们不受响应上下文的限制。
跨站点请求伪造(CSRF)发生在一个网站试图利用另一个网站对你的浏览器的信任(假设你是用户)时。基本上,你正在访问的网站会尝试获取或更改你已经访问并进行身份验证的网站的信息。想象一下,你正在访问一个网站,该网站有一张图片,加载了你已经进行身份验证的另一个网站的URL;想象一下,给定的URL请求了前一个网站的一个动作,并且该动作改变了你的账户的某些内容——例如,它的状态被修改为非活动状态。嗯,这就是CSRF攻击的一个简单案例。另一个常见的情况是发送JSONP请求。如果被攻击的网站,也就是你没有访问的那个网站,接受JSONP表单替换(JSONP用于跨域请求)并且没有CRSF保护,那么你将面临更加恶劣的攻击。
WTForms自带CSRF保护;Flask-WTF将整个过程与Flask粘合在一起,使你的生活更轻松。为了在使用该扩展时具有CSRF保护,你需要设置secret_key,就是这样:
app.secret_key='somesecretstringvalue'#ex:importos;os.urandom(24)然后,每当你编写一个应该具有CSRF保护的表单时,只需确保向其中添加CSRF令牌,就像这样:
在不希望表单受到CSRF保护的情况下,不要添加令牌。如果希望取消对表单的保护,必须关闭表单的CSRF保护,就像这样:
form=Form(csrf_enabled=False)在使用get方法但同时又使用表单进行验证的搜索字段的情况下,可能需要取消对表单的保护。
创建一个Web应用程序,接收一个名字,然后回答:“你好,”。如果表单为空发送,应显示错误消息。如果给定的名字是“查克·诺里斯”,答案应该是“旋风踢!”。
创建一个Web应用程序,显示一张图片,并询问用户看到了什么。然后应用程序应验证答案是否正确。如果不正确,向用户显示错误消息。否则,祝贺用户并显示一张新图片。使用Flask-WTF。
创建一个具有四种运算的计算器。它应该有用户可以点击的所有数字和运算符。确保它看起来像一个计算器(因为我们是完美主义者!),并且在用户尝试一些恶意操作时进行投诉,比如将0除以0。
学到了这么多...我能说什么呢!试试看也没什么坏处,对吧?嗯,我们已经学会了如何编写HTML表单;使用Flask读取表单;编写WTForms表单;使用纯Python和表单验证器验证表单数据;以及编写自定义验证器。我们还看到了如何使用Flask-WTF来编写和验证我们的表单,以及如何保护我们的应用程序免受CSRF攻击。
在下一章中,我们将看看如何使用出色、易于使用的库将Web应用程序数据存储在关系型和非关系型数据库中,并如何将它们与Flask集成。还将进行数据库的简要概述,以便更顺畅地吸收知识。
我就像一只松鼠。我偶尔会在家里的秘密藏匿处留下一些钱,以防我被抢劫,或者在一个月里花费太多。我真的忘记了我所有的藏匿处在哪里,这有点有趣也有点悲哀(对我来说)。
现在,想象一下,你正在存储一些同样重要甚至更重要的东西,比如客户数据或者甚至你公司的数据。你能允许自己将它存储在以后可能会丢失或者可以被某人干扰的地方吗?我们正处于信息时代;信息就是力量!
在网络应用程序世界中,我们有两个大的数据存储玩家:关系数据库和NoSQL数据库。第一种是传统的方式,其中您的数据存储在表和列中,事务很重要,期望有ACID,规范化是关键(双关语)!它使用SQL来存储和检索数据。在第二种方式中,情况变得有点疯狂。您的数据可能存储在不同的结构中,如文档、图形、键值映射等。写入和查询语言是特定于供应商的,您可能不得不放弃ACID以换取速度,大量的速度!
你可能已经猜到了!这一章是关于MVC中的M层,也就是如何以透明的方式存储和访问数据的章节!我们将看一下如何使用查询和写入两种数据库类型的示例,以及何时选择使用哪种。
SQLAlchemy是一个与关系数据库一起工作的惊人库。它是由Pocoo团队制作的,他们也是Flask的创始人,被认为是“事实上”的PythonSQL库。它可以与SQLite、Postgres、MySQL、Oracle和所有SQL数据库一起使用,这些数据库都有兼容的驱动程序。
尽管所有的例子都将以SQLite为主要考虑对象进行给出和测试,但它们应该在其他数据库中也能够以很少或没有改动的方式工作。在适当的时候,将会不时地给出特定于数据库的提示。
在我们的第一个例子之前,我们是否应该复习一下几个关系数据库的概念?
表是低级抽象结构,用于存储数据。它由列和行组成,其中每一列代表数据的一部分,每一行代表一个完整的记录。通常,每个表代表一个类模型的低级抽象。
行是给定类模型的单个记录。您可能需要将多个行记录分散到不同的表中,以记录完整的信息。一个很好的例子是MxN关系。
列代表存储的数据本身。每一列都有一个特定的类型,并且只接受该类型的输入数据。您可以将其视为类模型属性的抽象。
事务是用来将要执行的操作分组的方式。它主要用于实现原子性。这样,没有操作是半途而废的。
主键是一个数据库概念,记录的一部分数据用于标识数据库表中的给定记录。通常由数据库通过约束来实现。
外键是一个数据库概念,用于在不同表之间标识给定记录的一组数据。它的主要用途是在不同表的行之间构建关系。通常由数据库通过约束来实现。
有关规范形式的概述,请参阅以下链接:
我们现在可以继续了!
让我们开始将库安装到我们的环境中,并尝试一些示例:
pipinstallsqlalchemy我们的第一个示例!让我们为一家公司(也许是你的公司?)创建一个简单的员工数据库:
fromsqlalchemyimportcreate_enginedb=create_engine('sqlite:///employees.sqlite')#echooutputtoconsoledb.echo=Trueconn=db.connect()conn.execute("""CREATETABLEemployee(idINTEGERPRIMARYKEY,nameSTRING(100)NOTNULL,birthdayDATENOTNULL)""")conn.execute("INSERTINTOemployeeVALUES(NULL,'marcosmango',date('1990-09-06'));")conn.execute("INSERTINTOemployeeVALUES(NULL,'rosierinn',date('1980-09-06'));")conn.execute("INSERTINTOemployeeVALUES(NULL,'manniemoon',date('1970-07-06'));")forrowinconn.execute("SELECT*FROMemployee"):printrow#giveconnectionbacktotheconnectionpoolconn.close()前面的例子非常简单。我们创建了一个SQLAlchemy引擎,从连接池中获取连接(引擎会为您处理),然后执行SQL命令来创建表,插入几行数据并查询是否一切都如预期发生。
在我们的插入中,我们为主键id提供了值NULL。请注意,SQLite不会使用NULL填充主键;相反,它会忽略NULL值,并将列设置为新的、唯一的整数。这是SQLite特有的行为。例如,Oracle将要求您显式插入序列的下一个值,以便为主键设置一个新的唯一列值。
我们之前的示例使用了一个名为autocommit的功能。这意味着每次执行方法调用都会立即提交到数据库。这样,您无法一次执行多个语句,这在现实世界的应用程序中是常见的情况。
要一次执行多个语句,我们应该使用事务。我们可以通过事务重写我们之前的示例,以确保所有三个插入要么一起提交,要么根本不提交(严肃的表情...)。
#westartourtransactionhere#allactionsnowareexecutedwithinthetransactioncontexttrans=conn.begin()try:#weareusingaslightlydifferentinsertionsyntaxforconvenience,here;#idvalueisnotexplicitlyprovidedconn.execute("INSERTINTOemployee(name,birthday)VALUES('marcosmango',date('1990-09-06'));")conn.execute("INSERTINTOemployee(name,birthday)VALUES('rosierinn',date('1980-09-06'));")conn.execute("INSERTINTOemployee(name,birthday)VALUES('manniemoon',date('1970-07-06'));")#commitalltrans.commit()except:#allornothing.Undowhatwasexecutedwithinthetransactiontrans.rollback()raise到目前为止还没有什么花哨的。在我们的例子中,我们从连接创建了一个事务,执行了一些语句,然后提交以完成事务。如果在事务开始和结束之间发生错误,except块将被执行,并且在事务中执行的所有语句将被回滚或“撤消”。
我们可以通过在表之间创建关系来完善我们的示例。想象一下,我们的员工在公司档案中注册了一个或多个地址。我们将创建一个1xN关系,其中一个员工可以拥有一个或多个地址。
我们已经看到了如何创建表和关系,运行语句来查询和插入数据,并使用SQLAlchemy进行事务处理;我们还没有完全探索SQLAlchemy库的强大功能。
SQLAlchemy具有内置的ORM,允许您像使用本机对象实例一样使用数据库表。想象一下,读取列值就像读取实例属性一样,或者通过方法查询复杂的表关系,这就是SQLAlchemy的ORM。
让我们看看使用内置ORM的示例会是什么样子:
我们所有的类模型都应该从中继承。然后我们创建一个session。会话是您使用SQLAlchemyORM模型的方式。
会话具有内部正在进行的事务,因此它非常容易具有类似事务的行为。它还将您的模型映射到正确的引擎,以防您使用多个引擎;但等等,还有更多!它还跟踪从中加载的所有模型实例。例如,如果您将模型实例添加到其中,然后修改该实例,会话足够聪明,能够意识到其对象的更改。因此,它会将自身标记为脏(内容已更改),直到调用提交或回滚。
在示例中,在找到marcos之后,我们可以将"MarcosMango's"的名字更改为其他内容,比如"marcostangerine",就像这样:
marcos.name="marcostangerine"session.commit()现在,在Base.metadata之后注释掉整个代码,并添加以下内容:
marcos=session.query(Employee).filter(Employee.name.like(r"%marcos%")).first()marcos_last_name=marcos.name.split('')[-1]printmarcos_last_name现在,重新执行示例。Marcos的新姓氏现在是"tangerine"。神奇!
在谈论了这么多关于SQLAlchemy之后,您能否请醒来,因为我们将谈论Flask-SQLAlchemy,这个扩展将库与Flask集成在一起。
Flask-SQLAlchemy是一个轻量级的扩展,它将SQLAlchemy封装在Flask周围。它允许您通过配置文件配置SQLAlchemy引擎,并为每个请求绑定一个会话,为您提供了一种透明的处理事务的方式。让我们看看如何做到这一点。首先,确保我们已经安装了所有必要的软件包。加载虚拟环境后,运行:
pipinstallflask-wtfflask-sqlalchemy我们的代码应该是这样的:
自动生成表单非常方便。使用model_form,您可以自省定义的模型类并生成适合该模型的表单类。您还可以通过model_form参数field_args为字段提供参数,这对于添加元素类或额外验证器非常有用。
您可能还注意到Employee扩展了db.Model,这是您的ORM模型基类。所有您的模型都应该扩展它,以便被db所知,它封装了我们的引擎并保存我们的请求感知会话。
在index函数内部,我们实例化表单,然后检查它是否通过POST提交并且有效。在if块内部,我们实例化我们的员工模型,并使用populate_obj将表单的值放入模型实例中。我们也可以逐个字段地进行操作,就像这样:
employee.name=form.name.dataemployee.birthday=form.birthday.datapopulate_obj只是更方便。在填充模型后,我们将其添加到会话中以跟踪它,并提交会话。在此块中发生任何异常时,我们将其放在一个带有准备回滚的try/except块中。
请注意,我们使用Employee.query来查询存储在我们数据库中的员工。每个模型类都带有一个query属性,允许您从数据库中获取和过滤结果。对query的每个过滤调用将返回一个BaseQuery实例,允许您堆叠过滤器,就像这样:
queryset=Employee.query.filter_by(name='marcosmango')queryset=queryset.filter_by(birthday=datetime.strptime('1990-09-06','%Y-%m-%d'))queryset.all()#<=returnstheresultofbothfiltersappliedtogether这里有很多可能性。为什么不现在就尝试一些例子呢?
MongoDB是一个广泛使用的强大的NoSQL数据库。它允许您将数据存储在文档中;一个可变的、类似字典的、类似对象的结构,您可以在其中存储数据,而无需担心诸如“我的数据是否规范化到第三范式?”或“我是否必须创建另一个表来存储我的关系?”等问题。
MongoDB文档实际上是BSON文档,是JSON的超集,支持扩展的数据类型。如果您知道如何处理JSON文档,您应该不会有问题。
让我们在本地安装MongoDB,以便尝试一些例子:
sudoapt-getinstallmongodb现在,从控制台输入:
mongo您将进入MongoDB交互式控制台。从中,您可以执行命令,向数据库添加文档,查询、更新或删除。您可以通过控制台实现的任何语法,也可以通过控制台实现。现在,让我们了解两个重要的MongoDB概念:数据库和集合。
在MongoDB中,您的文档被分组在集合内,而集合被分组在数据库内。因此,在连接到MongoDB后,您应该做的第一件事是选择要使用的数据库。您不需要创建数据库,连接到它就足以创建数据库。对于集合也是一样。您也不需要在使用文档之前定义其结构,也不需要实现复杂的更改命令,如果您决定文档结构应该更改。这里有一个例子:
>useexampleswitchedtodbexample>db.employees.insert({name:'marcosmango',birthday:newDate('Sep06,1990')})WriteResult({"nInserted":1})>db.employees.find({'name':{$regex:/marcos/}})在上述代码中,我们切换到示例数据库,然后将一个新文档插入到员工集合中(我们不需要在使用之前创建它),最后,我们使用正则表达式搜索它。MongoDB控制台实际上是一个JavaScript控制台,因此新的Date实际上是JavaScript类Date的实例化。非常简单。
关于正确使用MongoDB,只需记住几个黄金规则:
避免将数据从一个集合保留到另一个集合,因为MongoDB不喜欢连接
在MongoDB中,将文档值作为列表是可以的,甚至是预期的
在MongoDB中,适当的文档索引(本书未涉及)对性能至关重要
写入比读取慢得多,可能会影响整体性能
MongoEngine是一个非常棒的Python库,用于访问和操作MongoDB文档,并使用PyMongo,MongoDB推荐的Python库。
由于PyMongo没有文档对象映射器(DOM),我们不直接使用它。尽管如此,有些情况下MongoEngineAPI将不够用,您需要使用PyMongo来实现您的目标。
在实际的日常开发中,确切地知道您应该在文档中存储什么样的信息是一个很好的反疯狂功能,MongoEngine可以直接为您提供。
由于您的机器上已经安装了MongoDB,只需安装MongoEngine库即可开始使用它编码:
pipinstallmongoenginepymongo==2.8让我们使用我们的新库将“RosieRinn”添加到数据库中:
#coding:utf-8frommongoengineimport*fromdatetimeimportdatetime#asthemongodaemon,mongod,isrunninglocally,wejustneedthedatabasenametoconnectconnect('example')classEmployee(Document):name=StringField()birthday=DateTimeField()def__unicode__(self):returnu'employee%s'%self.nameemployee=Employee()employee.name='rosierinn'employee.birthday=datetime.strptime('1980-09-06','%Y-%m-%d')employee.save()foreinEmployee.objects(name__contains='rosie'):printe理解我们的示例:首先,我们使用example数据库创建了一个MongoDB连接,然后像使用SQLAlchemy一样定义了我们的员工文档,最后,我们插入了我们的员工“Rosie”并查询是否一切正常。
由于MongoDB不支持事务(MongoDB不是ACID,记住?),MongoEngine也不支持,我们进行的每个操作都是原子的。当我们创建我们的“Rosie”并调用save方法时,“Rosie”立即插入数据库;不需要提交更改或其他任何操作。
MongoEngine本身非常容易,但有人认为事情可以变得更好,于是我们有了Flask-MongoEngine。它为您提供了三个主要功能:
Flask-DebugToolbar集成(嘿嘿!)
类似Django的查询集(get_or_404,first_or_404,paginate,paginate_field)
连接管理
现在,让我们看一个完整的flask-mongoengine示例。首先,在我们的虚拟环境中安装这个库:
pipinstallflask-mongoengine现在开始编码:
由于MongoDB不会解析你发送给它的数据以尝试弄清楚查询的内容,所以你不会遇到SQL注入的问题。
你可能会想知道何时使用关系型数据库,何时使用NoSQL。嗯,鉴于今天存在的技术和技术,我建议你选择你感觉更适合的类型来工作。NoSQL吹嘘自己是无模式、可扩展、快速等,但关系型数据库对于大多数需求也是相当快速的。一些关系型数据库,比如Postgres,甚至支持文档。那么扩展呢?嗯,大多数项目不需要扩展,因为它们永远不会变得足够大。其他一些项目,只需与它们的关系型数据库一起扩展。
这一章非常紧凑!我们对关系型和NoSQL数据库进行了概述,学习了MongoDB和MongoEngine,SQLite和SQLAlchemy,以及如何使用扩展来将Flask与它们集成。知识积累得很快!现在你能够创建更复杂的带有数据库支持、自定义验证、CSRF保护和用户通信的网络应用程序了。
在下一章中,我们将学习关于REST的知识,它的优势,以及如何创建服务供应用程序消费。
REST是一种架构风格,由于其许多特性和架构约束(如可缓存性、无状态行为和其接口要求),近年来一直在获得动力。
本章我们将专注于RESTfulWeb服务和API——即遵循REST架构的Web服务和WebAPI。让我们从开始说起:什么是Web服务?
Web服务是一个可以被你的应用程序查询的Web应用程序,就像它是一个API一样,提高了用户体验。如果你的RESTfulWeb服务不需要从传统的UI界面调用,并且可以独立使用,那么你拥有的是一个RESTfulWeb服务API,简称“RESTfulAPI”,它的工作方式就像一个常规API,但通过Web服务器。
对Web服务的调用可能会启动批处理过程、更新数据库或只是检索一些数据。对服务可能执行的操作没有限制。
RESTfulWeb服务应该通过URI(类似于URL)访问,并且可以通过任何Web协议访问,尽管HTTP在这里是王者。因此,我们将专注于HTTP。我们的Web服务响应,也称为资源,可以具有任何所需的格式;如TXT、XML或JSON,但最常见的格式是JSON,因为它非常简单易用。我们还将专注于JSON。在使用HTTP与Web服务时,一种常见的做法是使用HTTP默认方法(GET、POST、PUT、DELETE和OPTIONS)向服务器提供关于我们想要实现的更多信息。这种技术允许我们在同一个服务中拥有不同的功能。
让我们看看每个通常使用的方法通常用于什么:
GET:这用于检索资源。你想要信息?不需要更新数据库?使用GET!
POST:这用于将新数据插入服务器,比如在数据库中添加新员工。
PUT:这用于更新服务器上的数据。你有一个员工决定在系统中更改他的昵称?使用PUT来做到这一点!
DELETE:这是你在服务器上删除数据的最佳方法!
OPTIONS:这用于询问服务支持哪些方法。
到目前为止,有很多理论;让我们通过一个基于Flask的RESTWeb服务示例来实践。
首先,安装示例所需的库:
pipinstallmarshmallow现在,让我们来看一个例子:
让我们逐步讨论这个例子:
这种类型的服务通常由使用JavaScript(如jQuery或PrototypeJS)的Ajax请求来消耗。在发送Ajax请求时,这些库会添加一个特殊的标头,使我们能够识别给定请求是否实际上是Ajax请求。在我们的前面的例子中,我们为所有GET请求提供JSON响应。
我们可以更加选择性,只对Ajax请求发送JSON响应。常规请求将收到纯HTML响应。要做到这一点,我们需要对视图进行轻微更改,如下所示:
fromflaskimportrequest…@app.route("/articles/",methods=["GET"])@app.route("/articles/
我们的最后一个示例也可以采用不同的方法;我们可以通过Ajax请求加载所有数据,而不向其添加上下文变量来呈现HTML模板。在这种情况下,需要对代码进行以下更改:
articles.html文件将如下所示:
到目前为止,我们已经有了一些舒适的Ajax和RESTfulWeb服务的示例,但我们还没有使用服务将数据记录到我们的数据库中。现在试试吧?
使用Web服务记录到数据库与我们在上一章中所做的并没有太大的不同。我们将从Ajax请求中接收数据,然后检查使用了哪种HTTP方法以决定要做什么,然后我们将验证发送的数据并保存所有数据(如果没有发现错误)。在第四章请填写这张表格,夫人中,我们谈到了CSRF保护及其重要性。我们将继续使用我们的Web服务对数据进行CSRF验证。诀窍是将CSRF令牌添加到要提交的表单数据中。有关示例HTML,请参见随附的电子书代码。
这是我们的视图支持POST,PUT和REMOVE方法:
Flask-Restless是一个扩展,能够自动生成整个RESTfulAPI,支持GET、POST、PUT和DELETE,用于你的SQLAlchemy模型。大多数Web服务不需要更多。使用Flask-Restless的另一个优势是可以扩展自动生成的方法,进行身份验证验证、自定义行为和自定义查询。这是一个必学的扩展!
让我们看看我们的Web服务在Flask-Restless下会是什么样子。我们还需要为这个示例安装一个新的库:
pipinstallFlask-Restless然后:
在控制台中,输入以下命令发送GET请求到API,并测试您的示例是否正常工作:
create_api的serializer/deserializer参数在您需要为模型进行自定义序列化/反序列化时非常有用。使用方法很简单:
manager.create_api(Model,methods=METHODS,serializer=my_serializer,deserializer=my_deserializer)defmy_serializer(instance):returnsome_schema.dump(instance).datadefmy_deserializer(data):returnsome_schema.load(data).data您可以使用marshmallow生成模式,就像前面的示例一样。
create_api的另一个有用的选项是include_columns和exclude_columns。它们允许您控制API返回多少数据,并防止返回敏感数据。当设置include_columns时,只有其中定义的字段才会被GET请求返回。当设置exclude_columns时,只有其中未定义的字段才会被GET请求返回。例如:
#boththestatementsbelowareequivalentsmanager.create_api(Article,methods=['GET'],include_columns=['id','title'])manager.create_api(Article,methods=['GET'],exclude_columns=['content'])总结在本章中,我们学习了REST是什么,它的优势,如何创建FlaskRESTfulWeb服务和API,以及如何使用Flask-Restless使整个过程顺利运行。我们还概述了jQuery是什么,以及如何使用它发送Ajax请求来查询我们的服务。这些章节示例非常深入。尝试自己编写示例代码,以更好地吸收它们。
在下一章中,我们将讨论确保软件质量的一种方式:测试!我们将学习如何以各种方式测试我们的Web应用程序,以及如何将这些测试集成到我们的编码例程中。到时见!
您编写的软件是否具有质量?您如何证明?
通常根据特定的需求编写软件,无论是错误报告、功能和增强票据,还是其他。为了具有质量,软件必须完全和准确地满足这些需求;也就是说,它应该做到符合预期。
就像您会按下按钮来了解它的功能一样(假设您没有手册),您必须测试您的代码以了解它的功能或证明它应该做什么。这就是您确保软件质量的方式。
在软件开发过程中,通常会有许多共享某些代码库或库的功能。例如,您可以更改一段代码以修复错误,并在代码的另一个点上创建另一个错误。软件测试也有助于解决这个问题,因为它们确保您的代码执行了应该执行的操作;如果您更改了一段错误的代码并且破坏了另一段代码,您也将破坏一个测试。在这种情况下,如果您使用持续集成,则破损的代码将永远不会到达生产环境。
测试是如此重要,以至于有一个称为测试驱动开发(TDD)的软件开发过程,它规定测试应该在实际代码之前编写,并且只有当测试本身得到满足时,实际代码才是准备就绪。TDD在高级开发人员及以上中非常常见。就为了好玩,我们将在本章中从头到尾使用TDD。
我们想要测试,我们现在就想要;但是我们想要什么样的测试呢?
测试有两种主要分类,根据你对内部代码的访问程度:黑盒和白盒测试。
黑盒测试是指测试人员对其正在测试的实际代码没有知识和/或访问权限。在这些情况下,测试包括检查代码执行前后的系统状态是否符合预期,或者给定的输出是否对应于给定的输入。
白盒测试有所不同,因为您将可以访问您正在测试的实际代码内部,以及代码执行前后的系统预期状态和给定输入的预期输出。这种测试具有更强烈的主观目标,通常与性能和软件质量有关。
在本章中,我们将介绍如何实施黑盒测试,因为它们更容易让其他人接触并且更容易实施。另一方面,我们将概述执行白盒测试的工具。
代码库可能经过多种方式测试。我们将专注于两种类型的自动化测试(我们不会涵盖手动测试技术),每种测试都有不同的目标:单元测试和行为测试。这些测试各自有不同的目的,并相互补充。让我们看看这些测试是什么,何时使用它们以及如何在Flask中运行它们。
单元测试是一种技术,您可以针对具有有意义功能的最小代码片段(称为单元)对输入和预期输出进行测试。通常,您会对代码库中不依赖于您编写的其他函数和方法的函数和方法运行单元测试。
在某种意义上,测试实际上是将单元测试堆叠在一起的艺术(首先测试一个函数,然后相互交互的函数,然后与其他系统交互的函数),以便整个系统最终得到充分测试。
对于Python的单元测试,我们可以使用内置模块doctest或unittest。doctest模块用于作为测试用例运行来自对象文档的嵌入式交互式代码示例。Doctests是Unittest的一个很好的补充,Unittest是一个更健壮的模块,专注于帮助您编写单元测试(正如其名称所暗示的那样),最好不要单独使用。让我们看一个例子:
#coding:utf-8"""Doctestexample"""importdoctestimportunittestdefsum_fnc(a,b):"""Returnsa+b>>>sum_fnc(10,20)30>>>sum_fnc(-10,-20)-30>>>sum_fnc(10,-20)-10"""returna+bclassTestSumFnc(unittest.TestCase):deftest_sum_with_positive_numbers(self):result=sum_fnc(10,20)self.assertEqual(result,30)deftest_sum_with_negative_numbers(self):result=sum_fnc(-10,-20)self.assertEqual(result,-30)deftest_sum_with_mixed_signal_numbers(self):result=sum_fnc(10,-20)self.assertEqual(result,-10)if__name__=='__main__':doctest.testmod(verbose=1)unittest.main()在前面的例子中,我们定义了一个简单的sum_fnc函数,它接收两个参数并返回它们的和。sum_fnc函数有一个解释自身的文档字符串。在这个文档字符串中,我们有一个函数调用和输出的交互式代码示例。这个代码示例是由doctest.testmod()调用的,它检查给定的输出是否对于调用的函数是正确的。
接下来,我们有一个名为TestSumFnc的TestCase,它定义了三个测试方法(test_
如果我们希望检查我们的结果是否大于某个值,我们将使用assertGreater或assertGreaterEqual方法,这样断言错误也会告诉我们我们有什么样的错误。
良好的测试彼此独立,以便一个失败的测试永远不会阻止另一个测试的运行。从测试中导入测试依赖项并清理数据库是常见的做法。
在编写脚本或桌面应用程序时,前面的情况很常见。Web应用程序对测试有不同的需求。Web应用程序代码通常是响应通过浏览器请求的用户交互而运行,并返回响应作为输出。要在这种环境中进行测试,我们必须模拟请求并正确测试响应内容,这通常不像我们的sum_fnc的输出那样直截了当。响应可以是任何类型的文档,它可能具有不同的大小和内容,甚至您还必须担心响应的HTTP代码,这包含了很多上下文含义。
为了帮助您测试视图并模拟用户与您的Web应用程序的交互,Flask为您提供了一个测试客户端工具,通过它您可以向您的应用程序发送任何有效的HTTP方法的请求。例如,您可以通过PUT请求查询服务,或者通过GET请求查看常规视图。这是一个例子:
此外,要注意test_request_context,它用于在我们的测试中创建一个请求上下文。我们创建这个上下文,以便url_for能够返回我们的视图路径,如果没有设置SERVER_NAME配置,它需要一个请求上下文。
如果您的网站使用子域,设置SERVER_NAME配置。
在单元测试中,我们测试函数的输出与预期结果。如果结果不是我们等待的结果,将引发断言异常以通知问题。这是一个简单的黑盒测试。现在,一些奇怪的问题:您是否注意到您的测试是以与错误报告或功能请求不同的方式编写的?您是否注意到您的测试不能被非技术人员阅读,因为它实际上是代码?
Lettuce可以帮助您将实际用户编写的功能转换为测试方法调用。这样,一个功能请求就像:
功能:计算总和
为了计算总和
作为学生
实现sum_fnc
场景:正数之和
假设我有数字10和20
当我把它们加起来
然后我看到结果30
场景:负数之和
假设我有数字-10和-20
然后我看到结果-30
场景:混合信号之和
假设我有数字10和-20
然后我看到结果-10
该功能可以转换为将测试软件的实际代码。确保lettuce已正确安装:
pipinstalllettucepython-Levenshtein创建一个features目录,并在其中放置一个steps.py(或者您喜欢的任何其他Python文件名),其中包含以下代码:
#coding:utf-8fromlettuceimport*fromlibimportsum_fnc@step('GivenIhavethenumbers(\-\d+)and(\-\d+)')defhave_the_numbers(step,*numbers):numbers=map(lambdan:int(n),numbers)world.numbers=numbers@step('WhenIsumthem')defcompute_sum(step):world.result=sum_fnc(*world.numbers)@step('ThenIseetheresult(\-\d+)')defcheck_number(step,expected):expected=int(expected)assertworld.result==expected,"Got%d;expected%d"%(world.result,expected)我们刚刚做了什么?我们定义了三个测试函数,have_the_numbers,compute_sum和check_number,其中每个函数都接收一个step实例作为第一个参数,以及用于实际测试的其他参数。用于装饰我们的函数的step装饰器用于将从我们的Gherkin文本解析的字符串模式映射到函数本身。我们的装饰器的另一个职责是将从步骤参数映射到函数的参数的参数解析为参数。
例如,have_the_numbers的步骤具有正则表达式模式(\-\d+)和(\-\d+),它将两个数字映射到我们函数的numbers参数。这些值是从我们的Gherkin输入文本中获取的。对于给定的场景,这些数字分别是[10,20],[-10,-20]和[10,-20]。最后,world是一个全局变量,您可以在步骤之间共享值。
使用功能描述行为对开发过程非常有益,因为它使业务人员更接近正在创建的内容,尽管它相当冗长。另外,由于它冗长,不建议在测试孤立的函数时使用,就像我们在前面的例子中所做的那样。行为应该由业务人员编写,也应该测试编写人员可以直观证明的行为。例如,“如果我点击一个按钮,我会得到某物的最低价格”或“假设我访问某个页面,我会看到一些消息或一些链接”。
“点击这里,然后那里发生了什么”。检查渲染的请求响应有点棘手,如果您问我的话。为什么?在我们的第二个例子中,我们验证了给定的字符串值是否在我们的resp.data中,这是可以的,因为我们的响应返回complete。我们不使用JavaScript在页面加载后渲染任何内容或显示消息。如果是这种情况,我们的验证可能会返回错误的结果,因为JavaScript代码不会被执行。
与大多数Flask扩展一样,Flask-testing并没有做太多事情,但它所做的事情都做得很好!我们将讨论Flask-testing提供的一些非常有用的功能:LiveServer设置,额外的断言和JSON响应处理。在继续之前,请确保已安装:
编译PhantomJS后,确保它在您的PATH中被找到,将二进制文件bin/phantomjs移动到/usr/local/bin。
确保安装了Selenium:
pipinstallselenium我们的代码将如下所示:
然后我们定义IndexTest,它扩展了LiveServerTestCase,这是一个特殊的类,我们用它来运行我们的实时服务器测试。我们将我们的实时服务器设置为在不同的端口上运行,但这并不是必需的。
在setUp中,我们使用seleniumWebDriver创建一个driver。该驱动程序类似于浏览器。我们将使用它通过LiveServer访问和检查我们的应用程序。tearDown确保每次测试后关闭我们的驱动程序并释放资源。
test_server_is_up_and_running是不言自明的,在现实世界的测试中实际上是不必要的。
然后我们有test_random_text_was_loaded,这是一个非常繁忙的测试。我们使用test_request_context来创建一个请求上下文,以便使用url_open.get_server_url生成我们的URL路径,这将返回我们的实时服务器URL;我们将这个URL与我们的视图路径连接起来并加载到我们的驱动程序中。
使用加载的URL(请注意,URL不仅加载了,而且脚本也执行了),我们使用find_element_by_id来查找元素fillme并断言其文本内容具有预期值之一。这是一个简单的例子。例如,您可以测试按钮是否在预期位置;提交表单;并触发JavaScript函数。Selenium加上PhantomJS是一个强大的组合。
当您的开发是由功能测试驱动时,您实际上并没有使用TDD,而是行为驱动开发(BDD)。通常,两种技术的混合是您想要的。
在测试代码时,您会注意到一些测试有点重复。为了处理这种情况,可以创建一个具有特定例程的自定义TestCases,并相应地扩展测试。使用Flask-testing,您仍然需要这样做,但是要编写更少的代码来测试您的Flask视图,因为flask.ext.testing.TestCase捆绑了常见的断言,许多在Django等框架中找到。让我们看看最重要的(在我看来,当然)断言:
assert_context(name,value):这断言一个变量是否在模板上下文中。用它来验证给定的响应上下文对于一个变量具有正确的值。
assert_redirects(response,location):这断言了响应是一个重定向,并给出了它的位置。在写入存储后进行重定向是一个很好的做法,比如在成功的POST后,这是这个断言的一个很好的使用案例。
assert_template_used(name,tmpl_name_attribute='name'):这断言了请求中使用了给定的模板(如果您没有使用Jinja2,则需要tmpl_name_attribute;在我们的情况下不需要);无论何时您渲染一个HTML模板,都可以使用它!
assert404(response,message=None):这断言了响应具有404HTTP状态码;它对于“雨天”场景非常有用;也就是说,当有人试图访问不存在的内容时。它非常有用。
Flask-testing为您提供了一个可爱的技巧。每当您从视图返回一个JSON响应时,您的响应将有一个额外的属性叫做json。那就是您的JSON转换后的响应!以下是一个例子:
如果您使用Flask-SQLAlchemy来保存您的数据,您需要在您的测试中某个地方硬编码如下:
attributes={…}model=MyModel(**attributes)db.session.add(model)db.session.commit()这种方法不易扩展,因为它不容易重复使用(当您将其定义为一个函数和一个方法时,为每个测试定义它)。有两种方法可以为测试填充您的数据库:固定装置和伪随机数据。
使用伪随机数据通常是特定于库的,并且生成的数据是上下文特定的,而不是静态的,但有时可能需要特定的编码,就像当您定义自己的字段或需要字段的不同值范围时一样。
固定装置是最直接的方法,因为您只需在文件中定义您的数据,并在每个测试中加载它。您可以通过导出数据库数据,根据您的方便进行编辑,或者自己编写。JSON格式在这方面非常受欢迎。让我们看看如何实现这两种方法:
如果我们使用伪随机生成器来创建我们的用户(查找Google上关于这个主题的模糊测试),我们可以这样做:
defnew_user(**kw):#thiswayweonlyknowtheuserdatainexecutiontime#testsshouldconsideritkw['name']=kw.get('name',"%s%s"%(choice(names),choice(surnames)))kw['gender']=kw.get('gender',choice(['M','F','U']))returnkwuser=User(**new_user())db.session.add(user)db.session.commit()请注意,由于我们不是针对静态场景进行测试,我们的测试也会发生变化。通常情况下,固定装置在大多数情况下就足够了,但伪随机测试数据在大多数情况下更好,因为它迫使您的应用处理真实场景,而这些通常被忽略。
集成测试是一个非常常用的术语/概念,但其含义非常狭窄。它用于指代测试多个模块一起测试它们的集成。由于使用Python从同一代码库中测试多个模块通常是微不足道且透明的(这里导入,那里调用,以及一些输出检查),您通常会听到人们在指代测试他们的代码与不同代码库进行集成测试时使用术语集成测试,或者当系统添加了新的关键功能时。
哇!我们刚刚度过了一章关于软件测试的内容!这是令人自豪的成就。我们学到了一些概念,比如TDD、白盒测试和黑盒测试。我们还学会了如何创建单元测试;测试我们的视图;使用Gherkin语言编写功能并使用lettuce进行测试;使用Flask-testing、Selenium和PhantomJS来测试用户角度的HTML响应;还学会了如何使用固定装置来控制我们应用程序的状态,以进行正确可重复的测试。现在,您可以使用不同的技术以正确的方式测试Flask应用程序,以满足不同的场景和需求。
在下一章中,事情会变得非常疯狂,因为我们的研究对象将是使用Flask的技巧。下一章将涵盖蓝图、会话、日志记录、调试等内容,让您能够创建更加健壮的软件。到时见!
在尝试更高级的Flask主题之前,你还能等多久?我肯定不能!在本章中,我们将学习技术和模块,这些对于更好更高效地使用Flask至关重要。
如果你遇到这种情况,可以让你的客户更接近开发过程,以避免这种情况。尝试在Google或DuckDuckGo中搜索“scrum”。
在谈论生产力和可维护性时,方法有很多!你可以购买一个像PyCharm或WingIDE这样的好的集成开发环境(IDE)来提高你的生产力,或者雇佣第三方服务来帮助你测试你的代码或控制你的开发进度,但这些只能做到这么多。良好的架构和任务自动化将是大多数项目中的最好朋友。在讨论如何组织你的代码以及哪些模块将帮助你节省一些打字之前,让我们讨论一下过早优化和过度设计,这是焦虑的开发人员/分析师/好奇的经理的两个可怕的症状。
制作软件有点像制作公寓,有一些相似之处。在开始之前,你会提前计划你想要创造的东西,以便将浪费降到最低。与公寓相反,你不必计划你的软件,因为它在开发过程中很可能会发生变化,而且很多计划可能只是浪费。
这种“计划刚刚好”的方法的问题在于你不知道未来会发生什么,这可能会将我们内心的一点点偏执变成一些大问题。一个人可能最终会编写针对完全系统故障或复杂软件需求场景的代码,而这些可能永远不会发生。你不需要多层架构,缓存,数据库集成,信号系统等等,来创建一个helloworld,也不需要少于这些来创建一个Facebook克隆。
始终计划合理的安全性,复杂性和性能水平。
所以,开始Flask吧。
到目前为止,我们的应用程序都是平面的:美丽的,单文件的Web应用程序(不考虑模板和静态资源)。在某些情况下,这是一个不错的方法;减少了对导入的需求,易于使用简单的编辑器进行维护,但是...
随着我们的应用程序的增长,我们发现需要上下文地安排我们的代码。Flask蓝图允许你将项目模块化,将你的视图分片成“类似应用程序”的对象,称为蓝图,这些蓝图可以稍后由你的Flask应用程序加载和公开。大型应用程序受益于使用蓝图,因为代码变得更有组织性。
在功能上,它还可以帮助您以更集中的方式配置已注册的视图访问和资源查找。测试、模型、模板和静态资源可以按蓝图进行排序,使您的代码更易于维护。如果您熟悉Django,可以将蓝图视为Django应用程序。这样,注册的蓝图可以访问应用程序配置,并可以使用不同的路由进行注册。
与Django应用程序不同,蓝图不强制执行特定的结构,就像Flask应用程序本身一样。例如,您可以将蓝图结构化为模块,这在某种程度上是方便的。
例子总是有帮助的,对吧?让我们看一个蓝图的好例子。首先,在我们的虚拟环境中安装了示例所需的库:
#libraryforparsingandreadingourHTMLpipinstalllxml#ourtest-friendlylibrarypipinstallflask-testing然后我们定义了我们的测试(因为我们喜欢TDD!):
现在我们可以创建一个视图,使用满足我们测试的蓝图:
如果我们的蓝图名称是blog,并且我们注册了一个方法index_view,我们对该视图的端点将是blog.index_view。端点是对视图的“名称引用”,您可以将其转换为其URL路径。
下一步是在我们的Flask应用程序中注册我们的蓝图,以便使我们编写的视图可访问。还创建了一个database.py模块来保存我们的db实例。
请注意,我们的Post模型将被db.create_all识别,因为它是在blog.py中定义的;因此,当模块被导入时,它变得可见。
如果您在任何地方导入了一个模块中定义的模型类,那么它的表可能不会被创建,因为SQLAlchemy将不知道它。避免这种情况的一种方法是让所有模型都由定义蓝图的模块导入。
#coding:utf-8#database.pyfromflask.ext.sqlalchemyimportSQLAlchemydb=SQLAlchemy()##database.pyEND#coding:utf-8#main.pyfromflaskimportFlaskfromdatabaseimportdbfromblogimportappasblog_bpdefapp_factory(name=None):app=Flask(nameor__name__)app.config['SQLALCHEMY_DATABASE_URI']='sqlite:////tmp/ex01.db'db.init_app(app)#letFlaskknowaboutblogblueprintapp.register_blueprint(blog_bp)returnapp#runningorimportingif__name__=='__main__':app=app_factory()app.debug=True#makesureourtablesarecreatedwithapp.test_request_context():db.create_all()app.run()我们在这里有什么?一个app_factory,它创建我们的Flask应用程序,在/tmp/中设置默认数据库,这是一个常见的Linux临时文件夹;初始化我们的数据库管理器,在database.py中定义;并使用register_blueprint注册我们的蓝图。
我们设置了一个例行程序来验证我们是在运行还是导入给定的模块(对于runtests.py很有用,因为它从main.py导入);如果我们正在运行它,我们创建一个应用程序,将其设置为调试模式(因为我们正在开发),在临时测试上下文中创建数据库(create_all不会在上下文之外运行),并运行应用程序。
模板(post.html和posts.html)仍然需要编写。您能写出来使测试通过吗?我把它留给你来做!
我们当前的示例项目结构应该如下所示:
嗯,我们的项目仍然是平的;所有模块都在同一级别上,上下文排列,但是平的。让我们尝试将我们的博客蓝图移动到自己的模块中!我们可能想要这样的东西:
博客模板位于博客包中的模板文件夹中,我们的模型位于models.py中,我们的视图位于views.py中(就像Django应用程序一样,对吧?)。
可以轻松进行这种更改。主要是创建一个blog文件夹,并在其中放置一个带有以下内容的__init__.py文件:
#coding:utf-8fromviewsimport*将Post类定义和db导入移到models.py中,并将特定于博客的模板post.html和posts.html移到包内的templates文件夹中。由于template_folder是相对于当前模块目录的,因此无需更改我们的蓝图实例化。现在,运行您的测试。它们应该可以正常工作,无需修改。
喝一口水,戴上你的战斗头盔,让我们继续下一个话题:记录!
在面对一个你无法完全理解的神秘问题之前,你永远不会知道记录有多么重要。了解为什么会出现问题是人们将记录添加到他们的项目中的第一个,也可能是主要的原因。但是,嘿,什么是记录?
Python标准库捆绑了一个记录库,实际上非常强大,通过处理程序和消息,可以记录到流、文件、电子邮件或您认为合适的任何其他解决方案。让我们尝试一些有用的记录示例,好吗?
记录到文件中非常简单,主要用于跟踪应用程序中的执行流程(主要是INFO、DEBUG和WARN日志)。基本上,文件记录应该在您有应该记录但不应立即阅读甚至根本不阅读的消息时使用(如果发生意外情况,您可能希望阅读DEBUG日志,但其他情况则不需要)。这样,在出现问题时,您只需查看日志文件,看看出了什么问题。邮件记录有另一个目标…
要配置我们的邮件记录器,我们定义一个名为configure_mail_logger的函数。它创建并注册一个SMTPHandler到我们的记录器在给定的记录级别;这样,每当记录一个具有该记录级别或更高级别的消息时,就会向注册的管理员发送一封电子邮件。
邮件记录有一个主要目的:尽快通知某人(或很多人)发生了重要事件,比如可能危及应用程序的错误。您可能不希望为此类处理程序设置低于ERROR的记录级别,因为会有太多的邮件需要跟踪。
关于记录的最后一条建议是,理智的项目都有良好的记录。追溯用户问题报告甚至邮件错误消息是很常见的。定义良好的记录策略并遵循它们,构建工具来分析您的日志,并设置适合项目需求的记录轮换参数。产生大量记录的项目可能需要更大的文件,而没有太多记录的项目可能可以使用较高值的backupCount。一定要仔细考虑一下。
在调试模式下运行Flask项目(app.debug=True)时,每当Flask检测到您的代码已更改,它将重新启动您的应用程序。如果给定的更改破坏了您的应用程序,Flask将在控制台中显示一个非常简单的错误消息,可以很容易地分析。您可以从下往上阅读,直到找到第一行提到您编写的文件的行;这就是错误生成的地方。现在,从上往下阅读,直到找到一行告诉您确切的错误是什么。如果这种方法不够用,如果您需要读取变量值,例如更好地理解发生了什么,您可以使用pdb,标准的Python调试库,就像这样:
#coding:utf-8fromflaskimportFlaskapp=Flask(__name__)@app.route("/")defindex_view(arg=None):importpdb;pdb.set_trace()#@TODOremovemebeforecommitreturn'Argis%s'%argif__name__=='__main__':app.debug=Trueapp.run()每当调用pdb.set_trace时,将打开一个pdb控制台,它非常像Python控制台。因此,您可以查询任何您需要的值,甚至进行代码评估。
首先,确保已安装扩展:
pipinstallflask-debugtoolbar然后是一些精彩的代码:
右侧的可折叠面板是调试工具栏在每个HTML响应中插入的一小部分HTML,它允许您检查响应,而无需使用pdb等调试器。在示例中,我们将DEBUG_TB_TEMPLATE_EDITOR_ENABLED设置为True;此选项告诉DebugToolbar我们希望直接从浏览器中编辑渲染的模板。只需转到模板|编辑模板来尝试。
Flask会话是使用浏览器cookie和加密实现的请求之间的瞬时存储解决方案。Flask使用秘钥值来加密您在会话中设置的任何值,然后将其设置在cookie中;这样,即使恶意人士可以访问受害者的浏览器,也无法读取cookie的内容。
由于秘钥用于加密会话数据,因此为您的秘钥设置一个强大的值非常重要。os.urandom(24)可能会为部署环境创建一个强大的秘钥。
会话中存储的数据是瞬时的,因为不能保证它在任何时候都会存在,因为用户可能会清除浏览器的cookie,或者cookie可能会过期,但如果您设置了它,它很可能会存在。在开发时,始终考虑这一点。
Flask会话的一个重大优势是其简单性;您可以像使用常规字典一样使用它,就像这样:
让我们把知识付诸实践吧?尝试制作一个商店网站应用,比如一个在线宠物商店。它应该有宠物服务,例如洗澡和兽医咨询,还有一个小商店,出售宠物配饰。这应该足够简单(很多工作!但是简单)。
这是一个密集的章节。我们概述了重要的概念——如性能和可维护性、生产力和质量——快速讨论了过早优化和过度工程化,并将我们的努力集中在学习如何用Flask编写更好的代码上。
蓝图允许您使用Flask创建强大的大型项目,并通过一个完整的示例进行了讨论;我们学习了如何记录到文件和邮件以及每个的重要性,与Flask-DebugToolbar度过了愉快的时光(非常方便!),并且将默认的会话设置和使用牢记在心。
你现在是一个有能力的Flask开发者。我感到非常自豱!
就像一个人在尝试漂移之前先学会开车一样,我们将在下一章开始我们的Flask漂移。我们的重点将是利用Flask提供的广泛扩展生态系统来创建令人惊叹的项目。这将非常有趣!到时见!
我们已经在前几章中使用扩展来增强我们的示例;Flask-SQLAlchemy用于连接到关系数据库,Flask-MongoEngine用于连接到MongoDB,Flask-WTF用于创建灵活可重用的表单,等等。扩展是一种很好的方式,可以在不妨碍您的代码的情况下为项目添加功能,如果您喜欢我们迄今为止所做的工作,您会喜欢这一章,因为它专门介绍了扩展!
在本章中,我们将了解一些迄今为止忽视的非常流行的扩展。我们要开始了吗?
Flask扩展是您导入的模块,(通常)初始化,并用于与第三方库集成。它们通常是从flask.ext.
扩展最好有两种状态:未初始化和已初始化。这是一个好的做法,因为在实例化扩展时,您的Flask应用程序可能不可用。我们在上一章的示例中只有在主模块中导入Flask-SQLAlchemy后才进行初始化。好的,知道了,但初始化过程为何重要呢?
嗯,正是通过初始化,扩展才能从应用程序中获取其配置。例如:
fromflaskimportFlaskimportlogging#setconfigurationforyourFlaskapplicationorextensionsclassConfig(object):LOG_LEVEL=logging.WARNINGapp=Flask(__name__)app.config.from_object(Config)app.run()在上面的代码中,我们创建了一个配置类,并使用config.from_object加载了它。这样,LOG_LEVEL就可以在所有扩展中使用,通过对应用实例的控制。
app.config['LOG_LEVEL']将配置加载到app.config的另一种方法是使用环境变量。这种方法在部署环境中特别有用,因为您不希望将敏感的部署配置存储在版本控制存储库中(这是不安全的!)。它的工作原理如下:
…app.config.from_envvar('PATH_TO_CONFIGURATION')如果PATH_TO_CONFIGURATION设置为Python文件路径,例如/home/youruser/someconfig.py,那么someconfig.py将加载到配置中。像这样做:
#intheconsoleexportPATH_TO_CONFIGURATION=/home/youruser/someconfig.py然后创建配置:
#someconfig.pyimportloggingLOG_LEVEL=logging.WARNING早期的配置方案都有相同的结果。
请注意,from_envvar将从运行项目的用户加载环境变量。如果将环境变量导出到您的用户并作为另一个用户(如www-data)运行项目,则可能无法找到您的配置。
Flask-Principal通过四个简单的实体处理权限:Identity,IdentityContext,Need和Permission。
Identity:这意味着Flask-Principal识别用户的方式。
Need是您需要满足的标准(啊哈时刻!),以便做某事,比如拥有角色或权限。Principal提供了一些预设的需求,但您也可以轻松创建自己的需求,因为Need只是一个命名元组,就像这样一个:
pipinstallflask-wtfflask-loginflask-principalflask-sqlalchemy然后:
我们还创建了我们的admin_permission。我们的管理员权限只有一个需求:角色管理员。这样,我们定义了我们的权限接受用户时,这个用户需要拥有角色admin。
classLoginForm(Form):defget_user(self):returnUser.query.filter_by(username=self.username.data).first()user=property(get_user)username=StringField(validators=[validators.InputRequired()])password=PasswordField(validators=[validators.InputRequired()])defvalidate_username(self,field):"Validatesthattheusernamebelongstoanactualuser"ifself.userisNone:#donotsendaveryspecificerrormessagehere,otherwiseyou'll#betellingtheuserwhichusersareavailableinyourdatabaseraiseValidationError('Yourusernameandpassworddidnotmatch')defvalidate_password(self,field):username=field.datauser=User.query.get(username)ifuserisnotNone:ifnotuser.password==field.data:raiseValidationError('Yourusernameandpassworddidnotmatch')在这里,我们自己编写了LoginForm。你可能会说:“为什么不使用model_form呢?”嗯,在这里使用model_form,您将不得不使用您的应用程序初始化数据库(您目前还没有)并设置上下文。太麻烦了。
我们还定义了两个自定义验证器,一个用于检查username是否有效,另一个用于检查password和username是否匹配。
请注意,我们为这个特定表单提供了非常广泛的错误消息。我们这样做是为了避免向可能的攻击者提供太多信息。
@identity_loaded.connect_via(app)#weusethedecoratortoletthelogin_managerknowofourload_user#useridisthemodelidattributebydefault@login_manager.user_loaderdefload_user(userid):"""Loadsanuserusingtheuser_idUsedbyflask-logintoloadtheuserwiththeuseridstoredinsession"""returnUser.query.get(userid)defon_identity_loaded(sender,identity):#Settheidentityuserobjectidentity.user=current_user#incaseyouhaveresourcesthatbelongtoaspecificuserifhasattr(current_user,'id'):identity.provides.add(UserNeed(current_user.id))#AssumingtheUsermodelhasalistofroles,updatethe#identitywiththerolesthattheuserprovidesifhasattr(current_user,'roles'):forroleincurrent_user.roles:identity.provides.add(RoleNeed(role.name))load_user函数是Flask-Login要求的,用于使用会话存储中存储的userid加载用户。如果没有找到userid,它应该返回None。不要在这里抛出异常。
on_identity_loaded被注册到identity_loaded信号,并用于加载存储在模型中的身份需求。这是必需的,因为Flask-Principal是一个通用解决方案,不知道您如何存储权限。
defpopulate():"""Populatesourdatabasewithasingleuser,fortesting;)WhynotusefixturesJustdon'twanna..."""user=User(username='student',password='passwd',active=True)db.session.add(user)db.session.commit()role=Role(name='admin',user_id=user.id)db.session.add(role)db.session.commit()if__name__=='__main__':app=app_factory()#weneedtouseacontexthere,otherwisewe'llgetaruntimeerrorwithapp.test_request_context():db.drop_all()db.create_all()populate()app.run()populate用于在我们的数据库中添加适当的用户和角色,以便您进行测试。
现在这是一个你可以与前面的代码一起使用的base.html模板的示例:
Django之所以如此出名的原因之一是因为它有一个漂亮而灵活的管理界面,我们也想要一个!
就像Flask-Principal和Flask-Login一样,我们将用来构建我们的管理界面的扩展Flask-Admin不需要特定的数据库来使用。你可以使用MongoDB作为关系数据库(与SQLAlchemy或PeeWee一起),或者你喜欢的其他数据库。
与Django相反,Django的管理界面专注于应用程序/模型,而Flask-Admin专注于页面/模型。你不能(没有一些重编码)将整个蓝图(Flask的Django应用程序等效)加载到管理界面中,但你可以为你的蓝图创建一个页面,并将蓝图模型注册到其中。这种方法的一个优点是你可以轻松选择所有模型将被列出的位置。
在我们之前的例子中,我们创建了两个模型来保存我们的用户和角色信息,所以,让我们为这两个模型创建一个简单的管理员界面。我们确保我们的依赖已安装:
pipinstallflask-admin然后:
我们向管理员视图添加身份验证和权限验证的一种方法是通过扩展ModelView和IndexView。我们还将使用一个称为mixin的很酷的设计模式:
由于我们的模型已经具有创建、读取、更新、删除(CRUD)和权限控制访问,我们如何修改我们的CRUD以仅显示特定字段,或阻止添加其他字段?
就像DjangoAdmin一样,Flask-Admin允许你通过设置类属性来更改你的ModelView行为。我个人最喜欢的几个是这些:
can_create:这允许用户使用CRUD创建模型。
can_edit:这允许用户使用CRUD更新模型。
can_delete:这允许用户使用CRUD删除模型。
list_template、edit_template和create_template:这些是默认的CRUD模板。
list_columns:这意味着列在列表视图中显示。
column_editable_list:这表示可以在列表视图中编辑的列。
form:这是CRUD用来编辑和创建视图的表单。
form_args:这用于传递表单字段参数。像这样使用它:
form_args={'form_field_name':{'parameter':'value'}}#parametercouldbename,forexampleform_overrides:像这样使用它来覆盖表单字段:form_overrides={'form_field':wtforms.SomeField}form_choices:允许你为表单字段定义选择。像这样使用它:form_choices={'form_field':[('valuestoreindb','valuedisplayinthecombobox')]}一个例子看起来像这样:
classAuthModelView(AuthMixinView,ModelView):can_edit=Falseform=MyAuthForm@expose()@login_requireddefindex_view(self):returnsuper(ModelView,self).index_view()自定义页面现在,如果你想要在管理界面中添加一个自定义的报告页面,你肯定不会使用模型视图来完成这个任务。对于这些情况,像这样添加一个自定义的BaseView:
#coding:utf-8fromflaskimportFlaskfromflask.ext.adminimportAdmin,BaseView,exposeclassReportsView(BaseView):@expose('/')defindex(self):#makesurereports.htmlexistsreturnself.render('reports.html')app=Flask(__name__)admin=Admin(app)admin.add_view(ReportsView(name='ReportsPage'))if__name__=='__main__':app.debug=Trueapp.run()现在你有了一个带有漂亮的报告页面链接的管理界面。不要忘记编写一个reports.html页面,以使前面的示例工作。
那么,如果你不希望链接显示在导航栏中,因为你已经在其他地方有了它,怎么办?覆盖BaseView.is_visible方法,因为它控制视图是否会出现在导航栏中。像这样做:
现在,我的朋友,你知道如何开发健壮的Flask应用程序,使用MVC、TDD、与权限和认证控制集成的关系型和NoSQL数据库;表单;如何实现跨站点伪造保护;甚至如何使用开箱即用的管理工具。
我们的研究重点是了解Flask开发世界中所有最有用的工具(当然是我认为的),以及如何在一定程度上使用它们。由于范围限制,我们没有深入探讨任何一个,但基础知识肯定是展示过的。
希望你到目前为止已经喜欢这本书,并且对最后的笔记感到非常愉快。
部署不是每个人都熟悉的术语;如果你最近还不是一个Web开发人员,你可能对它不太熟悉。以一种粗犷的斯巴达方式,我们可以将部署定义为准备和展示你的应用程序给世界的行为,确保所需的资源可用,并对其进行调整,因为适合开发阶段的配置与适合部署的配置是不同的。在Web开发的背景下,我们谈论的是一些非常具体的行动:
将你的代码放在服务器上
设置你的数据库
设置你的HTTP服务器
设置你可能使用的其他服务
将所有内容联系在一起
首先,什么是服务器?我们所说的服务器是指具有高可靠性、可用性和可维护性(RAS)等服务器特性的计算机。这些特性使服务器中运行的应用程序获得一定程度的信任,即使在出现任何环境问题(如硬件故障)之后,服务器也会继续运行。
现在,鉴于我们的Web应用程序已经准备就绪(我的意思是,我们的最小可行产品已经准备就绪),我们必须在某个对我们的目标受众可访问的地方运行代码。这通常意味着我们需要一个Web服务器。从前面一段提到的公司中选择两台便宜的虚拟机,使用Ubuntu进行设置,然后让我们开始吧!
关于数据库,部署过程中你应该知道的最基本的事情之一是,最好的做法是让你的数据库和Web应用程序在不同的(虚拟)机器上运行。你不希望它们竞争相同的资源,相信我。这就是为什么我们雇了两台虚拟服务器——一台将运行我们的HTTP服务器,另一台将运行我们的数据库。
让我们开始设置数据库服务器;首先,我们将我们的SSH凭据添加到远程服务器,这样我们就可以在不需要每次输入远程服务器用户密码的情况下进行身份验证。在此之前,如果你没有它们,生成你的SSH密钥,就像这样:
#typetherootpasswordwhenrequestedssh-copy-idroot@ipaddress现在,退出您的远程终端,尝试SSHroot@ipaddress。密码将不再被请求。
#asrootapt-getpurgeapache2-*apt-getinstallpostgresql#typetocheckwhichversionofpostgreswasinstalled(mostlikely9.x)psql-V现在我们设置数据库。
将默认用户Postgres连接到角色postgres:
sudo-upostgrespsql为我们的项目创建一个名为mydb的数据库:
CREATEDATABASEmydb;创建一个新的用户角色来访问我们的数据库:
CREATEUSERyouWITHPASSWORD'passwd';#please,useastrongpassword#Wenowmakesure"you"candowhateveryouwantwithmydb#Youdon'twanttokeepthissetupforlong,bewarnedGRANTALLPRIVILEGESONDATABASEmydbTOyou;到目前为止,我们已经完成了很多工作。首先,我们删除了不必要的包(只有很少);安装了我们的数据库Postgres的最新支持版本;创建了一个新的数据库和一个新的“用户”;并授予了我们的用户对我们的新数据库的完全权限。让我们了解每一步。
然后我们安装Postgres。根据您的背景,您可能会问——为什么是Postgres而不是MariaDB/MySQL?嗯,嗯,亲爱的读者,Postgres是一个完整的解决方案,支持ACID,文档(JSONB)存储,键值存储(使用HStore),索引,文本搜索,服务器端编程,地理定位(使用PostGIS)等等。如果您知道如何安装和使用Postgres,您就可以在一个单一的解决方案中访问所有这些功能。我也更喜欢它比其他开源/免费解决方案,所以我们将坚持使用它。
现在测试您的数据库设置是否正常工作。从控制台连接到它:
psql-Uuser_you-ddatabase_mydb-h127.0.0.1-W在被要求时输入你的密码。我们之前的命令实际上是我们在使用Postgres时使用的一个技巧,因为我们是通过网络接口连接到数据库的。默认情况下,Postgres假设你试图使用与你的系统用户名相同的角色和数据库进行连接。除非你像我们一样通过网络接口连接,否则你甚至不能以与你的系统用户名不同的角色名称进行连接。
设置你的web服务器会更加复杂,因为它涉及修改更多的文件,并确保它们之间的配置是稳固的,但我们会做到的,你会看到的。
首先,我们要确保我们的项目代码在我们的web服务器上(这不是与数据库服务器相同的服务器,对吧?)。我们可以以多种方式之一来做到这一点:使用FTP(请不要),简单的fabric加rsync,版本控制,或者版本加fabric(开心脸!)。让我们看看如何做后者。
假设你已经在你的web服务器虚拟机中创建了一个名为myuser的常规用户,请确保已经安装了fabric:
sudoapt-getinstallpython-devpipinstallfabric还有,在你的项目根目录中创建一个名为fabfile.py的文件:
#coding:utf-8fromfabric.apiimport*fromfabric.contrib.filesimportexistsenv.linewise=True#forward_agentallowsyoutogitpullfromyourrepository#ifyouhaveyoursshkeysetupenv.forward_agent=Trueenv.hosts=['your.host.ip.address']defcreate_project():ifnotexists('~/project'):run('gitclonegit://path/to/repo.git')defupdate_code():withcd('~/project'):run('gitpull')defreload():"Reloadsprojectinstance"run('touch--no-dereference/tmp/reload')有了上述代码和安装了fabric,假设你已经将你的SSH密钥复制到了远程服务器,并已经与你的版本控制提供商(例如github或bitbucket)进行了设置,create_project和update_code就可以使用了。你可以像这样使用它们:
fabcreate_project#createsourprojectinthehomefolderofourremotewebserverfabupdate_code#updatesourprojectcodefromtheversioncontrolrepository这非常容易。第一条命令将你的代码放入存储库,而第二条命令将其更新到你的最后一次提交。
我们的web服务器设置将使用一些非常流行的工具:
uWSGI:这用于应用服务器和进程管理
Nginx:这用作我们的HTTP服务器
UpStart:这用于管理我们的uWSGI生命周期
UpStart已经随Ubuntu一起提供,所以我们以后会记住它。对于uWSGI,我们需要像这样安装它:
pipinstalluwsgi现在,在你的虚拟环境bin文件夹中,会有一个uWSGI命令。记住它的位置,因为我们很快就会需要它。
在你的项目文件夹中创建一个wsgi.py文件,内容如下:
#coding:utf-8frommainimportapp_factoryapp=app_factory(name="myproject")uWSGI使用上面的文件中的应用实例来连接到我们的应用程序。app_factory是一个创建应用程序的工厂函数。到目前为止,我们已经看到了一些。只需确保它返回的应用程序实例已经正确配置。就应用程序而言,这就是我们需要做的。接下来,我们将继续将uWSGI连接到我们的应用程序。
我们可以在命令行直接调用我们的uWSGI二进制文件,并提供加载wsgi.py文件所需的所有参数,或者我们可以创建一个ini文件,其中包含所有必要的配置,并将其提供给二进制文件。正如你可能猜到的那样,第二种方法通常更好,因此创建一个看起来像这样的ini文件:
我们现在可以设置我们的HTTP服务器,这是一个非常简单的步骤。只需按照以下方式安装Nginx:
server{listen80;server_namePROJECT_DOMAIN;location/media{alias/path/to/media;}location/static{alias/path/to/static;}location/{include/etc/nginx/uwsgi_params;uwsgi_passunix:/path/to/socket/file.sock;}}前面的配置文件创建了一个虚拟服务器,运行在端口80上,监听域server_name,通过/static和/media提供静态和媒体文件,并监听将所有访问指向/的路径,使用我们的套接字处理。我们现在打开我们的配置并关闭nginx的默认配置:
sudorm/etc/nginx/sites-enabled/defaultln-s/etc/nginx/sites-available/project/etc/nginx/sites-enabled/project我们刚刚做了什么?虚拟服务器的配置文件位于/etc/nginx/sites-available/中,每当我们希望nginx看到一个配置时,我们将其链接到已启用的站点。在前面的配置中,我们刚刚禁用了default并通过符号链接启用了project。Nginx不会自行注意到并加载我们刚刚做的事情;我们需要告诉它重新加载其配置。让我们把这一步留到以后。
我们需要在/etc/init中创建一个最后的文件,它将使用upstart将我们的uWSGI进程注册为服务。这部分非常简单;只需创建一个名为project.conf(或任何其他有意义的名称)的文件,内容如下:
description"uWSGIapplicationmyproject"startonrunlevel[2345]stoponrunlevel[!2345]setuidyour-usersetgidwww-dataexec/path/to/uwsgi--ini/path/to/ini/file.ini前面的脚本使用我们的项目ini文件(我们之前创建的)作为参数运行uWSGI,用户为"your-user",组为www-data。用您的用户替换your-user(…),但不要替换www-data组,因为这是必需的配置。前面的运行级别配置只是告诉upstart何时启动和停止此服务。您不必进行干预。
运行以下命令行来启动您的服务:
startproject接下来重新加载Nginx配置,就像这样:
sudo/etc/init.d/nginxreload如果一切顺利,媒体路径和静态路径存在,项目数据库设置指向私有网络内的远程服务器,并且上帝对您微笑,您的项目应该可以从您注册的域名访问。击掌!
由于Flask不强制执行项目结构,您有很大的自由度来尝试最适合您的方式。大型单文件项目可行,类似Django的结构化项目可行,平面架构也可行;可能性很多!因此,许多项目都提出了自己建议的架构;这些项目被称为样板或骨架。它们专注于为您提供一个快速启动新Flask项目的方法,利用他们建议的代码组织方式。
如果您计划使用Flask创建一个大型Web应用程序,强烈建议您至少查看其中一个这些项目,因为它们可能已经面临了一些您可能会遇到的问题,并提出了解决方案:
我必须承认,我写这本书是为了自己。在一个地方找到构建Web应用程序所需的所有知识是如此困难,以至于我不得不把我的笔记放在某个地方,浓缩起来。我希望,如果您读到这一段,您也和我一样觉得,这本书是为您写的。这是一次愉快的挑战之旅。
作为一个个人挑战,拿出你一直梦想编码的项目,但从未有勇气去做的,然后制作一个MVP(最小可行产品)。创建你想法的一个非常简单的实现,并将其发布到世界上看看;然后,给我留言。我很乐意看看你的作品!