上一篇hexo源码分析 主要分析了hexo.extend.generator的流程
本篇就是实际使用hexo.extend.generator来实现自定义分类页面的功能
预期是像这个C++大佬的博客https://www.fluentcpp.com/posts/一样,存在一个文章总览目录
可以很清晰的进行分类
但是我的需求还要稍微复杂一些,需要支持多级分类,可以实现出一个简单的思维导图分类,例如使用hexo的分类标注的两篇博文
1 2 3 4 title: 为什么我用这个blog categories: - [Blog, Test1] - [Blog, Test2, Test2_Sub]
以及
1 2 3 title: 如何创建这样一个blog categories: - Blog
那么预期如下
1 2 3 4 5 6 7 Blog 如何创建这样一个blog Test1 为什么我用这个blog Test2 Test2_Sub 为什么我用这个blog
并且整个页面需要支持分类的全部展开和全部折叠,以及单个分类点击展开和折叠
单纯的折叠功能曾经有人给next提出了类似的需求有方法可以使category的菜单实现折叠吗 ,但是由于项目的交接,这个issue最后无疾而终,还是得靠自己来实现了
添加模板
首先在眼前的难题就是如何在next主题上新加一个模板,如果直接放到模板文件夹,那以后的next主题更新就很麻烦了
翻到issue上看到Custom
File / Theme Injects 是否支持修改原有的代码?
作者回答说需要使用hexo扩展插件https://github.com/jiangtj/hexo-extend-theme
看到有人提到看了这个插件的源码,可以几行代码引入新的模板
正好今天有类似的需求,那个插件为了通用可配置做的逻辑比较复杂,其实替换文件顺着该插件思路,直接在根目录的
scripts
子目录下加一个 js 文件就可以。
1 2 3 4 5 6 const { readFileSync } = require('fs'); hexo.extend.filter.register('before_generate', () => { const file = readFileSync('layout/notes.njk').toString(); hexo.theme.setView('notes.njk', file); })
其中 layout/notes.njk
是相对根目录的路径,也就是你自定义的文件。hexo.theme.setView('notes.njk', file);
插入或覆盖原有相对于 layout 路径为 notes.njk
的文件。如有多个文件待替换,则重复中间两行。
所以在根目录的scripts下新增custom-category.js,用于给hexo框架注册模板
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 const { readFileSync } = require ('fs' );hexo.extend .filter .register ('before_generate' , () => { const file = readFileSync ('layout/custom-category.njk' ).toString (); hexo.theme .setView ('custom-category.njk' , file); }) hexo.extend .generator .register ('custom-category' , function (locals ) { var categoriesTree = []; return { path : 'custom-categories/index.html' , data : { categoriesTree : categoriesTree, }, layout : ['custom-category' ] } });
随后就可以新建模板文件layout/custom-category.njk
,网站的custom-categories/index.html
页面就会使用custom-category.njk
来渲染
根据之前的hexo源码分析,这个模板文件可以使用page.categoriesTree来接收hexo.extend.generator注册函数的返回值
实现分类树
hexo的数据结构是这样的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 class Post { string name; string _id; string slug; string path; string link; Category* owner; ...其他字段省略 }; class Category { string name; string _id; string slug; string path; string permalink; Post** posts; string parent; ...其他字段省略 };
由于Category和Post形成了环,因此不能直接json序列化打印内容,只能用Object.keys(category)
方法查看字段
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 locals.categories .forEach (category => { hexo.log .info ('category.name:' , category.name ); hexo.log .info ('category.id:' , category._id ); hexo.log .info ('category.slug:' , category.slug ); hexo.log .info ('category.path:' , category.path ); hexo.log .info ('category.permalink:' , category.permalink ); hexo.log .info ('category.parent:' , category.parent ); category.posts .forEach (post => { hexo.log .info ('post.id:' , post._id ); hexo.log .info ('post.slug:' , post.slug ); hexo.log .info ('post.path:' , post.path ); }) })
输出如下:
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 INFO category.name: Blog INFO category.id: cm0r3lkxy0007fsv32fez76fi INFO category.slug: Blog INFO category.path: categories/Blog/ INFO category.permalink: https://weakyon.com/categories/Blog/ INFO category.parent: undefined INFO post.id: cm0r3lkxr0001fsv3fva40vrq INFO post.slug: why-I-use-this INFO post.path: 2011/01/01/why-I-use-this.html INFO post.id: cm0r3lky7000qfsv3869l2qdh INFO post.slug: how-to-setup-this-blog INFO post.path: 2014/08/20/how-to-setup-this-blog.html --------------------------------------------------------------- INFO category.name: Test1 INFO category.id: cm0r3lky3000efsv3cjmyhlz3 INFO category.slug: Blog/Test1 INFO category.path: categories/Blog/Test1/ INFO category.permalink: https://weakyon.com/categories/Blog/Test1/ INFO category.parent: cm0r3lkxy0007fsv32fez76fi INFO post.id: cm0r3lkxr0001fsv3fva40vrq INFO post.slug: why-I-use-this INFO post.path: 2011/01/01/why-I-use-this.html
那么遍历category,创建它的哈希表,主键为category._id
,值为Category添加了children列表的数据结构
然后再遍历一次category,如果存在parent,就把他挂到Map[parent].parent下面,就可以了
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 const { readFileSync } = require ('fs' );hexo.extend .filter .register ('before_generate' , () => { const file = readFileSync ('layout/custom-category.njk' ).toString (); hexo.theme .setView ('custom-category.njk' , file); }) hexo.extend .generator .register ('custom-category' , function (locals ) { var categoriesData = locals.categories .toArray (); var categoriesMap = {}; categoriesData.forEach (category => { categoriesMap[category._id ] = { id : category._id , name : category.name , slug : category.slug , posts : category.posts .toArray (), posts_length : category.posts .length , children : [] }; }); var categoriesTree = []; categoriesData.forEach (category => { if (category.parent ) { const parentId = category.parent ; if (categoriesMap[parentId]) { categoriesMap[parentId].children .push (categoriesMap[category._id ]); } } else { categoriesTree.push (categoriesMap[category._id ]); } }); return { path : 'custom-categories/index.html' , data : { categoriesTree : categoriesTree, }, layout : ['custom-category' ] } });
但是这样创建出来的Blog分类下,会有子分类的文章,所以需要对其去重,然后再返回
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 function getSubtreePostIds (category, includeNodePosts ) { let subtreePostIds = new Set (); category.children .forEach (child => { const childPostIds = getSubtreePostIds (child, true ); childPostIds.forEach (id => subtreePostIds.add (id)); }); if (includeNodePosts) { const nodePostIds = category.posts .map (post => post._id ); nodePostIds.forEach (id => subtreePostIds.add (id)); } return subtreePostIds; } function removeSubtreePostIdsFromNode (category ) { category.children .forEach (child => { removeSubtreePostIdsFromNode (child); }); const subtreePostIds = getSubtreePostIds (category, false ); category.posts = category.posts .filter (post => { return !subtreePostIds.has (post._id ); }); category.posts_length = category.posts .length ; } categoriesTree.forEach (removeSubtreePostIdsFromNode);
实现模板
模板是上文使用readFileSync注入进来的,它的实际路径在themes/next/layout
所以可以根据相对路径使用next主题已经实现的一些方法,例如sidebar_template
等
1 2 3 4 5 6 7 8 9 10 11 <!-- 标题 --> {% extends '_layout.njk' %} {% import '_macro/sidebar.njk' as sidebar_template with context %} {% block content %} <!-- 正文 --> {% endblock %} {% block sidebar %} {{ sidebar_template.render(false) }} {% endblock %}
这样的一个模板文件,就可以渲染出复合next主题的空白页面
那么下面只需要根据传入的分类树参数,渲染出分类树即可
我首先渲染出一个分类树的结构,不带博文,用于跳转到这个页面的正式分类树上
然后再渲染正式的带博文的分类树
完整代码如下:
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 {% extends '_layout.njk' %} {% import '_macro/sidebar.njk' as sidebar_template with context %} {% macro renderCategory(category) %} <li style="display: inline; margin-right: 15px;"> <a href="#{{ category.name }}">{{ category.name }} <span>[{{ category.directly_posts_length }}]</span></a> </li> {% endmacro %} {% macro renderPosts(category) %} <h4 class="archive-ul show" onclick="toggleCategory('{{ category.name }}', this)"> <span class="triangle-right"></span> {{ category.name }} </h4> <ul id="{{ category.name }}" class="category-content"> {% for post in category.posts %} <li class="listing-category"><a href="{{ url_for(post.path) }}">{{ post.title }}</a></li> {% endfor %} {% if category.children.length %} <ul> {% for child in category.children %} {{ renderPosts(child) }} {% endfor %} </ul> {% endif %} </ul> {% endmacro %} {% block content %} <div class="button-container"> <button onclick="toggleAll(true)">展开全部</button> <button onclick="toggleAll(false)">收起全部</button> </div> <div class="widget"> <ul class="tag_box inline list-unstyled"> {% for category in page.categoriesTree %} {{ renderCategory(category) }} {% endfor %} </ul> </div> <div class="archive"> {% for category in page.categoriesTree %} {{ renderPosts(category) }} {% endfor %} </div> {% endblock %} {% block sidebar %} {{ sidebar_template.render(false) }} {% endblock %}
展开和折叠
上文的完整模板中创建了一个按钮,用于实现展开和折叠功能
1 2 3 4 5 6 7 <h4 class ="archive-ul show" onclick="toggleCategory('{{ category.name }}', this)" > <span class ="triangle-right" > </span > {{ category.name }} </h4> <ul id ="{{ category.name }}" class ="category-content" > 略过... </ul >
其中triangle-right用来实现一个旋转的三角按钮,它的样式由css控制,当onclick回调被触发,将它变成.triangle-right.collapsed
从而旋转90度
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 .triangle-right { display : inline-block; margin-right : 10px ; width : 0 ; height : 0 ; border-top : 5px solid transparent; border-bottom : 5px solid transparent; border-left : 5px solid black; cursor : pointer; transition : transform 0.3s ; } .triangle-right .collapsed { transform : rotate (90deg ); }
最后通过hexo的injector注入一段js代码,用来实现展开和折叠
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 hexo.extend.injector.register('body_end', function () { return ` <link rel="stylesheet" href="/css/custom-category.css"> <script> //实现单个按钮的展开和折叠,通过修改categoryContent的属性来实现 function toggleCategory(categoryName, el) { var categoryContent = document.getElementById(categoryName); var triangle = el.querySelector('.triangle-right'); if (categoryContent.style.display === "none" || categoryContent.style.display === "") { categoryContent.style.display = "block"; triangle.classList.remove('collapsed'); } else { categoryContent.style.display = "none"; triangle.classList.add('collapsed'); } } //实现全部按钮的展开和折叠,通过修改categoryContent的属性来实现 function toggleAll(expand) { var categories = document.querySelectorAll('.category-content'); categories.forEach(function(categoryContent) { var triangle = categoryContent.previousElementSibling.querySelector('.triangle-right'); if (expand) { categoryContent.style.display = 'block'; triangle.classList.remove('collapsed'); } else { categoryContent.style.display = 'none'; triangle.classList.add('collapsed'); } }); } </script> ` })
参考资料