diff --git a/.gitignore b/.gitignore index 7c6ad5f..0e230ab 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ _ebook env venv site +.obsidian diff --git a/CHANGES.md b/CHANGES.md index 1359c2e..164f140 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,13 @@ # Changelog +## 4.0 + +Released: 2025/10/12 + +- 增加对 uv 的介绍 +- 升级到 SQLAlchemy 2.x +- 针对 Flask 3.1.x 进行内容更新 +- 增加对蓝本、工厂函数的介绍 ## 3.0 diff --git a/README.md b/README.md index 1220598..793ddc4 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,13 @@ # Flask 入门教程 -这里是《Flask 入门教程》的源码仓库。请访问[本书主页](http://helloflask.com/book/3)在线阅读或下载电子书文件。 +> 使用 Python 和 Flask 开发你的第一个 Web 程序 + +这里是《Flask 入门教程》的源码仓库。请访问[本书主页](http://helloflask.com/book/3)在线阅读本书。 如果你发现了书中的错误,或是有任何意见或建议,欢迎[创建 Issue](https://github.com/helloflask/flask-tutorial/issues/new) 反馈或提交 Pull Request 进行修正。对于较大的内容变动,建议先[创建 Issue](https://github.com/helloflask/flask-tutorial/issues/new) 进行讨论。谢谢! ![](http://helloflask.com/static/tutorial-cover-s.png) -© 2018 [李辉](http://greyli.com)(Grey Li) / [HelloFlask.com](http://helloflask.com) +© 2018 - 2025 [李辉](http://greyli.com)(Grey Li) / [HelloFlask](http://helloflask.com) 本书采用 [CC BY-NC-ND 3.0](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh) 协议授权,禁止商用、演绎后分发或无署名转载。 diff --git a/book.json b/book.json deleted file mode 100644 index 1dd52f2..0000000 --- a/book.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "root": "./chapters", - "title": "flask-tutorial", - "language": "zh-hans", - "author": "李辉(Grey Li)", - "plugins": ["disqus"], - "pluginsConfig": { - "disqus": { - "shortName": "flask-tutorial" - } - } -} diff --git a/chapters/1-preparation-code-listing.md b/chapters/1-preparation-code-listing.md new file mode 100644 index 0000000..c441ca9 --- /dev/null +++ b/chapters/1-preparation-code-listing.md @@ -0,0 +1,104 @@ +# 第 1 章:代码清单 + +每一章后面都会有一个代码清单章节,这里会列出上一章所有的代码变动和相关命令。本章的目的是提供一个更清晰的代码改动列表,供你在编写代码时作为参考。 + +## 目录 + +```text +watchlist/ +├── .venv/ +├── .git/ +└── .gitignore +``` + +## 代码 + +### .gitignore + +```text +*.pyc +*~ +__pycache__ +.DS_Store +.venv +``` + +## 命令 + +### 创建项目文件夹 + +```bash +$ mkdir watchlist +$ cd watchlist +``` + +### 设置 Git 身份信息 + +```bash +$ git config --global user.name "Your Name" # 替换成你的名字 +$ git config --global user.email "your_email@example.com" # 替换成你的邮箱地址 +``` + +### 初始化 Git 仓库 + +```bash +$ git init +Initialized empty Git repository in ~/watchlist/.git/ +``` + +### 创建 .gitignore 文件 + +```bash +$ nano .gitignore +``` + +### 生成 SSH 密钥 + +```bash +$ ssh-keygen -t ed25519 -C "your_email@example.com" +$ cat ~/.ssh/id_ed25519.pub +``` + +### 设置远程仓库 + +```bash +$ git remote add origin git@github.com:greyli/watchlist.git # 注意更换地址中的用户名 +``` + +### 创建虚拟环境 + +```bash +$ python -m venv .venv # Windows +``` + +或: + +```bash +$ python3 -m venv .venv # Linux 和 macOS +``` + +### 激活虚拟环境 + +```bash +$ .venv\Scripts\activate # Windows +``` + +或: + +```bash +$ source .venv/bin/activate # Linux 或 macOS +``` + +### 安装 Flask + +```bash +(.venv) $ pip install flask +``` + +### 提交代码 + +```bash +$ git add . +$ git commit -m "Init the project" +$ git push -u origin main +``` diff --git a/chapters/1-preparation.md b/chapters/1-preparation.md new file mode 100644 index 0000000..35df39b --- /dev/null +++ b/chapters/1-preparation.md @@ -0,0 +1,329 @@ +# 第 1 章:准备工作 + +在通过这本书学习 Flask 开发前,我假设你已经了解了 Python 和 Web 开发(主要是 HTML 和 CSS)的基础知识。如果还没有,那么可以先从下面这些在线资源入手: + +* 《[使用 HTML、CSS 和 JavaScript 构建简单的网站](https://learn.microsoft.com/zh-cn/training/modules/get-started-with-web-development/?WT.mc_id=OSS-MVP-5003485)》 - Microsoft Learn +* 《[Web 入门教程](https://developer.mozilla.org/zh-CN/docs/learn)》 - MDN +* 《[Python 教程](https://docs.python.org/zh-cn/3/tutorial/)》 - Python.org + +这个教程对操作系统没有要求:你可以使用 Windows,也可以使用 macOS 或 Linux。不过你的 Python 版本需要是 3.9 及以上版本。 + +## 安装编辑器和浏览器 + +对于代码编辑器来说,每个人都有不同的偏好,你可以自由选择。可以选择功能丰富的IDE(集成开发环境),比如 [PyCharm](https://www.jetbrains.com/pycharm/);也可以选择相对轻量的编辑器,比如 [VS Code](https://code.visualstudio.com/) 或 [Sublime Text](https://www.sublimetext.com/)。浏览器建议使用 [Firefox](https://www.mozilla.org/en-US/firefox/new/) 或 [Chrome](https://www.google.com/chrome/)。 + +## 使用命令行 + +在本书中,你需要使用命令行窗口来执行许多操作。在 Windows 下,推荐使用 PowerShell 而不是 CMD.exe,在 macOS 和 Linux 下则可以使用任何你喜欢的终端程序(默认为 Terminal)。打开你的终端,下面我们执行一个最简单的 `whoami` 命令(即 Who Am I?): + +```bash +$ whoami +greyli +``` + +这个命令会打印出当前计算机用户的名称。其他常用的命令还有 `cd` 命令,用来切换目录(**c**hange **d**irectory);`mkdir` 命令,用来创建目录(**m**a**k**e **dir**ectory)。在不同的操作系统上,执行某个操作的命令可能会有所不同,在必要的地方,本书会进行提示。 + +先来为我们的程序创建一个文件夹: + +```bash +$ mkdir watchlist +$ cd watchlist +``` + +除非特别说明,从现在开始,本书假设你的工作目录将是在项目的根目录,即 watchlist/ 目录。 + +为了确保你已经正确安装了 Python,可以执行下面的命令测试是否有报错: + +```bash +$ python --version +Python 3.9.10 +``` + +在 Linux 和 macOS 中,对应 Python 3 版本的命令将会是 `python3`(Python 3 对应的 pip 命令为 `pip3`): + +```bash +$ python3 --version +Python 3.9.10 +``` + +对于 Windows 用户,可以考虑在这个教程的学习过程中使用 [WSL](https://learn.microsoft.com/en-us/windows/wsl/install?WT.mc_id=OSS-MVP-5003485)(Windows Subsystem for Linux,在 Windows 上运行的 Linux 子系统)或是 Git Bash(安装 Git for Windows 后附带的终端程序,下一节会介绍 Git 安装)。Git Bash 支持一些在 Linux 或 macOS 下才能使用的命令(程序),比如 ls、cat、nano、ssh 等,这些命令我们在后面会用到。 + +> **提示** 如果你打算在这个教程的学习中继续使用 CMD.exe 或 PowerShell,那么需要注意下列命令的区别: +> +> - 在 CMD.exe 中使用 `dir` 命令替代 `ls` 命令,使用 `type` 命令替代 `cat` 命令 +> - 对于 `nano` 命令,你可以替换为其他已安装的编辑器命令,比如对于 VS Code,可以使用 `code` 命令。或者,你也可以直接使用编辑器的图形界面创建文件并编辑。 +> - 对于 Windows 10 1809,OpenSSH 程序(相关命令 `ssh`、`ssh-keygen`)可以在控制面板中额外安装([相关文档](https://docs.microsoft.com/en-us/windows-server/administration/openssh/openssh_install_firstuse));旧版本 Windows 可以安装第三方 OpenSSH 客户端,比如 Putty,或直接使用 Git Bash。 + + +## 使用 Git + +[Git](https://git-scm.com) 是一个流行的版本控制工具,我们可以用它来记录程序源码和文件的变动情况,或是在开发时进行多人协作,你可以把它看做一个代码变动备份工具。 + +如果你还不熟悉 Git 也没关系,可以先按照书中的命令去操作,有时间再去了解原理。现在要做的第一件事就是在你的电脑上[安装 Git](https://git-scm.com/book/zh/v1/%E8%B5%B7%E6%AD%A5-%E5%AE%89%E8%A3%85-Git)。 + +> **附注** 阅读短教程[《Git 简明指南》](http://rogerdudler.github.io/git-guide/index.zh.html)或访问 Microsoft Learn 上的引导式教程《[Git 简介](https://docs.microsoft.com/zh-cn/learn/modules/intro-to-git?WT.mc_id=OSS-MVP-5003485)》了解相关基础知识。 + +安装后可以在命令行先使用下面的命令查看版本,没有报错则表示已正确安装: + +```bash +$ git --version +git version 2.17.1 +``` + +为了让 Git 知道你是谁,以便在提交代码到版本仓库的时候进行记录,使用下面的命令设置你的信息: + +```bash +$ git config --global user.name "Your Name" # 替换成你的名字 +$ git config --global user.email "your_email@example.com" # 替换成你的邮箱地址 +``` + +现在为我们的项目文件夹创建一个 Git 仓库,这会在我们的项目根目录创建一个 .git 文件夹: + +```bash +$ git init +Initialized empty Git repository in ~/watchlist/.git/ +``` + +Git 默认会追踪项目文件夹(或者说代码仓库)里所有文件的变化,但是有些无关紧要的文件不需要记录变化。我们在项目根目录创建一个 .gitignore 文件,在文件中写入忽略文件的规则。因为文件内容比较简单,我们直接在命令行使用 nano 来创建: + +```bash +$ nano .gitignore +``` + +在 nano 编辑界面写入常见的可忽略文件规则: + +```text +*.pyc +*~ +__pycache__ +.DS_Store +``` + +按下 Control + O 键保存,然后按下 Enter 键保存,最后按下 Control + X 键退出。在后续章节,对于简单的文件,都会使用 nano 创建,这部分操作你也可以使用编辑器的图形界面来完成。 + +## 将程序托管到 GitHub(可选) + +这一步是可选的。将程序托管到 GitHub、GitLab 或是 BitBucket 等平台上,可以更方便地备份、协作和部署。这些托管平台作为 Git 服务器,可以用来为本地仓库创建一个远程仓库,然后将其上传到这些平台上。 + +首先要注册一个 GitHub 账户,点击访问[注册页面](https://github.com/join),根据指示完成注册流程。登录备用。 + +> **附注** 你可以访问 Microsoft Learn 上的引导式教程《[GitHub 简介](https://docs.microsoft.com/zh-cn/learn/modules/introduction-to-github?WT.mc_id=OSS-MVP-5003485)》了解相关基础知识。 + +### 设置 SSH 密钥 + +一般情况下,当推送本地改动到远程仓库时,需要输入用户名和密码。因为传输通常是通过 SSH 加密,所以可以通过设置 SSH 密钥来省去验证账号的步骤。 + +使用下面的命令生成 SSH 密钥对(记得替换命令中的邮箱地址): + +```bash +$ ssh-keygen -t ed25519 -C "your_email@example.com" +``` + +一路按下 Enter 键采用默认值,最后会在用户根目录 .ssh 文件夹下创建两个文件:id_ed25519 和 id_ed25519.pub,前者是私钥,不能泄露出去;后者是公钥,用于认证身份,也就是我们要保存到 GitHub 上的密钥值。使用下面的命令获取文件内容: + +```bash +$ cat ~/.ssh/id_ed25519.pub +ssh-rsa AAAAB3Nza...省略 N 个字符...3aph book@greyli +``` + +如果你使用的 OpenSSH 版本过低,无法使用更安全的 ed25519 算法,可以改为下面的命令使用 RSA 算法: + +```shell +$ ssh-keygen -t rsa -b 4096 -C "your_email@example.com" +``` + +这时生成的密钥文件则分别为 id_rsa 和 id_rsa.pub。获取公钥内容的命令相应变为: + +```shell +$ cat ~/.ssh/id_rsa.pub +``` + +选中并复制输出的公钥内容,访问 GitHub 的 [SSH 设置页面](https://github.com/settings/keys)(导航栏头像 - Settings - SSH and GPG keys),点击 New SSH key 按钮,将复制的内容粘贴到 Key 输入框里,再填一个标题,比如“My PC”,最后点击“Add SSH key”按钮保存。 + +### 创建远程仓库 + +访问[新建仓库页面](https://github.com/new)(导航栏“+” - New repository),在“Repository name”处填写仓库名称,这里填“watchlist”即可,接着选择仓库类型(公开或私有)等选项,最后点击“Create repository”按钮创建仓库。 + +因为我们已经提前创建了本地仓库,所以需要指定本地仓库的远程仓库地址: + +```bash +$ git remote add origin git@github.com:greyli/watchlist.git # 注意更换地址中的用户名 +``` + +这会为本地仓库关联一个名为“origin”的远程仓库,**注意将仓库地址中的“greyli”换成你的 GitHub 用户名**。 + +如果还没有创建本地仓库,则可以直接将远程仓库克隆到本地(这会在当前目录创建一个名为 watchlist 的文件夹): + +```bash +$ git clone git@github.com:greyli/watchlist.git # 注意更换地址中的用户名 +``` + +## 创建虚拟环境 + +虚拟环境是独立于 Python 全局环境的 Python 解释器环境,使用它的好处如下: + +* 保持全局环境的干净 +* 为同一个库在不同环境下指定不同的版本 +* 方便记录和管理某个项目相关的依赖 + +我们将使用 Python 3 内置的 venv 模块创建虚拟环境,使用下面的命令即可为当前项目创建一个虚拟环境: + +```bash +$ python -m venv .venv # Windows +``` + +或: + +```bash +$ python3 -m venv .venv # Linux 和 macOS +``` + +如果 + +> **提示** 上述命令的最后一个参数是虚拟环境名称,你可以自由定义,比如 venv、env、.venv,或是“项目名-venv”,这里使用了 .venv。 + +这会在当前目录创建一个包含 Python 解释器环境的虚拟环境文件夹,名称为 .venv。这个文件的内容不需要提交到 Git 仓库,因此需要将文件夹名称 `.venv` 加入到项目根目录的 .gitignore 文件中。 + +```text +*.pyc +*~ +__pycache__ +.DS_Store +.venv +``` + +## 激活虚拟环境 + +创建虚拟环境后,我们可以使用下面的命令来激活虚拟环境(通过执行环境内的激活脚本实现): + +```bash +$ .venv\Scripts\activate # Windows +``` + +> **提示** 如果你在 Windows 中使用 Git Bash,则需要使用 `source .venv/Scripts/activate` 命令 + +或: + +```bash +$ source .venv/bin/activate # Linux 或 macOS +``` + +这时命令提示符前会显示虚拟环境的名称,表示已经激活成功: + +```bash +(.venv) $ +``` + +在激活虚拟环境后,无论操作系统和 Python 版本,都可以统一使用 `python` 和 `pip` 命令来调用当前虚拟环境内的 Python 和 pip 程序/二进制文件。此时执行 `python` 或 `pip` 命令指向的程序和激活脚本在同一个目录下,在 Windows 下所在目录为 `.venv\Scripts\`,Linux 和 macOS 下所在目录为 `.venv/bin/`。 + +最后,执行 `deactivate` 即可退出虚拟环境: + +```bash +(.venv) $ deactivate +$ +``` + +> **注意** 除了 Git 相关命令外,除非特别说明,本书后续的所有命令均需要在激活虚拟环境后执行。 + +> **提示** 建议为 pip 更新 PyPI 源,也就是存储依赖包的服务器,改为使用国内的 PyPI 镜像服务器以提高下载速度,具体见[这篇文章](https://zhuanlan.zhihu.com/p/57872888)。 + +## 安装 Flask + +激活虚拟环境后,使用下面的命令来安装 Flask: + +```bash +(.venv) $ pip install flask +``` + +这会把 Flask 以及相关的一些依赖包安装到已经激活的虚拟环境,而不是全局解释器环境。本书写作时的 Flask 最新版本为 3.1.2,你执行这条命令时也许会安装更新的版本。如果你想指定安装 3.1.2 版本,可以使用下面的命令: + +```bash +(.venv) $ pip install flask==3.1.2 +``` + +> **提示** 如果你没有使用虚拟环境,并且此前已经安装了 Flask,记得将其更新到最新版本(`pip install -U flask`)。 + +## 使用 uv 管理虚拟环境和依赖(可选) + +uv()是一个使用 Rust 编写的 Python 依赖和虚拟环境管理工具。前面我们学习了如何使用 venv 模块创建虚拟环境,以及用 pip 来安装依赖包。你可以使用 uv 来加速这些操作。这一小节是可选的,简单了解也可以。 + +首先安装 uv。macOS 或 Linux: + +```shell +$ curl -LsSf https://astral.sh/uv/install.sh | sh +``` + +Windows: + +```shell +$ powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex" +``` + +你也可以使用 pip 进行安装: + +```shell +$ pip install --user uv +``` + +现在使用 `uv venv` 命令重新创建虚拟环境: + +```shell +$ uv venv +``` + +uv 默认也使用 .venv 作为虚拟环境文件夹名称,所以会提示是否覆盖已有的虚拟环境,按下 y 确认即可。现在激活虚拟环境: + +```shell +$ source .venv/bin/activate # Windows 使用 .venv\Scripts\activate 命令 +``` + +然后使用 `uv pip` 命令安装依赖: + +```shell +(.venv) $ uv pip install flask +``` + +除了像这样使用 `uv pip` 命令来代理 pip 操作,你也可以完成使用 uv 自带的依赖管理系统,详情可以访问文档 了解。 + +除此之外,uv 也可以用来管理 Python 版本。下面是一些常用的命令示例。 + +安装指定的 Python 版本,之后可以使用 `python3.10`、`python3.11`、`python3.12` 命令来打开对应的 Python 解释器: + +```shell +$ uv python install 3.10 3.11 3.12 +``` + +用指定的版本创建虚拟环境: + +```shell +$ uv venv --python 3.12 +``` + +固定当前项目使用的 Python 版本,将会写入版本信息到 `.python-version` 文件(这个文件不需要提交到仓库,因此需要写入文件名到 .gitignore 文件): + +```shell +$ uv python pin 3.12 +``` + + +## 本章小结 + +当你进行到这里,就意味这我们已经做好学习和开发 Flask 程序的全部准备了。使用 `git status` 命令可以查看当前仓库的文件变动状态: + +```bash +$ git status +``` + +下面让我们将文件改动提交进 Git 仓库,并推送到在 GitHub 上创建的远程仓库: + +```bash +$ git add . +$ git commit -m "Init the project" +$ git push -u origin main # 如果你没有把仓库托管到 GitHub,则跳过这条命令,后面章节亦同 +``` + +这里最后一行命令添加了 `-u` 参数,会将推送的目标仓库和分支设为默认值,后续的推送直接使用 `git push` 命令即可。在 GitHub 上,你可以通过 [https://github.com/你的用户名/watchlist](https://github.com/helloflask/watchlist) 查看你的仓库内容。 + +## 进阶提示 + +* 如果你打算开源你的程序,在项目根目录中添加一个 README.md(自述文件)和 LICENSE(授权声明)文件是很有必要的。详情可以访问 [Open Source Guides](https://opensource.guide/) 了解。 diff --git a/chapters/test.md b/chapters/10-test.md similarity index 71% rename from chapters/test.md rename to chapters/10-test.md index ed3a88c..a3e244a 100644 --- a/chapters/test.md +++ b/chapters/10-test.md @@ -1,15 +1,15 @@ -# 第 9 章:测试 +# 第 10 章:测试 -在此之前,每次为程序添加了新功能,我们都要手动在浏览器里访问程序进行测试。除了测试新添加的功能,你还要确保旧的功能依然正常工作。在功能复杂的大型程序里,如果每次修改代码或添加新功能后手动测试所有功能,那会产生很大的工作量。另一方面,手动测试并不可靠,重复进行测试操作也很枯燥。 +在前几章我们开发的过程中,每次为程序添加了新功能,都要手动在浏览器里访问程序进行测试。除了测试新添加的功能,你还要确保旧的功能依然正常工作。在功能复杂的大型程序里,如果每次修改代码或添加新功能后手动测试所有功能,那会产生很大的工作量。另一方面,手动测试并不可靠,重复进行测试操作也很枯燥。 基于这些原因,为程序编写自动化测试就变得非常重要。 -> **注意** 为了便于介绍,本书统一在这里介绍关于测试的内容。在实际的项目开发中,你应该在开发每一个功能后立刻编写相应的测试,确保测试通过后再开发下一个功能。 +> **注意** 为了便于行文,本书统一在这里介绍关于测试的内容。在实际的项目开发中,你应该在开发每一个功能后立刻编写相应的测试,确保全部测试通过后再开发下一个功能。 ## 单元测试 -单元测试指对程序中的函数等独立单元编写的测试,它是自动化测试最主要的形式。这一章我们将会使用 Python 标准库中的测试框架 unittest 来编写单元测试,首先通过一个简单的例子来了解一些基本概念。假设我们编写了下面这个函数,并保存到一个 hello.py 模块里: +单元测试指对程序中的函数等独立单元编写的测试,它是自动化测试最主要的形式。这一章我们将会使用 Python 标准库中的测试框架 unittest 来编写单元测试。首先通过一个简单的例子来了解一些基本概念。我们编写了下面这个用来打招呼的 sayhello 函数,并保存到一个 hello.py 模块里: ```python def sayhello(to=None): @@ -23,8 +23,6 @@ def sayhello(to=None): ```python import unittest -from hello import sayhello - class SayHelloTestCase(unittest.TestCase): # 测试用例 @@ -47,15 +45,15 @@ if __name__ == '__main__': unittest.main() ``` -测试用例继承 `unittest.TestCase` 类,在这个类中创建的以 `test_` 开头的方法将会被视为测试方法。 +测试用例(test case)继承 `unittest.TestCase` 类,在这个类中创建的以 `test_` 开头的方法将会被视为测试方法。 -内容为空的两个方法很特殊,它们是测试固件,用来执行一些特殊操作。比如 `setUp()` 方法会在每个测试方法执行前被调用,而 `tearDown()` 方法则会在每一个测试方法执行后被调用(注意这两个方法名称的大小写)。 +内容为空的两个方法很特殊,它们是测试固件(test fixtures),用来执行一些特殊操作。比如 `setUp()` 方法会在每个测试方法执行前被调用,而 `tearDown()` 方法则会在每一个测试方法执行后被调用(注意这两个方法名称的大小写)。 -如果把执行测试方法比作战斗,那么准备弹药、规划战术的工作就要在 `setUp()` 方法里完成,而打扫战场则要在 `tearDown()` 方法里完成。 +如果把执行测试方法比作烹饪,那么准备食材、规划要做哪些菜的工作就要在 `setUp()` 方法里完成,而打扫厨房则要在 `tearDown()` 方法里完成。 -每一个测试方法(名称以 `test_` 开头的方法)对应一个要测试的函数 / 功能 / 使用场景。在上面我们创建了两个测试方法,`test_sayhello()` 方法测试 `sayhello()` 函数,`test_sayhello_to_somebody()` 方法测试传入参数时的 `sayhello()` 函数。 +每一个测试方法(名称以 `test_` 开头的方法)对应一个要测试的函数 / 功能 / 使用场景。在上面我们创建了两个测试方法,`test_sayhello()` 方法测试直接调用 `sayhello()` 函数,`test_sayhello_to_somebody()` 方法测试传入参数时的 `sayhello()` 函数。 -在测试方法里,我们使用断言方法来判断程序功能是否正常。以第一个测试方法为例,我们先把 `sayhello()` 函数调用的返回值保存为 `rv` 变量(return value),然后使用 `self.assertEqual(rv, 'Hello!')` 来判断返回值内容是否符合预期。如果断言方法出错,就表示该测试方法未通过。 +在测试方法里,我们使用断言(assert)方法来判断程序功能是否正常。以第一个测试方法为例,我们先把 `sayhello()` 函数调用的返回值保存为 `rv` 变量(return value),然后使用 `self.assertEqual(rv, 'Hello!')` 来判断返回值内容是否符合预期。如果断言方法出错,就表示该测试方法未通过。 下面是一些常用的断言方法: @@ -70,31 +68,35 @@ if __name__ == '__main__': - assertIn(a, b) - assertNotIn(a, b) -这些方法的作用从方法名称上基本可以得知。 +这些方法的作用从方法名称上基本可以得知。它们由测试用例类继承的 `unittest.TestCase` 提供,通过 `self.` 的形式调用。你可以访问[测试用例类的 API 文档](https://docs.python.org/zh-cn/3.13/library/unittest.html#unittest.TestCase)详细了解所有所有断言方法。 假设我们把上面的测试代码保存到 test_sayhello.py 文件中,通过执行 `python test_sayhello.py` 命令即可执行所有测试,并输出测试的结果、通过情况、总耗时等信息。 ## 测试 Flask 程序 -回到我们的程序,我们在项目根目录创建一个 test_watchlist.py 脚本来存储测试代码,我们先编写测试固件和两个简单的基础测试: +回到我们的程序,我们在项目根目录创建一个 test_watchlist.py 脚本来存储测试代码。下面先编写测试固件和两个简单的基础测试: *test_watchlist.py:测试固件* ```python import unittest -from app import app, db, Movie, User +from watchlist import create_app +from watchlist.extensions import db +from watchlist.models import Movie, User class WatchlistTestCase(unittest.TestCase): def setUp(self): - # 更新配置 - app.config.update( - TESTING=True, - SQLALCHEMY_DATABASE_URI='sqlite:///:memory:' - ) - # 创建数据库和表 + # 使用测试配置创建程序实例 + self.app = create_app(config_name='testing') + # 创建程序上下文 + self.context = self.app.app_context() + # 激活上下文 + self.context.push() + + # 创建数据库和表 db.create_all() # 创建测试数据,一个用户,一个电影条目 user = User(name='Test', username='test') @@ -104,12 +106,13 @@ class WatchlistTestCase(unittest.TestCase): db.session.add_all([user, movie]) db.session.commit() - self.client = app.test_client() # 创建测试客户端 - self.runner = app.test_cli_runner() # 创建测试命令运行器 + self.client = self.app.test_client() # 创建测试客户端 + self.runner = self.app.test_cli_runner() # 创建测试命令运行器 def tearDown(self): db.session.remove() # 清除数据库会话 db.drop_all() # 删除数据库表 + self.context.pop() # 清除上下文 # 测试程序实例是否存在 def test_app_exist(self): @@ -120,14 +123,29 @@ class WatchlistTestCase(unittest.TestCase): self.assertTrue(app.config['TESTING']) ``` -某些配置,在开发和测试时通常需要使用不同的值。在 `setUp()` 方法中,我们更新了两个配置变量的值,首先将 `TESTING` 设为 `True` 来开启测试模式,这样在出错时不会输出多余信息;然后将 `SQLALCHEMY_DATABASE_URI` 设为 `'sqlite:///:memory:'`,这会使用 SQLite 内存型数据库,不会干扰开发时使用的数据库文件。你也可以使用不同文件名的 SQLite 数据库文件,但内存型数据库速度更快。 +某些配置在开发和测试时通常需要使用不同的值。在 `setUp()` 方法中,我们调用工厂函数创建程序实例,传入测试配置对应的配置名。下面是对应的测试配置类: + +```python +class TestingConfig(BaseConfig): + TESTING = True + SQLALCHEMY_DATABASE_URI = 'sqlite:///:memory:' +``` + +这里将 `TESTING` 设为 `True` 来开启测试模式,这样在出错时不会输出多余信息;然后将 `SQLALCHEMY_DATABASE_URI` 设为 `'sqlite:///:memory:'`,这会使用 SQLite 内存型数据库,不会干扰开发时使用的数据库文件。你也可以使用不同文件名的 SQLite 数据库文件,但内存型数据库速度更快。 + +某些程序操作需要在激活 Flask 上下文时执行,比如前面介绍的 `url_for()` 函数,或是创建数据库表的 `db.session.create_all()` 操作。我们在 `setUp()` 方法中创建并激活程序上下文: + +```python +self.context = self.app.app_context() +self.context.push() +``` 接着,我们调用 `db.create_all()` 创建数据库和表,然后添加测试数据到数据库中。在 `setUp()` 方法最后创建的两个类属性分别为测试客户端和测试命令运行器,前者用来模拟客户端请求,后者用来触发自定义命令,下一节会详细介绍。 -在 `tearDown()` 方法中,我们调用 `db.session.remove()` 清除数据库会话并调用 `db.drop_all()` 删除数据库表。测试时的程序状态和真实的程序运行状态不同,所以需要调用 `db.session.remove()` 来确保数据库会话被清除。 +在 `tearDown()` 方法中,我们调用 `db.session.remove()` 清除数据库会话并调用 `db.drop_all()` 删除数据库表。测试时的程序状态和真实的程序运行状态不同,所以需要调用 `db.session.remove()` 来确保数据库会话被清除。最后调用 `self.context.pop()` 清除上下文。 -### 测试客户端 +### 测试程序功能 `app.test_client()` 返回一个测试客户端对象,可以用来模拟客户端(浏览器),我们创建类属性 `self.client` 来保存它。对它调用 `get()` 方法就相当于浏览器向服务器发送 GET 请求,调用 `post()` 则相当于浏览器向服务器发送 POST 请求,以此类推。下面是两个发送 GET 请求的测试方法,分别测试 404 页面和主页: @@ -369,28 +387,29 @@ class WatchlistTestCase(unittest.TestCase): ``` -### 测试命令 +### 测试自定义命令 -除了测试程序的各个视图函数,我们还需要测试自定义命令。`app.test_cli_runner()` 方法返回一个命令运行器对象,我们创建类属性 `self.runner` 来保存它。通过对它调用 `invoke()` 方法可以执行命令,传入命令函数对象,或是使用 `args` 关键字直接给出命令参数列表。`invoke()` 方法返回的命令执行结果对象,它的 `output` 属性返回命令的输出信息。下面是我们为各个自定义命令编写的测试方法: +除了测试程序的各个视图函数,我们还需要测试自定义命令。`app.test_cli_runner()` 方法返回一个命令运行器对象,我们创建类属性 `self.runner` 来保存它。通过对它调用 `invoke()` 方法可以执行命令,传入命令函数对象,或是使用 `args` 关键字直接给出一个包含命令和参数的列表。`invoke()` 方法返回的命令执行结果对象,它的 `output` 属性返回命令的输出信息。下面是我们为各个自定义命令编写的测试方法: *test_watchlist.py:测试自定义命令行命令* ```python # 导入命令函数 -from app import app, db, Movie, User, forge, initdb +from watchlist.extensions import db +from watchlist.models import Movie, User class WatchlistTestCase(unittest.TestCase): # ... # 测试虚拟数据 def test_forge_command(self): - result = self.runner.invoke(forge) + result = self.runner.invoke(args=['forge']) self.assertIn('Done.', result.output) self.assertNotEqual(Movie.query.count(), 0) # 测试初始化数据库 def test_initdb_command(self): - result = self.runner.invoke(initdb) + result = self.runner.invoke(args=['init-db']) self.assertIn('Initialized database.', result.output) # 测试生成管理员账户 @@ -430,7 +449,7 @@ if __name__ == '__main__': 使用下面的命令执行测试: ```bash -(env) $ python test_watchlist.py +(.venv) $ python test_watchlist.py ............... ---------------------------------------------------------------------- Ran 15 tests in 2.942s @@ -446,54 +465,61 @@ OK 为了让程序更加强壮,你可以添加更多、更完善的测试。那么,如何才能知道程序里有哪些代码还没有被测试?整体的测试覆盖率情况如何?我们可以使用 [Coverage.py](https://coverage.readthedocs.io/en/v4.5.x/) 来检查测试覆盖率,首先安装它: ```bash -(env) $ pip install coverage +(.venv) $ pip install coverage ``` -使用下面的命令执行测试并检查测试覆盖率: +使用下面的命令执行测试并检查测试覆盖率,通过 `--source` 选项来指定要检查的模块或包: ```bash -(env) $ coverage run --source=app test_watchlist.py +(.venv) $ coverage run --source=watchlist test_watchlist.py ``` -因为我们只需要检查程序脚本 app.py 的测试覆盖率,所以使用 `--source` 选项来指定要检查的模块或包。 + + > **提示** 你可以创建配置文件来预先定义 `--source` 选项,避免每次执行命令都给出这个选项,具体可以参考文档[配置文件章节](https://coverage.readthedocs.io/en/v4.5.x/config.html)。 最后使用下面的命令查看覆盖率报告: ```bash $ coverage report -Name Stmts Miss Cover ----------------------------- -app.py 146 5 97% +Name Stmts Miss Cover +------------------------------------------------------ +watchlist/__init__.py 23 0 100% +watchlist/blueprints/__init__.py 0 0 100% +watchlist/blueprints/auth.py 28 0 100% +watchlist/blueprints/main.py 61 1 98% +watchlist/commands.py 41 1 98% +watchlist/errors.py 11 2 82% +watchlist/extensions.py 13 3 77% +watchlist/models.py 20 0 100% +watchlist/settings.py 15 0 100% +------------------------------------------------------ +TOTAL 212 7 97% ``` -从上面的表格可以看出,一共有 146 行代码,没测试到的代码有 5 行,测试覆盖率为 97%。 - -你还可以使用 coverage html 命令获取详细的 HTML 格式的覆盖率报告,它会在当前目录生成一个 htmlcov 文件夹,打开其中的 index.html 即可查看覆盖率报告。点击文件名可以看到具体的代码覆盖情况,如下图所示: +测试覆盖率报告列出了包内文件的覆盖率情况,包括每个文件的行数(Stmts),没测试到的代码行数(Miss),以及测试覆盖率(Cover)。 -![覆盖率报告](images/9-1.png) +你还可以使用 `coverage html` 命令获取详细的 HTML 格式的覆盖率报告,它会在当前目录生成一个 htmlcov 文件夹,打开其中的 index.html 即可查看覆盖率报告。点击文件名可以看到具体的代码覆盖情况,如下图所示: +![Coverage report](images/9-1.png) -同时在 .gitignore 文件后追加下面两行,忽略掉生成的覆盖率报告文件: +最后记得在 .gitignore 文件后追加下面两行,忽略掉生成的覆盖率报告文件: ``` htmlcov/ .coverage ``` - ## 本章小结 -通过测试后,我们就可以准备上线程序了。结束前,让我们提交代码: +你也可以将测试文件拆分成多个模块,创建一个 tests 包来存储这些模块。因为目前的测试代码还比较少,暂时可以不做改动。通过测试后,我们就可以准备上线程序了。结束前,让我们提交代码: ```bash $ git add . -$ git commit -m "Add unit test with unittest" +$ git commit -m "Add unit tests with unittest" $ git push ``` -> **提示** 你可以在 GitHub 上查看本书示例程序的对应 commit:[66dc487](https://github.com/helloflask/watchlist/commit/66dc48719c797da00a9e29355b39d77abb45f574)。 ## 进阶提示 * 访问 [Coverage.py 文档](https://coverage.readthedocs.io)或执行 coverage help 命令来查看更多用法。 * 使用标准库中的 unittest 编写单元测试并不是唯一选择,你也可以使用第三方测试框架,比如非常流行的 [pytest](https://pytest.org)。 -* 如果你是[《Flask Web 开发实战》](http://helloflask.com/book/1)的读者,第 12 章详细介绍了测试 Flask 程序的相关知识,包括使用 [Selenium](https://www.seleniumhq.org/) 编写用户界面测试,使用 [Flake8](https://github.com/PyCQA/flake8) 检查代码质量等。 diff --git a/chapters/deploy.md b/chapters/11-deployment.md similarity index 65% rename from chapters/deploy.md rename to chapters/11-deployment.md index 06dff46..a3b7ebd 100644 --- a/chapters/deploy.md +++ b/chapters/11-deployment.md @@ -10,25 +10,28 @@ Web 程序通常有两种部署方式:传统部署和云部署。传统部署 首先,我们需要生成一个依赖列表,方便在部署环境里安装。使用下面的命令把当前依赖列表写到一个 requirements.txt 文件里: ```bash -(env) $ pip freeze > requirements.txt +(.venv) $ pip freeze > requirements.txt ``` -对于某些配置,生产环境下需要使用不同的值。为了让配置更加灵活,我们把需要在生产环境下使用的配置改为优先从环境变量中读取,如果没有读取到,则使用默认值: +对于某些配置,生产环境下需要使用不同的值。这是我们之前为生产环境创建的配置类: ```python -app.config['SECRET_KEY'] = os.getenv('SECRET_KEY', 'dev') -app.config['SQLALCHEMY_DATABASE_URI'] = prefix + os.path.join(os.path.dirname(app.root_path), os.getenv('DATABASE_FILE', 'data.db')) +class ProductionConfig(BaseConfig): + SQLALCHEMY_DATABASE_URI = os.getenv('DATABASE_URL', SQLITE_PREFIX + str(BASE_DIR / 'data.db')) ``` -以第一个配置变量为例,`os.getenv('SECRET_KEY', 'dev')` 表示读取系统环境变量 `SECRET_KEY` 的值,如果没有获取到,则使用 `dev`。 +在示例程序里,因为我们部署后将继续使用 SQLite,所以只需要为生产环境设置不同的数据库文件名。另外,继承自 BaseConfig 的配置 SECRET_KEY 需要改为使用随机字符: -> **注意** 像密钥这种敏感信息,保存到环境变量中要比直接写在代码中更加安全。 +```python +class BaseConfig: + SECRET_KEY = os.getenv('SECRET_KEY', 'dev') +``` -对于第二个配置变量,我们仅改动了最后的数据库文件名。在示例程序里,因为我们部署后将继续使用 SQLite,所以只需要为生产环境设置不同的数据库文件名,否则的话,你可以像密钥一样设置优先从环境变量读取整个数据库 URL。 +在生产环境,为了安全考虑,需要把具体的值作为环境变量定义,而不是放到代码里。我们将在远程服务器环境创建新的 .env 文件写入 SECRET_KEY 的值,具体将在下一节介绍。 -在部署程序时,我们不会使用 Flask 内置的开发服务器运行程序,因此,对于写到 .env 文件的环境变量,我们需要手动使用 python-dotenv 导入。下面在项目根目录创建一个 wsgi.py 脚本,在这个脚本中加载环境变量,并导入程序实例以供部署时使用: +在部署程序时,我们不会使用 Flask 内置的开发服务器运行程序,因此,对于写到 .env 文件的环境变量,我们需要手动使用 python-dotenv 导入。下面在项目根目录创建一个 wsgi.py 脚本,作为生产环境的程序入口脚本。我们在这个脚本中加载环境变量,并创建程序实例以供部署时使用: -*wsgi.py:手动设置环境变量并导入程序实例* +*wsgi.py:手动设置环境变量并创建程序实例* ```python import os @@ -39,10 +42,10 @@ dotenv_path = os.path.join(os.path.dirname(__file__), '.env') if os.path.exists(dotenv_path): load_dotenv(dotenv_path) -from watchlist import app -``` +from watchlist import create_app -这两个环境变量的具体定义,我们将在远程服务器环境创建新的 .env 文件写入。 +app = create_app(config_name='production') +``` 最后让我们把改动提交到 Git 仓库,并推送到 GitHub 上的远程仓库: @@ -52,14 +55,11 @@ $ git commit -m "Ready to deploy" $ git push ``` -> **提示** 你可以在 GitHub 上查看本书示例程序的对应 commit:[92eabc8](https://github.com/helloflask/watchlist/commit/92eabc89a669a8b3e2d2a56177a875938923fd52)。 - - ## 使用 PythonAnywhere 部署程序 首先访问[注册页面](https://www.pythonanywhere.com/registration/register/beginner/)注册一个免费账户。注册时填入的用户名将作为你的程序域名的子域部分,以及分配给你的 Linux 用户名。比如,如果你的用户名为 greyli,最终为你分配的程序域名就是 。 -注册完成后会有一个简单的教程,你可以跳过,也可以跟着了解一下基本用法。管理面板主页如下所示: +注册完成后会有一个简单的教程,你可以跳过,也可以跟着了解一下基本用法。管理面板(Dashboard)主页如下所示: ![管理面板主页](images/11-1.png) @@ -71,7 +71,7 @@ $ git push - Tasks(任务):创建计划任务 - Databases(数据库):设置数据库,免费账户可以使用 MySQL -这些链接对应页面的某些功能也可以直接在管理面板主页打开。 +这些链接对应页面的某些功能也可以直接在管理面板主页对应部分打开。 我们需要先来创建一个 Web 程序,你可以点击导航栏的 Web 链接,或是主页上的“Open Web tab”按钮打开 Web 面板: @@ -85,7 +85,7 @@ $ git push ![选择 Web 框架](images/11-4.png) -接着选择你想使用的 Python 版本: +接着选择你想使用的 Python 版本(最低需要 3.9 版本): ![选择 Python 版本](images/11-5.png) @@ -103,7 +103,7 @@ $ git push * 从 GitHub 拉取我们的程序 * 在本地将代码存储为压缩文件,然后在 Files 标签页上传压缩包 -因为我们的代码已经推送到 GitHub 上,这里将采用第一种方式。首先通过管理面板主页的“Bash”按钮或是 Consoles 面板下的“Bash”链接创建一个命令行会话: +因为我们的代码已经推送到 GitHub 上,这里将采用第一种方式。首先通过管理面板主页的“Bash”按钮或是 Consoles 面板下的“Bash”链接创建一个命令行会话,后续的命令都需要在新打开的会话中执行: ![打开新的命令行会话](images/11-7.png) @@ -116,11 +116,11 @@ $ cd watchlist # 切换进程序仓库 这会把程序代码克隆到 PythonAnywhere 为你分配的用户目录中,路径即 `/home/你的 PythonAnywhere 用户名/你的仓库名称`,比如 `/home/greyli/watchlist`。 -注意替换 git clone 命令后的 Git 地址,将 `greyli` 替换为你的 GitHub 用户名,将 `watchlist` 替换为你的仓库名称。 +注意替换 git clone 命令后的 Git 地址,将 `helloflask` 替换为你的 GitHub 用户名,将 `watchlist` 替换为你的仓库名称。 > **提示** 如果你在 GitHub 上的仓库类型为私有仓库,那么需要将 PythonAnywhere 服务器的 SSH 密钥添加到 GitHub 账户中,具体参考第 1 章“设置 SSH 密钥”小节。 -下面我们在项目根目录创建 .env 文件,并写入生产环境下需要设置的两个环境变量。其中,密钥(`SECRET_KEY`)的值是随机字符串,我们可以使用 uuid 模块来生成: +下面我们在项目根目录创建 .env 文件,并写入生产环境下需要设置的环境变量。密钥(`SECRET_KEY`)的值需要为随机字符串,我们可以使用 uuid 模块来生成。在你本地的终端中打开 Python Shell 并执行相应代码: ```python $ python3 @@ -129,30 +129,33 @@ $ python3 '3d6f45a5fc12445dbac2f59c3b6c7cb1' ``` -复制生成的随机字符备用,接着创建 .env 文件: +复制生成的随机字符备用,接着回到 PythonAnywhere 上打开的命令行会话,执行下面的命令创建 .env 文件: ```bash $ nano .env ``` -写入设置密钥和数据库名称的环境变量: +写入设置密钥的环境变量(按下 Control+O 和 Enter 保存,然后按下 Control+X 退出): ```ini SECRET_KEY=3d6f45a5fc12445dbac2f59c3b6c7cb1 -DATABASE_FILE=data-prod.db ``` 最后安装依赖并执行初始化操作: ```bash -$ python3 -m venv env # 创建虚拟环境 -$ . env/bin/activate # 激活虚拟环境 -(env) $ pip install -r requirements.txt # 安装所有依赖 -(env) $ flask initdb # 初始化数据库 -(env) $ flask admin # 创建管理员账户 +$ python3 -m venv .venv # 创建虚拟环境 +$ source .venv/bin/activate # 激活虚拟环境 +(.venv) $ pip install -r requirements.txt # 安装所有依赖 +(.venv) $ flask init-db # 初始化数据库,也可以执行 flask forge 创建虚拟数据 +(.venv) $ flask admin # 创建管理员账户 ``` -先不要关闭这个标签页,后面我们还要在这里执行一些命令。点击右上角的菜单按钮,并在浏览器的新标签页打开 Web 面板。 +你的命令行窗口和依次输入的命令(省略了后两条命令)类似下图: + +![代码配置](images/11-8.png) + +先不要关闭这个标签页,后面我们还要在这里执行一些命令。点击右上角的菜单按钮,并在浏览器的后台标签页打开 Web 面板。 ## 设置并启动程序 @@ -162,9 +165,9 @@ $ . env/bin/activate # 激活虚拟环境 ### 代码 -回到 Web 标签页,先来设置 Code 部分的配置: +切换到 Web 面板(标签页),先来设置 Code 部分的配置: -![代码配置](images/11-8.png) +![代码配置](images/11-9.png) 点击源码(Source code)和工作目录(Working directory)后的路径并填入项目根目录,目录规则为“/home/用户名/项目文件夹名”。 @@ -189,36 +192,36 @@ PythonAnywhere 会自动从这个文件里导入名称为 `application` 的程 为了让程序正确运行,我们需要在 Virtualenv 部分填入虚拟环境文件夹的路径: -![虚拟环境配置](images/11-9.png) +![虚拟环境配置](images/11-10.png) -对应我们的项目就是 `/home/greyli/watchlist/env/`,注意替换其中的用户名、项目名称和虚拟环境名称部分。点击 Virtualenv 部分的红色字体链接,填入并保存。 +对应我们的项目就是 `/home/greyli/watchlist/.venv/`,注意替换其中的用户名、项目名称和虚拟环境名称部分。点击 Virtualenv 部分的红色字体链接,填入并保存。 ### 静态文件 -静态文件可以交给 PythonAnywhere 设置的服务器来处理,这样会更高效。要让 PythonAnywhere 处理静态文件,我们只需要在 Static files 部分指定静态文件 URL 和对应的静态文件文件夹目录,如下所示: +静态文件可以交给 PythonAnywhere 设置的 Web 服务器来处理,这样处理性能会更好。要让 PythonAnywhere 处理静态文件,我们只需要在 Static files 部分指定静态文件 URL 和对应的静态文件文件夹目录,如下所示: -![静态文件配置](images/11-10.png) +![静态文件配置](images/11-11.png) -注意更新目录中的用户名和项目文件夹名称。 +Flask 程序的静态文件默认 URL 为“/static”,因此这里的 URL 填入“/static/”;对应的目录是程序包里的静态文件夹,比如“/home/greyli/watchlist/watchlist/static/”。注意更新目录中的用户名和项目文件夹名称。 ### 启动程序 -一切就绪,点击绿色的重载按钮即可让配置生效: +一切就绪,点击绿色的重载(Reload)按钮即可让配置生效: -![重载程序](images/11-11.png) +![重载程序](images/11-12.png) -现在访问你的程序网址“”(Web 面板顶部的链接),比如 即可访问程序。 +现在访问你的程序网址“”(可以在 Web 面板顶部找到链接),比如 即可访问程序。 最后还要注意的是,免费账户需要每三个月点击一次黄色的激活按钮(在过期前你会收到提醒邮件): -![激活程序](images/11-12.png) +![激活程序](images/11-13.png) ## 更新部署后的程序 -当你需要更新程序时,流程和部署类似。在本地完成更新,确保程序通过测试后,将代码推送到 GitHub 上的远程仓库。登录到 PythonAnywhere,打开一个命令行会话(Bash),切换到项目目录,使用 git pull 命令从远程仓库拉取更新: +当你需要更新程序时,流程和部署类似。在本地完成更新,确保程序通过测试后,将代码推送到 GitHub 上的远程仓库。然后登录到 PythonAnywhere,打开一个命令行会话(Bash),切换到项目目录,使用 git pull 命令从远程仓库拉取更新: ```bash $ cd watchlist @@ -232,11 +235,10 @@ $ git pull 程序部署上线以后,你可以考虑继续为它开发新功能,也可以从零编写一个新的程序。虽然本书即将接近尾声,但你的学习之路才刚刚开始,因为本书只是介绍了 Flask 入门所需的基础知识,你还需要进一步学习。在后记中,你可以看到进一步学习的推荐读物。 -接下来,有一个挑战在等着你。 +接下来,还有一个挑战在等着你。 ## 进阶提示 * 因为 PythonAnywhere 支持在线管理文件、编辑代码、执行命令,你可以在学习编程的过程中使用它来在线开发 Web 程序。 * PythonAnywhere 的 Web 面板还有一些功能设置:Log files 部分可以查看你的程序日志,Traffic 部分显示了你的程序访问流量情况,Security 部分可以为你的程序程序开启强制启用 HTTPS 和密码保护。 -* 如果你是[《Flask Web 开发实战》](http://helloflask.com/book/1)的读者,第 14 章详细介绍了部署 Flask 程序的两种方式:传统部署和云部署。 diff --git a/chapters/challenge.md b/chapters/12-challenge.md similarity index 64% rename from chapters/challenge.md rename to chapters/12-challenge.md index 24149d4..d4dbf2a 100644 --- a/chapters/challenge.md +++ b/chapters/12-challenge.md @@ -1,17 +1,17 @@ # 小挑战 -经过本书的学习,你应该有能力独立开发一个简单的 Web 程序了。所以这里有一个小挑战:为你的 Watchlist 添加一个留言板功能,效果类似 [SayHello](http://sayhello.helloflask.com)。 +经过本书的学习,你应该有能力独立开发一个简单的 Web 程序了。这里为你准备的小挑战是:给你的 Watchlist 程序添加一个留言板功能,效果类似 [SayHello](http://sayhello.helloflask.com)。 下面是一些编写提示: * 编写表示留言的模型类,更新数据库表 * 创建留言页面的模板 * 在模板中添加留言表单 -* 添加显示留言页面的视图函数 +* 创建用来显示留言页面的视图函数 * 在显示留言页面的视图函数编写处理表单的代码 * 生成一些虚拟数据进行测试 * 编写单元测试 * 更新到部署后的程序 * 可以参考 [SayHello 源码](https://github.com/greyli/sayhello) -如果在完成这个挑战的过程中遇到了困难,可以在 [代码厨房社区](https://codekitchen.community)发起讨论(设置帖子分类为“Flask 入门教程”)。除此之外,你可以在后记查看更多讨论的去处。 +如果在完成这个挑战的过程中遇到了困难,可以在 [代码厨房社区](https://codekitchen.community)发起讨论(设置帖子分类为“Flask 入门教程”)。 diff --git a/chapters/2-hello-code-listing.md b/chapters/2-hello-code-listing.md new file mode 100644 index 0000000..8a67d92 --- /dev/null +++ b/chapters/2-hello-code-listing.md @@ -0,0 +1,93 @@ +# 第 2 章:代码清单 + +## 目录 + +```text +watchlist/ +├── app.py +├── .env +├── .flaskenv +└── .gitignore +``` + +> **提示** 实际目录中的 .venv 和 .git 没有列出。 +## 代码 + +### app.py + +```python +from flask import Flask + +app = Flask(__name__) + +@app.route('/') +def hello(): + return 'Welcome to My Watchlist!' +``` + +### .gitignore + +```diff +*.pyc +*~ +__pycache__ +.DS_Store +.venv ++.env +``` + +## 命令 + +### 激活虚拟环境 + +```bash +$ .venv\Scripts\activate # Windows +``` + +或: + +```bash +$ source .venv/bin/activate # Linux 或 macOS +``` + +> **提示** 确保在执行 `flask`、`python`、`pip` 等命令前激活了虚拟环境,后续章节不再列出激活命令。 + +### 启动程序 + +默认启动命令: + +```bash +(.venv) $ flask run +``` + +以调试模式启动程序: + +```bash +(.venv) $ flask run --debug +``` + +设置不同的端口: + +```bash +(.venv) $ flask run --debug --port 8000 +``` + +### 安装 python-dotenv + +```bash +(.venv) $ pip install python-dotenv +``` + +### 创建 .env 和 .flaskenv 文件 + +```bash +$ touch .env .flaskenv +``` + +### 提交代码 + +```bash +$ git add . +$ git commit -m "Add a minimal home page" +$ git push +``` diff --git a/chapters/hello.md b/chapters/2-hello.md similarity index 63% rename from chapters/hello.md rename to chapters/2-hello.md index 2edef15..c80f09b 100644 --- a/chapters/hello.md +++ b/chapters/2-hello.md @@ -1,10 +1,9 @@ # 第 2 章:Hello, Flask! -追溯到最初,Flask 诞生于 Armin Ronacher 在 2010 年愚人节开的一个玩笑。后来,它逐渐发展成为一个成熟的 Python Web 框架,越来越受到开发者的喜爱。目前它在 GitHub 上是 Star 数量最多的 Python Web 框架,没有之一。根据 2018、2019、2020、2021 连续四年的《[Python 开发者调查报告](https://lp.jetbrains.com/python-developers-survey-2021)》统计数据,它也是目前最流行的 Python Web 框架。 +追溯到最初,Flask 诞生于 Armin Ronacher 在 2010 年愚人节开的一个玩笑。后来,它逐渐发展成为一个成熟的 Python Web 框架,越来越受到开发者的喜爱。根据近年的《[Python 开发者调查报告](https://lp.jetbrains.com/python-developers-survey-2024)》统计数据,它是目前最流行的 Python Web 框架之一。 Flask 是典型的微框架,作为 Web 框架来说,它仅保留了核心功能:**请求响应处理**和**模板渲染**。这两类功能分别由 Werkzeug(WSGI 工具库)完成和 Jinja(模板渲染库)完成,因为 Flask 包装了这两个依赖,我们暂时不用深入了解它们。 - ## 主页 这一章的主要任务就是为我们的程序编写一个简单的主页。主页的 URL 一般就是根地址,即 `/`。当用户访问根地址的时候,我们需要返回一行欢迎文字。这个任务只需要下面几行代码就可以完成: @@ -21,10 +20,10 @@ def hello(): return 'Welcome to My Watchlist!' ``` -按照惯例,我们把程序保存为 app.py,确保当前目录是项目的根目录,并且激活了虚拟环境,然后在命令行窗口执行 `flask run` 命令启动程序(按下 Control + C 可以退出): +按照惯例,我们把程序保存为 app.py。确保当前目录是项目的根目录,并且激活了虚拟环境,然后在命令行窗口执行 `flask run` 命令启动程序(按下 Control + C 可以退出): ```bash -(env) $ flask run +(.venv) $ flask run * Serving Flask app "app.py" * Environment: production WARNING: Do not use the development server in a production environment. @@ -33,30 +32,42 @@ def hello(): * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit) ``` -现在打开浏览器,输入 并按下 Enter 即可访问我们的程序主页,并看到我们在程序里返回的问候语,如下图所示: +现在打开浏览器,输入 并按下 Enter 即可访问我们的程序主页。你会在主页看到程序里返回的问候语,如下图所示: ![主页](images/2-1.png) -执行 `flask run` 命令时,Flask 会使用内置的开发服务器来运行程序。这个服务器默认监听本地机的 5000 端口,也就是说,我们可以通过在地址栏输入 或是 访问程序。 +执行 `flask run` 命令时,Flask 会使用内置的开发服务器来运行程序。这个服务器默认监听本地机的 5000 端口。本地机的地址为 。URL 中的端口使用冒号分隔,因此我们可以通过在地址栏输入 或是 访问程序。 > **注意** 内置的开发服务器只能用于开发时使用,部署上线的时候要换用性能更好的生产服务器,我们会在最后一章学习。 +在启动程序时,可以通过 `--debug` 选项来开启调试模式(debug mode)。调试模式开启后,当程序出错,浏览器页面上会显示错误信息;代码出现变动后,程序会自动重载。 + +```bash +(.venv) $ flask run --debug +``` + +在 macOS 上,也许你会遇到 5000 端口被占用的报错,这时可以使用 `--port` 选项来指定新的端口,比如 8000: + +```bash +(.venv) $ flask run --debug --port 8000 +``` +这时在浏览器访问程序主页时的地址也相应变为 。 ## 解剖时间 下面我们来分解这个 Flask 程序,了解它的基本构成。 -首先我们从 `flask` 包导入 `Flask` 类,通过实例化这个类,创建一个程序对象 `app`: +首先从 `flask` 包导入 `Flask` 类,通过实例化这个类,创建一个程序对象 `app`: ```python from flask import Flask -app = Flask(__name__) +app = Flask(__name__) # 传入存储当前模块名称的特殊变量 __name__ ``` -接下来,我们要注册一个处理函数,这个函数是处理某个请求的处理函数,Flask 官方把它叫做视图函数(view funciton),你可以理解为“**请求处理函数**”。 +接下来,我们要注册一个处理函数,这个函数是处理某个请求的处理函数,Flask 官方把它叫做视图函数(view function),你可以理解为“**请求处理函数**”。 -所谓的“注册”,就是给这个函数戴上一个装饰器帽子。我们使用 `app.route()` 装饰器来为这个函数绑定对应的 URL,当用户在浏览器访问这个 URL 的时候,就会触发这个函数,获取返回值,并把返回值显示到浏览器窗口: +所谓的“注册”,就是给这个函数戴上一个装饰器帽子。我们使用 `app.route()` 装饰器来为这个函数绑定对应的 URL,当用户在浏览器访问这个 URL 的时候,就会触发这个函数,获取函数返回值,并把返回值显示到浏览器窗口: ```python @app.route('/') @@ -64,25 +75,24 @@ def hello(): return 'Welcome to My Watchlist!' ``` -> **提示** 为了便于理解,你可以把 Web 程序看作是一堆这样的视图函数的集合:编写不同的函数处理对应 URL 的请求。 +> **提示** 为了便于理解,你可以把 Web 程序看作是一堆这样的视图函数的集合:包含不同的函数处理对应 URL 的请求。 填入 `app.route()` 装饰器的第一个参数是 URL 规则字符串,这里的 `/`指的是根地址。 -我们只需要写出相对地址,主机地址、端口号等都不需要写出。所以说,这里的 `/` 对应的是主机名后面的路径部分,完整 URL 就是 。如果我们这里定义的 URL 规则是 `/hello`,那么完整 URL 就是 。 +我们只需要写出相对地址,主机地址、端口号等都不需要写出。因此,这里的 `/` 对应的是主机名后面的路径部分,完整(绝对)URL 就是 。如果我们这里定义的 URL 规则是 `/hello`,那么完整 URL 就是 。当你把程序部署到线上,并设置了自己的域名,比如 helloflask.com,那么对应的 URL 就会是 。 整个请求的处理过程如下所示: 1. 当用户在浏览器地址栏访问这个地址,在这里即 2. 服务器解析请求,发现请求 URL 匹配的 URL 规则是 `/`,因此调用对应的处理函数 `hello()` 3. 获取 `hello()` 函数的返回值,处理后返回给客户端(浏览器) -4. 浏览器接受响应,将其显示在窗口上 +4. 浏览器接收到响应,将其显示在窗口上 > **提示** 在 Web 程序的语境下,虽然客户端可能有多种类型,但在本书里通常是指浏览器。 - ## 程序发现机制 -如果你把上面的程序保存成其他的名字,比如 hello.py,接着执行 `flask run` 命令会返回一个错误提示。这是因为 Flask 默认会假设你把程序存储在名为 app.py 或 wsgi.py 的文件中。如果你使用了其他名称,就要设置系统环境变量 `FLASK_APP` 来告诉 Flask 你要启动哪个程序: +如果你把上面的程序保存成其他的名字,比如 hello.py,接着执行 `flask run` 命令会返回一个错误提示“Error: Could not locate a Flask application. ”,意思是找不到要运行的 Flask 程序。这是因为 Flask 默认会假设你把程序存储在名为 app.py 或 wsgi.py 的文件中。如果你使用了其他名称,就要设置系统环境变量 `FLASK_APP` 或通过命令行选项 `--app` 来告诉 Flask 你要启动哪个程序。下面是设置环境变量 `FLASK_APP` 的示例: ```bash $ export FLASK_APP=hello.py @@ -106,25 +116,21 @@ Flask 通过读取这个环境变量值对应的模块寻找要运行的程序 * Python 导入路径 * 文件目录路径 - -## 管理环境变量 - -现在在启动 Flask 程序的时候,我们通常要和两个环境变量打交道:`FLASK_APP` 和 `FLASK_DEBUG`。因为我们的程序现在的名字是 app.py,暂时不需要设置 `FLASK_APP`。`FLASK_DEBUG` 用来开启调试模式(debug mode)。调试模式开启后,当程序出错,浏览器页面上会显示错误信息;代码出现变动后,程序会自动重载。 - -下面是手动设置环境变量 `FLASK_DEBUG` 来开启调试模式的示例: +你也可以通过命令行选项 `--app` 来给出这个值: ```bash -(env) $ export FLASK_DEBUG=1 # 注意在 Windows 系统使用 set 或 $env: 替代 export,参考前面的示例 -(env) $ flask run +(.venv) $ flask --app hello.py run --debug ``` -为了不用每次打开新的终端会话都要设置环境变量,我们安装用来自动导入系统环境变量的 python-dotenv: +## 管理环境变量 + +因为我们的程序现在的名字是 app.py,暂时不需要设置 `FLASK_APP`。随着程序变大,你可能会引入其他环境变量。为了不用每次打开新的终端会话都要设置环境变量,我们安装用来自动导入系统环境变量的 python-dotenv: ```bash -(env) $ pip install python-dotenv +(.venv) $ pip install python-dotenv ``` -当 python-dotenv 安装后,Flask 会从项目根目录的 .flaskenv 和 .env 文件读取环境变量并设置。我们分别使用文本编辑器创建这两个文件,或是使用更方便的 `touch` 命令创建(注意不要漏掉文件名开头的点): +当 python-dotenv 安装后,Flask 会从项目根目录的 .flaskenv 和 .env 文件读取环境变量并设置到当前环境。我们分别使用文本编辑器创建这两个文件(目前内容均为空),或是使用更方便的 `touch` 命令创建(注意不要漏掉文件名开头的点): ```bash $ touch .env .flaskenv @@ -136,24 +142,9 @@ $ touch .env .flaskenv .env ``` -在新创建的 .flaskenv 文件里,我们写入一行 `FLASK_DEBUG=1`,将环境变量 `FLASK_DEBUG` 的值设为 `1`,以便开启调试模式: - -```bash -# .flaskenv 文件 -FLASK_DEBUG=1 -``` - -顺便说一句,如果你安装的 Flask 版本是 2.3 或更高版本,则可以直接使用 `--debug` 命令行选项来开启调试模式,即: - -```bash -(env) $ flask run --debug -``` - - ## 实验时间 -在这个小节,我们可以通过做一些实验,来扩展和加深对本节内容的理解。 - +在这个小节,我们通过做一些实验来扩展和加深对本节内容的理解。 ### 修改视图函数返回值 @@ -165,7 +156,7 @@ def hello(): return '欢迎来到我的 Watchlist!' ``` -返回值作为响应的主体,默认会被浏览器作为 HTML 格式解析,所以我们可以添加一个 HTML 元素标记: +返回值作为响应的主体,默认会被浏览器作为 HTML 格式解析,所以我们可以添加 HTML 元素标记。下面的返回值把欢迎语设为了 `h1` 标题,并使用 `` 元素填了一张图片: ```python @app.route('/') @@ -173,11 +164,10 @@ def hello(): return '

