Hexo 主题开发系列教程(二):扩展系统详解(上篇)
前言
在上一篇教程中,我们学习了 Hexo 的七大核心概念:事件、本地变量、路由、Box、渲染、文章和模板。这些是 Hexo 的”骨架”,定义了系统如何运作。
本章我们将深入学习 Hexo 扩展系统,它是 Hexo 的”肌肉”,让我们能够在核心系统之上构建自定义功能。扩展系统包括十个重要组件,本篇(上篇)将介绍前五个:
- Console(控制台) - 自定义命令行工具
- Deployer(部署器) - 自动化部署方案
- Filter(过滤器) - 数据处理管道
- Generator(生成器) - 页面生成逻辑
- Helper(辅助函数) - 模板工具函数
下篇将介绍后五个扩展组件。
核心概念回顾与扩展的关系
在开始之前,让我们理解核心概念与扩展系统的关系:
1 2 3 4 5 6 7 8 9
| 核心概念(第一章) → 扩展系统(第二章) ───────────────────────────────────────────────── 事件系统 → 所有扩展都可以监听事件 本地变量 → Helper/Filter 扩展变量 路由系统 → Generator 创建路由 Box/文件处理 → Processor 处理文件 渲染引擎 → Renderer 注册渲染器 文章数据 → Filter 修改文章数据 模板脚手架 → Tag 创建模板标签
|
核心区别:
- 第一章:理解 Hexo 如何工作(机制)
- 第二章:学习 Hexo 如何扩展(实践)
1. Console(控制台)
概念介绍
Console 扩展允许你创建自定义的 Hexo 命令行指令。就像 hexo server、hexo generate 这样的内置命令,你可以创建专属的命令来自动化工作流程。
- Console 命令执行时会触发事件系统
- Console 可以访问本地变量和操作Box
基础语法
1 2 3 4
| hexo.extend.console.register(name, desc, options, function(args) { });
|
参数详解
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| hexo.extend.console.register( 'mytheme', '我的主题自定义命令', { usage: '[layout] <title>', arguments: [ {name: 'layout', desc: '文章布局'}, {name: 'title', desc: '文章标题'} ], options: [ {name: '-d, --draft', desc: '创建草稿'}, {name: '-s, --slug <slug>', desc: '文件名'} ] }, function(args) { } );
|
实战案例一:主题初始化命令
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56
|
hexo.extend.console.register('theme:init', '初始化主题配置', { usage: '[options]', options: [ {name: '--dark-mode', desc: '启用暗色模式'}, {name: '--comments <provider>', desc: '评论系统 (disqus/gitalk/valine)'}, {name: '--analytics', desc: '启用统计分析'} ] }, function(args) { const fs = require('hexo-fs'); const path = require('path'); const yaml = require('js-yaml'); const configPath = path.join(hexo.theme_dir, '_config.yml'); let config = {}; if (fs.existsSync(configPath)) { config = yaml.load(fs.readFileSync(configPath)); } if (args['dark-mode']) { config.appearance = config.appearance || {}; config.appearance.dark_mode = true; hexo.log.info('✓ 暗色模式已启用'); } if (args.comments) { config.comments = { enable: true, provider: args.comments }; hexo.log.info(`✓ 评论系统: ${args.comments}`); } if (args.analytics) { config.analytics = { enable: true, google_analytics: '', baidu_analytics: '' }; hexo.log.info('✓ 统计分析已启用(请配置 tracking ID)'); } fs.writeFileSync(configPath, yaml.dump(config)); hexo.log.success('主题配置初始化完成!'); hexo.log.info(''); hexo.log.info('后续步骤:'); hexo.log.info('1. 编辑 themes/your-theme/_config.yml 完善配置'); hexo.log.info('2. 运行 hexo server 预览效果'); });
|
使用方法:
1
| hexo theme:init --dark-mode --comments gitalk --analytics
|
实战案例二:内容统计命令
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84
|
hexo.extend.console.register('stats', '显示博客统计信息', { options: [ {name: '--detail', desc: '显示详细信息'}, {name: '--export <file>', desc: '导出为 JSON 文件'} ] }, function(args) { const posts = hexo.locals.get('posts'); const pages = hexo.locals.get('pages'); const tags = hexo.locals.get('tags'); const categories = hexo.locals.get('categories'); const stats = { posts: { total: posts.length, published: posts.filter(p => p.published).length, draft: posts.filter(p => !p.published).length }, pages: { total: pages.length }, tags: { total: tags.length, top5: tags.sort('length', -1).limit(5).map(t => ({ name: t.name, count: t.length })) }, categories: { total: categories.length, top5: categories.sort('length', -1).limit(5).map(c => ({ name: c.name, count: c.length })) }, words: { total: 0, average: 0 } }; posts.forEach(post => { const words = post.content.replace(/<[^>]+>/g, '').split(/\s+/).length; stats.words.total += words; }); stats.words.average = Math.round(stats.words.total / posts.length); hexo.log.info(''); hexo.log.info('📊 博客统计信息'); hexo.log.info('═══════════════════════════════'); hexo.log.info(`📝 文章: ${stats.posts.total} 篇 (已发布: ${stats.posts.published}, 草稿: ${stats.posts.draft})`); hexo.log.info(`📄 页面: ${stats.pages.total} 个`); hexo.log.info(`🏷️ 标签: ${stats.tags.total} 个`); hexo.log.info(`📁 分类: ${stats.categories.total} 个`); hexo.log.info(`📖 总字数: ${stats.words.total.toLocaleString()} 字`); hexo.log.info(`📊 平均字数: ${stats.words.average.toLocaleString()} 字/篇`); if (args.detail) { hexo.log.info(''); hexo.log.info('🔥 热门标签 TOP 5:'); stats.tags.top5.forEach((tag, i) => { hexo.log.info(` ${i + 1}. ${tag.name} (${tag.count} 篇)`); }); hexo.log.info(''); hexo.log.info('📚 热门分类 TOP 5:'); stats.categories.top5.forEach((cat, i) => { hexo.log.info(` ${i + 1}. ${cat.name} (${cat.count} 篇)`); }); } if (args.export) { const fs = require('hexo-fs'); fs.writeFileSync(args.export, JSON.stringify(stats, null, 2)); hexo.log.success(`统计数据已导出到: ${args.export}`); } hexo.log.info(''); });
|
使用方法:
1 2 3
| hexo stats hexo stats --detail hexo stats --export stats.json
|
实战案例三:文章质量检查命令
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90
|
hexo.extend.console.register('check', '检查文章质量', { options: [ {name: '--fix', desc: '自动修复可修复的问题'} ] }, function(args) { const posts = hexo.locals.get('posts'); const issues = []; posts.forEach(post => { const postIssues = []; if (post.title.length < 5) { postIssues.push('标题过短(少于5个字符)'); } if (post.title.length > 60) { postIssues.push('标题过长(超过60个字符)'); } if (!post.excerpt) { postIssues.push('缺少摘要'); } if (!post.tags || post.tags.length === 0) { postIssues.push('没有标签'); } if (!post.categories || post.categories.length === 0) { postIssues.push('没有分类'); } const words = post.content.replace(/<[^>]+>/g, '').split(/\s+/).length; if (words < 100) { postIssues.push(`内容过短(仅 ${words} 字)`); } const hasImages = /<img/.test(post.content); if (!hasImages && words > 500) { postIssues.push('长文章建议添加配图'); } const brokenLinks = []; const linkRegex = /href="([^"]+)"/g; let match; while ((match = linkRegex.exec(post.content)) !== null) { const url = match[1]; if (url.startsWith('http') && url.includes('localhost')) { brokenLinks.push(url); } } if (brokenLinks.length > 0) { postIssues.push(`发现本地链接: ${brokenLinks.join(', ')}`); } if (postIssues.length > 0) { issues.push({ title: post.title, path: post.source, issues: postIssues }); } }); if (issues.length === 0) { hexo.log.success('✓ 所有文章检查通过!'); } else { hexo.log.warn(`发现 ${issues.length} 篇文章存在问题:`); hexo.log.info(''); issues.forEach(item => { hexo.log.warn(`📄 ${item.title}`); hexo.log.info(` 文件: ${item.path}`); item.issues.forEach(issue => { hexo.log.info(` ⚠️ ${issue}`); }); hexo.log.info(''); }); if (args.fix) { hexo.log.info('自动修复功能开发中...'); } } });
|
Console 最佳实践
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| hexo.extend.console.register('async-command', 'description', async function(args) { await someAsyncOperation(); return 'completed'; });
hexo.extend.console.register('safe-command', 'description', function(args) { try { } catch (err) { hexo.log.error('命令执行失败:', err.message); throw err; } });
hexo.extend.console.register('progress-command', 'description', function(args) { const total = 100; for (let i = 0; i < total; i++) { hexo.log.info(`进度: ${i + 1}/${total}`); } });
hexo.extend.console.register('interactive', 'description', async function(args) { const inquirer = require('inquirer'); const answers = await inquirer.prompt([ { type: 'confirm', name: 'continue', message: '是否继续?', default: true } ]); if (answers.continue) { } });
|
2. Deployer(部署器)
概念介绍
Deployer 扩展让你能够自定义 hexo deploy 命令的行为,实现自动化部署到各种平台。
- Deployer 在事件系统的
deployBefore 和 deployAfter 之间执行
- Deployer 访问生成的静态文件(在 public Box 中)
基础语法
1 2 3
| hexo.extend.deployer.register(name, function(args) { });
|
实战案例一:自定义 FTP 部署器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83
|
hexo.extend.deployer.register('myftp', function(args) { const fs = require('hexo-fs'); const FtpClient = require('ftp'); const path = require('path'); const config = this.config.deploy; return new Promise((resolve, reject) => { const client = new FtpClient(); client.on('ready', async function() { hexo.log.info('已连接到 FTP 服务器'); try { const publicDir = hexo.public_dir; const files = fs.listDirSync(publicDir); hexo.log.info(`准备上传 ${files.length} 个文件...`); for (let i = 0; i < files.length; i++) { const file = files[i]; const localPath = path.join(publicDir, file); const remotePath = path.join(config.root || '/', file); const remoteDir = path.dirname(remotePath); await mkdirp(client, remoteDir); await uploadFile(client, localPath, remotePath); hexo.log.info(`[${i + 1}/${files.length}] ${file}`); } hexo.log.success('部署完成!'); client.end(); resolve(); } catch (err) { client.end(); reject(err); } }); client.on('error', reject); client.connect({ host: config.host, port: config.port || 21, user: config.user, password: config.password }); }); function mkdirp(client, dir) { return new Promise((resolve, reject) => { client.mkdir(dir, true, (err) => { if (err && err.code !== 550) { reject(err); } else { resolve(); } }); }); } function uploadFile(client, local, remote) { return new Promise((resolve, reject) => { client.put(local, remote, (err) => { if (err) reject(err); else resolve(); }); }); } });
|
配置文件:
1 2 3 4 5 6 7 8
| deploy: type: myftp host: ftp.example.com port: 21 user: username password: password root: /public_html
|
实战案例二:腾讯云 COS 部署器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118
|
hexo.extend.deployer.register('cos', function(args) { const COS = require('cos-nodejs-sdk-v5'); const fs = require('hexo-fs'); const path = require('path'); const crypto = require('crypto'); const config = this.config.deploy; const cos = new COS({ SecretId: config.secretId, SecretKey: config.secretKey }); const bucket = config.bucket; const region = config.region; const publicDir = this.public_dir; return new Promise(async (resolve, reject) => { try { hexo.log.info('开始上传到腾讯云 COS...'); const files = fs.listDirSync(publicDir); const remoteFiles = await getRemoteFiles(cos, bucket, region); const remoteFileMap = new Map( remoteFiles.map(f => [f.Key, f.ETag]) ); let uploadCount = 0; let skipCount = 0; for (const file of files) { const localPath = path.join(publicDir, file); const localETag = await getFileETag(localPath); const remoteETag = remoteFileMap.get(file); if (localETag === remoteETag) { skipCount++; continue; } await uploadToCOS(cos, bucket, region, localPath, file); uploadCount++; hexo.log.info(`[${uploadCount + skipCount}/${files.length}] ${file}`); } hexo.log.success(`部署完成!上传: ${uploadCount}, 跳过: ${skipCount}`); if (config.cdn) { await refreshCDN(config.cdn); } resolve(); } catch (err) { hexo.log.error('部署失败:', err.message); reject(err); } }); function getRemoteFiles(cos, bucket, region) { return new Promise((resolve, reject) => { cos.getBucket({ Bucket: bucket, Region: region }, (err, data) => { if (err) reject(err); else resolve(data.Contents || []); }); }); } function getFileETag(filePath) { return new Promise((resolve, reject) => { const hash = crypto.createHash('md5'); const stream = fs.createReadStream(filePath); stream.on('data', data => hash.update(data)); stream.on('end', () => resolve(hash.digest('hex'))); stream.on('error', reject); }); } function uploadToCOS(cos, bucket, region, localPath, key) { return new Promise((resolve, reject) => { cos.putObject({ Bucket: bucket, Region: region, Key: key, Body: fs.createReadStream(localPath) }, (err, data) => { if (err) reject(err); else resolve(data); }); }); } async function refreshCDN(cdnConfig) { if (!cdnConfig.enable) return; hexo.log.info('正在刷新 CDN...'); hexo.log.success('CDN 刷新完成'); } });
|
配置文件:
1 2 3 4 5 6 7 8 9 10 11
| deploy: type: cos secretId: your-secret-id secretKey: your-secret-key bucket: your-bucket-name region: ap-guangzhou cdn: enable: true urls: - https://example.com
|
实战案例三:多目标部署器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62
|
hexo.extend.deployer.register('multi', async function(args) { const config = this.config.deploy; if (!Array.isArray(config.targets)) { throw new Error('multi deployer 需要配置 targets 数组'); } hexo.log.info(`准备部署到 ${config.targets.length} 个目标...`); const results = []; for (let i = 0; i < config.targets.length; i++) { const target = config.targets[i]; hexo.log.info(''); hexo.log.info(`[${i + 1}/${config.targets.length}] 部署到: ${target.type}`); try { const originalDeploy = this.config.deploy; this.config.deploy = target; const deployer = hexo.extend.deployer.get(target.type); if (!deployer) { throw new Error(`未找到部署器: ${target.type}`); } await deployer.call(this, args); this.config.deploy = originalDeploy; results.push({ target: target.type, success: true }); hexo.log.success(`✓ ${target.type} 部署成功`); } catch (err) { results.push({ target: target.type, success: false, error: err.message }); hexo.log.error(`✗ ${target.type} 部署失败: ${err.message}`); if (config.stopOnError) { throw err; } } } hexo.log.info(''); hexo.log.info('部署总结:'); const successCount = results.filter(r => r.success).length; hexo.log.info(`成功: ${successCount}/${results.length}`); if (successCount < results.length) { hexo.log.warn('部分目标部署失败'); } });
|
配置文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| deploy: type: multi stopOnError: false targets: - type: git repo: git@github.com:user/repo.git branch: gh-pages - type: cos secretId: xxx secretKey: xxx bucket: my-bucket region: ap-guangzhou - type: myftp host: ftp.example.com user: username password: password
|
Deployer 最佳实践
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70
| hexo.extend.deployer.register('safe-deployer', function(args) { const config = this.config.deploy; const required = ['host', 'user', 'password']; for (const key of required) { if (!config[key]) { throw new Error(`缺少配置项: ${key}`); } } });
hexo.extend.deployer.register('incremental', async function(args) { });
hexo.extend.deployer.register('confirm-deploy', async function(args) { const config = this.config.deploy; hexo.log.warn('即将部署到生产环境!'); hexo.log.info(`目标: ${config.host}`); if (!args.yes) { const inquirer = require('inquirer'); const answer = await inquirer.prompt([{ type: 'confirm', name: 'continue', message: '确认继续?', default: false }]); if (!answer.continue) { hexo.log.info('已取消部署'); return; } } });
hexo.extend.deployer.register('notify-deploy', async function(args) { try { await actualDeploy(); await sendNotification({ type: 'success', message: '部署成功', time: new Date() }); } catch (err) { await sendNotification({ type: 'error', message: `部署失败: ${err.message}`, time: new Date() }); throw err; } });
|
3. Filter(过滤器)
概念介绍
Filter 是 Hexo 数据处理的核心机制,它在数据流的特定节点介入,允许你修改、增强或验证数据。这是一个强大的钩子系统。
- Filter 在事件系统的不同阶段被触发
- Filter 修改本地变量和文章数据
- Filter 影响渲染引擎的输出
Filter 的执行时机
1 2 3 4 5 6 7 8 9
| 数据流向 可用的 Filter ──────────────────────────────────────────── 1. 读取配置文件 after_init 2. 处理源文件 before_post_render 3. 渲染 Markdown after_post_render 4. 生成页面 before_generate 5. 模板渲染 template_locals 6. 输出 HTML after_render 7. 生成完成 after_generate
|
基础语法
1 2 3 4
| hexo.extend.filter.register(type, function(data, ...args) { return data; }, priority);
|
Filter 类型详解
1. before_post_render - 文章渲染前
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71
|
hexo.extend.filter.register('before_post_render', function(data) { if (data.layout === 'post') { const copyright = `
---
**版权声明:** 本文为原创文章,版权归 ${this.config.author} 所有。 转载请注明出处:${this.config.url}${data.path}
`; data.content += copyright; } return data; });
hexo.extend.filter.register('before_post_render', function(data) { const text = data.content.replace(/<[^>]+>/g, ''); const wordCount = text.split(/\s+/).length; const readingTime = Math.ceil(wordCount / 200); data.word_count = wordCount; data.reading_time = readingTime; data.reading_minutes = `${readingTime} 分钟`; return data; });
hexo.extend.filter.register('before_post_render', function(data) { if (!this.theme.lazyload || !this.theme.lazyload.enable) { return data; } data.content = data.content.replace( /<img([^>]*?)src="([^"]*?)"([^>]*?)>/g, '<img$1src="$2" loading="lazy"$3>' ); return data; });
hexo.extend.filter.register('before_post_render', function(data) { const siteUrl = this.config.url; data.content = data.content.replace( /<a\s+href="(https?:\/\/[^"]+)"([^>]*)>/g, (match, url, attrs) => { if (!url.startsWith(siteUrl)) { if (!attrs.includes('target=')) { attrs += ' target="_blank"'; } if (!attrs.includes('rel=')) { attrs += ' rel="noopener noreferrer"'; } return `<a href="${url}"${attrs}>`; } return match; } ); return data; });
|
2. after_post_render - 文章渲染后
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85
|
hexo.extend.filter.register('after_post_render', function(data) { if (data.toc === false) return data; const cheerio = require('cheerio'); const $ = cheerio.load(data.content); const headings = []; $('h1, h2, h3, h4, h5, h6').each(function() { const level = parseInt(this.name.substring(1)); const text = $(this).text(); const id = text.toLowerCase().replace(/\s+/g, '-').replace(/[^\w-]/g, ''); $(this).attr('id', id); headings.push({ level: level, text: text, id: id }); }); data.content = $.html(); data.toc_headings = headings; return data; });
hexo.extend.filter.register('after_post_render', function(data) { const $ = require('cheerio').load(data.content); $('pre code').each(function() { const $code = $(this); const lang = $code.attr('class')?.replace('language-', '') || 'text'; const $wrapper = $('<div class="code-wrapper"></div>'); const $header = $(` <div class="code-header"> <span class="lang">${lang}</span> <button class="copy-btn" data-clipboard-target="#code-${Date.now()}"> 复制 </button> </div> `); $code.attr('id', `code-${Date.now()}`); $wrapper.append($header); $wrapper.append($code.parent()); $code.parent().parent().replaceWith($wrapper); }); data.content = $.html(); return data; });
hexo.extend.filter.register('after_post_render', function(data) { const $ = require('cheerio').load(data.content); $('img').each(function() { const $img = $(this); const src = $img.attr('src'); const $figure = $('<figure class="image-container"></figure>'); $figure.append($img); const alt = $img.attr('alt'); if (alt) { $figure.append(`<figcaption>${alt}</figcaption>`); } $img.replaceWith($figure); }); data.content = $.html(); return data; });
|
3. template_locals - 模板变量注入
这部分在第一章已经详细介绍过,这里补充一些高级用法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
|
hexo.extend.filter.register('template_locals', function(locals) { const config = this.config; const theme = this.theme; locals.nav_menu = (theme.menu || []).map(item => { const isActive = locals.path.startsWith(item.path); return { ...item, active: isActive, url: this.url_for(item.path) }; }); return locals; });
hexo.extend.filter.register('template_locals', function(locals) { locals.performance = { build_time: Date.now(), posts_count: locals.site.posts.length, pages_count: locals.site.pages.length }; return locals; });
hexo.extend.filter.register('template_locals', function(locals) { if (locals.path === '' || locals.path === 'index.html') { locals.popular_posts = locals.site.posts .sort((a, b) => (b.views || 0) - (a.views || 0)) .limit(10) .toArray(); } return locals; });
|
4. after_render - HTML 输出处理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61
|
hexo.extend.filter.register('after_render:html', function(str, data) { if (!this.config.minify_html) return str; const htmlMinifier = require('html-minifier').minify; return htmlMinifier(str, { removeComments: true, collapseWhitespace: true, minifyJS: true, minifyCSS: true }); });
hexo.extend.filter.register('after_render:css', function(str, data) { const autoprefixer = require('autoprefixer'); const postcss = require('postcss'); return postcss([autoprefixer]) .process(str, { from: undefined }) .then(result => result.css); });
hexo.extend.filter.register('after_render:js', function(str, data) { if (!this.config.minify_js) return str; const UglifyJS = require('uglify-js'); const result = UglifyJS.minify(str); if (result.error) { hexo.log.error('JS 压缩失败:', result.error); return str; } return result.code; });
hexo.extend.filter.register('after_render:html', function(str, data) { if (!this.theme.analytics || !this.theme.analytics.enable) { return str; } const analyticsCode = ` <script async src="https://www.googletagmanager.com/gtag/js?id=${this.theme.analytics.google_id}"></script> <script> window.dataLayer = window.dataLayer || []; function gtag(){dataLayer.push(arguments);} gtag('js', new Date()); gtag('config', '${this.theme.analytics.google_id}'); </script> `; return str.replace('</head>', analyticsCode + '</head>'); });
|
Filter 优先级
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
|
hexo.extend.filter.register('before_post_render', function(data) { return data; }, 1);
hexo.extend.filter.register('before_post_render', function(data) { return data; });
hexo.extend.filter.register('before_post_render', function(data) { return data; }, 100);
|
Filter 最佳实践
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
| hexo.extend.filter.register('before_post_render', function(data) { if (data.layout !== 'post') return data; if (!this.theme.feature_enabled) return data; return data; });
hexo.extend.filter.register('after_post_render', function(data) { try { data.content = processContent(data.content); } catch (err) { hexo.log.warn(`处理文章失败: ${data.title}`, err.message); } return data; });
const cache = new Map();
hexo.extend.filter.register('template_locals', function(locals) { const cacheKey = `stats-${locals.site.posts.length}`; if (!cache.has(cacheKey)) { const stats = calculateStats(locals.site.posts); cache.set(cacheKey, stats); } locals.site_stats = cache.get(cacheKey); return locals; });
hexo.extend.filter.register('before_post_render', function(data) { data = addReadingTime(data); data = addTableOfContents(data); data = processImages(data); return data; });
|
由于篇幅限制,我将在这里停止上篇,将 Generator 和 Helper 放到下篇。
小结
本篇(上篇)介绍了 Hexo 扩展系统的前三个核心组件:
- Console(控制台) - 创建自定义命令,实现工作流自动化
- Deployer(部署器) - 自定义部署流程,支持多平台部署
- Filter(过滤器) - 在数据流的各个节点介入,修改和增强数据
这三个扩展让我们能够:
- 通过 Console 扩展 Hexo 的命令行能力
- 通过 Deployer 实现灵活的部署方案
- 通过 Filter 深度定制数据处理流程
在下一篇教程中,我们将继续学习 Generator、Helper、Injector、Migrator、Processor、Renderer 和 Tag,完成扩展系统的全面学习。
参考资源