# 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//` 下,与 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` 类尾部的配置项: ```python # ---- 插件 ---- # 站点业务模块(导航 / 友链 / 关于 / 访问统计)均通过 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` ```python 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` ```python 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 启用 ```python 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/`、`/system/nav/enable`、`/system/nav/disable` - 权限码:`system:nav:main / system:nav:add / system:nav:edit / system:nav:remove` ### 6.2 创建插件骨架 ```bash mkdir -p plugins/navManager/{view,templates/system/nav} ``` ### 6.3 写 `plugins/navManager/__init__.json` ```json { "plugin_name": "导航管理", "plugin_version": "1.0.0", "plugin_description": "后台导航管理模块。对 nav 表的增删改查、分页查询、关键字 + 分类过滤、启用/禁用切换。" } ``` ### 6.4 写 `plugins/navManager/__init__.py` ```python 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`: ```python 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 也只看这里做迁移: ```python # 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`: ```html 导航管理 {% include 'system/common/header.html' %} {% include 'system/common/footer.html' %} {% raw %} {% endraw %} ``` **注意 footer include 必须放在所有业务 `