插件
插件是一个通过单一构造函数参数扩展 starlette-admin 的 Python 包。插件可以将字段、模板、静态资源、模型转换器、过滤器、导入/导出格式、存储后端、事件订阅者、视图、路由、中间件、主题资源和翻译目录按任意组合打包在一起。
使用插件
构造 Admin 实例时,通过 plugins 参数传入插件:
from starlette_admin_geospatial import GeospatialPlugin
from starlette_admin.contrib.sqla import Admin
admin = Admin(engine, plugins=[GeospatialPlugin(default_zoom=13)])
选项由插件的构造函数接收,列表则直接传给 Admin,无需任何额外的设置或注册。选项会从构造函数一路传递到 Python 后端、Jinja 模板和前端 JavaScript。
构建插件
编写插件时,请从官方 Cookiecutter 模板开始。它会生成一个目录结构和配置正确、可直接发布的包。
前置要求
使用你的包管理器安装 cookiecutter。详情参见官方安装指南:
脚手架
在任意位置运行该 Cookiecutter 模板:
模板会提示你输入插件名称、包名(slug)、版本以及其他几个变量。完成后,你将得到一个自包含的包,其中包含:
- 一个存放插件类和字段的
src/目录。 - 命名空间正确的
templates/、static/和translations/文件夹。 - 一套完整的测试套件。
- 一个可运行的示例应用。
插件 API
每个插件的核心都是一个 BasePlugin(starlette_admin.plugins.BasePlugin)的子类,它提供的钩子让你能够在 Admin 初始化期间注册自己的功能。
name 属性是一个唯一的 kebab-case 标识符,同时充当模板和静态资源的命名空间。插件附带的每一个模板和静态文件都必须位于 plugins/<name>/ 之下。
资源文件夹
插件在其包的根目录下只允许携带三个文件夹。无需任何注册,因为 admin 会按约定找到它们:
templates/:Jinja 模板,必须位于templates/plugins/<name>/之下。static/:CSS 和 JS 文件等静态资源,必须位于static/plugins/<name>/之下。translations/:Babel 翻译目录。
将资源保持在 plugins/<name>/ 命名空间内,既能避免它们与核心文件或其他插件冲突,又能让用户可以通过自己的 templates_dir 或 static_dir 进行覆盖。
声明式钩子
重写声明式钩子即可注入资源、注册视图或挂载路由。
css_links(self, request: Request) -> Sequence[str]:向每个 admin 页面布局添加样式表。js_links(self, request: Request) -> Sequence[str]:向每个 admin 页面布局添加脚本。views(self) -> Sequence[BaseView]:返回要在 admin 侧边栏中注册的视图。返回一个DropDown即可将它们分组。routes(self) -> Sequence[Route | Mount]:返回挂载在/plugins/<name>/下的无界面端点,非常适合 Webhook 和代理端点。middlewares(self) -> Sequence[Middleware]:添加 Starlette 中间件。template_globals(self) -> dict[str, Any]:暴露 Jinja 全局变量,带有<name>_前缀以避免冲突。template_filters(self) -> dict[str, Callable]:以同样的方式暴露带有<name>_前缀的 Jinja 过滤器。
setup 钩子
setup(self, admin: BaseAdmin) -> None 将插件接入核心注册表。用它来注册模型转换器、过滤器、导入和导出格式、存储后端以及事件订阅者。它在声明式钩子应用之后运行。
生命周期钩子
on_mount(self, admin: BaseAdmin) -> None 只运行一次,时机是 Starlette 子应用构建并挂载之后。构建好的应用可通过 admin.app 访问。
模板与覆盖
插件模板会自动加入加载链。用户只需在自己的 templates_dir 中按匹配路径放置一个文件即可覆盖模板,用户文件始终优先。例如,要覆盖 plugins/geospatial/fields/form/point.html,用户需创建 templates_dir/plugins/geospatial/fields/form/point.html。
为了让用户的覆盖模板能够安全地扩展原始模板,每个插件都会获得一个 @<name> 前缀映射,其工作方式与 @core 前缀相同。覆盖模板以 {% extends "@geospatial/fields/form/point.html" %} 开头,从而扩展插件的基础模板而不会递归包含自身。
前端 JavaScript 集成
附带自定义字段的插件应按照字段初始化器契约打包其前端脚本。这能确保脚本在整页加载和动态插入的片段中都能正常工作。
- 局部定位: 只在传入的
container元素内进行查询,绝不查询全局document。 - 保持幂等: 核心代码会在 DOM 就绪时运行初始化器,并在每次插入行内行或片段时再次运行。
- 使用 data 属性: 从渲染在字段元素上的
data-*属性读取配置。
(function () {
function initSlider(container) {
var input = container.querySelector('input[type="range"]');
var output = container.querySelector(".sa-slider-output");
var suffix = container.dataset.suffix || "";
input.addEventListener("input", function () {
output.textContent = input.value + suffix;
});
}
// Register the initializer so core runs it on the right lifecycle events
window.StarletteAdmin.registerFieldInitializer(function (element) {
element.querySelectorAll("[data-sa-slider]").forEach(initSlider);
});
})();
通过 setup 钩子进行扩展
插件使用现有的公共注册表,而没有自己单独的扩展途径。
- 转换器:从目标 contrib 后端调用
register_converter,将 ORM 列类型映射到你的字段类。将字段本身定义为普通的StringField子类,以 WKT 文本的形式存储和显示几何数据:
from dataclasses import dataclass
from typing import Any
from starlette_admin.contrib.sqla.converters import register_converter
from starlette_admin.fields import StringField
@dataclass
class MyGeoField(StringField):
...
@register_converter("Geometry")
def convert_geometry(*args: Any, **kwargs: Any) -> MyGeoField:
return MyGeoField(*args, **kwargs)
- 过滤器:调用
register_filters将过滤器类附加到字段类型上。
from starlette_admin.contrib.sqla.filters import register_filters
register_filters(MyGeoField, WithinBoundingBoxFilter)
- 存储:调用
register_storage以提供新的后端,例如 Azure 或 GCS。
- 导入器和导出器:使用
register_import_format和register_export_format。
from starlette_admin.export import register_export_format
register_export_format("pdf", PDFExporter())
一个插件可以支持多个 ORM 后端,因此应在 setup() 内部有条件地导入它们。这样,即使用户只安装了其中一个,插件也能正常加载:
def setup(self, admin: "BaseAdmin") -> None:
try:
from starlette_admin_geospatial.contrib.sqla import register_sqla_converters
register_sqla_converters()
except ImportError:
pass # geoalchemy2 or sqlalchemy not installed