├── .github └── FUNDING.yml ├── .gitignore ├── CHANGES.md ├── README.md ├── book.json ├── chapters ├── README.md ├── SUMMARY.md ├── _assets │ ├── banner.png │ ├── extra.css │ ├── favicon.png │ ├── helloflask-logo.png │ └── logo.ico ├── _templates │ └── main.html ├── challenge.md ├── cover.jpg ├── cover.md ├── cover_small.jpg ├── database.md ├── deploy.md ├── form.md ├── hello.md ├── images │ ├── 11-1.png │ ├── 11-10.png │ ├── 11-11.png │ ├── 11-12.png │ ├── 11-2.png │ ├── 11-3.png │ ├── 11-4.png │ ├── 11-5.png │ ├── 11-6.png │ ├── 11-7.png │ ├── 11-8.png │ ├── 11-9.png │ ├── 2-1.png │ ├── 2-2.png │ ├── 3-1.png │ ├── 4-1.png │ ├── 6-1.png │ ├── 6-2.png │ ├── 6-3.png │ ├── 7-1.png │ ├── 7-2.png │ ├── 8-1.png │ ├── 8-2.png │ ├── 8-3.png │ ├── 9-1.png │ ├── qr_alipay.jpg │ └── qr_wechat.jpg ├── login.md ├── organize.md ├── postscript.md ├── preface.md ├── ready.md ├── static.md ├── template.md ├── template2.md └── test.md ├── mkdocs.yml ├── netlify.toml ├── requirements.in └── requirements.txt /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: ['http://greyli.com/support'] # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | _book 3 | _ebook 4 | env 5 | venv 6 | site 7 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | 4 | ## 3.0 5 | 6 | Released: 2022/7/16 7 | 8 | - 针对 Flask 2.1.3 版本进行更新 9 | - 去掉 Python 2.x 相关内容 10 | - 优化部分章节的措辞 11 | - 添加 Netlify 镜像到 tutorial.helloflask.com 12 | 13 | 14 | ## 2.0 15 | 16 | Released: 2019/12/6 17 | 18 | - 去掉对 Pipenv 的介绍,改为使用 venv/virtualenv+pip 19 | - 完善部分描述 20 | - 修复部分笔误 21 | - 对提示、附注内容使用引用样式 22 | 23 | 24 | ## 1.0 25 | 26 | Released: 2019/2/1 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Flask 入门教程 2 | 3 | 这里是《Flask 入门教程》的源码仓库。请访问[本书主页](http://helloflask.com/book/3)在线阅读或下载电子书文件。 4 | 5 | 如果你发现了书中的错误,或是有任何意见或建议,欢迎[创建 Issue](https://github.com/helloflask/flask-tutorial/issues/new) 反馈或提交 Pull Request 进行修正。对于较大的内容变动,建议先[创建 Issue](https://github.com/helloflask/flask-tutorial/issues/new) 进行讨论。谢谢! 6 | 7 | ![](http://helloflask.com/static/tutorial-cover-s.png) 8 | 9 | © 2018 [李辉](http://greyli.com)(Grey Li) / [HelloFlask.com](http://helloflask.com) 10 | 11 | 本书采用 [CC BY-NC-ND 3.0](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh) 协议授权,禁止商用、演绎后分发或无署名转载。 12 | -------------------------------------------------------------------------------- /book.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": "./chapters", 3 | "title": "flask-tutorial", 4 | "language": "zh-hans", 5 | "author": "李辉(Grey Li)", 6 | "plugins": ["disqus"], 7 | "pluginsConfig": { 8 | "disqus": { 9 | "shortName": "flask-tutorial" 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /chapters/README.md: -------------------------------------------------------------------------------- 1 | # Flask 入门教程 2 | 3 | 这是一本 Flask 入门教程,提供了入门 Flask 所需的最少信息,你可以跟随本书自己动手开发一个简单的 [Watchlist 程序](https://github.com/helloflask/watchlist)。本书主页为 。 4 | 5 | 6 | ## 关于作者 7 | 8 | 我叫李辉(Grey Li),我是[《Flask Web 开发实战》](http://helloflask.com/book/1)的作者,Pallets Team 成员。你可以在我的[个人主页](http://greyli.com)了解更多关于我的信息。 9 | 10 | 11 | ## 目录 12 | 13 | - 前言 14 | - 第 1 章:准备工作 15 | - 第 2 章:Hello, Flask! 16 | - 第 3 章:模板 17 | - 第 4 章:静态文件 18 | - 第 5 章:数据库 19 | - 第 6 章:模板优化 20 | - 第 7 章:表单 21 | - 第 8 章:用户认证 22 | - 第 9 章:测试 23 | - 第 10 章:组织你的代码 24 | - 第 11 章:部署上线 25 | - 小挑战 26 | - 后记 27 | 28 | 29 | ## 版权信息 30 | 31 | 书名:Flask 入门教程 32 | 33 | 副书名:使用 Python 和 Flask 开发你的第一个 Web 程序 34 | 35 | 作者:[李辉](http://greyli.com) 36 | 37 | 版本:3.0 38 | 39 | 发布时间:2022.7.16 40 | 41 | © 2018 - 2022 [李辉](http://greyli.com)(Grey Li) / [HelloFlask.com](http://helloflask.com) 42 | 43 | 本书采用 [CC BY-NC-ND 3.0](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh) 协议授权,禁止商用、演绎后分发或无署名转载。 44 | -------------------------------------------------------------------------------- /chapters/SUMMARY.md: -------------------------------------------------------------------------------- 1 | # Summary 2 | 3 | * [简介](README.md) 4 | * [前言](preface.md) 5 | * [第 1 章:准备工作](ready.md) 6 | * [第 2 章:Hello, Flask!](hello.md) 7 | * [第 3 章:模板](template.md) 8 | * [第 4 章:静态文件](static.md) 9 | * [第 5 章:数据库](database.md) 10 | * [第 6 章:模板优化](template2.md) 11 | * [第 7 章:表单](form.md) 12 | * [第 8 章:用户认证](login.md) 13 | * [第 9 章:测试](test.md) 14 | * [第 10 章:组织你的代码](organize.md) 15 | * [第 11 章:部署上线](deploy.md) 16 | * [小挑战](challenge.md) 17 | * [后记](postscript.md) 18 | -------------------------------------------------------------------------------- /chapters/_assets/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/helloflask/flask-tutorial/614626bd3ed8f853d716c305a48ae56d5bcf73c0/chapters/_assets/banner.png -------------------------------------------------------------------------------- /chapters/_assets/extra.css: -------------------------------------------------------------------------------- 1 | .progress-label { 2 | position: absolute; 3 | text-align: center; 4 | font-weight: 700; 5 | width: 100%; 6 | margin: 0; 7 | line-height: 1.2rem; 8 | white-space: nowrap; 9 | overflow: hidden; 10 | } 11 | 12 | .progress-bar { 13 | height: 1.2rem; 14 | float: left; 15 | background-color: #2979ff; 16 | } 17 | 18 | .progress { 19 | display: block; 20 | width: 100%; 21 | margin: 0.5rem 0; 22 | height: 1.2rem; 23 | background-color: #eeeeee; 24 | position: relative; 25 | } 26 | -------------------------------------------------------------------------------- /chapters/_assets/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/helloflask/flask-tutorial/614626bd3ed8f853d716c305a48ae56d5bcf73c0/chapters/_assets/favicon.png -------------------------------------------------------------------------------- /chapters/_assets/helloflask-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/helloflask/flask-tutorial/614626bd3ed8f853d716c305a48ae56d5bcf73c0/chapters/_assets/helloflask-logo.png -------------------------------------------------------------------------------- /chapters/_assets/logo.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/helloflask/flask-tutorial/614626bd3ed8f853d716c305a48ae56d5bcf73c0/chapters/_assets/logo.ico -------------------------------------------------------------------------------- /chapters/_templates/main.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block announce %} 4 | 📌《Flask 入门教程》第 3 版已于 2022 年 7 月 16 日发布,新版本基于 Flask 2.1.x 版本和 Python 3.6+ 版本改写。欢迎关注 Twitter微信公众号获取更多动态。 5 | {% endblock %} 6 | 7 | {% block extrahead %} 8 | {% set title = config.site_name %} 9 | {% if page and page.title and not page.is_homepage %} 10 | {% set title = config.site_name ~ " - " ~ page.title | striptags %} 11 | {% endif %} 12 | {% set image = config.site_url ~ '_assets/banner.png' %} 13 | {% set fav_image = config.site_url ~ '_assets/favicon.png' %} 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | · 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | {% endblock %} 31 | 32 | {% block analytics %} 33 | 34 | 42 | {% endblock %} 43 | -------------------------------------------------------------------------------- /chapters/challenge.md: -------------------------------------------------------------------------------- 1 | # 小挑战 2 | 3 | 经过本书的学习,你应该有能力独立开发一个简单的 Web 程序了。所以这里有一个小挑战:为你的 Watchlist 添加一个留言板功能,效果类似 [SayHello](http://sayhello.helloflask.com)。 4 | 5 | 下面是一些编写提示: 6 | 7 | * 编写表示留言的模型类,更新数据库表 8 | * 创建留言页面的模板 9 | * 在模板中添加留言表单 10 | * 添加显示留言页面的视图函数 11 | * 在显示留言页面的视图函数编写处理表单的代码 12 | * 生成一些虚拟数据进行测试 13 | * 编写单元测试 14 | * 更新到部署后的程序 15 | * 可以参考 [SayHello 源码](https://github.com/greyli/sayhello) 16 | 17 | 如果在完成这个挑战的过程中遇到了困难,可以在 [代码厨房社区](https://codekitchen.community)发起讨论(设置帖子分类为“Flask 入门教程”)。除此之外,你可以在后记查看更多讨论的去处。 18 | -------------------------------------------------------------------------------- /chapters/cover.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/helloflask/flask-tutorial/614626bd3ed8f853d716c305a48ae56d5bcf73c0/chapters/cover.jpg -------------------------------------------------------------------------------- /chapters/cover.md: -------------------------------------------------------------------------------- 1 | ![](cover.jpg) 2 | -------------------------------------------------------------------------------- /chapters/cover_small.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/helloflask/flask-tutorial/614626bd3ed8f853d716c305a48ae56d5bcf73c0/chapters/cover_small.jpg -------------------------------------------------------------------------------- /chapters/database.md: -------------------------------------------------------------------------------- 1 | # 第 5 章:数据库 2 | 3 | 大部分程序都需要保存数据,所以不可避免要使用数据库。用来操作数据库的数据库管理系统(DBMS)有很多选择,对于不同类型的程序,不同的使用场景,都会有不同的选择。在这个教程中,我们选择了属于关系型数据库管理系统(RDBMS)的 [SQLite](https://www.sqlite.org/),它基于文件,不需要单独启动数据库服务器,适合在开发时使用,或是在数据库操作简单、访问量低的程序中使用。 4 | 5 | 6 | ## 使用 SQLAlchemy 操作数据库 7 | 8 | 为了简化数据库操作,我们将使用 [SQLAlchemy](https://www.sqlalchemy.org/)——一个 Python 数据库工具(ORM,即对象关系映射)。借助 SQLAlchemy,你可以通过定义 Python 类来表示数据库里的一张表(类属性表示表中的字段 / 列),通过对这个类进行各种操作来代替写 SQL 语句。这个类我们称之为**模型类**,类中的属性我们将称之为**字段**。 9 | 10 | Flask 有大量的第三方扩展,这些扩展可以简化和第三方库的集成工作。我们下面将使用一个叫做 [Flask-SQLAlchemy](https://flask-sqlalchemy.palletsprojects.com/en/2.x) 的扩展来集成 SQLAlchemy。 11 | 12 | 首先安装它: 13 | 14 | ```bash 15 | (env) $ pip install flask-sqlalchemy==2.5.1 sqlalchemy==1.4.47 16 | ``` 17 | 18 | > **提示** Flask-SQLAlchemy 3.x / SQLAlchemy 2.x 版本有一些大的变化,这里分别固定安装 2.5.1 和 1.4.47 版本。后续教程改写后会删除这里的版本限制。 19 | 20 | 大部分扩展都需要执行一个“初始化”操作。你需要导入扩展类,实例化并传入 Flask 程序实例: 21 | 22 | ```python 23 | from flask_sqlalchemy import SQLAlchemy # 导入扩展类 24 | 25 | app = Flask(__name__) 26 | 27 | db = SQLAlchemy(app) # 初始化扩展,传入程序实例 app 28 | ``` 29 | 30 | ## 设置数据库 URI 31 | 32 | 为了设置 Flask、扩展或是我们程序本身的一些行为,我们需要设置和定义一些配置变量。Flask 提供了一个统一的接口来写入和获取这些配置变量:`Flask.config` 字典。配置变量的名称必须使用大写,写入配置的语句一般会放到扩展类实例化语句之前。 33 | 34 | 下面写入了一个 `SQLALCHEMY_DATABASE_URI` 变量来告诉 SQLAlchemy 数据库连接地址: 35 | 36 | ```python 37 | import os 38 | 39 | # ... 40 | 41 | app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////' + os.path.join(app.root_path, 'data.db') 42 | ``` 43 | 44 | > **注意** 这个配置变量的最后一个单词是 URI,而不是 URL。 45 | 46 | 对于这个变量值,不同的 DBMS 有不同的格式,对于 SQLite 来说,这个值的格式如下: 47 | 48 | ```python 49 | sqlite:////数据库文件的绝对地址 50 | ``` 51 | 52 | 数据库文件一般放到项目根目录即可,`app.root_path` 返回程序实例所在模块的路径(目前来说,即项目根目录),我们使用它来构建文件路径。数据库文件的名称和后缀你可以自由定义,一般会使用 .db、.sqlite 和 .sqlite3 作为后缀。 53 | 54 | 另外,如果你使用 Windows 系统,上面的 URI 前缀部分只需要写入三个斜线(即 `sqlite:///`)。在本书的示例程序代码里,做了一些兼容性处理,另外还新设置了一个配置变量,实际的代码如下: 55 | 56 | *app.py:数据库配置* 57 | 58 | ```python 59 | import os 60 | import sys 61 | 62 | from flask import Flask 63 | from flask_sqlalchemy import SQLAlchemy 64 | 65 | WIN = sys.platform.startswith('win') 66 | if WIN: # 如果是 Windows 系统,使用三个斜线 67 | prefix = 'sqlite:///' 68 | else: # 否则使用四个斜线 69 | prefix = 'sqlite:////' 70 | 71 | app = Flask(__name__) 72 | app.config['SQLALCHEMY_DATABASE_URI'] = prefix + os.path.join(app.root_path, 'data.db') 73 | app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False # 关闭对模型修改的监控 74 | # 在扩展类实例化前加载配置 75 | db = SQLAlchemy(app) 76 | ``` 77 | 78 | 如果你固定在某一个操作系统上进行开发,部署时也使用相同的操作系统,那么可以不用这么做,直接根据你的需要写出前缀即可。 79 | 80 | > **提示** 你可以访问 [Flask 文档的配置页面](https://flask.palletsprojects.com/config/)查看 Flask 内置的配置变量;同样的,在 [Flask-SQLAlchemy 文档的配置页面](https://flask-sqlalchemy.palletsprojects.com/en/2.x/config/)可以看到 Flask-SQLAlchemy 提供的配置变量。 81 | 82 | 83 | ## 创建数据库模型 84 | 85 | 在 Watchlist 程序里,目前我们有两类数据要保存:用户信息和电影条目信息。下面分别创建了两个模型类来表示这两张表: 86 | 87 | *app.py:创建数据库模型* 88 | 89 | ```python 90 | class User(db.Model): # 表名将会是 user(自动生成,小写处理) 91 | id = db.Column(db.Integer, primary_key=True) # 主键 92 | name = db.Column(db.String(20)) # 名字 93 | 94 | 95 | class Movie(db.Model): # 表名将会是 movie 96 | id = db.Column(db.Integer, primary_key=True) # 主键 97 | title = db.Column(db.String(60)) # 电影标题 98 | year = db.Column(db.String(4)) # 电影年份 99 | ``` 100 | 101 | 模型类的编写有一些限制: 102 | 103 | * 模型类要声明继承 `db.Model`。 104 | * 每一个类属性(字段)要实例化 `db.Column`,传入的参数为字段的类型,下面的表格列出了常用的字段类。 105 | * 在 `db.Column()` 中添加额外的选项(参数)可以对字段进行设置。比如,`primary_key` 设置当前字段是否为主键。除此之外,常用的选项还有 `nullable`(布尔值,是否允许为空值)、`index`(布尔值,是否设置索引)、`unique`(布尔值,是否允许重复值)、`default`(设置默认值)等。 106 | 107 | 常用的字段类型如下表所示: 108 | 109 | | 字段类 | 说明 | 110 | | ---------------- | --------------------------------------------- | 111 | | db.Integer | 整型 | 112 | | db.String (size) | 字符串,size 为最大长度,比如 `db.String(20)` | 113 | | db.Text | 长文本 | 114 | | db.DateTime | 时间日期,Python `datetime` 对象 | 115 | | db.Float | 浮点数 | 116 | | db.Boolean | 布尔值 | 117 | 118 | 119 | ## 创建数据库表 120 | 121 | 模型类创建后,还不能对数据库进行操作,因为我们还没有创建表和数据库文件。下面在 Python Shell 中创建了它们: 122 | 123 | ```python 124 | (env) $ flask shell 125 | >>> from app import db 126 | >>> db.create_all() 127 | ``` 128 | 129 | 打开文件管理器,你会发现项目根目录下出现了新创建的数据库文件 data.db。这个文件不需要提交到 Git 仓库,我们在 .gitignore 文件最后添加一行新规则: 130 | 131 | ``` 132 | *.db 133 | ``` 134 | 135 | 如果你改动了模型类,想重新生成表模式,那么需要先使用 `db.drop_all()` 删除表,然后重新创建: 136 | 137 | ```python 138 | >>> db.drop_all() 139 | >>> db.create_all() 140 | ``` 141 | 142 | 注意这会一并删除所有数据,如果你想在不破坏数据库内的数据的前提下变更表的结构,需要使用数据库迁移工具,比如集成了 [Alembic](https://alembic.sqlalchemy.org/en/latest/) 的 [Flask-Migrate](https://github.com/miguelgrinberg/Flask-Migrate) 扩展。 143 | 144 | > **提示** 上面打开 Python Shell 使用的是 `flask shell`命令,而不是 `python`。使用这个命令启动的 Python Shell 激活了“程序上下文”,它包含一些特殊变量,这对于某些操作是必须的(比如上面的 `db.create_all()`调用)。请记住,后续的 Python Shell 都会使用这个命令打开。 145 | 146 | 和 `flask shell`类似,我们可以编写一个自定义命令来自动执行创建数据库表操作: 147 | 148 | *app.py:自定义命令 initdb* 149 | 150 | ```python 151 | import click 152 | 153 | 154 | @app.cli.command() # 注册为命令,可以传入 name 参数来自定义命令 155 | @click.option('--drop', is_flag=True, help='Create after drop.') # 设置选项 156 | def initdb(drop): 157 | """Initialize the database.""" 158 | if drop: # 判断是否输入了选项 159 | db.drop_all() 160 | db.create_all() 161 | click.echo('Initialized database.') # 输出提示信息 162 | ``` 163 | 164 | 默认情况下,如果没有指定,函数名称就是命令的名字(注意函数名中的下划线会被转换为连接线),现在执行 `flask initdb` 命令就可以创建数据库表: 165 | 166 | ```bash 167 | (env) $ flask initdb 168 | ``` 169 | 170 | 使用 `--drop` 选项可以删除表后重新创建: 171 | 172 | ```bash 173 | (env) $ flask initdb --drop 174 | ``` 175 | 176 | 177 | ## 创建、读取、更新、删除 178 | 179 | 在前面打开的 Python Shell 里,我们来测试一下常见的数据库操作。你可以跟着示例代码来操作,也可以自由练习。 180 | 181 | 182 | ### 创建 183 | 184 | 下面的操作演示了如何向数据库中添加记录: 185 | 186 | ```python 187 | >>> from app import User, Movie # 导入模型类 188 | >>> user = User(name='Grey Li') # 创建一个 User 记录 189 | >>> m1 = Movie(title='Leon', year='1994') # 创建一个 Movie 记录 190 | >>> m2 = Movie(title='Mahjong', year='1996') # 再创建一个 Movie 记录 191 | >>> db.session.add(user) # 把新创建的记录添加到数据库会话 192 | >>> db.session.add(m1) 193 | >>> db.session.add(m2) 194 | >>> db.session.commit() # 提交数据库会话,只需要在最后调用一次即可 195 | ``` 196 | 197 | > **提示** 在实例化模型类的时候,我们并没有传入 `id` 字段(主键),因为 SQLAlchemy 会自动处理这个字段。 198 | 199 | 最后一行 `db.session.commit()` 很重要,只有调用了这一行才会真正把记录提交进数据库,前面的 `db.session.add()` 调用是将改动添加进数据库会话(一个临时区域)中。 200 | 201 | 202 | ### 读取 203 | 204 | 通过对模型类的 `query` 属性调用可选的过滤方法和查询方法,我们就可以获取到对应的单个或多个记录(记录以模型类实例的形式表示)。查询语句的格式如下: 205 | 206 | ```python 207 | <模型类>.query.<过滤方法(可选)>.<查询方法> 208 | ``` 209 | 210 | 下面是一些常用的过滤方法: 211 | 212 | | 过滤方法 | 说明 | 213 | | ----------- | ------------------------------------------------------------ | 214 | | filter() | 使用指定的规则过滤记录,返回新产生的查询对象 | 215 | | filter_by() | 使用指定规则过滤记录(以关键字表达式的形式),返回新产生的查询对象 | 216 | | order_by() | 根据指定条件对记录进行排序,返回新产生的查询对象 | 217 | | group_by() | 根据指定条件对记录进行分组,返回新产生的查询对象 | 218 | 219 | 下面是一些常用的查询方法: 220 | 221 | | 查询方法 | 说明 | 222 | | -------------- | ------------------------------------------------------------ | 223 | | all() | 返回包含所有查询记录的列表 | 224 | | first() | 返回查询的第一条记录,如果未找到,则返回 None | 225 | | get(id) | 传入主键值作为参数,返回指定主键值的记录,如果未找到,则返回 None | 226 | | count() | 返回查询结果的数量 | 227 | | first_or_404() | 返回查询的第一条记录,如果未找到,则返回 404 错误响应 | 228 | | get_or_404(id) | 传入主键值作为参数,返回指定主键值的记录,如果未找到,则返回 404 错误响应 | 229 | | paginate() | 返回一个 Pagination 对象,可以对记录进行分页处理 | 230 | 231 | 下面的操作演示了如何从数据库中读取记录,并进行简单的查询: 232 | 233 | ```python 234 | >>> from app import Movie # 导入模型类 235 | >>> movie = Movie.query.first() # 获取 Movie 模型的第一个记录(返回模型类实例) 236 | >>> movie.title # 对返回的模型类实例调用属性即可获取记录的各字段数据 237 | 'Leon' 238 | >>> movie.year 239 | '1994' 240 | >>> Movie.query.all() # 获取 Movie 模型的所有记录,返回包含多个模型类实例的列表 241 | [, ] 242 | >>> Movie.query.count() # 获取 Movie 模型所有记录的数量 243 | 2 244 | >>> Movie.query.get(1) # 获取主键值为 1 的记录 245 | 246 | >>> Movie.query.filter_by(title='Mahjong').first() # 获取 title 字段值为 Mahjong 的记录 247 | 248 | >>> Movie.query.filter(Movie.title=='Mahjong').first() # 等同于上面的查询,但使用不同的过滤方法 249 | 250 | ``` 251 | 252 | > **提示** 我们在说 Movie 模型的时候,实际指的是数据库中的 movie 表。表的实际名称是模型类的小写形式(自动生成),如果你想自己指定表名,可以定义 `__tablename__` 属性。 253 | 254 | 对于最基础的 `filter()` 过滤方法,SQLAlchemy 支持丰富的查询操作符,具体可以访问[文档相关页面](http://docs.sqlalchemy.org/en/latest/core/sqlelement.html#sqlalchemy.sql.operators.ColumnOperators)查看。除此之外,还有更多的查询方法、过滤方法和数据库函数可以使用,具体可以访问文档的 [Query API](https://docs.sqlalchemy.org/en/latest/orm/query.html) 部分查看。 255 | 256 | 257 | ### 更新 258 | 259 | 下面的操作更新了 `Movie` 模型中主键为 `2` 的记录: 260 | 261 | ```python 262 | >>> movie = Movie.query.get(2) 263 | >>> movie.title = 'WALL-E' # 直接对实例属性赋予新的值即可 264 | >>> movie.year = '2008' 265 | >>> db.session.commit() # 注意仍然需要调用这一行来提交改动 266 | ``` 267 | 268 | ### 删除 269 | 270 | 下面的操作删除了 `Movie` 模型中主键为 `1` 的记录: 271 | 272 | ```python 273 | >>> movie = Movie.query.get(1) 274 | >>> db.session.delete(movie) # 使用 db.session.delete() 方法删除记录,传入模型实例 275 | >>> db.session.commit() # 提交改动 276 | ``` 277 | 278 | ## 在程序里操作数据库 279 | 280 | 经过上面的一番练习,我们可以在 Watchlist 里进行实际的数据库操作了。 281 | 282 | 283 | ### 在主页视图读取数据库记录 284 | 285 | 因为设置了数据库,负责显示主页的 `index` 可以从数据库里读取真实的数据: 286 | 287 | ```python 288 | @app.route('/') 289 | def index(): 290 | user = User.query.first() # 读取用户记录 291 | movies = Movie.query.all() # 读取所有电影记录 292 | return render_template('index.html', user=user, movies=movies) 293 | ``` 294 | 295 | 在 `index` 视图中,原来传入模板的 `name` 变量被 `user` 实例取代,模板 index.html 中的两处 `name` 变量也要相应的更新为 `user.name` 属性: 296 | 297 | ```jinja2 298 | {{ user.name }}'s Watchlist 299 | ``` 300 | 301 | 302 | ### 生成虚拟数据 303 | 304 | 因为有了数据库,我们可以编写一个命令函数把虚拟数据添加到数据库里。下面是用来生成虚拟数据的命令函数: 305 | 306 | *app.py:创建自定义命令 forge* 307 | 308 | ```python 309 | import click 310 | 311 | 312 | @app.cli.command() 313 | def forge(): 314 | """Generate fake data.""" 315 | db.create_all() 316 | 317 | # 全局的两个变量移动到这个函数内 318 | name = 'Grey Li' 319 | movies = [ 320 | {'title': 'My Neighbor Totoro', 'year': '1988'}, 321 | {'title': 'Dead Poets Society', 'year': '1989'}, 322 | {'title': 'A Perfect World', 'year': '1993'}, 323 | {'title': 'Leon', 'year': '1994'}, 324 | {'title': 'Mahjong', 'year': '1996'}, 325 | {'title': 'Swallowtail Butterfly', 'year': '1996'}, 326 | {'title': 'King of Comedy', 'year': '1999'}, 327 | {'title': 'Devils on the Doorstep', 'year': '1999'}, 328 | {'title': 'WALL-E', 'year': '2008'}, 329 | {'title': 'The Pork of Music', 'year': '2012'}, 330 | ] 331 | 332 | user = User(name=name) 333 | db.session.add(user) 334 | for m in movies: 335 | movie = Movie(title=m['title'], year=m['year']) 336 | db.session.add(movie) 337 | 338 | db.session.commit() 339 | click.echo('Done.') 340 | ``` 341 | 342 | 现在执行 `flask forge` 命令就会把所有虚拟数据添加到数据库里: 343 | 344 | ```bash 345 | (env) $ flask forge 346 | ``` 347 | 348 | 349 | ## 本章小结 350 | 351 | 本章我们学习了使用 SQLAlchemy 操作数据库,后面你会慢慢熟悉相关的操作。结束前,让我们提交代码: 352 | 353 | ```bash 354 | $ git add . 355 | $ git commit -m "Add database support with Flask-SQLAlchemy" 356 | $ git push 357 | ``` 358 | 359 | > **提示** 你可以在 GitHub 上查看本书示例程序的对应 commit:[4d2442a](https://github.com/helloflask/watchlist/commit/4d2442a41e55fb454e092864206af08e4e3eeddf)。 360 | 361 | 362 | ## 进阶提示 363 | 364 | * 在生产环境,你可以更换更合适的 DBMS,因为 SQLAlchemy 支持多种 SQL 数据库引擎,通常只需要改动非常少的代码。 365 | * 我们的程序只有一个用户,所以没有将 User 表和 Movie 表建立关联。访问 Flask-SQLAlchemy 文档的“[声明模型](https://flask-sqlalchemy.palletsprojects.com/en/2.x/models/#one-to-many-relationships)”章节可以看到相关内容。 366 | * 阅读 [SQLAlchemy 官方文档和教程](https://docs.sqlalchemy.org/en/latest/)详细了解它的用法。注意我们在这里使用 Flask-SQLAlchemy 来集成它,所以用法和单独使用 SQLAlchemy 有一些不同。作为参考,你可以同时阅读 [Flask-SQLAlchemy 官方文档](https://flask-sqlalchemy.palletsprojects.com/en/2.x/)。 367 | * 如果你是[《Flask Web 开发实战》](http://helloflask.com/book/1)的读者,第 5 章详细介绍了 SQLAlchemy 和 Flask-Migrate 的使用,第 8 章和第 9 章引入了更复杂的模型关系和查询方法。 368 | -------------------------------------------------------------------------------- /chapters/deploy.md: -------------------------------------------------------------------------------- 1 | # 第 11 章:部署上线 2 | 3 | 在这个教程的最后一章,我们将会把程序部署到互联网上,让网络中的其他所有人都可以访问到。 4 | 5 | Web 程序通常有两种部署方式:传统部署和云部署。传统部署指的是在使用物理主机或虚拟主机上部署程序,你通常需要在一个 Linux 系统上完成所有的部署操作;云部署则是使用其他公司提供的云平台,这些平台为你设置好了底层服务,包括 Web 服务器、数据库等等,你只需要上传代码并进行一些简单设置即可完成部署。这一章我们会介绍使用云平台 [PythonAnywhere](https://www.pythonanywhere.com) 来部署程序。 6 | 7 | 8 | ## 部署前的准备 9 | 10 | 首先,我们需要生成一个依赖列表,方便在部署环境里安装。使用下面的命令把当前依赖列表写到一个 requirements.txt 文件里: 11 | 12 | ```bash 13 | (env) $ pip freeze > requirements.txt 14 | ``` 15 | 16 | 对于某些配置,生产环境下需要使用不同的值。为了让配置更加灵活,我们把需要在生产环境下使用的配置改为优先从环境变量中读取,如果没有读取到,则使用默认值: 17 | 18 | ```python 19 | app.config['SECRET_KEY'] = os.getenv('SECRET_KEY', 'dev') 20 | app.config['SQLALCHEMY_DATABASE_URI'] = prefix + os.path.join(os.path.dirname(app.root_path), os.getenv('DATABASE_FILE', 'data.db')) 21 | ``` 22 | 23 | 以第一个配置变量为例,`os.getenv('SECRET_KEY', 'dev')` 表示读取系统环境变量 `SECRET_KEY` 的值,如果没有获取到,则使用 `dev`。 24 | 25 | > **注意** 像密钥这种敏感信息,保存到环境变量中要比直接写在代码中更加安全。 26 | 27 | 对于第二个配置变量,我们仅改动了最后的数据库文件名。在示例程序里,因为我们部署后将继续使用 SQLite,所以只需要为生产环境设置不同的数据库文件名,否则的话,你可以像密钥一样设置优先从环境变量读取整个数据库 URL。 28 | 29 | 在部署程序时,我们不会使用 Flask 内置的开发服务器运行程序,因此,对于写到 .env 文件的环境变量,我们需要手动使用 python-dotenv 导入。下面在项目根目录创建一个 wsgi.py 脚本,在这个脚本中加载环境变量,并导入程序实例以供部署时使用: 30 | 31 | *wsgi.py:手动设置环境变量并导入程序实例* 32 | 33 | ```python 34 | import os 35 | 36 | from dotenv import load_dotenv 37 | 38 | dotenv_path = os.path.join(os.path.dirname(__file__), '.env') 39 | if os.path.exists(dotenv_path): 40 | load_dotenv(dotenv_path) 41 | 42 | from watchlist import app 43 | ``` 44 | 45 | 这两个环境变量的具体定义,我们将在远程服务器环境创建新的 .env 文件写入。 46 | 47 | 最后让我们把改动提交到 Git 仓库,并推送到 GitHub 上的远程仓库: 48 | 49 | ```bash 50 | $ git add . 51 | $ git commit -m "Ready to deploy" 52 | $ git push 53 | ``` 54 | 55 | > **提示** 你可以在 GitHub 上查看本书示例程序的对应 commit:[92eabc8](https://github.com/helloflask/watchlist/commit/92eabc89a669a8b3e2d2a56177a875938923fd52)。 56 | 57 | 58 | ## 使用 PythonAnywhere 部署程序 59 | 60 | 首先访问[注册页面](https://www.pythonanywhere.com/registration/register/beginner/)注册一个免费账户。注册时填入的用户名将作为你的程序域名的子域部分,以及分配给你的 Linux 用户名。比如,如果你的用户名为 greyli,最终为你分配的程序域名就是 。 61 | 62 | 注册完成后会有一个简单的教程,你可以跳过,也可以跟着了解一下基本用法。管理面板主页如下所示: 63 | 64 | ![管理面板主页](images/11-1.png) 65 | 66 | 导航栏包含几个常用的链接,可以打开其他面板: 67 | 68 | - Consoles(控制台):可以打开 Bash、Python Shell、MySQL 等常用的控制台 69 | - Files(文件):创建、删除、编辑、上传文件,你可以在这里直接修改代码 70 | - Web:管理 Web 程序 71 | - Tasks(任务):创建计划任务 72 | - Databases(数据库):设置数据库,免费账户可以使用 MySQL 73 | 74 | 这些链接对应页面的某些功能也可以直接在管理面板主页打开。 75 | 76 | 我们需要先来创建一个 Web 程序,你可以点击导航栏的 Web 链接,或是主页上的“Open Web tab”按钮打开 Web 面板: 77 | 78 | ![Web 面板](images/11-2.png) 79 | 80 | 点击“Add a new web app”按钮创建 Web 程序,第一步提示升级账户后可以自定义域名,我们直接点击“Next”按钮跳到下一步: 81 | 82 | ![自定义域名](images/11-3.png) 83 | 84 | 这一步选择 Web 框架,为了获得更灵活的控制,选择手动设置(Manual configuration): 85 | 86 | ![选择 Web 框架](images/11-4.png) 87 | 88 | 接着选择你想使用的 Python 版本: 89 | 90 | ![选择 Python 版本](images/11-5.png) 91 | 92 | 最后点击“Next”按钮即可完成创建 Web 程序流程: 93 | 94 | ![结束创建 Web 程序流程](images/11-6.png) 95 | 96 | 接下来我们需要进行一系列程序初始化操作,最后再回到 Web 面板进行具体的设置。 97 | 98 | 99 | ## 初始化程序运行环境 100 | 101 | 我们首先要考虑把代码上传到 PythonAnywhere 的服务器上。上传代码一般有两种方式: 102 | 103 | * 从 GitHub 拉取我们的程序 104 | * 在本地将代码存储为压缩文件,然后在 Files 标签页上传压缩包 105 | 106 | 因为我们的代码已经推送到 GitHub 上,这里将采用第一种方式。首先通过管理面板主页的“Bash”按钮或是 Consoles 面板下的“Bash”链接创建一个命令行会话: 107 | 108 | ![打开新的命令行会话](images/11-7.png) 109 | 110 | 在命令行下输入下面的命令: 111 | 112 | ```bash 113 | $ git clone https://github.com/helloflask/watchlist # 注意替换 Git 仓库地址 114 | $ cd watchlist # 切换进程序仓库 115 | ``` 116 | 117 | 这会把程序代码克隆到 PythonAnywhere 为你分配的用户目录中,路径即 `/home/你的 PythonAnywhere 用户名/你的仓库名称`,比如 `/home/greyli/watchlist`。 118 | 119 | 注意替换 git clone 命令后的 Git 地址,将 `greyli` 替换为你的 GitHub 用户名,将 `watchlist` 替换为你的仓库名称。 120 | 121 | > **提示** 如果你在 GitHub 上的仓库类型为私有仓库,那么需要将 PythonAnywhere 服务器的 SSH 密钥添加到 GitHub 账户中,具体参考第 1 章“设置 SSH 密钥”小节。 122 | 123 | 下面我们在项目根目录创建 .env 文件,并写入生产环境下需要设置的两个环境变量。其中,密钥(`SECRET_KEY`)的值是随机字符串,我们可以使用 uuid 模块来生成: 124 | 125 | ```python 126 | $ python3 127 | >>> import uuid 128 | >>> uuid.uuid4().hex 129 | '3d6f45a5fc12445dbac2f59c3b6c7cb1' 130 | ``` 131 | 132 | 复制生成的随机字符备用,接着创建 .env 文件: 133 | 134 | ```bash 135 | $ nano .env 136 | ``` 137 | 138 | 写入设置密钥和数据库名称的环境变量: 139 | 140 | ```ini 141 | SECRET_KEY=3d6f45a5fc12445dbac2f59c3b6c7cb1 142 | DATABASE_FILE=data-prod.db 143 | ``` 144 | 145 | 最后安装依赖并执行初始化操作: 146 | 147 | ```bash 148 | $ python3 -m venv env # 创建虚拟环境 149 | $ . env/bin/activate # 激活虚拟环境 150 | (env) $ pip install -r requirements.txt # 安装所有依赖 151 | (env) $ flask initdb # 初始化数据库 152 | (env) $ flask admin # 创建管理员账户 153 | ``` 154 | 155 | 先不要关闭这个标签页,后面我们还要在这里执行一些命令。点击右上角的菜单按钮,并在浏览器的新标签页打开 Web 面板。 156 | 157 | 158 | ## 设置并启动程序 159 | 160 | 代码部分我们已经设置完毕,接下来进行一些简单设置就可以启动程序了。 161 | 162 | 163 | ### 代码 164 | 165 | 回到 Web 标签页,先来设置 Code 部分的配置: 166 | 167 | ![代码配置](images/11-8.png) 168 | 169 | 点击源码(Source code)和工作目录(Working directory)后的路径并填入项目根目录,目录规则为“/home/用户名/项目文件夹名”。 170 | 171 | 点击 WSGI 配置文件(WSGI configuration file)后的链接打开编辑页面,删掉这个文件内的所有内容,填入下面的代码: 172 | 173 | ```python 174 | import sys 175 | 176 | path = '/home/greyli/watchlist' # 路径规则为 /home/你的用户名/项目文件夹名 177 | if path not in sys.path: 178 | sys.path.append(path) 179 | 180 | from wsgi import app as application 181 | ``` 182 | 183 | 完成后点击绿色的 Save 按钮或按下 Ctrl+S 保存修改,点击右上角的菜单按钮返回 Web 面板。 184 | 185 | PythonAnywhere 会自动从这个文件里导入名称为 `application` 的程序实例,所以我们从项目目录的 wsgi 模块中导入程序实例 `app`,并将名称映射为 `application`。 186 | 187 | 188 | ### 虚拟环境 189 | 190 | 为了让程序正确运行,我们需要在 Virtualenv 部分填入虚拟环境文件夹的路径: 191 | 192 | ![虚拟环境配置](images/11-9.png) 193 | 194 | 对应我们的项目就是 `/home/greyli/watchlist/env/`,注意替换其中的用户名、项目名称和虚拟环境名称部分。点击 Virtualenv 部分的红色字体链接,填入并保存。 195 | 196 | 197 | ### 静态文件 198 | 199 | 静态文件可以交给 PythonAnywhere 设置的服务器来处理,这样会更高效。要让 PythonAnywhere 处理静态文件,我们只需要在 Static files 部分指定静态文件 URL 和对应的静态文件文件夹目录,如下所示: 200 | 201 | ![静态文件配置](images/11-10.png) 202 | 203 | 注意更新目录中的用户名和项目文件夹名称。 204 | 205 | 206 | ### 启动程序 207 | 208 | 一切就绪,点击绿色的重载按钮即可让配置生效: 209 | 210 | ![重载程序](images/11-11.png) 211 | 212 | 现在访问你的程序网址“”(Web 面板顶部的链接),比如 即可访问程序。 213 | 214 | 最后还要注意的是,免费账户需要每三个月点击一次黄色的激活按钮(在过期前你会收到提醒邮件): 215 | 216 | ![激活程序](images/11-12.png) 217 | 218 | 219 | ## 更新部署后的程序 220 | 221 | 当你需要更新程序时,流程和部署类似。在本地完成更新,确保程序通过测试后,将代码推送到 GitHub 上的远程仓库。登录到 PythonAnywhere,打开一个命令行会话(Bash),切换到项目目录,使用 git pull 命令从远程仓库拉取更新: 222 | 223 | ```bash 224 | $ cd watchlist 225 | $ git pull 226 | ``` 227 | 228 | 然后你可以执行一些必要的操作,比如安装新的依赖等等。最后在 Web 面板点击绿色的重载(Reload)按钮即可完成更新。 229 | 230 | 231 | ## 本章小结 232 | 233 | 程序部署上线以后,你可以考虑继续为它开发新功能,也可以从零编写一个新的程序。虽然本书即将接近尾声,但你的学习之路才刚刚开始,因为本书只是介绍了 Flask 入门所需的基础知识,你还需要进一步学习。在后记中,你可以看到进一步学习的推荐读物。 234 | 235 | 接下来,有一个挑战在等着你。 236 | 237 | 238 | ## 进阶提示 239 | 240 | * 因为 PythonAnywhere 支持在线管理文件、编辑代码、执行命令,你可以在学习编程的过程中使用它来在线开发 Web 程序。 241 | * PythonAnywhere 的 Web 面板还有一些功能设置:Log files 部分可以查看你的程序日志,Traffic 部分显示了你的程序访问流量情况,Security 部分可以为你的程序程序开启强制启用 HTTPS 和密码保护。 242 | * 如果你是[《Flask Web 开发实战》](http://helloflask.com/book/1)的读者,第 14 章详细介绍了部署 Flask 程序的两种方式:传统部署和云部署。 243 | -------------------------------------------------------------------------------- /chapters/form.md: -------------------------------------------------------------------------------- 1 | # 第 7 章:表单 2 | 3 | 在 HTML 页面里,我们需要编写表单来获取用户输入。一个典型的表单如下所示: 4 | 5 | ```html 6 |
7 | 8 |
9 | 10 |
11 | 12 |
13 | ``` 14 | 15 | 编写表单的 HTML 代码有下面几点需要注意: 16 | 17 | * 在 `
` 标签里使用 `method` 属性将提交表单数据的 HTTP 请求方法指定为 POST。如果不指定,则会默认使用 GET 方法,这会将表单数据通过 URL 提交,容易导致数据泄露,而且不适用于包含大量数据的情况。 18 | * `` 元素必须要指定 `name` 属性,否则无法提交数据,在服务器端,我们也需要通过这个 `name` 属性值来获取对应字段的数据。 19 | 20 | > **提示** 填写输入框标签文字的 `