前言
Hexo 是一个快速、简洁且高效的博客框架,它使用 Node.js 驱动,支持 Markdown 解析。本系列教程将带你深入了解 Hexo 主题开发的方方面面,从基础概念到实战技巧。
在开始制作主题之前,理解 Hexo 的核心概念至关重要。本文将详细介绍主题开发中最重要的七个核心概念,为后续的实战开发打下坚实基础。
1. 事件系统 (Events)
概念介绍
Hexo 的事件系统基于 Node.js 的 EventEmitter,允许开发者在 Hexo 执行的不同阶段插入自定义逻辑。这是一个发布-订阅模式的实现,使得插件和主题能够对 Hexo 的生命周期做出响应。
常用事件类型
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| hexo.on('ready', function() { console.log('Hexo 已准备就绪'); });
hexo.on('generateBefore', function() { console.log('开始生成静态文件'); });
hexo.on('generateAfter', function() { console.log('静态文件生成完成'); });
hexo.on('exit', function() { console.log('Hexo 正在退出'); });
|
实际应用场景
场景1:在生成前自动处理图片
1 2 3 4 5 6 7 8
| hexo.on('generateBefore', function() { const images = hexo.locals.get('images'); images.forEach(img => { }); });
|
场景2:生成自定义数据文件
1 2 3 4 5 6 7 8 9 10
| hexo.on('generateAfter', function() { const posts = hexo.locals.get('posts'); const searchData = posts.map(post => ({ title: post.title, url: post.path, content: post.content })); });
|
2. 本地变量 (Local Variables)
概念介绍
本地变量是 Hexo 在模板渲染时可用的数据对象。这些变量包含了网站的所有内容和配置信息,是连接数据和视图的桥梁。
全局变量
1 2 3 4 5 6 7 8
| site page config theme path url env
|
核心变量详解
site 变量
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| <!-- 访问所有文章 --> <% site.posts.each(function(post) { %> <h2><%= post.title %></h2> <p><%= post.excerpt %></p> <% }) %>
<!-- 访问所有分类 --> <% site.categories.each(function(category) { %> <li><%= category.name %> (<%= category.length %>)</li> <% }) %>
<!-- 访问所有标签 --> <% site.tags.each(function(tag) { %> <span><%= tag.name %></span> <% }) %>
|
page 变量
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| <!-- 文章页面 --> <% if (page.layout === 'post') { %> <article> <h1><%= page.title %></h1> <time><%= date(page.date, 'YYYY-MM-DD') %></time> <%- page.content %> <!-- 标签 --> <% if (page.tags && page.tags.length) { %> <% page.tags.each(function(tag) { %> <a href="<%- url_for(tag.path) %>"><%= tag.name %></a> <% }) %> <% } %> </article> <% } %>
|
config 和 theme 变量
1 2 3 4 5 6 7 8 9 10 11 12 13
| <!-- 使用网站配置 --> <title><%= config.title %></title> <meta name="author" content="<%= config.author %>">
<!-- 使用主题配置 --> <% if (theme.sidebar) { %> <aside><%- partial('_partial/sidebar') %></aside> <% } %>
<!-- 主题自定义选项 --> <% if (theme.custom_logo) { %> <img src="<%= theme.custom_logo %>" alt="Logo"> <% } %>
|
变量作用域
作用域层级
在 Hexo 模板系统中,变量存在明确的作用域层级,理解这些层级对于正确访问和修改数据至关重要。
1 2 3 4 5 6
| 1. 页面级变量 (page) 2. 主题配置变量 (theme) 3. 全局配置变量 (config) 4. 站点变量 (site) 5. Helper 函数
|
在模板中扩展变量
方法一:使用 Helper 注册全局函数
Helper 是最常用的扩展变量的方式,可以在任何模板中调用。
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.helper.register('site_info', function() { return { author: this.config.author, posts_count: this.site.posts.length, pages_count: this.site.pages.length, tags_count: this.site.tags.length, categories_count: this.site.categories.length }; });
hexo.extend.helper.register('get_posts_by_year', function(year) { return this.site.posts.filter(post => { return post.date.year() === year; }); });
hexo.extend.helper.register('reading_stats', function() { const posts = this.site.posts; const totalWords = posts.reduce((sum, post) => { const words = post.content.replace(/<[^>]+>/g, '').split(/\s+/).length; return sum + words; }, 0); return { total_posts: posts.length, total_words: totalWords, avg_words: Math.round(totalWords / posts.length), total_reading_time: Math.ceil(totalWords / 200) }; });
hexo.extend.helper.register('is_current_path', function(path) { return this.path === path; });
hexo.extend.helper.register('sidebar_data', function() { const recentPosts = this.site.posts.sort('date', -1).limit(5); const hotTags = this.site.tags.sort('length', -1).limit(10); return { recent_posts: recentPosts.toArray(), hot_tags: hotTags.toArray(), archive_count: this.site.posts.length, config: { show_categories: this.theme.sidebar.show_categories, show_tags: this.theme.sidebar.show_tags } }; });
|
在模板中使用 Helper:
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
| <!-- 使用简单 Helper --> <% const info = site_info() %> <div class="site-stats"> <p>文章数:<%= info.posts_count %></p> <p>标签数:<%= info.tags_count %></p> </div>
<!-- 使用带参数的 Helper --> <% const posts2024 = get_posts_by_year(2024) %> <h3>2024年的文章 (<%= posts2024.length %>)</h3>
<!-- 使用复杂 Helper --> <% const stats = reading_stats() %> <div class="reading-stats"> <span>总字数:<%= stats.total_words.toLocaleString() %></span> <span>平均字数:<%= stats.avg_words %></span> <span>预计阅读:<%= stats.total_reading_time %> 分钟</span> </div>
<!-- 使用侧边栏数据 Helper --> <% const sidebar = sidebar_data() %> <aside> <h3>最近文章</h3> <ul> <% sidebar.recent_posts.forEach(post => { %> <li><a href="<%- url_for(post.path) %>"><%= post.title %></a></li> <% }) %> </ul> <% if (sidebar.config.show_tags) { %> <h3>热门标签</h3> <div class="tags"> <% sidebar.hot_tags.forEach(tag => { %> <a href="<%- url_for(tag.path) %>"> <%= tag.name %> (<%= tag.length %>) </a> <% }) %> </div> <% } %> </aside>
|
方法二:使用 Filter 修改数据
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 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
|
hexo.extend.filter.register('template_locals', function(locals) { locals.custom_menu = [ { name: '首页', path: '/', icon: 'home' }, { name: '归档', path: '/archives/', icon: 'archive' }, { name: '关于', path: '/about/', icon: 'user' } ]; locals.current_year = new Date().getFullYear(); locals.social_links = this.theme.social || {}; const startDate = new Date(this.config.site_start_date || '2020-01-01'); const days = Math.floor((Date.now() - startDate) / (1000 * 60 * 60 * 24)); locals.site_running_days = days; return locals; });
hexo.extend.filter.register('template_locals', function(locals) { if (locals.page.layout === 'post') { const currentPost = locals.page; const relatedPosts = locals.site.posts .filter(post => { if (post._id === currentPost._id) return false; if (currentPost.tags && post.tags) { const currentTags = currentPost.tags.map(t => t.name); const postTags = post.tags.map(t => t.name); const commonTags = currentTags.filter(t => postTags.includes(t)); return commonTags.length > 0; } return false; }) .sort('date', -1) .limit(5); locals.related_posts = relatedPosts.toArray(); } if (locals.page.layout === 'archive') { const posts = locals.site.posts; const yearStats = {}; posts.forEach(post => { const year = post.date.year(); yearStats[year] = (yearStats[year] || 0) + 1; }); locals.archive_stats = yearStats; } return locals; });
hexo.extend.filter.register('before_post_render', function(data) { const words = data.content.replace(/<[^>]+>/g, '').split(/\s+/).length; data.reading_time = Math.ceil(words / 200); data.word_count = words; if (data.updated) { const daysSinceUpdate = Math.floor( (Date.now() - data.updated.valueOf()) / (1000 * 60 * 60 * 24) ); data.is_recent_update = daysSinceUpdate < 30; data.days_since_update = daysSinceUpdate; } if (!data.excerpt && data.content) { const plainText = data.content.replace(/<[^>]+>/g, ''); data.auto_excerpt = plainText.substring(0, 200) + '...'; } return data; });
|
在模板中使用 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 48 49 50 51 52 53 54 55 56 57
| <!-- 使用全局注入的自定义菜单 --> <nav> <% custom_menu.forEach(item => { %> <a href="<%= item.path %>" class="<%= is_current(item.path) ? 'active' : '' %>"> <i class="icon-<%= item.icon %>"></i> <%= item.name %> </a> <% }) %> </nav>
<!-- 使用运行时间 --> <footer> <p>本站已运行 <%= site_running_days %> 天</p> <p>© 2020-<%= current_year %> <%= config.author %></p> </footer>
<!-- 在文章页面使用相关文章 --> <% if (page.layout === 'post' && related_posts && related_posts.length > 0) { %> <div class="related-posts"> <h3>相关文章</h3> <ul> <% related_posts.forEach(post => { %> <li> <a href="<%- url_for(post.path) %>"><%= post.title %></a> <span class="meta"> <%= date(post.date, 'YYYY-MM-DD') %> · <%= post.reading_time %> 分钟阅读 </span> </li> <% }) %> </ul> </div> <% } %>
<!-- 使用文章的扩展数据 --> <% if (page.layout === 'post') { %> <article> <header> <h1><%= page.title %></h1> <div class="meta"> <time><%= date(page.date, 'YYYY-MM-DD HH:mm') %></time> <span>· <%= page.word_count %> 字</span> <span>· 约 <%= page.reading_time %> 分钟</span> <% if (page.is_recent_update) { %> <span class="badge-new">最近更新</span> <% } else if (page.days_since_update > 365) { %> <span class="badge-old"> 内容可能过时(距更新 <%= page.days_since_update %> 天) </span> <% } %> </div> </header> <%- page.content %> </article> <% } %>
|
方法三:使用 Generator 创建自定义数据页面
Generator 可以创建包含自定义变量的新页面。
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
|
hexo.extend.generator.register('tag-cloud', function(locals) { const tags = locals.tags.sort('length', -1); const maxCount = tags.first() ? tags.first().length : 1; const minCount = tags.last() ? tags.last().length : 1; const tagCloud = tags.map(tag => { const weight = (tag.length - minCount) / (maxCount - minCount); const fontSize = 12 + Math.floor(weight * 20); return { name: tag.name, path: tag.path, count: tag.length, weight: weight, fontSize: fontSize }; }); return { path: 'tag-cloud/index.html', layout: 'tag-cloud', data: { title: '标签云', tags: tagCloud, total_tags: tags.length, total_posts: locals.posts.length } }; });
hexo.extend.generator.register('timeline', function(locals) { const posts = locals.posts.sort('date', -1); const timeline = {}; posts.forEach(post => { const year = post.date.year(); const month = post.date.format('MM'); if (!timeline[year]) { timeline[year] = {}; } if (!timeline[year][month]) { timeline[year][month] = []; } timeline[year][month].push({ title: post.title, path: post.path, date: post.date, tags: post.tags ? post.tags.toArray() : [] }); }); return { path: 'timeline/index.html', layout: 'timeline', data: { title: '时间轴', timeline: timeline, years: Object.keys(timeline).sort((a, b) => b - a) } }; });
|
对应的模板文件:
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
| <!-- themes/your-theme/layout/tag-cloud.ejs --> <div class="tag-cloud-page"> <h1><%= page.title %></h1> <p class="stats"> 共 <%= page.total_tags %> 个标签, <%= page.total_posts %> 篇文章 </p> <div class="tag-cloud"> <% page.tags.forEach(tag => { %> <a href="<%- url_for(tag.path) %>" style="font-size: <%= tag.fontSize %>px; opacity: <%= 0.6 + tag.weight * 0.4 %>" title="<%= tag.count %> 篇文章" > <%= tag.name %> </a> <% }) %> </div> </div>
<!-- themes/your-theme/layout/timeline.ejs --> <div class="timeline-page"> <h1><%= page.title %></h1> <div class="timeline"> <% page.years.forEach(year => { %> <div class="timeline-year"> <h2><%= year %></h2> <% Object.keys(page.timeline[year]).sort((a, b) => b - a).forEach(month => { %> <div class="timeline-month"> <h3><%= month %> 月</h3> <ul> <% page.timeline[year][month].forEach(post => { %> <li> <time><%= post.date.format('MM-DD') %></time> <a href="<%- url_for(post.path) %>"><%= post.title %></a> <% if (post.tags.length > 0) { %> <span class="tags"> <% post.tags.slice(0, 3).forEach(tag => { %> <span class="tag"><%= tag.name %></span> <% }) %> </span> <% } %> </li> <% }) %> </ul> </div> <% }) %> </div> <% }) %> </div> </div>
|
变量作用域的最佳实践
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
|
hexo.extend.helper.register('mytheme_utils', function() { return { formatDate: (date) => date.format('YYYY-MM-DD'), truncate: (text, length) => text.substring(0, length) + '...', }; });
hexo.extend.filter.register('template_locals', function(locals) { global.myCustomData = 'some data'; return locals; });
hexo.extend.filter.register('template_locals', function(locals) { locals.mytheme = { version: '1.0.0', features: ['dark-mode', 'search', 'toc'], custom_data: 'some data' }; return locals; });
hexo.extend.filter.register('template_locals', function(locals) { if (locals.page.need_heavy_data) { locals.heavy_data = computeExpensiveData(); } return locals; });
const cache = new Map();
hexo.extend.helper.register('expensive_calculation', function() { const cacheKey = 'my_calculation'; if (cache.has(cacheKey)) { return cache.get(cacheKey); } const result = performExpensiveCalculation(); cache.set(cacheKey, result); return result; });
|
3. 路由系统 (Router)
概念介绍
路由系统负责将 URL 映射到具体的生成器和模板,决定了网站的 URL 结构和页面生成逻辑。
默认路由规则
Hexo 有几种默认的路由模式:
1 2 3 4 5
| permalink: :year/:month/:day/:title/ tag_dir: tags category_dir: categories archive_dir: archives
|
自定义路由
创建自定义页面路由
1 2 3 4 5 6 7 8
| hexo.extend.generator.register('custom-page', function(locals) { return { path: 'custom-page/index.html', data: locals.posts, layout: 'custom-layout' }; });
|
动态路由生成
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| hexo.extend.generator.register('author-pages', function(locals) { const authors = new Set(); locals.posts.forEach(post => { if (post.author) authors.add(post.author); }); return Array.from(authors).map(author => ({ path: `author/${author}/index.html`, data: { author: author, posts: locals.posts.filter(p => p.author === author) }, layout: 'author' })); });
|
路由优先级
1 2 3 4 5 6 7 8 9
| hexo.route.set('special-page.html', function() { return 'Special content'; });
hexo.extend.generator.register('normal-generator', function(locals) { });
|
4. Box - 文件处理系统
概念介绍
Box 是 Hexo 的文件处理抽象层,负责监控、读取和处理项目中的文件。每个 Box 实例代表一个文件夹,可以监听文件变化并触发相应的处理器。
Box 的类型
Hexo 中有几个预定义的 Box,分别用于处理特定的文件夹:
1 2 3
| hexo.source hexo.theme hexo.public
|
文件处理器
注册处理器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| hexo.extend.processor.register('posts/:slug.md', function(file) { const data = { title: file.data.title, date: file.data.date, content: file.content }; return data; });
hexo.extend.processor.register('data/**/*.json', function(file) { const jsonData = JSON.parse(file.content); return processCustomData(jsonData); });
|
监听文件变化
1 2 3 4 5 6 7
| hexo.source.on('processAfter', function(file) { console.log('文件处理完成:', file.path); });
hexo.source.on('processBefore', function(file) { console.log('准备处理文件:', file.path); });
|
实战:自定义数据文件夹
1 2 3 4 5 6 7 8 9 10 11 12 13
| const Box = require('hexo-fs');
hexo.extend.processor.register('_data/**/*.yml', function(file) { const name = file.path.replace(/^_data\//, '').replace(/\.yml$/, ''); hexo.locals.set(name, () => { return hexo.render.render({ path: file.source, engine: 'yaml' }); }); });
|
5. 渲染引擎 (Renderer)
概念介绍
渲染引擎负责将源文件转换为 HTML。Hexo 支持多种模板引擎和标记语言,通过渲染器系统实现灵活的内容转换。
常用渲染器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| hexo.config.markdown = { preset: 'default', render: { html: true, breaks: true } };
hexo.config.stylus = { compress: true };
|
注册自定义渲染器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| hexo.extend.renderer.register('pug', 'html', function(data, options) { const pug = require('pug'); return pug.compile(data.text)(options); }, true);
hexo.extend.renderer.register('md', 'html', function(data, options) { const marked = require('marked'); const renderer = new marked.Renderer(); renderer.heading = function(text, level) { return `<h${level} class="custom-heading">${text}</h${level}>`; }; return marked(data.text, { renderer }); }, true);
|
渲染流程
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| hexo.render.render({ text: '# Hello World', engine: 'markdown' }).then(result => { console.log(result); });
hexo.render.render({ path: 'path/to/file.md' }).then(result => { });
|
渲染器配置优化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| hexo.on('ready', function() { hexo.config.highlight = { enable: true, line_number: true, auto_detect: true, tab_replace: ' ', wrap: true, hljs: false }; hexo.config.marked = { gfm: true, pedantic: false, breaks: true, smartLists: true, smartypants: true }; });
|
6. 文章数据模型 (Post)
概念介绍
文章是 Hexo 中最核心的内容类型,包含了丰富的元数据和内容信息。理解文章的数据结构对于主题开发至关重要。
文章属性
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| { title: '文章标题', date: Date, updated: Date, comments: true, layout: 'post', content: '...', excerpt: '...', more: '...', source: '...', full_source: '...', path: '...', permalink: '...', categories: [], tags: [], photos: [], link: '', raw: '', published: true, _content: '...', _id: '...' }
|
Front-matter 使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| --- title: Hexo 主题开发入门 date: 2025-01-15 10:00:00 updated: 2025-01-16 15:30:00 tags: - Hexo - 主题开发 categories: - 技术教程 - 前端开发 excerpt: 这是一篇关于 Hexo 主题开发的详细教程 cover: /images/hexo-cover.jpg author: Mahoo custom_field: 自定义数据 ---
文章正文内容...
<!-- more -->
更多内容...
|
访问和操作文章
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
| <!-- 遍历所有文章 --> <% site.posts.sort('date', -1).each(function(post) { %> <article> <h2> <a href="<%- url_for(post.path) %>"><%= post.title %></a> </h2> <time><%= date(post.date, 'YYYY-MM-DD') %></time> <!-- 分类 --> <% if (post.categories && post.categories.length) { %> <div class="categories"> <% post.categories.each(function(cat) { %> <a href="<%- url_for(cat.path) %>"><%= cat.name %></a> <% }) %> </div> <% } %> <!-- 标签 --> <% if (post.tags && post.tags.length) { %> <div class="tags"> <% post.tags.each(function(tag) { %> <span><%= tag.name %></span> <% }) %> </div> <% } %> <!-- 摘要 --> <% if (post.excerpt) { %> <%- post.excerpt %> <a href="<%- url_for(post.path) %>">阅读更多</a> <% } else { %> <%- truncate(strip_html(post.content), {length: 200}) %> <% } %> </article> <% }) %>
|
文章过滤和排序
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
| <!-- 获取最新的5篇文章 --> <% site.posts.sort('date', -1).limit(5).each(function(post) { %> <li><a href="<%- url_for(post.path) %>"><%= post.title %></a></li> <% }) %>
<!-- 按分类过滤 --> <% site.posts.find({categories: 'Tech'}).each(function(post) { %> <%= post.title %> <% }) %>
<!-- 按标签过滤 --> <% site.posts.filter(function(post) { return post.tags.some(function(tag) { return tag.name === 'JavaScript'; }); }).each(function(post) { %> <%= post.title %> <% }) %>
<!-- 获取置顶文章 --> <% site.posts.filter(function(post) { return post.sticky || post.top; }).sort('sticky', -1).each(function(post) { %> <li class="pinned"><%= post.title %></li> <% }) %>
|
7. 模板脚手架 (Scaffold)
概念介绍
Scaffold 是用于创建新文章的模板文件,定义了不同类型内容的默认结构和 Front-matter。
默认 Scaffold
Hexo 提供了三种默认脚手架:
post.md - 文章模板
1 2 3 4 5 6
| --- title: {{ title }} date: {{ date }} tags: categories: ---
|
page.md - 页面模板
1 2 3 4
| --- title: {{ title }} date: {{ date }} ---
|
draft.md - 草稿模板
1 2 3 4
| --- title: {{ title }} tags: ---
|
自定义 Scaffold
创建自定义模板
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| --- title: {{ title }} date: {{ date }} photos: - tags: - 摄影 categories: - 相册 layout: photo ---
<!-- 照片描述 -->
|
使用自定义模板
高级 Scaffold 技巧
使用变量和逻辑
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
| --- title: {{ title }} date: {{ date }} author: {{ author || '匿名' }} tags: categories: - {{ category || '未分类' }} cover: /images/default-cover.jpg toc: true comments: true description: {{ description || title }} keywords: {{ keywords || title }} ---
## 简介
{{ title }} 的详细内容...
## 正文
<!-- 在这里编写内容 -->
## 总结
---
**相关文章推荐:**
-
|
项目特定的 Scaffold
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
| # scaffolds/tutorial.md --- title: {{ title }} subtitle: {{ subtitle }} date: {{ date }} updated: {{ date }} author: {{ author }} series: 教程系列 tags: - 教程 categories: - 技术文档 difficulty: 初级 time_required: 30分钟 cover: /images/tutorial-cover.jpg toc: true ---
## 📚 教程概述
### 学习目标
- -
### 前置要求
- -
---
## 📝 正文内容
### 步骤一:准备工作
### 步骤二:核心实现
### 步骤三:测试验证
---
## 🎯 总结
### 关键要点
### 下一步
---
## 📖 参考资料
-
|
程序化生成 Scaffold
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| hexo.extend.filter.register('new_post_path', function(data, replace) { if (data.categories && data.categories.length > 0) { const category = data.categories[0]; return `source/_posts/${category}/${data.slug}.md`; } return data.path; });
hexo.extend.filter.register('before_post_render', function(data) { if (!data.author) { data.author = this.config.author; } if (!data.cover) { data.cover = '/images/default-cover.jpg'; } return data; });
|
核心概念关系图
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
| ┌─────────────────────────────────────────────┐ │ Hexo 核心系统 │ ├─────────────────────────────────────────────┤ │ │ │ ┌──────────┐ ┌──────────┐ │ │ │ Events │◄──────►│ Router │ │ │ │ 事件系统 │ │ 路由系统 │ │ │ └────┬─────┘ └────┬─────┘ │ │ │ │ │ │ ▼ ▼ │ │ ┌──────────┐ ┌──────────┐ │ │ │ Box │◄──────►│ Renderer │ │ │ │ 文件处理 │ │ 渲染引擎 │ │ │ └────┬─────┘ └────┬─────┘ │ │ │ │ │ │ └────────┬──────────┘ │ │ ▼ │ │ ┌─────────────┐ │ │ │Local Vars │ │ │ │ 本地变量 │ │ │ └──────┬──────┘ │ │ │ │ │ ┌────────┴────────┐ │ │ ▼ ▼ │ │ ┌─────────┐ ┌──────────┐ │ │ │ Post │ │ Scaffold │ │ │ │ 文章 │ │ 模板 │ │ │ └─────────┘ └──────────┘ │ │ │ └─────────────────────────────────────────────┘
|
实战:整合核心概念
让我们通过一个完整的例子来整合这些核心概念:
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
|
hexo.extend.helper.register('reading_time', function(content) { const wordsPerMinute = 200; const words = content.replace(/<[^>]+>/g, '').split(/\s+/).length; const minutes = Math.ceil(words / wordsPerMinute); return minutes; });
hexo.extend.filter.register('before_post_render', function(data) { if (data.content) { const readingTime = this.extend.helper.get('reading_time').bind(this)(data.content); data.reading_time = readingTime; } return data; });
hexo.extend.generator.register('reading-stats', function(locals) { const posts = locals.posts.sort('date', -1); const stats = { total_posts: posts.length, total_reading_time: 0, avg_reading_time: 0, posts_by_time: [] }; posts.forEach(post => { if (post.reading_time) { stats.total_reading_time += post.reading_time; stats.posts_by_time.push({ title: post.title, path: post.path, reading_time: post.reading_time }); } }); stats.avg_reading_time = Math.ceil(stats.total_reading_time / stats.total_posts); return { path: 'reading-stats/index.html', data: stats, layout: 'reading-stats' }; });
hexo.on('generateAfter', function() { const posts = this.locals.get('posts'); const totalTime = posts.reduce((sum, post) => sum + (post.reading_time || 0), 0); console.log(`📚 总计 ${posts.length} 篇文章,预计阅读时间 ${totalTime} 分钟`); });
|
对应的模板文件:
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
| <!-- themes/your-theme/layout/reading-stats.ejs --> <!DOCTYPE html> <html> <head> <title>阅读统计 - <%= config.title %></title> </head> <body> <h1>📊 阅读统计</h1> <div class="stats-overview"> <div class="stat-item"> <h3>总文章数</h3> <p><%= page.total_posts %></p> </div> <div class="stat-item"> <h3>总阅读时间</h3> <p><%= page.total_reading_time %> 分钟</p> </div> <div class="stat-item"> <h3>平均阅读时间</h3> <p><%= page.avg_reading_time %> 分钟</p> </div> </div> <h2>文章列表</h2> <ul> <% page.posts_by_time.forEach(function(post) { %> <li> <a href="<%- url_for(post.path) %>"><%= post.title %></a> <span>(约 <%= post.reading_time %> 分钟)</span> </li> <% }) %> </ul> </body> </html>
|
小结
本文介绍了 Hexo 主题开发的七大核心概念:
- 事件系统:响应 Hexo 生命周期的不同阶段
- 本地变量:在模板中访问网站数据
- 路由系统:管理 URL 和页面生成
- Box:处理和监控文件系统
- 渲染引擎:转换各种格式为 HTML
- 文章数据:理解和操作文章对象
- 模板脚手架:快速创建标准化内容
这些概念构成了 Hexo 的基础架构,定义了:
- 📁 文件如何被读取和处理(Box)
- 🔄 内容如何被渲染(Renderer)
- 📄 数据如何被组织(Post、Local Variables)
- 🌐 页面如何被访问(Router)
- ⚡ 系统如何响应变化(Events)
如果说本章的核心概念是 Hexo 的”骨架”,那么下一章的扩展系统就是 Hexo 的”肌肉”。
核心概念告诉我们Hexo 是什么, 如何工作,扩展系统将告诉我们:如何定制和扩展 Hexo,如何创造新功能。
参考资源