上一篇我们用 Gunicorn 把应用跑上了服务器。部署时还会反复做几件事:
- 第一次:
flask db upgrade建表 - 创建管理员账号
- 导入测试数据、同步权限......
每次都要记一长串 shell,容易漏步骤。Flask 自带 CLI 扩展,可以把这些操作收成 flask xxx 一条命令。
这一篇做一件事:学会写自己的 Flask CLI 命令,并理解 flask db 是怎么挂上去的。
例子仍是通用的 Note 备忘录项目,不涉及任何真实业务。
1. 学完后你能做什么
- 用
@app.cli.command()注册命令 - 写
flask init-db、flask create-admin - 给命令加 参数、选项(Click 语法)
- 把 CLI 代码 拆到单独文件 再注册
- 知道命令里为什么要
app.app_context() - 在 第十四篇部署流程 里固定调用这些命令
2. flask db 从哪来
第七篇装过 Flask-Migrate 后,终端里多了:
flask db init
flask db migrate -m "add is_pinned"
flask db upgrade
这不是 Flask 内置的,是 Migrate 扩展注册进 CLI 的。
你写的自定义命令,走的也是同一条路:挂到 app.cli 上。
flask db upgrade ← Flask-Migrate 注册的
flask create-admin ← 你自己注册的
│
▼
app.cli(Click 命令组)
│
▼
你的 Python 函数
3. 最小示例:hello 命令
app/__init__.py 末尾(或单独 app/cli.py):
import click
from app import app
@app.cli.command("hello")
def hello_command():
"""打个招呼(docstring 会出现在 flask --help 里)。"""
click.echo("Hello from Flask CLI!")
运行:
export FLASK_APP=app
flask hello
Hello from Flask CLI!
查看帮助:
flask --help
flask hello --help
习惯:开发、服务器上都设 FLASK_APP=app(或 manage.py 里 from app import app 时用 flask --app app)。
4. 实用命令一:init-db
有时新机器第一次部署,想 确保表存在(Migrate 仍推荐做主流程,这里演示 CLI 写法):
app/cli_commands.py:
import click
from app import app, db
@app.cli.command("init-db")
def init_db_command():
"""创建所有表(开发/救急用;生产优先 flask db upgrade)。"""
click.echo("正在 create_all ...")
db.create_all()
click.echo("完成。")
app/__init__.py 里导入(触发注册):
import app.cli_commands # noqa: F401
flask init-db
生产环境 仍应以 flask db upgrade 为主;create_all() 不会帮你做列变更,和第七篇讲的一致。
5. 实用命令二:create-admin
第十一篇有 admin 后台,首次部署要有一个管理员:
import click
from werkzeug.security import generate_password_hash
from app import app, db
from app.models import User # 假设 User 表:name, pwd, is_admin
@app.cli.command("create-admin")
@click.argument("username")
@click.option("--password", default=None, help="不传则交互式输入")
def create_admin_command(username, password):
"""创建或重置管理员账号。"""
from app.models import User
username = (username or "").strip()
if not username:
raise click.ClickException("用户名不能为空")
if not password:
password = click.prompt("密码", hide_input=True, confirmation_prompt=True)
row = User.query.filter_by(name=username).first()
if row is None:
row = User(name=username, is_admin=True)
click.echo("将新建管理员:%s" % username)
else:
click.echo("账号已存在,将重置密码:%s" % username)
row.pwd = generate_password_hash(password)
row.is_admin = True
db.session.add(row)
db.session.commit()
click.echo("完成。")
用法:
flask create-admin liousa
提示输入密码
flask create-admin liousa --password 'YourStrongPass123'
出错时用 raise click.ClickException("原因"),终端会红色提示并以非 0 退出码结束,适合脚本判断。
6. 参数与选项(Click 速查)
| 装饰器 | 含义 | 示例 |
|---|---|---|
@click.argument("name") |
positional positional 必填位置参数 | flask cmd alice |
@click.option("--dry-run", is_flag=True) |
开关,出现即为 True | flask sync --dry-run |
@click.option("--count", default=10, type=int) |
可选参数 | flask seed --count 5 |
@click.prompt("密码", hide_input=True) |
交互输入 | 创建用户 |
带 --dry-run 的同步示例:
@app.cli.command("sync-permissions")
@click.option("--dry-run", is_flag=True, help="只打印,不写库")
def sync_permissions_command(dry_run):
"""把路由表里的后台 URL 同步进权限表。"""
would_add = "admin.note_list", "admin.note_export" # 实际应扫描 views
for name in would_add:
click.echo("dry-run 将新增 %s" % name if dry_run else "新增 %s" % name)
if dry_run:
click.echo("dry-run 结束,未写库。")
return
db.session.commit() ...
click.echo("已写入。")
7. 命令里访问数据库:app_context
Flask 请求外(终端、定时任务、CLI)没有自动 application context。
若直接 User.query... 可能报错。
两种写法:
写法 A:命令函数里包一层(简单项目够用)
@app.cli.command("count-notes")
def count_notes_command():
with app.app_context():
from app.models import Note
n = Note.query.count()
click.echo("共 %s 条备忘录。" % n)
写法 B:把逻辑抽到 services/,CLI 只负责调(推荐,和第十篇一致)
app/note_service.py
def count_all_notes():
from app.models import Note
return Note.query.count()
app/cli_commands.py
@app.cli.command("count-notes")
def count_notes_command():
with app.app_context():
from app.note_service import count_all_notes
click.echo("共 %s 条。" % count_all_notes())
8. CLI 拆文件 + register_cli
项目变大后,别把几十条命令全堆在 __init__.py。可学 Migrate / 真实项目 的注册方式:
app/cli_commands.py:
import click
def register_cli(app):
@app.cli.command("hello")
def hello_command():
click.echo("Hello!")
@app.cli.command("create-admin")
@click.argument("username")
def create_admin_command(username):
...
app/__init__.py:
from app.cli_commands import register_cli
register_cli(app)
一个文件 register_cli(app),里面注册多条命令,边界清晰。
9. 和部署流程怎么接(接第十四篇)
新服务器 最小闭环 可以写成:
cd /var/www/myapp
source .venv/bin/activate
set -a && source /etc/myapp.env && set +a
export FLASK_APP=app
pip install -r requirements.txt
flask db upgrade # 表结构
flask create-admin admin # 首个管理员(仅首次)
sudo systemctl restart myapp
写进 部署文档 或 CI 脚本,比口头记命令可靠。
10. flask run vs manage.py vs CLI
| 入口 | 干什么 |
|---|---|
python manage.py |
开发服务器 app.run() |
flask run |
同上(需 FLASK_APP) |
flask db upgrade |
Migrate |
flask create-admin |
你写的运维命令 |
gunicorn app:app |
生产 HTTP |
CLI 命令和 Gunicorn 互不替代:Gunicorn 跑 Web;CLI 跑 一次性/运维任务。
11. 流程示意
运维 / 开发者
│
▼
flask create-admin bob
│
▼
Click 解析参数
│
▼
app.app_context()
│
▼
create_admin 逻辑(写 User 表)
│
▼
click.echo("完成")
app/init.py
│
├── register_cli(app) → hello, create-admin, ...
├── Migrate(app, db) → flask db *
└── 其它 register_cli → 各业务模块自带命令
12. 新手常踩的 6 个坑
- 没设
FLASK_APP--- 报找不到应用;设export FLASK_APP=app或flask --app app。 - CLI 里直接 query 不报 context --- 包
with app.app_context():。 - 循环 import --- CLI 文件里
from app import app时,把注册放在register_cli(app)里,由__init__.py最后调用。 - 密码写进命令行历史 --- 用
click.prompt(hide_input=True),别flask xxx --password 123上生产。 - 生产滥用
init-db/create_all--- 表结构变更走 Migrate。 - 命令无 docstring --- 补一行说明,三个月后的你会感谢现在。
13. 小结
记住五件事:
@app.cli.command("名字")注册命令- Click 管参数:
argument、option、prompt - 数据库操作包
app.app_context() - 逻辑放 service,CLI 只当入口(和第十篇一致)
- 部署清单里固定
flask db upgrade+ 你的运维命令
十五篇下来,开发、API、上线、运维命令这条线就闭合了。
