plugins/navManager:导航 CRUD + 导航分类 CRUD(独立 blueprint) plugins/friendManager:友情链接 CRUD plugins/aboutManager:关于本站 CRUD plugins/siteStats:访问统计主页 + JSON 接口 + /site/* GET 埋点 + nav 卡片点击埋点 applications/view/public/nav.py:导航聚合页 / 分类详情 / JSON 接口,支持 visit_count 注入 applications/view/public/about.py:关于本站公开页 applications/view/public/friend.py:友情链接公开页 templates/public/base.html:响应式首页(搜索框 / 分类抽屉 / 5 主题切换 / footer 隐私小字) templates/public/index.html / category.html:卡片右下角访问次数 templates/public/about.html / friend.html:关于 + 友链前台页 docs/plugins-development.md:插件开发完整指南(生命周期 + 4 个示例 + FAQ + framework 迁移) scripts/:探针与端到端验证脚本(probe_*/verify_*/test_*)
19 KiB
Pear Admin Flask 插件开发操作手册
适用版本:Pear Admin Flask master 分支(fork 自 lab.lovepikachu.top 文档站点的最新文档)
文档维护:根据 plugins// 下的实际插件实现同步更新
Pear Admin Flask 的插件机制是其核心竞争力——不修改框架代码即可扩展新页面、新菜单、新数据埋点。
本文以仓库中已落地的 navManager / friendManager / aboutManager / siteStats 四个插件为依据,整理出从设计到上线的完整操作步骤。
1. 插件是什么
| 概念 | framework(框架) | plugin(插件) |
|---|---|---|
| 代码位置 | applications/、templates/system/ 之外的共用代码 |
plugins/<pluginName>/ 下,与 framework 解耦 |
| 启用方式 | 自动加载 | 在 applications/config.py 的 PLUGIN_ENABLE_FOLDERS 列表中显式声明 |
| 修改影响 | 改 framework 会影响所有升级 | 改 plugin 不影响 framework 与其他 plugin |
| 典型用途 | 用户/角色/权限/部门等所有项目共用的基础模块 | 站点特有业务(导航、友链、关于、统计等) |
2. 目录结构最小模型
一个最简插件的目录是这样:
plugins/
└── helloworld/ # 插件文件夹名(与 PLUGIN_ENABLE_FOLDERS 中写的保持一致)
├── __init__.json # 【可选】插件元信息(名称/版本/描述),删掉也能加载
├── __init__.py # 必须。定义 event_init / event_finish 等事件函数
└── view/ # 建议。存放 view.py(Blueprint 定义和路由)
└── main.py # 自定义视图
# 复杂插件(仿 navManager/friendManager/aboutManager/siteStats):
plugins/
└── navManager/
├── __init__.json
├── __init__.py # 入口
├── view/
│ └── nav.py # Blueprint 'nav',url_prefix='/system/nav'
└── templates/
└── system/
└── nav/
├── main.html
├── add.html
└── edit.html
3. 启用插件
打开 applications/config.py,找到 BaseConfig 类尾部的配置项:
# ---- 插件 ----
# 站点业务模块(导航 / 友链 / 关于 / 访问统计)均通过 plugins 方式开发,启用即可挂载路由
PLUGIN_ENABLE_FOLDERS = ["navManager", "friendManager", "aboutManager", "siteStats"]
顺序就是加载顺序,越靠前越早被 import;按需新增或删除。
修改完成后,重启 Flask 即可生效,启动日志会列出:
* Plugin: Loaded plugin: 导航管理 .
* Plugin: Loaded plugin: 友情链接 .
* Plugin: Loaded plugin: 关于本站 .
* Plugin: Loaded plugin: 访问统计 .
* navManager: registered /system/nav/* blueprint
* friendManager: registered /system/friend/* blueprint
* aboutManager: registered /system/about/* blueprint
* siteStats: registered /system/stats/* blueprint
* siteStats: registered /site/* before_request hook
4. 插件生命周期(4 个事件)
插件入口 __init__.py 中可以暴露 4 个事件函数(任意函数缺省将自动跳过):
| 函数 | 时机 | 典型用途 |
|---|---|---|
event_begin(app) |
框架功能注册之前(数据库连上之前) | 极少数兼容场景;多数插件不需要 |
event_init(app) |
所有 framework 蓝图注册之后 | app.register_blueprint(bp) |
event_finish(app) |
数据库/CLI/脚本初始化完毕之后 | 注册 @app.before_request 钩子、@app.cli.command |
event_context(app) |
第一个请求到来之前(自动 app.app_context()) |
预热缓存、检查/修正数据库初始数据 |
调用顺序:event_begin → ... framework 注册 ... → event_init → ... 启动余下逻辑 ... → event_finish → (自动)event_context。
注意:如果同时定义 event_init 和 event_finish,前者注册蓝图、后者挂 before_request / CLI,不能用一次事件全做——否则会与 framework 自身的广播节奏冲突。
5. 最小可运行示例:Hello World(来自仓库 plugins/helloworld/)
5.1 plugins/helloworld/__init__.py
import os
from flask import Flask
from .main import helloworld_blueprint
from applications.models import Dept
from applications.common import curd
from applications.schemas import DeptSchema
dir_path = os.path.dirname(__file__).replace("\\", "/")
def event_begin(app: Flask):
print("所有功能初始化之前加载")
def event_init(app: Flask):
"""初始化完成时会调用这里"""
print("初始插件初始化视图")
app.register_blueprint(helloworld_blueprint)
def event_finish(app: Flask):
print("所有初始化完毕")
def event_context(app: Flask):
"""Flask 初始化完成,等待第一个请求之前,等同于 with app.app_context():"""
dept = Dept.query.order_by(Dept.sort).all()
print(curd.model_to_dicts(schema=DeptSchema, data=dept))
5.2 plugins/helloworld/main.py
from flask import render_template, Blueprint
helloworld_blueprint = Blueprint('hello_world', __name__,
template_folder='templates',
static_folder='static',
url_prefix='/hello_world')
@helloworld_blueprint.route('/')
def index():
return render_template('helloworld_index.html')
5.3 启用
PLUGIN_ENABLE_FOLDERS = ["helloworld"]
启动后访问 http://127.0.0.1:5000/hello_world/。
6. 含数据库的完整插件(实操示例:navManager)
这一节以仓库实际存在的 navManager 为模板,演示创建"导航表 + 后台 CRUD + 模板 + 权限码绑定 + 菜单种子"完整业务插件的全流程。
6.1 设计
- 数据模型:单表
site_nav(继承自applications.models.admin_nav.Nav) - 路由:
/system/nav/、/system/nav/data、/system/nav/add、/system/nav/save、/system/nav/edit、/system/nav/update、/system/nav/remove/<int:nav_id>、/system/nav/enable、/system/nav/disable - 权限码:
system:nav:main / system:nav:add / system:nav:edit / system:nav:remove
6.2 创建插件骨架
mkdir -p plugins/navManager/{view,templates/system/nav}
6.3 写 plugins/navManager/__init__.json
{
"plugin_name": "导航管理",
"plugin_version": "1.0.0",
"plugin_description": "后台导航管理模块。对 nav 表的增删改查、分页查询、关键字 + 分类过滤、启用/禁用切换。"
}
6.4 写 plugins/navManager/__init__.py
from flask import Flask
from .view.nav import bp
def event_init(app: Flask):
"""初始化完成时注册蓝图到 app。"""
app.register_blueprint(bp)
print(" * navManager: registered /system/nav/* blueprint")
6.5 写 plugins/navManager/view/nav.py
最关键的一步——要把 Blueprint 的 url_prefix 拼成 /system/nav 并指定独立 template_folder:
import os
from flask import Blueprint, render_template, request
from flask_login import current_user
from applications.common import curd
from applications.common.utils.http import table_api, fail_api, success_api
from applications.common.utils.rights import authorize
from applications.common.utils.validate import str_escape
from applications.extensions import db
from applications.extensions.init_limit import limiter
from applications.models import Nav
from applications.schemas import NavManageSchema
dir_path = os.path.dirname(os.path.abspath(__file__))
bp = Blueprint(
'nav', __name__,
url_prefix='/system/nav',
template_folder=os.path.join(dir_path, '..', 'templates'),
)
@bp.get('/')
@authorize("system:nav:main")
def main():
return render_template('system/nav/main.html')
@bp.get('/data')
@limiter.limit("60 per minute")
@authorize("system:nav:main")
def data():
keyword = str_escape(request.args.get('keyword', type=str))
category = str_escape(request.args.get('category', type=str))
query = Nav.query.filter()
if keyword:
query = query.filter(
db.or_(
Nav.title.contains(keyword),
Nav.url.contains(keyword),
Nav.description.contains(keyword),
)
)
if category:
query = query.filter(Nav.category == category)
items = query.order_by(Nav.category.asc(), Nav.sort.asc(), Nav.id.asc()).layui_paginate()
return table_api(
msg='请求成功',
data=curd.model_to_dicts(schema=NavManageSchema, data=items.items),
count=items.total,
)
# ... 其余 CRUD 路由基本按 user.py 风格展开
⚠️ 与 framework 视图的关键区别:
Blueprint的url_prefix直接是/system/nav,不再注册到 framework 的system_bp,避免对 framework 文件的依赖template_folder显式指向 plugin 自带的templates/,与根templates/解耦
6.6 数据库 Schema
模型仍然需要放在 framework 的 applications/models/admin_nav.py,因为 Flask 在 init_databases(app) 时只 import 这一目录,而且 Alembic 也只看这里做迁移:
# applications/models/admin_nav.py
class Nav(db.Model):
__tablename__ = 'site_nav'
id = db.Column(db.Integer, primary_key=True)
...
这点是 Pear Admin Flask 的设计局限:模型层不能放到 plugin。Alembic migration 也需要落到
migrations/versions/下。
6.7 模板文件
把模板放到 plugins/navManager/templates/system/nav/main.html:
<!DOCTYPE html>
<html>
<head>
<title>导航管理</title>
{% include 'system/common/header.html' %}
</head>
<body class="pear-container">
<!-- 查询表单 + 表格 -->
</body>
</body>
{% include 'system/common/footer.html' %}
{% raw %}
<script type="text/html" id="col-status">...</script>
{% endraw %}
<script>
layui.use(['table', 'form', 'layer'], function () {
var table = layui.table;
table.render({
elem: '#nav-table',
url: '/system/nav/data',
cols: [[...]],
page: true, limit: 20,
skin: 'line',
text: {none: '暂无导航数据'},
});
});
</script>
注意 footer include 必须放在所有业务 <script> 之前(这是 Pear Admin 2.x 同步加载的特性,错误顺序会 layui is not defined)。
6.8 菜单种子(Power 表)
site_nav 表的菜单项需要 seed 到 RBAC 的 Power 表:
Power(
id=60, name='导航管理', type='1',
code='system:nav:main',
url='/system/nav/', open_type='_iframe',
parent_id=64, icon='layui-icon layui-icon-link',
enable=1, sort=1,
),
Power(
name='导航新增', type='2', code='system:nav:add', ...
),
# 编辑 / 删除 同理
子权限的 parent_id 必须指向主菜单的 id;Python seed 时可先 commit 主菜单拿到 id。
6.9 启用
PLUGIN_ENABLE_FOLDERS = ["navManager"]
启动 Flask 后访问 http://127.0.0.1:5000/system/nav/(仍需要登录),可见后台菜单「站点管理 / 导航管理」。
7. 含 before_request 钩子的插件(实操示例:siteStats)
类似访问统计这种需要监听全局请求的插件,用 event_finish 钩子:
# plugins/siteStats/__init__.py
from flask import Flask
from .view.stat import bp, record_visit
def event_init(app: Flask):
app.register_blueprint(bp)
def event_finish(app: Flask):
@app.before_request
def _stat_record():
record_visit()
# plugins/siteStats/view/stat.py
def record_visit():
"""埋点 /site/* GET 请求。"""
if not request.path.startswith('/site'):
return
if request.method != 'GET':
return
# ... 累计 PV/UV 到 PageStat
优势:未来增加新的"统计规则"或"埋点路径"只需要改 plugin 文件,不动 framework 的 app.py。
8. 含自定义 CLI 子命令的插件(实操示例:giftManager)
若插件需要自定义 flask <your-cmd> 命令:
# plugins/giftManager/cli/__init__.py
from flask.cli import AppGroup
gift_cli = AppGroup('gift')
@gift_cli.command('init')
def init_db():
"""初始化兑换码管理插件的菜单和种子数据"""
# seed Power / seed Gift data
pass
# plugins/giftManager/__init__.py
from flask import Flask
from .cli import gift_cli
from .view.gift import bp
def event_init(app: Flask):
app.register_blueprint(bp)
def event_finish(app: Flask):
app.cli.add_command(gift_cli)
执行 flask gift init 即可。
9. 插件开发检查清单
下面是一份自检表,开发完成后逐项确认:
plugins/<name>/__init__.json写好plugins/<name>/__init__.py定义event_init(注册蓝图)或/和event_finish(before_request / CLI)- 视图文件
view/<name>.py中 Blueprint 的name全项目唯一;url_prefix在/system/<xxx>或自定其它前缀 - 蓝图
template_folder显式指向 plugin 内 templates,避免与 framework 模板同名冲突 - 模型文件保留在
applications/models/admin_<name>.py(Pear Admin 限制) - Schema 保留在
applications/schemas/admin_<name>.py - 权限码 seed 已写入 Power 表(type=1 主菜单 + type=2 子权限)
- 管理员 role 的 power 列表中已挂载本插件相关权限码
applications/config.py中PLUGIN_ENABLE_FOLDERS加上插件文件夹名- framework 中没有与该插件同名的 Blueprint 注册(避免 endpoint 冲突)
- 模板放在
{% include 'system/common/footer.html' %}之前 +<script>layui.use(...)之后 仍然运行正常 - 用 Playwright/Selenium 验证一次页面渲染(控制台无
layui is not defined) - alembic migration 已生成(如果模型新增列):
flask db migrate -m "..."+flask db upgrade
10. 常见问题(FAQ)
Q1:出现 ValueError: The name 'xxx' is already registered for this blueprint
答:framework 里有个同名 Blueprint 已注册 + 当前 plugin 又注册了同名 Blueprint。检查:
applications/view/system/__init__.py的register_system_bps是否仍包含迁移前的 bp 注册extensions/__init__.py:33-34早期版本同时调用了event_init/event_finish,后又由init_bps/init_script重复触发——必须删除其中一处(建议只在view/__init__.py触发event_init,只在script/__init__.py触发event_finish)。
Q2:event_finish 调用了两次,before_request 钩子被注册两次
答:同 Q1,问题来源 + 修复同。
Q3:菜单在前台后台不显示 / 点菜单 403
答:检查 applications/models/admin_power.py 的 Power 表里菜单的 code、parent_id 是否正确;管理员 role 的 power 关联表里是否绑定了相关 code。
Q4:模板渲染找不到文件 TemplateNotFound
答:检查 Blueprint 实例化时是否显式设置 template_folder=os.path.join(dir_path, '..', 'templates'),不要省略。
Q5:插件导入顺序错误(a 插件 import 时触发 b 插件未加载)
答:把"a"放 PLUGIN_ENABLE_FOLDERS 更前面。Pear Admin 的 loader 通过 importlib.import_module 顺序装载。
Q6:插件里如何获取当前请求
答:直接 request 对象或 flask.session;访问数据库用 with app.app_context(): 或在 event_context 函数体内。
Q7:插件改动后必须重启 Flask 吗?
答:是的。所有 __init__.py、event_* 函数、Blueprint 注册只发生在进程启动时。模板可用 TEMPLATES_AUTO_RELOAD=True 动态生效。
11. 插件模式 vs framework 内置
| 维度 | plugin 内置 | framework 内置 |
|---|---|---|
| 升级 pear-admin-framework 后是否受影响 | 无 | 有 |
| 删除即下线的便利性 | 注释 PLUGIN_ENABLE_FOLDERS 即可 | 改 framework 才能下 |
| 团队协作时移植到新项目 | 直接复制 plugins/ 整目录 | diff 框架代码 |
| 适合场景 | 大多数业务模块 | 不变的用户/角色/权限/部门等基础模块 |
结论:除非改动涉及 framework 自身的核心能力(如重写 @authorize 实现),否则一律走 plugin。这正是 Pear Admin Flask 推荐的最佳实践。
12. 当前仓库已实现的插件
| 插件 | 文件夹 | 用途 | 状态 |
|---|---|---|---|
| 导航管理 | plugins/navManager/ |
后台 CRUD nav 表;前台 /site/ 入口 + 类目聚合走 framework 的 public_nav |
✅ |
| 友情链接 | plugins/friendManager/ |
后台 CRUD friend 表;前台 /site/friend |
✅ |
| 关于本站 | plugins/aboutManager/ |
后台编辑表单 + 保存;前台 /site/about 渲染 |
✅ |
| 访问统计 | plugins/siteStats/ |
后台 PV/UV 主页 + 3 个 JSON 接口 + 全站 /site/* 埋点 |
✅ |
| 兑换码示例 | plugins/giftManager/ |
官方示例,需要 flask gift init 初始化 |
✅(未启用) |
| Hello World | plugins/helloworld/ |
官方示例 | ✅(未启用) |
| Replace Page | plugins/replacePage/ |
framework 页面替换示例 | ✅(未启用) |
| Real IP | plugins/realip/ |
Flask 上下文修改示例 | ✅(未启用) |
启用列表见 applications/config.py 的 PLUGIN_ENABLE_FOLDERS。
13. 首次启用 nav / friend / about / stat 时的手动操作
这些插件依赖数据库模型和权限码已经在 framework 中存在。首次部署到新环境:
flask db migrate -m "add admin_nav / admin_friend / admin_about / admin_stat tables"(如已存在表跳过)flask db upgrade- 为 site_nav / site_about / site_friend 注册菜单权限(Power)记录:
# 用 Flask shell 或在 admin.py 中 seed Power(name='导航管理', type='1', code='system:nav:main', url='/system/nav/', open_type='_iframe', parent_id=1, ...) # ... 类似地注册 site:friend:main / site:about:main / site:stats:main 与子权限 - 把上述
code加到 admin role 的power列表 - 在
PLUGIN_ENABLE_FOLDERS写好四项 - 重启 Flask,用 admin / 123456 登录后台,可见新增菜单
14. 进阶:把现有 framework 业务迁出到 plugin 的步骤
如果有一天希望把用户管理(applications/view/system/user.py)也从 framework 迁移到 plugin,按以下顺序:
- 复制
applications/view/system/user.py内容到plugins/userManager/view/user.py - 修改 Blueprint:
url_prefix='/system/user'、加template_folder=... - 复制
templates/system/user/到plugins/userManager/templates/system/user/ - 从
applications/view/system/__init__.py删除user_bp的 import 和system_bp.register_blueprint(user_bp)一行 - 删除
applications/view/system/user.py与applications/templates/system/user/(如果都已迁出) PLUGIN_ENABLE_FOLDERS = [..., "userManager"]- 重启验证
⚠️ 注意保留模型和 Schema 不动。
文档结束。