Flask 笔记十五:自定义 Flask CLI 命令

上一篇我们用 Gunicorn 把应用跑上了服务器。部署时还会反复做几件事:

  • 第一次:flask db upgrade 建表
  • 创建管理员账号
  • 导入测试数据、同步权限......

每次都要记一长串 shell,容易漏步骤。Flask 自带 CLI 扩展,可以把这些操作收成 flask xxx 一条命令。

这一篇做一件事:学会写自己的 Flask CLI 命令,并理解 flask db 是怎么挂上去的。

例子仍是通用的 Note 备忘录项目,不涉及任何真实业务。


1. 学完后你能做什么

  • @app.cli.command() 注册命令
  • flask init-dbflask 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.pyfrom 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 个坑

  1. 没设 FLASK_APP --- 报找不到应用;设 export FLASK_APP=appflask --app app
  2. CLI 里直接 query 不报 context --- 包 with app.app_context():
  3. 循环 import --- CLI 文件里 from app import app 时,把注册放在 register_cli(app) 里,由 __init__.py 最后调用。
  4. 密码写进命令行历史 --- 用 click.prompt(hide_input=True),别 flask xxx --password 123 上生产。
  5. 生产滥用 init-db / create_all --- 表结构变更走 Migrate。
  6. 命令无 docstring --- 补一行说明,三个月后的你会感谢现在。

13. 小结

记住五件事:

  1. @app.cli.command("名字") 注册命令
  2. Click 管参数:argumentoptionprompt
  3. 数据库操作包 app.app_context()
  4. 逻辑放 service,CLI 只当入口(和第十篇一致)
  5. 部署清单里固定 flask db upgrade + 你的运维命令

十五篇下来,开发、API、上线、运维命令这条线就闭合了。