Hello Totoro!

' ``` -保存修改后,只需要在浏览器里刷新页面,你就会看到页面上的内容也会随之变化。 +确保你已经通过 `flask run --debug` 命令启动了程序。保存修改后,只需要在浏览器里刷新页面,你就会看到页面上的内容也会随之变化。 ![2-2](images/2-2.png) - ### 修改 URL 规则 另外,你也可以自由修改传入 `app.route` 装饰器里的 URL 规则字符串,但要注意以斜线 `/` 作为开头。比如: @@ -188,7 +178,7 @@ def hello(): return 'Welcome to My Watchlist!' ``` -保存修改,这时刷新浏览器,则会看到一个 404 错误提示,提示页面未找到(Page Not Found)。这是因为视图函数的 URL 改成了 `/home`,而我们刷新后访问的地址仍然是旧的 `/`。如果我们把访问地址改成 ,就会正确看到返回值。 +保存修改,这时刷新浏览器,则会看到一个 404 错误提示,提示页面未找到(Page Not Found)。这是因为 hello 视图函数的 URL 改成了 `/home`,而我们刷新后访问的地址仍然是旧的 `/`。如果我们把访问地址改成 ,就会正确看到返回值。 一个视图函数也可以绑定多个 URL,这通过附加多个装饰器实现,比如: @@ -200,9 +190,9 @@ def hello(): return 'Welcome to My Watchlist!' ``` -现在无论是访问 还是 都可以看到返回值。 +现在无论是访问 还是 都可以看到正确的返回值。 -在前面,我们之所以把传入 `app.route` 装饰器的参数称为 URL 规则,是因为我们也可以在 URL 里定义变量部分。比如下面这个视图函数会处理所有类似 `/user/` 的请求: +在前面,我们之所以把传入 `app.route` 装饰器的参数称为 URL 规则,是因为我们也可以在 URL 里定义变量部分。比如下面这个视图函数会处理所有类似 `/user/` 的请求,其中 `` 部分为变量,它的值会被作为 name 关键字传入视图函数: ```python @app.route('/user/') @@ -210,7 +200,7 @@ def user_page(name): return 'User page' ``` -不论你访问 ,还是 ,抑或是 ,都会触发这个函数。通过下面的方式,我们也可以在视图函数里获取到这个变量值: +不论你访问 ,还是 ,抑或是 ,都会触发这个函数。下面我们在视图函数里获取并使用这个 name 变量值: ```python from markupsafe import escape @@ -220,14 +210,13 @@ def user_page(name): return f'User: {escape(name)}' ``` -> **注意** 用户输入的数据会包含恶意代码,所以不能直接作为响应返回,需要使用 MarkupSafe(Flask 的依赖之一)提供的 `escape()` 函数对 `name` 变量进行转义处理,比如把 `<` 转换成 `<`。这样在返回响应时浏览器就不会把它们当做代码执行。 - +> **注意** 用户输入的数据会包含恶意代码,所以不能直接作为响应返回。正确做法是使用 MarkupSafe(Flask 的依赖之一)提供的 `escape()` 函数对 `name` 变量进行转义处理,比如把 `<` 转换成 `<`,这样在返回响应时浏览器就不会把它们当做代码执行。 -### 修改视图函数名? +### 修改视图函数名 -最后一个可以修改的部分就是视图函数的名称了。首先,视图函数的名字是自由定义的,和 URL 规则无关。和定义其他函数或变量一样,只需要让它表达出所要处理页面的含义即可。 +最后一个可以修改的部分就是视图函数的名称了。视图函数的名字是自由定义的,和 URL 规则无关。和定义其他函数或变量一样,只需要让它表达出所要处理页面的含义即可。 -除此之外,它还有一个重要的作用:作为代表某个路由的端点(endpoint),同时用来生成视图函数对应的 URL。对于程序内的 URL,为了避免手写,Flask 提供了一个 `url_for` 函数来生成 URL,它接受的第一个参数就是端点值,默认为视图函数的名称: +除此之外,它还有一个重要的作用:作为代表某个路由的端点(endpoint),可以用来生成视图函数对应的 URL。对于程序内的 URL,为了避免手写,Flask 提供了一个 `url_for` 函数来生成 URL,它接受的第一个参数就是端点值,默认为视图函数的名称: ```python from flask import url_for @@ -245,7 +234,7 @@ def user_page(name): @app.route('/test') def test_url_for(): - # 下面是一些调用示例(请访问 http://localhost:5000/test 后在命令行窗口查看输出的 URL): + # 下面是一些调用示例(访问 http://localhost:5000/test 后在命令行窗口查看输出的 URL): print(url_for('hello')) # 生成 hello 视图函数对应的 URL,将会输出:/ # 注意下面两个调用是如何生成包含 URL 变量的 URL 的 print(url_for('user_page', name='greyli')) # 输出:/user/greyli @@ -258,28 +247,24 @@ def test_url_for(): 实验过程中编写的代码可以删掉,也可以保留,但记得为根地址返回一行问候,这是我们这一章的任务。 - ## 本章小结 这一章我们为程序编写了主页,同时学习了 Flask 视图函数的基本编写方式。结束前,让我们提交代码: ```bash $ git add . -$ git commit -m "Add minimal home page" +$ git commit -m "Add a minimal home page" $ git push ``` 为了保持简单,我们统一在章节最后一次提交所有改动。在现实世界里,通常会根据需要分为多个 commit;同样的,这里使用 `-m` 参数给出简单的提交信息。在现实世界里,你可能需要撰写更完整的提交信息。 -> **提示** 你可以在 GitHub 上查看本书示例程序的对应 commit:[eca06dc](https://github.com/helloflask/watchlist/commit/eca06dcdf682dfa2883a8632814e4c65b6eae141)。 - - ## 进阶提示 -* 对于 URL 变量,Flask 支持在 URL 规则字符串里对变量设置处理器,对变量进行预处理。比如 `/user/` 会将 URL 中的 number 部分转换成整型。 +* 对于 URL 变量,Flask 支持在 URL 规则字符串里对变量设置处理器,对变量进行预处理,语法格式为 `<处理器:变量名>`。比如 `/user/` 会将 URL 中的 number 部分转换成整型,使用 `/uploads/` 支持传入包含斜线的路径字符串。 * 因为 Flask 的上下文机制,有一些变量和函数(比如 `url_for`函数)只能在特定的情况下才能正确执行,比如视图函数内。我们先暂时不用纠结,后面再慢慢了解。 * 名字以 `.` 开头的文件默认会被隐藏,执行 `ls` 命令时会看不到它们,这时你可以使用 `ls -f` 命令来列出所有文件。 * 了解 HTTP 基本知识将会有助于你了解 Flask 的工作原理。 * 阅读文章[《互联网是如何工作的》](https://tutorial.djangogirls.org/zh/how_the_internet_works/)。 * 阅读文章[《从 HTTP 请求 - 响应循环探索 Flask 的基本工作方式》](https://zhuanlan.zhihu.com/p/42231394)。 -* 如果你是[《Flask Web 开发实战》](http://helloflask.com/book/1)的读者,这部分的进阶内容可以在第 1 章《初识 Flask》和第 2 章《HTTP 和 Flask》找到。 + diff --git a/chapters/template.md b/chapters/3-template.md similarity index 88% rename from chapters/template.md rename to chapters/3-template.md index aa60417..a6256ac 100644 --- a/chapters/template.md +++ b/chapters/3-template.md @@ -2,7 +2,7 @@ 在一般的 Web 程序里,访问一个地址通常会返回一个包含各类信息的 HTML 页面。因为我们的程序是动态的,页面中的某些信息需要根据不同的情况来进行调整,比如对登录和未登录用户显示不同的信息,所以页面需要在用户访问时根据程序逻辑动态生成。 -我们把包含变量和运算逻辑的 HTML 或其他格式的文本叫做**模板**,执行这些变量替换和逻辑计算工作的过程被称为**渲染**,这个工作由我们这一章要学习使用的模板渲染引擎——Jinja2 来完成。 +我们把包含变量和运算逻辑的 HTML 或其他格式的文本叫做**模板(template)**,执行这些变量替换和逻辑计算工作的过程被称为**渲染(rendering)**,这个工作由我们这一章要学习使用的模板渲染引擎——Jinja2 来完成。 按照默认的设置,Flask 会从程序实例所在模块同级目录的 templates 文件夹中寻找模板,我们的程序目前存储在项目根目录的 app.py 文件里,所以我们要在项目根目录创建这个文件夹: @@ -10,10 +10,20 @@ $ mkdir templates ``` +目前的目录结构如下: + +```bash +watchlist/ +├── templates/ +├── app.py +├── .env +├── .flaskenv +└── .gitignore +``` ## 模板基本语法 -在社交网站上,每个人都有一个主页,借助 Jinja2 就可以写出一个通用的模板: +在社交网站上,每个人都有一个主页,借助 Jinja2 就可以写出一个通用的用户主页模板: ```jinja2

