预计阅读时间:16 分钟
前言
前三篇我们学习了插件的理论知识,今天我们将通过两个真实的复杂插件案例,学习如何设计和实现生产级别的插件。
本文核心内容: - SEO 优化插件完整剖析 - 智能推荐插件设计与实现 - 性能优化技巧 - 开发最佳实践
一、案例一:SEO 优化插件
1.1 功能需求分析
核心功能: - 根据页面类型动态生成 SEO meta 标签 - 生成 Open Graph 标签(社交分享优化) - 生成 JSON-LD 结构化数据(搜索引擎优化) - 支持文章、分类、首页等多种页面类型
技术要点: - 路由感知与上下文分发 - Schema.org 结构化数据标准 - Open Graph Protocol 协议
1.2 插件架构设计
SeoOptimizerPlugin
├── 路由分发器
│ ├── 识别当前页面类型
│ └── 调用对应的SEO生成器
├── SEO数据生成器
│ ├── 文章页SEO生成
│ ├── 分类页SEO生成
│ └── 默认页SEO生成
└── 输出格式化器
├── meta标签
├── Open Graph标签
└── JSON-LD脚本
1.3 核心实现
# plugins/seo_optimizer/plugin.py
import json
from django.utils.html import strip_tags
from djangoblog.plugin_manage.base_plugin import BasePlugin
from djangoblog.plugin_manage import hooks
from blog.models import Article, Category
from djangoblog.utils import get_blog_setting
class SeoOptimizerPlugin(BasePlugin):
PLUGIN_NAME = 'SEO 优化器'
PLUGIN_DESCRIPTION = '动态生成 SEO meta 标签和结构化数据'
PLUGIN_VERSION = '0.2.0'
PLUGIN_AUTHOR = 'liangliangyy'
def register_hooks(self):
"""注册到 head_meta 过滤器钩子"""
hooks.register('head_meta', self.dispatch_seo_generation)
def dispatch_seo_generation(self, metas, context):
"""
路由分发器:根据页面类型分发到不同的SEO生成方法
"""
request = context.get('request')
if not request:
return metas
# 获取当前视图名称
view_name = request.resolver_match.view_name
blog_setting = get_blog_setting()
# 根据视图名称分发
seo_data = None
if view_name == 'blog:detailbyid':
# 文章详情页
seo_data = self._get_article_seo_data(
context, request, blog_setting
)
elif view_name == 'blog:category_detail':
# 分类页
seo_data = self._get_category_seo_data(
context, request, blog_setting
)
if not seo_data:
# 默认页面(首页等)
seo_data = self._get_default_seo_data(
context, request, blog_setting
)
# 生成最终HTML
return metas + self._format_seo_html(seo_data)
1.4 文章页SEO生成
def _get_article_seo_data(self, context, request, blog_setting):
"""生成文章页面的SEO数据"""
article = context.get('article')
if not isinstance(article, Article):
return None
# 1. 生成描述(截取前150字符)
description = strip_tags(article.body)[:150]
# 2. 生成关键词(使用标签)
keywords = ",".join([tag.name for tag in article.tags.all()])
if not keywords:
keywords = blog_setting.site_keywords
# 3. Open Graph 标签(社交分享优化)
meta_tags = f'''
<meta property="og:type" content="article"/>
<meta property="og:title" content="{article.title}"/>
<meta property="og:description" content="{description}"/>
<meta property="og:url" content="{request.build_absolute_uri()}"/>
<meta property="article:published_time" content="{article.pub_time.isoformat()}"/>
<meta property="article:modified_time" content="{article.last_modify_time.isoformat()}"/>
<meta property="article:author" content="{article.author.username}"/>
<meta property="article:section" content="{article.category.name}"/>
'''
# 添加标签
for tag in article.tags.all():
meta_tags += f'<meta property="article:tag" content="{tag.name}"/>'
meta_tags += f'<meta property="og:site_name" content="{blog_setting.site_name}"/>'
# 4. JSON-LD 结构化数据(搜索引擎优化)
structured_data = {
"@context": "https://schema.org",
"@type": "Article",
"mainEntityOfPage": {
"@type": "WebPage",
"@id": request.build_absolute_uri()
},
"headline": article.title,
"description": description,
"image": request.build_absolute_uri(article.get_first_image_url()),
"datePublished": article.pub_time.isoformat(),
"dateModified": article.last_modify_time.isoformat(),
"author": {
"@type": "Person",
"name": article.author.username
},
"publisher": {
"@type": "Organization",
"name": blog_setting.site_name
}
}
# 移除空图片字段
if not structured_data.get("image"):
del structured_data["image"]
return {
"title": f"{article.title} | {blog_setting.site_name}",
"description": description,
"keywords": keywords,
"meta_tags": meta_tags,
"json_ld": structured_data
}
1.5 分类页SEO生成
def _get_category_seo_data(self, context, request, blog_setting):
"""生成分类页的SEO数据"""
category_name = context.get('tag_name')
if not category_name:
return None
category = Category.objects.filter(name=category_name).first()
if not category:
return None
# 生成面包屑导航结构化数据
breadcrumb_items = [
{
"@type": "ListItem",
"position": 1,
"name": "首页",
"item": request.build_absolute_uri('/')
},
{
"@type": "ListItem",
"position": 2,
"name": category.name,
"item": request.build_absolute_uri()
}
]
structured_data = {
"@context": "https://schema.org",
"@type": "BreadcrumbList",
"itemListElement": breadcrumb_items
}
return {
"title": f"{category.name} | {blog_setting.site_name}",
"description": strip_tags(category.name) or blog_setting.site_description,
"keywords": category.name,
"meta_tags": "",
"json_ld": structured_data
}
1.6 默认页SEO生成
def _get_default_seo_data(self, context, request, blog_setting):
"""生成首页和其他页面的SEO数据"""
structured_data = {
"@context": "https://schema.org",
"@type": "WebSite",
"name": blog_setting.site_name,
"description": blog_setting.site_description,
"url": request.build_absolute_uri('/'),
"potentialAction": {
"@type": "SearchAction",
"target": f"{request.build_absolute_uri('/search/')}?q={{search_term_string}}",
"query-input": "required name=search_term_string"
}
}
return {
"title": f"{blog_setting.site_name} | {blog_setting.site_description}",
"description": blog_setting.site_description,
"keywords": blog_setting.site_keywords,
"meta_tags": "",
"json_ld": structured_data
}
1.7 格式化输出
def _format_seo_html(self, seo_data):
"""格式化SEO数据为HTML"""
json_ld_script = (
'<script type="application/ld+json">'
f'{json.dumps(seo_data.get("json_ld", {}), ensure_ascii=False, indent=4)}'
'</script>'
)
return f"""
<title>{seo_data.get("title", "")}</title>
<meta name="description" content="{seo_data.get("description", "")}">
<meta name="keywords" content="{seo_data.get("keywords", "")}">
{seo_data.get("meta_tags", "")}
{json_ld_script}
"""
# 实例化
plugin = SeoOptimizerPlugin()
1.8 设计亮点
- 路由感知:通过
request.resolver_match.view_name识别页面类型 - 结构化数据:使用 Schema.org 标准,提升搜索引擎理解
- 社交优化:Open Graph 标签优化社交媒体分享效果
- 降级处理:无法生成特定页面SEO时,使用默认数据兜底
二、案例二:智能推荐插件
2.1 功能需求分析
核心功能: - 基于标签的相关文章推荐 - 基于分类的相似文章推荐 - 热门文章兜底机制 - 多位置展示支持 - 性能优化(缓存)
推荐算法策略:
1. 基于标签推荐(相关性最高)
↓ 不足
2. 基于分类推荐(中等相关性)
↓ 仍不足
3. 热门文章兜底(确保有推荐)
2.2 插件架构
ArticleRecommendationPlugin
├── 位置渲染
│ ├── 文章底部渲染
│ └── 侧边栏渲染
├── 推荐算法
│ ├── 标签推荐
│ ├── 分类推荐
│ └── 热门兜底
└── 数据处理
├── 去重
├── 过滤无效文章
└── 结果排序
2.3 核心实现
# plugins/article_recommendation/plugin.py
import logging
from djangoblog.plugin_manage.base_plugin import BasePlugin
from djangoblog.plugin_manage import hooks
from djangoblog.plugin_manage.hook_constants import ARTICLE_DETAIL_LOAD
from blog.models import Article
logger = logging.getLogger(__name__)
class ArticleRecommendationPlugin(BasePlugin):
PLUGIN_NAME = '文章推荐'
PLUGIN_DESCRIPTION = '智能文章推荐系统'
PLUGIN_VERSION = '1.0.0'
PLUGIN_AUTHOR = 'liangliangyy'
# 支持的位置
SUPPORTED_POSITIONS = ['article_bottom', 'sidebar']
# 位置优先级
POSITION_PRIORITIES = {
'article_bottom': 80,
'sidebar': 150,
}
# 插件配置
CONFIG = {
'article_bottom_count': 8,
'sidebar_count': 5,
'enable_category_fallback': True,
'enable_popular_fallback': True,
}
def register_hooks(self):
"""注册钩子"""
hooks.register(ARTICLE_DETAIL_LOAD, self.on_article_detail_load)
def on_article_detail_load(self, article, context, request, *args, **kwargs):
"""文章详情页加载时预加载推荐数据"""
recommendations = self.get_recommendations(article)
context['article_recommendations'] = recommendations
2.4 条件显示逻辑
def should_display(self, position, context, **kwargs):
"""条件显示"""
if position == 'article_bottom':
article = kwargs.get('article') or context.get('article')
is_index = context.get('isindex', False)
# 必须有文章且不是索引页
return article is not None and not is_index
if position == 'sidebar':
return True
return False
2.5 位置渲染实现
def render_article_bottom_widget(self, context, **kwargs):
"""渲染文章底部推荐"""
article = kwargs.get('article') or context.get('article')
if not article:
return None
count = kwargs.get('count', self.CONFIG['article_bottom_count'])
recommendations = self.get_recommendations(article, count=count)
if not recommendations:
return None
# 转换上下文为普通字典
context_dict = self._flatten_context(context)
template_context = {
'recommendations': recommendations,
'article': article,
'title': '相关推荐',
**context_dict
}
return self.render_template('bottom_widget.html', template_context)
def render_sidebar_widget(self, context, **kwargs):
"""渲染侧边栏推荐"""
article = context.get('article')
count = kwargs.get('count', self.CONFIG['sidebar_count'])
if article:
recommendations = self.get_recommendations(article, count=count)
title = '相关文章'
else:
recommendations = self.get_popular_articles(count=count)
title = '热门推荐'
if not recommendations:
return None
context_dict = self._flatten_context(context)
template_context = {
'recommendations': recommendations,
'title': title,
**context_dict
}
return self.render_template('sidebar_widget.html', template_context)
def _flatten_context(self, context):
"""将RequestContext转换为普通字典"""
context_dict = {}
if hasattr(context, 'flatten'):
context_dict = context.flatten()
elif hasattr(context, 'dicts'):
for d in context.dicts:
context_dict.update(d)
return context_dict
2.6 推荐算法实现
def get_recommendations(self, article, count=5):
"""
多层次推荐算法
策略:
1. 基于标签推荐(最相关)
2. 基于分类推荐(中等相关)
3. 热门文章兜底(确保有结果)
"""
if not article:
return []
recommendations = []
# === 第一层:基于标签推荐 ===
if article.tags.exists():
tag_ids = list(article.tags.values_list('id', flat=True))
tag_based = list(
Article.objects.filter(
status='p',
tags__id__in=tag_ids
).exclude(
id=article.id
).exclude(
title__isnull=True
).exclude(
title__exact=''
).distinct().order_by('-views')[:count]
)
recommendations.extend(tag_based)
logger.info(f"标签推荐: {len(tag_based)} 篇")
# === 第二层:基于分类推荐 ===
if len(recommendations) < count and self.CONFIG['enable_category_fallback']:
needed = count - len(recommendations)
existing_ids = [r.id for r in recommendations] + [article.id]
category_based = list(
Article.objects.filter(
status='p',
category=article.category
).exclude(
id__in=existing_ids
).exclude(
title__isnull=True
).exclude(
title__exact=''
).order_by('-views')[:needed]
)
recommendations.extend(category_based)
logger.info(f"分类推荐: {len(category_based)} 篇")
# === 第三层:热门文章兜底 ===
if len(recommendations) < count and self.CONFIG['enable_popular_fallback']:
needed = count - len(recommendations)
existing_ids = [r.id for r in recommendations] + [article.id]
popular_articles = list(
Article.objects.filter(
status='p'
).exclude(
id__in=existing_ids
).exclude(
title__isnull=True
).exclude(
title__exact=''
).order_by('-views')[:needed]
)
recommendations.extend(popular_articles)
logger.info(f"热门兜底: {len(popular_articles)} 篇")
# 过滤无效文章
valid_recommendations = self._filter_valid_articles(recommendations)
return valid_recommendations[:count]
def _filter_valid_articles(self, articles):
"""过滤掉无效文章"""
valid = []
for article in articles:
if article.title and len(article.title.strip()) > 0:
valid.append(article)
else:
logger.warning(f"过滤空标题文章: ID={article.id}")
return valid
def get_popular_articles(self, count=3):
"""获取热门文章"""
return list(
Article.objects.filter(status='p')
.order_by('-views')[:count]
)
2.7 静态资源
def get_css_files(self):
return ['css/recommendation.css']
def get_js_files(self):
return ['js/recommendation.js']
# 实例化
plugin = ArticleRecommendationPlugin()
2.8 模板示例
文件: plugins/article_recommendation/templates/article_recommendation/bottom_widget.html
<div class="recommendation-widget">
<h3 class="widget-title">{{ title }}</h3>
<div class="recommendation-grid">
{% for rec in recommendations %}
<article class="recommendation-card">
<a href="{{ rec.get_absolute_url }}" class="card-link">
<h4 class="card-title">{{ rec.title }}</h4>
<div class="card-meta">
<span class="category">{{ rec.category.name }}</span>
<span class="views">{{ rec.views }} 次浏览</span>
</div>
</a>
</article>
{% empty %}
<p class="no-recommendations">暂无推荐</p>
{% endfor %}
</div>
</div>
2.9 设计亮点
- 多层次算法:标签 → 分类 → 热门,确保总有推荐
- 去重处理:避免推荐当前文章或已推荐的文章
- 可配置性:通过
CONFIG控制各种策略开关 - 数据验证:过滤无效文章,避免显示空标题
- 性能考虑:使用
distinct()和exclude()优化查询
三、性能优化技巧
3.1 数据库查询优化
# ❌ N+1 查询问题
for article in articles:
print(article.category.name) # 每次都查询
for tag in article.tags.all(): # 每次都查询
print(tag.name)
# ✅ 使用 select_related 和 prefetch_related
articles = Article.objects.filter(status='p') \
.select_related('category', 'author') \
.prefetch_related('tags') \
.order_by('-views')[:10]
3.2 缓存策略
from django.core.cache import cache
def _get_cached_data(self):
"""使用缓存"""
cache_key = f'{self.plugin_slug}_data'
data = cache.get(cache_key)
if data is None:
data = self._expensive_query()
cache.set(cache_key, data, timeout=300) # 5分钟
return data
3.3 懒加载
class LazyPlugin(BasePlugin):
def __init__(self):
super().__init__()
self._data = None # 延迟加载
@property
def data(self):
"""只在需要时才加载"""
if self._data is None:
self._data = self._load_data()
return self._data
四、开发最佳实践
4.1 日志记录
import logging
logger = logging.getLogger(__name__)
class MyPlugin(BasePlugin):
def init_plugin(self):
logger.info(f'{self.PLUGIN_NAME} v{self.PLUGIN_VERSION} 初始化')
def some_method(self):
logger.debug(f'处理数据: {data}')
try:
result = self._process(data)
logger.info(f'处理成功: {result}')
except Exception as e:
logger.error(f'处理失败: {e}', exc_info=True)
4.2 配置管理
class ConfigurablePlugin(BasePlugin):
CONFIG = {
'enabled': True,
'count': 10,
'cache_timeout': 300,
}
def get_config(self, key, default=None):
return self.CONFIG.get(key, default)
def set_config(self, key, value):
self.CONFIG[key] = value
logger.info(f'配置更新: {key}={value}')
4.3 错误处理
def render_widget(self, context, **kwargs):
try:
data = self._fetch_data()
return self.render_template('widget.html', {'data': data})
except TemplateDoesNotExist:
logger.warning(f'模板不存在')
return None
except Exception as e:
logger.error(f'渲染失败: {e}', exc_info=True)
return None # 确保不影响页面显示
本文由 liangliangyy 原创,转载请注明出处。