{{ username }}的个人主页

@@ -30,8 +40,7 @@ Jinja2 的语法和 Python 大致相同,你在后面会陆续接触到一些 - `{% ... %}` 用来标记语句,比如 if 语句,for 语句等。 - `{# ... #}` 用来写注释。 -模板中使用的变量需要在渲染的时候传递进去,具体我们后面会了解。 - +模板中使用的变量需要在渲染的时候传递进去,具体我们下面会了解。 ## 编写主页模板 @@ -56,7 +65,7 @@ Jinja2 的语法和 Python 大致相同,你在后面会陆续接触到一些 {% endfor %} {# 使用 endfor 标签结束 for 语句 #} @@ -122,22 +131,18 @@ def index(): ![主页电影列表](images/3-1.png) - ## 本章小结 这一章我们编写了一个简单的主页。结束前,让我们提交代码: ```bash $ git add . -$ git commit -m "Add index page" +$ git commit -m "Add the index page template" $ git push ``` -> **提示** 你可以在 GitHub 上查看本书示例程序的对应 commit:[8537d98](https://github.com/helloflask/watchlist/commit/8537d98bdd7828b1f7aa2431bbd5a16e757a3cc4)。 - ## 进阶提示 * 使用 [Faker](https://github.com/joke2k/faker) 可以实现自动生成虚拟数据,它支持丰富的数据类型,比如时间、人名、地名、随机字符等。 * 除了过滤器,Jinja2 还在模板中提供了一些测试器、全局函数可以使用;除此之外,还有更丰富的控制结构支持,有一些我们会在后面学习到,更多的内容则可以访问 [Jinja2 文档](https://jinja.palletsprojects.com/en/3.0.x/templates)学习。 -* 如果你是[《Flask Web 开发实战》](http://helloflask.com/book/1)的读者,模板相关内容可以在第 3 章《模板》找到,Faker 相关内容可以在第 7 章找到。 diff --git a/chapters/static.md b/chapters/4-static.md similarity index 84% rename from chapters/static.md rename to chapters/4-static.md index dfd38d6..167023a 100644 --- a/chapters/static.md +++ b/chapters/4-static.md @@ -2,20 +2,31 @@ 静态文件(static files)和我们的模板概念相反,指的是内容不需要动态生成的文件。比如图片、CSS 文件和 JavaScript 脚本等。 -在 Flask 中,我们需要创建一个 static 文件夹来保存静态文件,它应该和程序模块、templates 文件夹在同一目录层级,所以我们在项目根目录创建它: +在 Flask 中,我们需要创建一个 static 文件夹来保存静态文件,它应该和程序模块(app.py)、templates 文件夹在同一目录层级,所以我们在项目根目录创建它: ```bash $ mkdir static ``` +目前的目录结构如下: + +```bash +watchlist/ +├── templates/ +├── static/ +├── app.py +├── .env +├── .flaskenv +└── .gitignore +``` ## 生成静态文件 URL 在 HTML 文件里,引入这些静态文件需要给出资源所在的 URL。为了更加灵活,这些文件的 URL 可以通过 Flask 提供的 `url_for()` 函数来生成。 -在第 2 章的最后,我们学习过 `url_for()` 函数的用法,传入端点值(视图函数的名称)和参数,它会返回对应的 URL。对于静态文件,需要传入的端点值是 `static`,同时使用 `filename` 参数来传入相对于 static 文件夹的文件路径。 +在第 2 章的最后,我们学习过 `url_for()` 函数的用法,传入端点值(视图函数的名称)和参数,它会返回对应的 URL。对于静态文件,需要传入的端点值为 `static`,同时使用 `filename` 参数来传入相对于 static 文件夹的文件路径。 -假如我们在 static 文件夹的根目录下面放了一个 foo.jpg 文件,下面的调用可以获取它的 URL: +假设我们在 static 文件夹的根目录下面放了一个 foo.jpg 文件,下面的调用可以获取它的 URL: ```jinja2 @@ -23,7 +34,7 @@ $ mkdir static 花括号部分的调用会返回 `/static/foo.jpg`。 -> **提示** 在 Python 脚本里,`url_for()` 函数需要从 `flask` 包中导入,而在模板中则可以直接使用,因为 Flask 把一些常用的函数和对象添加到了模板上下文(环境)里。 +> **提示** 在 Python 脚本里,`url_for()` 函数需要从 `flask` 包中导入,而在模板中则可以直接使用,因为 Flask 自动把一些常用的函数和对象添加到了模板上下文(环境)里。 ## 添加 Favicon @@ -44,14 +55,14 @@ Favicon(favourite icon) 是显示在标签页和书签栏的网站头像。 ## 添加图片 -为了让页面不那么单调,我们来添加两个图片:一个是显示在页面标题旁边的头像,另一个是显示在页面底部的龙猫动图。我们在 static 目录下面创建一个子文件夹 images,把这两个图片都放到这个文件夹里: +为了让页面不那么单调,我们来添加两个图片:一个是显示在页面标题旁边的头像,另一个是显示在页面底部的龙猫动图。在 static 目录下面创建一个子文件夹 images,把这两个图片都放到这个文件夹里: ```bash $ cd static $ mkdir images ``` -创建子文件夹并不是必须的,这里只是为了更好的组织同类文件。同样的,如果你有多个 CSS 文件,也可以创建一个 css 文件夹来组织他们。下面我们在页面模板中添加这两个图片,注意填写正确的文件路径: +创建子文件夹并不是必须的,这里只是为了更好的组织同类文件。类似地,如果你有多个 CSS 文件,也可以创建一个 css 文件夹来组织它们。下面我们在页面模板中添加这两个图片,注意填写正确的文件路径: *templates/index.html:添加图片* @@ -135,7 +146,7 @@ footer { ``` -> **提示** 当你把 CSS 写到单独的文件后,浏览器获取到这个文件后会对其进行缓存(其他静态文件同理,比如 JavaScript 文件)。Flask 从 2.0 版本开始支持自动重载静态文件的变化,如果你使用的仍然是旧版本的 Flask,那么每当你对 CSS 文件的内容进行更新后,都需要使用下面的快捷键清除缓存: +> **提示** 当你把 CSS 写到单独的文件后,浏览器获取到这个文件后会对其进行缓存(其他静态文件同理,比如 JavaScript 文件)。Flask 从 2.0 版本开始支持自动重载静态文件,如果你使用的仍然是旧版本的 Flask,那么每当你对 CSS 文件的内容进行更新后,都需要使用下面的快捷键清除缓存: > > - Google Chrome(Mac):Command + Shift + R > - Google Chrome(Windows & Linux):Ctrl + F5 @@ -174,8 +185,6 @@ $ git commit -m "Add static files" $ git push ``` -> **提示** 你可以在 GitHub 上查看本书示例程序的对应 commit:[e51c579](https://github.com/helloflask/watchlist/commit/e51c579735ae837824f10af5c1b7d454014d3c59)。 - ## 进阶提示 diff --git a/chapters/database.md b/chapters/5-database.md similarity index 59% rename from chapters/database.md rename to chapters/5-database.md index 3fbbe5c..d4e8b73 100644 --- a/chapters/database.md +++ b/chapters/5-database.md @@ -12,21 +12,26 @@ Flask 有大量的第三方扩展,这些扩展可以简化和第三方库的 首先安装它: ```bash -(env) $ pip install flask-sqlalchemy==2.5.1 sqlalchemy==1.4.47 +(.venv) $ pip install flask-sqlalchemy ``` -> **提示** Flask-SQLAlchemy 3.x / SQLAlchemy 2.x 版本有一些大的变化,这里分别固定安装 2.5.1 和 1.4.47 版本。后续教程改写后会删除这里的版本限制。 - 大部分扩展都需要执行一个“初始化”操作。你需要导入扩展类,实例化并传入 Flask 程序实例: ```python +from flask import Flask from flask_sqlalchemy import SQLAlchemy # 导入扩展类 +from sqlalchemy.orm import DeclarativeBase app = Flask(__name__) -db = SQLAlchemy(app) # 初始化扩展,传入程序实例 app +class Base(DeclarativeBase): + pass + +db = SQLAlchemy(app, model_class=Base) # 初始化扩展,传入程序实例 app ``` +实例化扩展类时,除了程序实例,需要额外传入一个继承自 DeclarativeBase 的子类作为 model_class 参数的值。目前这个类是空的,后续你可以按照需要对这个基类进行自定义。 + ## 设置数据库 URI 为了设置 Flask、扩展或是我们程序本身的一些行为,我们需要设置和定义一些配置变量。Flask 提供了一个统一的接口来写入和获取这些配置变量:`Flask.config` 字典。配置变量的名称必须使用大写,写入配置的语句一般会放到扩展类实例化语句之前。 @@ -34,11 +39,11 @@ db = SQLAlchemy(app) # 初始化扩展,传入程序实例 app 下面写入了一个 `SQLALCHEMY_DATABASE_URI` 变量来告诉 SQLAlchemy 数据库连接地址: ```python -import os +from pathlib import Path # ... -app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////' + os.path.join(app.root_path, 'data.db') +app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////' + str(Path(app.root_path) / 'data.db') ``` > **注意** 这个配置变量的最后一个单词是 URI,而不是 URL。 @@ -51,7 +56,7 @@ sqlite:////数据库文件的绝对地址 数据库文件一般放到项目根目录即可,`app.root_path` 返回程序实例所在模块的路径(目前来说,即项目根目录),我们使用它来构建文件路径。数据库文件的名称和后缀你可以自由定义,一般会使用 .db、.sqlite 和 .sqlite3 作为后缀。 -另外,如果你使用 Windows 系统,上面的 URI 前缀部分只需要写入三个斜线(即 `sqlite:///`)。在本书的示例程序代码里,做了一些兼容性处理,另外还新设置了一个配置变量,实际的代码如下: +另外,如果你使用 Windows 系统,上面的 URI 前缀部分只需要写入三个斜线(即 `sqlite:///`)。在本书的示例程序代码里,做了一些兼容性处理,实际的代码如下: *app.py:数据库配置* @@ -62,17 +67,12 @@ import sys from flask import Flask from flask_sqlalchemy import SQLAlchemy -WIN = sys.platform.startswith('win') -if WIN: # 如果是 Windows 系统,使用三个斜线 - prefix = 'sqlite:///' -else: # 否则使用四个斜线 - prefix = 'sqlite:////' +SQLITE_PREFIX = 'sqlite:///' if sys.platform.startswith('win') else 'sqlite:////' app = Flask(__name__) -app.config['SQLALCHEMY_DATABASE_URI'] = prefix + os.path.join(app.root_path, 'data.db') -app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False # 关闭对模型修改的监控 -# 在扩展类实例化前加载配置 -db = SQLAlchemy(app) +app.config['SQLALCHEMY_DATABASE_URI'] = SQLITE_PREFIX + str(Path(app.root_path) / 'data.db') + +db = SQLAlchemy(app, model_class=Base) ``` 如果你固定在某一个操作系统上进行开发,部署时也使用相同的操作系统,那么可以不用这么做,直接根据你的需要写出前缀即可。 @@ -87,41 +87,47 @@ db = SQLAlchemy(app) *app.py:创建数据库模型* ```python -class User(db.Model): # 表名将会是 user(自动生成,小写处理) - id = db.Column(db.Integer, primary_key=True) # 主键 - name = db.Column(db.String(20)) # 名字 +from sqlalchemy import String +from sqlalchemy.orm import Mapped, mapped_column + +class User(db.Model): + __tablename__ = 'user' # 定义表名称 + id: Mapped[int] = mapped_column(primary_key=True) # 主键 + name: Mapped[str] = mapped_column(String(20)) # 名字 class Movie(db.Model): # 表名将会是 movie - id = db.Column(db.Integer, primary_key=True) # 主键 - title = db.Column(db.String(60)) # 电影标题 - year = db.Column(db.String(4)) # 电影年份 + __tablename__ = 'movie' + id: Mapped[int] = mapped_column(primary_key=True) # 主键 + title: Mapped[str] = mapped_column(String(60)) # 电影标题 + year: Mapped[str] = mapped_column(String(4)) # 电影年份 ``` -模型类的编写有一些限制: +模型类的编写规则如下: * 模型类要声明继承 `db.Model`。 -* 每一个类属性(字段)要实例化 `db.Column`,传入的参数为字段的类型,下面的表格列出了常用的字段类。 -* 在 `db.Column()` 中添加额外的选项(参数)可以对字段进行设置。比如,`primary_key` 设置当前字段是否为主键。除此之外,常用的选项还有 `nullable`(布尔值,是否允许为空值)、`index`(布尔值,是否设置索引)、`unique`(布尔值,是否允许重复值)、`default`(设置默认值)等。 +* 使用 `__tablename__` 属性定义表名称。 +* 每一个类属性(字段)的类型通过类型标注(type hint)定义,类型信息通过 `Mapped[]` 传入。下面的表格列出了常用的字段类型和对应的类型对象。 +* 如果对字段有额外的设置,可以使用 `mapped_column()` 调用传入额外的参数。比如,`primary_key` 设置当前字段是否为主键。除此之外,常用的选项还有 `index`(布尔值,是否设置索引)、`unique`(布尔值,是否允许重复值)、`default`(设置默认值)等。 常用的字段类型如下表所示: -| 字段类 | 说明 | -| ---------------- | --------------------------------------------- | -| db.Integer | 整型 | -| db.String (size) | 字符串,size 为最大长度,比如 `db.String(20)` | -| db.Text | 长文本 | -| db.DateTime | 时间日期,Python `datetime` 对象 | -| db.Float | 浮点数 | -| db.Boolean | 布尔值 | - +| 字段类型 | 说明 | +| -------- | ------------------------------------------------------------------------------- | +| int | 整型 | +| str | 字符串,可以通过 `mapped_column(String(size)` 的形式声明字符串长度。字段类 String 需要从 sqlalchemy 模块导入 | +| str | 长文本,需要同时使用 `mapped_column(Text)` 声明。字段类 Text 需要从 sqlalchemy 模块导入 | +| datetime | 时间日期,即 Python `datetime` 对象,需要先从 `datetime` 模块导入 `datetime` 对象 | +| float | 浮点数 | +| bool | 布尔值 | +> **提示** 如果你对类型标注不熟悉,可以阅读[相关文档](https://docs.python.org/zh-cn/3.12/library/typing.html)。简单来说,我们可以通过特殊语法来为变量、函数参数、函数返回值等标注相应的类型,比如 `message: str = 'How are you?'`。其中的“: str”部分用来标注 message 变量的类型为字符串(str)。大部分内置类型都可以直接用作类型标注,比如 str、int、list、dict、bool。对于复杂的类型标注,可以从 typing 模块导入相应的类型。 ## 创建数据库表 模型类创建后,还不能对数据库进行操作,因为我们还没有创建表和数据库文件。下面在 Python Shell 中创建了它们: ```python -(env) $ flask shell +(.venv) $ flask shell >>> from app import db >>> db.create_all() ``` @@ -145,15 +151,15 @@ class Movie(db.Model): # 表名将会是 movie 和 `flask shell`类似,我们可以编写一个自定义命令来自动执行创建数据库表操作: -*app.py:自定义命令 initdb* +*app.py:自定义命令 init-db* ```python import click -@app.cli.command() # 注册为命令,可以传入 name 参数来自定义命令 +@app.cli.command('init-db') # 注册为命令,传入自定义命令名 @click.option('--drop', is_flag=True, help='Create after drop.') # 设置选项 -def initdb(drop): +def init_database(drop): """Initialize the database.""" if drop: # 判断是否输入了选项 db.drop_all() @@ -161,16 +167,16 @@ def initdb(drop): click.echo('Initialized database.') # 输出提示信息 ``` -默认情况下,如果没有指定,函数名称就是命令的名字(注意函数名中的下划线会被转换为连接线),现在执行 `flask initdb` 命令就可以创建数据库表: +默认情况下,如果没有指定,函数名称就是命令的名字(注意函数名中的下划线会被转换为连接线),现在执行 `flask init-db` 命令就可以创建数据库表: ```bash -(env) $ flask initdb +(.venv) $ flask init-db ``` 使用 `--drop` 选项可以删除表后重新创建: ```bash -(env) $ flask initdb --drop +(.venv) $ flask init-db --drop ``` @@ -201,10 +207,23 @@ def initdb(drop): ### 读取 -通过对模型类的 `query` 属性调用可选的过滤方法和查询方法,我们就可以获取到对应的单个或多个记录(记录以模型类实例的形式表示)。查询语句的格式如下: +有两种方式从数据库读取记录,一种是使用 `db.session.get()` 方法根据主键 ID 来查询记录: ```python -<模型类>.query.<过滤方法(可选)>.<查询方法> +>>> db.session.get(Movie, 1) # 获取主键为 1 的 movie 表记录 +``` + +另一种是通过 select() 函数来构建一个完整的 SELECT 语句。然后使用 `db.session.execute()` 方法执行语句并通过提取方法来提取记录(记录以模型类实例的形式表示)。下面是查询语句的基本格式: + +``` +db.session.execute(select(...).<过滤方法(可选)>).<提取方法> +``` + +其中的 select 函数从 SQLAlchemy 导入: + + +```python +from sqlalchemy import select ``` 下面是一些常用的过滤方法: @@ -216,42 +235,79 @@ def initdb(drop): | order_by() | 根据指定条件对记录进行排序,返回新产生的查询对象 | | group_by() | 根据指定条件对记录进行分组,返回新产生的查询对象 | -下面是一些常用的查询方法: +下面是一些常用的提取方法: + +| 查询方法 | 说明 | +| -------------- | ---------------------------------------------------------- | +| all() | 返回包含所有查询记录的列表 | +| first() | 返回查询的第一条记录,如果未找到,则返回 None | +| scalar() | 类似 first(),但返回标量值 | +| scalars() | 将 Result 对象转换为 ScalarResult 对象,继续调用上述提取方法会获取到标量值而不是 Row 对象 | +| first_or_404() | 返回查询的第一条记录,如果未找到,则返回 404 错误响应 | +| get_or_404() | 传入主键值作为参数,返回指定主键值的记录,如果未找到,则返回 404 错误响应 | +| paginate() | 返回一个 Pagination 对象,可以对记录进行分页处理 | +在实际使用时,我们一般会分别使用 `scalars.first()` 和 `scalars().all()` 来获取单条记录或多条记录。这两个调用会返回标量值(scalar),也就是模型类实例。`scalars.first()` 等同于调用 `scalar()`。 + +需要额外注意的是,`first_or_404()`、`get_or_404()` 以及 `paginate()` 方法由扩展 Flask-SQLAlchemy 提供,所以使用时直接通过 db 对象调用。以 `get_or_404()` 为例: -| 查询方法 | 说明 | -| -------------- | ------------------------------------------------------------ | -| all() | 返回包含所有查询记录的列表 | -| first() | 返回查询的第一条记录,如果未找到,则返回 None | -| get(id) | 传入主键值作为参数,返回指定主键值的记录,如果未找到,则返回 None | -| count() | 返回查询结果的数量 | -| first_or_404() | 返回查询的第一条记录,如果未找到,则返回 404 错误响应 | -| get_or_404(id) | 传入主键值作为参数,返回指定主键值的记录,如果未找到,则返回 404 错误响应 | -| paginate() | 返回一个 Pagination 对象,可以对记录进行分页处理 | +```python +movie = db.get_or_404(Movie, id) +``` 下面的操作演示了如何从数据库中读取记录,并进行简单的查询: ```python +>>> from sqlalchemy import select >>> from app import Movie # 导入模型类 ->>> movie = Movie.query.first() # 获取 Movie 模型的第一个记录(返回模型类实例) +>>> movie = db.session.execute(select(Movie)).scalar() # 获取 Movie 模型的第一个记录(返回模型类实例) >>> movie.title # 对返回的模型类实例调用属性即可获取记录的各字段数据 'Leon' >>> movie.year '1994' ->>> Movie.query.all() # 获取 Movie 模型的所有记录,返回包含多个模型类实例的列表 +>>> db.session.execute(select(Movie)).scalars().all() # 获取 Movie 模型的所有记录,返回包含多个模型类实例的列表 [, ] ->>> Movie.query.count() # 获取 Movie 模型所有记录的数量 -2 ->>> Movie.query.get(1) # 获取主键值为 1 的记录 +>>> db.session.get(Movie, 1) # 获取主键值为 1 的记录 ->>> Movie.query.filter_by(title='Mahjong').first() # 获取 title 字段值为 Mahjong 的记录 +# 获取 title 字段值为 Mahjong 的记录 +>>> db.session.execute(select(Movie).filter_by(title='Mahjong')).scalar() + +# 等同于上面的查询,但使用 filter 过滤方法 +>>> db.session.execute(select(Movie).filter(Movie.title=='Mahjong')).scalar() ->>> Movie.query.filter(Movie.title=='Mahjong').first() # 等同于上面的查询,但使用不同的过滤方法 +``` + + 附加过滤方法后整个查询语句会变得很长,这时可以把查询语句部分拆出来 + +```python +>>> stmt = select(Movie).filter(Movie.title=='Mahjong') +>>> db.session.execute(stmt).scalar() ``` -> **提示** 我们在说 Movie 模型的时候,实际指的是数据库中的 movie 表。表的实际名称是模型类的小写形式(自动生成),如果你想自己指定表名,可以定义 `__tablename__` 属性。 +等你熟练掌握这些用法之后,可以尝试使用 `db.session.scalars()` 和 `db.session.scalar()` 快捷方法来简化语句: + +```python +# scalars +movies = db.session.execute(select(Movie)).scalars().all() +# 使用简化方法 db.session.scalars() +movies = db.session.scalars(select(Movie)).all() +# scalar +movie = db.session.execute(select(Movie)).scalar() +# 使用简化方法 db.session.scalar() +movie = db.session.scalar(select(Movie)) +``` + +如果你想统计某个表的记录数量,可以通过数据库函数 `func.count` 实现: + +```python +>>> from sqlalchemy import select, func +>>> db.session.execute(select(func.count(Movie.id))).scalar() +2 +``` + +> **提示** 我们在说 Movie 模型的时候,实际指的是数据库中的 movie 表。 -对于最基础的 `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) 部分查看。 +对于最基础的 `filter()` 过滤方法,SQLAlchemy 支持丰富的查询操作符,具体可以访问[文档相关页面](http://docs.sqlalchemy.org/en/latest/core/sqlelement.html#sqlalchemy.sql.operators.ColumnOperators)查看。访问 [SQLAlchemy 文档](https://docs.sqlalchemy.org/en/20/core/connections.html#sqlalchemy.engine.Result)查看所有可用的提取方法。 ### 更新 @@ -259,7 +315,7 @@ def initdb(drop): 下面的操作更新了 `Movie` 模型中主键为 `2` 的记录: ```python ->>> movie = Movie.query.get(2) +>>> movie = db.session.get(Movie, 2) >>> movie.title = 'WALL-E' # 直接对实例属性赋予新的值即可 >>> movie.year = '2008' >>> db.session.commit() # 注意仍然需要调用这一行来提交改动 @@ -270,7 +326,7 @@ def initdb(drop): 下面的操作删除了 `Movie` 模型中主键为 `1` 的记录: ```python ->>> movie = Movie.query.get(1) +>>> movie = db.session.get(Movie, 1) >>> db.session.delete(movie) # 使用 db.session.delete() 方法删除记录,传入模型实例 >>> db.session.commit() # 提交改动 ``` @@ -287,11 +343,13 @@ def initdb(drop): ```python @app.route('/') def index(): - user = User.query.first() # 读取用户记录 - movies = Movie.query.all() # 读取所有电影记录 + user = db.session.execute(select(User)).scalar() # 读取用户记录 + movies = db.session.execute(select(Movie)).scalars().all() # 读取所有电影记录 return render_template('index.html', user=user, movies=movies) ``` +> **提示** `scalars()` 返回的 Result 对象可以直接作为迭代器使用。如果你对返回的结果只需要调用 for 循环迭代,那么可以仅调用 `scalars()` 而不是 `scalars().all()`,这样会比后者把所有结果都加载出来再进行 for 循环性能更好。 + 在 `index` 视图中,原来传入模板的 `name` 变量被 `user` 实例取代,模板 index.html 中的两处 `name` 变量也要相应的更新为 `user.name` 属性: ```jinja2 @@ -312,8 +370,9 @@ import click @app.cli.command() def forge(): """Generate fake data.""" + db.drop_all() db.create_all() - + # 全局的两个变量移动到这个函数内 name = 'Grey Li' movies = [ @@ -328,13 +387,13 @@ def forge(): {'title': 'WALL-E', 'year': '2008'}, {'title': 'The Pork of Music', 'year': '2012'}, ] - + user = User(name=name) db.session.add(user) for m in movies: movie = Movie(title=m['title'], year=m['year']) db.session.add(movie) - + db.session.commit() click.echo('Done.') ``` @@ -342,7 +401,7 @@ def forge(): 现在执行 `flask forge` 命令就会把所有虚拟数据添加到数据库里: ```bash -(env) $ flask forge +(.venv) $ flask forge ``` @@ -356,12 +415,8 @@ $ git commit -m "Add database support with Flask-SQLAlchemy" $ git push ``` -> **提示** 你可以在 GitHub 上查看本书示例程序的对应 commit:[4d2442a](https://github.com/helloflask/watchlist/commit/4d2442a41e55fb454e092864206af08e4e3eeddf)。 - - ## 进阶提示 * 在生产环境,你可以更换更合适的 DBMS,因为 SQLAlchemy 支持多种 SQL 数据库引擎,通常只需要改动非常少的代码。 * 我们的程序只有一个用户,所以没有将 User 表和 Movie 表建立关联。访问 Flask-SQLAlchemy 文档的“[声明模型](https://flask-sqlalchemy.palletsprojects.com/en/2.x/models/#one-to-many-relationships)”章节可以看到相关内容。 * 阅读 [SQLAlchemy 官方文档和教程](https://docs.sqlalchemy.org/en/latest/)详细了解它的用法。注意我们在这里使用 Flask-SQLAlchemy 来集成它,所以用法和单独使用 SQLAlchemy 有一些不同。作为参考,你可以同时阅读 [Flask-SQLAlchemy 官方文档](https://flask-sqlalchemy.palletsprojects.com/en/2.x/)。 -* 如果你是[《Flask Web 开发实战》](http://helloflask.com/book/1)的读者,第 5 章详细介绍了 SQLAlchemy 和 Flask-Migrate 的使用,第 8 章和第 9 章引入了更复杂的模型关系和查询方法。 diff --git a/chapters/template2.md b/chapters/6-advanced-template.md similarity index 91% rename from chapters/template2.md rename to chapters/6-advanced-template.md index 54d60bb..8531ee7 100644 --- a/chapters/template2.md +++ b/chapters/6-advanced-template.md @@ -47,9 +47,11 @@ *app.py:404 错误处理函数* ```python +from sqlalchemy import select + @app.errorhandler(404) # 传入要处理的错误代码 -def page_not_found(e): # 接受异常对象作为参数 - user = User.query.first() +def page_not_found(error): # 接受异常对象作为参数 + user = db.session.execute(select(User)).scalar() return render_template('404.html', user=user), 404 # 返回模板和状态码 ``` @@ -74,9 +76,11 @@ def page_not_found(e): # 接受异常对象作为参数 *app.py:模板上下文处理函数* ```python +from sqlalchemy import select + @app.context_processor def inject_user(): # 函数名可以随意修改 - user = User.query.first() + user = db.session.execute(select(User)).scalar() return dict(user=user) # 需要返回字典,等同于 return {'user': user} ``` @@ -87,22 +91,22 @@ def inject_user(): # 函数名可以随意修改 ```python @app.context_processor def inject_user(): - user = User.query.first() + user = db.session.execute(select(User)).scalar() return dict(user=user) @app.errorhandler(404) -def page_not_found(e): +def page_not_found(error): return render_template('404.html'), 404 @app.route('/') def index(): - movies = Movie.query.all() + movies = db.session.execute(select(Movies)).scalars() return render_template('index.html', movies=movies) ``` -同样的,后面我们创建的任意一个模板,都可以在模板中直接使用 `user` 变量。 +类似地,后面我们创建的任意一个模板,都可以在模板中直接使用 `user` 变量。 ## 使用模板继承组织模板 @@ -223,7 +227,7 @@ nav li a:hover { 第一行使用 `extends` 标签声明扩展自模板 base.html,可以理解成“这个模板继承自 base.html“。接着我们定义了 `content` 块,这里的内容会插入到基模板中 `content` 块的位置。 -> **提示** 默认的块重写行为是覆盖,如果你想向父块里追加内容,可以在子块中使用 `super()` 声明,即 `{{ super() }}`。 +> **提示** 默认的块重写行为是覆盖,如果你想向父块里追加内容,可以在子块中使用 `super()` 声明,即在块的开始插入一行 `{{ super() }}`。 404 错误页面的模板类似,如下所示: @@ -290,11 +294,10 @@ $ git commit -m "Add base template and error template" $ git push ``` -> **提示** 你可以在 GitHub 上查看本书示例程序的对应 commit:[3bca489](https://github.com/helloflask/watchlist/commit/3bca489421cc498289734cfef9d6ff90232df8be)。 - ## 进阶提示 * 本章介绍的自定义错误页面是为了引出两个重要的知识点,因此并没有着重介绍错误页面本身。这里只为 404 错误编写了自定义错误页面,对于另外两个常见的错误 400 错误和 500 错误,你可以自己试着为它们编写错误处理函数和对应的模板。 -* 因为示例程序的语言和电影标题使用了英文,所以电影网站的搜索链接使用了 IMDb,对于中文,你可以使用豆瓣电影或时光网。以豆瓣电影为例,它的搜索链接为 ,对应的 `href` 属性即 `https://movie.douban.com/subject_search?search_text={{ movie.title }}`。 +* `abort()` 函数接受传入一个 description 参数,你可以使用它来传入自定义错误消息。在错误处理函数中,记得从错误对象获取这个参数的值(`error.description`),并将其渲染到错误页面中。类似地,上一章介绍的 `get_or_404()` 和 `first_or_404()` 也支持传入 description 参数。 +* 因为示例程序的语言和电影标题使用了英文,所以电影网站的搜索链接使用了 IMDb。对于中文,你可以使用豆瓣电影或时光网。以豆瓣电影为例,它的搜索链接为 ,对应的 `href` 属性值即 `https://movie.douban.com/subject_search?search_text={{ movie.title }}`。 * 因为基模板会被所有其他页面模板继承,如果你在基模板中使用了某个变量,那么这个变量也需要使用模板上下文处理函数注入到模板里。 diff --git a/chapters/form.md b/chapters/7-form.md similarity index 82% rename from chapters/form.md rename to chapters/7-form.md index 535cd07..73d1ee8 100644 --- a/chapters/form.md +++ b/chapters/7-form.md @@ -1,6 +1,6 @@ # 第 7 章:表单 -在 HTML 页面里,我们需要编写表单来获取用户输入。一个典型的表单如下所示: +在 HTML 页面里,我们需要编写表单来获取用户输入。一个典型的表单定义如下所示: ```html
@@ -15,12 +15,12 @@ 编写表单的 HTML 代码有下面几点需要注意: * 在 `` 标签里使用 `method` 属性将提交表单数据的 HTTP 请求方法指定为 POST。如果不指定,则会默认使用 GET 方法,这会将表单数据通过 URL 提交,容易导致数据泄露,而且不适用于包含大量数据的情况。 -* `` 元素必须要指定 `name` 属性,否则无法提交数据,在服务器端,我们也需要通过这个 `name` 属性值来获取对应字段的数据。 +* `` 元素必须要指定 `name` 属性,否则无法提交数据。在服务器端,我们也需要通过 `name` 属性值来获取对应字段的数据。 -> **提示** 填写输入框标签文字的 `