# 前言

怎么说呢... 一开始根本没想花这么多时间去换主题的。

一开始我只是单纯想给老主题 anodyne 加一个『目录』的功能罢了...

看到一个 hexo-theme-particlex ,感觉不错。
又看到一个 hexo-theme-shoka,感觉也挺可以,Shoka 主题记得一开始是看的
再看一个 hexo-theme-icarus,感觉也不错啊 (✧◡✧)。

一顿操作后,越看越觉得自己的主题太简洁了。

对比了下几个主题,icarus 也是简洁风。而 Particlex 和 Shoka 都差不多类似类型,一开始想换成 Particlex。不过 Particlex 自定义的字体集有点大... 整个主题几十兆大半容量都在字体集上了。尝试去掉字体就变得难看起来,默认设置白茫茫地也不好看... 感觉头像 widget 还有点歪,主页文章列表内容预览截取字符没有处理标题以后的逻辑 (开始就是『前言』标题的话就空白了),而且也没有目录功能,换过去又改一次是吧?

于是对比了解了下,最终决定换 Shoka—— 感觉一定能省不少事(不会说是因为看重随机背景图片好看且方便 —— 简直是选择困难症的福音)
(虽然最后依然花了挺多时间研究去结合自己的改动)

# 过程

# 最新评论、随机文章数量修改

一开始本来不想展示这个的,后面又仔细一想:我这博客也没个评论提示,最近的留言不是正好可以当做一个提示吗?看到最近留言就可以知道谁谁加了评论 (虽然我觉得这个冷清的博客不大可能会有啥评论),不过多一个提示也是好的。

但是又感觉这个主题页面下面的随机文章和最近评论太长了:作者两者默认都是写死的 10 条,想减少一点。
随机文章还好说,直接改主题代码就行了

在 _config.shoka.yml 里边加了一个 count 的配置:

widgets:
  # if true, will show random posts
  random_posts: true
  # if true, will show recent comments
  recent_comments: true
  count: 3

然后找到 widgets.njk 加入对数量参数的解析:

// 注意少了个取参的花括号,因为不知道为啥位于代码块也会被转义掉
  <ul class="leancloud-recent-comment count_{ theme.widgets.count }"></ul>

然而最近评论的数量改起来就比较麻烦了,因为功能是直接写死在 MiniValine 的代码里的,没有支持自定义的功能。
如果想要减少,得改源码才行。
由于该主题的 js 是代码动态加载,并且在配置路径上说千万不要动:除非知道在干什么!
看起来,原因是作者对加速脚本做了合并处理,然后我调试了一下,感觉 MiniValine 并没有处于合并列表,相当于还是单个加载的。
于是就想着一下,fork 了一个工程,先在本地改着试试。

拉下来后,覆盖 vendors.js.valine 路径设置:

vendors:
  js:
    valine: /MiniValine/dist/MiniValine.min.js #gh/amehime/MiniValine@4.2.2-beta10/dist/MiniValine.min.js

本地调试了下,感觉没什么大问题。

于是准备开改。

第一步就是编译 MiniValine ,因为没在 Readme 上看到编译方式,就自己研究了下,发现『可能』是用的 webpack。

于是尝试 npm i webpack 安装重新试,又缺失 webpack-cli ...... 一波安装下去,缺失 Python 都来了 —— 也不知道哪个依赖包需要 Python 编译。

现在这个电脑还没安装过 py,于是下载了最新版:编译确实开始了,结果又报新的错误。

npm ERR! path E:\项目\Blog\HexoBlog\MiniValine\node_modules\fibers
npm ERR! command failed
npm ERR! command C:\WINDOWS\system32\cmd.exe /d /s /c node build.js || nodejs build.js
npm ERR! gyp info it worked if it ends with ok
npm ERR! gyp info using node-gyp@3.8.0
npm ERR! gyp info using node@18.12.1 | win32 | x64
npm ERR! gyp ERR! configure error
npm ERR! gyp ERR! stack Error: Command failed: C:\Users\CWHIS\AppData\Local\Programs\Python\Python311\python.EXE -c import sys; print "%s.%s.%s" % sys.version_info[:3];
npm ERR! gyp ERR! stack   File "<string>", line 1
npm ERR! gyp ERR! stack     import sys; print "%s.%s.%s" % sys.version_info[:3];
npm ERR! gyp ERR! stack                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
npm ERR! gyp ERR! stack SyntaxError: Missing parentheses in call to 'print'. Did you mean print(...)?
npm ERR! gyp ERR! stack
npm ERR! gyp ERR! stack     at ChildProcess.exithandler (node:child_process:412:12)
npm ERR! gyp ERR! stack     at ChildProcess.emit (node:events:513:28)
npm ERR! gyp ERR! stack     at maybeClose (node:internal/child_process:1091:16)
npm ERR! gyp ERR! stack     at ChildProcess._handle.onexit (node:internal/child_process:302:5)
npm ERR! gyp ERR! System Windows_NT 10.0.22621
npm ERR! gyp ERR! command "D:\\Solfware\\GreenSolfware\\node-v18.12.1-win-x64\\node.exe" "E:\\项目\\Blog\\HexoBlog\\MiniValine\\node_modules\\node-gyp\\bin\\node-gyp.js" "rebuild" "--release"
npm ERR! gyp ERR! cwd E:\项目\Blog\HexoBlog\MiniValine\node_modules\fibers
npm ERR! gyp ERR! node -v v18.12.1
npm ERR! gyp ERR! node-gyp -v v3.8.0
npm ERR! gyp ERR! not ok
npm ERR! node-gyp exited with code: 1
npm ERR! Please make sure you are using a supported platform and node version. If you
npm ERR! would like to compile fibers on this machine please make sure you have setup your
npm ERR! build environment--
npm ERR! Windows + OS X instructions here: https://github.com/nodejs/node-gyp
npm ERR! Ubuntu users please run: `sudo apt-get install g++ build-essential`
npm ERR! RHEL users please run: `yum install gcc-c++` and `yum groupinstall 'Development Tools'`
npm ERR! Alpine users please run: `sudo apk add python make g++`
npm ERR! 'nodejs' �����ڲ����ⲿ���Ҳ���ǿ����еij���
npm ERR! ���������ļ���

看着就像是 python 版本过高,依赖包还在用老版本的语法?

这已经是反复安装和卸载 npm 包管理尝试后了
毕竟我不是专业的... 这时候已经花了不少时间了 —— 就换个主题... 加个小功能而已,我还有更重要的东西要做,怎么能在这里费这么大功夫,干脆直接在 MiniValine.min.js 基础上修改算了。

于是打开控制台要网页调试窗口,下了个断点,很快就把 count_数量 这个配置解析成查询数量上限值了。

var recentCount = 10;
t && t.classList.forEach(x=>{
    if (x.toString().startsWith("count_"))
        recentCount = x.toString().split("_")[1]
});

很简单的一行代码。

不过由于需要拉取工程改 MiniValine,如果有需要的话可以直接用我改好的 (... 如果真的还有人需要的话):

vendors:
  js:
    valine: gh/CWHISME/MiniValine/dist/MiniValine.min.js #gh/amehime/MiniValine@4.2.2-beta10/dist/MiniValine.min.js

以及改过的 Shoka 地址。

# 标题显示开头显示成 # 号

最开始发现的是,文章目录标题不像作者那样显示 H1、H2 之类的,而是『#』号开头,比如:

对比了下作者和其它主题使用者的情况,发现是这个 markdownIt-Anchor class 的问题,手动将其改成 anchor 就正常了。

知道原因,就得找为什么会这样?
安装过程也是按照要求操作的,为何 anchor 变成了 markdownIt-Anchor?

于是先后经历了反复 安装、卸载,查询依赖包版本等等操作...... 检查得怀疑人生!
结果最后才发现:是安装说明中的 hexo-renderer-multi-markdown-it 配置没有沾下来!

主题自带的 _config.yml 并没有关于这个的默认配置,必须手动粘贴进自己的 _config.yml 然后我之前安装的时候估计漏了,然后就变成这样了... 😭

# 头像

看到作者在关于页面写的自设,并留下了一个 Picrew 的链接,点进去试了下,感觉还意外的不错。

于是把自己的上古头像也换了。

# 页顶图片添加网格蒙版

因为感觉有时候放大太糊了,参考 Lavender 的文章,增加了网格效果。

作者是直接修改的 themes\shoka\source\css\_common\outline\header\header.styl

其实在 source/_data/ 目录建立一个 custom.styl 自定义样式也是可以的。

并且因为我没有把图改成全屏,所以代码有所差异:

#header {
  &::before{
    background: url(/img/dot.png);
    content: '';
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 70vh;
    z-index: -4;
    background-attachment: fixed;
  }
  margin: 0 auto;
  position: relative;
  width: 100%;
  height: 50vh;
  text-shadow: 0rem .2rem .3rem alpha(#000, .5);
  color: var(--header-text-color);
  a:hover {
    color: currentColor;
  }
}

相当于只是在 Shoka 本来基础上加了一个网格蒙版效果。


后边想着把普通 cover 也加了一个,不过降低了透明度。

说道这个,GIMP 中的颜色模式,就算黑白色也不能采用灰度,还是得 RGB 颜色才行。

想着减少大小 (其实根本没必要) 试了下感觉蒙版格子都不对味了。

# 音乐播放器

注意 QQ 音乐的链接必须带 .html 后缀,否则音乐播放器会坏掉的。

筛选使用的正则表达式规则如下:

['music.163.com.*song.*id=(\\d+)', 'netease', 'song'],
['music.163.com.*album.*id=(\\d+)', 'netease', 'album'],
['music.163.com.*artist.*id=(\\d+)', 'netease', 'artist'],
['music.163.com.*playlist.*id=(\\d+)', 'netease', 'playlist'],
['music.163.com.*discover/toplist.*id=(\\d+)', 'netease', 'playlist'],
['y.qq.com.*song/(\\w+).html', 'tencent', 'song'],
['y.qq.com.*album/(\\w+).html', 'tencent', 'album'],
['y.qq.com.*singer/(\\w+).html', 'tencent', 'artist'],
['y.qq.com.*playsquare/(\\w+).html', 'tencent', 'playlist'],
['y.qq.com.*playlist/(\\w+).html', 'tencent', 'playlist'],
['xiami.com.*song/(\\w+)', 'xiami', 'song'],
['xiami.com.*album/(\\w+)', 'xiami', 'album'],
['xiami.com.*artist/(\\w+)', 'xiami', 'artist'],
['xiami.com.*collect/(\\w+)', 'xiami', 'playlist'],

# 歌单更换后错位问题

自定义的音乐,更换了音乐列表音乐需要手动清空一下 loacalStorage,因为作者为了优化,每个歌单数据拉取一次后都是直接存储本地的。

// 例如 utils.parse 返回数据
meta: (3) ['tencent', 'playlist', '8733616535']
// 本地有就取本地,否则解析存储本地
var meta = utils.parse(raw)
if(meta[0]) {
  var skey = JSON.stringify(meta)
  var playlist = store.get(skey)
  if(playlist) {
    list.push.apply(list, JSON.parse(playlist));
    resolve(list);
  } else {
    fetch('https://api.i-meto.com/meting/api?server='+meta[0]+'&type='+meta[1]+'&id='+meta[2]+'&r='+ Math.random())
      .then(function(response) {
        return response.json()
      }).then(function(json) {
        store.set(skey, JSON.stringify(json))
        list.push.apply(list, json);
        resolve(list);
      }).catch(function(ex) {})
  }
} else {
  list.push(raw);
  resolve(list);
}

这样要是实时在另外一边更换了歌单中的歌曲就会出问题:比如歌曲错位啊、名字不对、播放失败之类的。

似乎访问一次后,歌单记录下来变不了了 (对访问者来说),因为貌似没有看到删除的地方...

# 自定义脚本执行时机问题

想把之前显示文章『多少天』前的脚本放进来,不过一时间没找到可以配置的地方。

最后还是直接改的主题代码,在 layout.njk 导入 app.js 主题代码之后添加自定义的脚本。

其中有个问题是,这个点击进去另外的页面貌似做了优化的,不会重新请求所有页面数据,因此导入脚本若是想每次页面发送改变执行,就不能直接用老方式了。

目前 Shoka 加载流程是:

  1. 页面初始化,绑定 DOMContentLoaded siteInit 事件
  2. 在该方法中绑定动态事件,例如 pjax 动态成功加载了新页面的处理 siteRefresh 函数
  3. 在该方法中调用一次 siteRefresh 执行一次手动刷新
  4. siteRefresh 中进行实际页面的刷新处理

所以关键点就在于 pjax:success 绑定的 siteRefresh 函数了。

主题里面的代码肯定是不好去改动的,于是尝试在自己导入的脚本中绑定 pjax:success 事件:

//shoka 版本
const refreshDateTimeOfDay = function () {
    $.each('time', x => {
        if (x.innerText.indexOf("(") > -1) return;
        var date = new Date(x.dateTime);
        var dateNow = new Date();
        var dayCount = parseInt((dateNow - date) / 86400 / 1000);
        var finalStr = dayCount == 0 ? nowStr : dayCount + dayStr;
        x.innerText = x.innerText.concat("(", finalStr, ")");
    });
}
window.addEventListener('pjax:success', refreshDateTimeOfDay)
refreshDateTimeOfDay();

试了下可以用:

那就先这样了。

引用 live2d 的方式也差不多,之前我已经完全分离了 live2d 库,只需要引用脚本就行了。

—— 不过由于 live2d 之前 ui 使用的是 FontAwesome 图标,导致显示不出来。

不可能为了 live2d 专门再导入一个 FontAwesome 库的...... 从 shoka 已有的 Iconfont 图标里找了几个替换一下吧。

# HTML 标签没有被解析

以前使用默认 markdown 渲染器的时候,因为没有修改字体颜色功能,都是直接采用 html 标签如 <font color=red> 静态对象的所有直接光照、间接光照、阴影均烘焙 </font> 这种方式实现的。

然后就发现 shoka 没有解析了,直接被当做纯文本给显示出来。

查了并试了下,是由于 hexo-renderer-multi-markdown-it 渲染器的配置问题,markdown.render.html 字段需要设置为 true—— 作者默认给出的模板是 false,所以作为纯文本显示了。

markdown:
  render: # 渲染器设置
    html: false # 过滤 HTML 标签 ----- true 表示会转义 html 标签,否则作为纯文本

# 压缩插件问题

我这边 hexo-renderer-multi-markdown-it 自带压缩插件用起来有点问题,压缩自己的代码跟 Shoka 的竟然还产生了随机方法名的冲突,换回 hexo-all-minifier 了。

# 引入自定义脚本增加配置功能

上面直接通过直接修改主题模板引入了自定义脚本代码,不过感觉可以将这个修改转移到配置中。
于是仿照 已有的渲染方式,在 主题 _config.yml(_config.shoka.yml) 增加 customJs 配置,例如

customJs:
  - /js/DateTimeAfeterCalc.js

然后在 asset.js 注册方法:

hexo.extend.helper.register('_custom_js', () => {
  const customJs = hexo.theme.config.customJs;
  if (!customJs) return '';
  let str='';
  for(let i in customJs)
  {
    str+= htmlTag('script', { src:  customJs[i] },'');
  }
  return str;
});

最后在 layout.njk 调用:

{ _vendor_js() }
{ _js('app.js')}
{_custom_js()}

如此后续要是还有引用自己的脚本,只需要改 _config.shoka.yml 配置,不会动到主题本身代码这边了。

# 搜索

最后是 algolia 搜索,这个没什么改的,虽然之前也没用过,安装好 hexo-algoliasearch,然后注册一个账号按照建议来就行了 —— 需要注意的是,algolia 配置必须配置到 Hexo 根目录的 _config.yml 中,如果配到了主题如 _config.shoka.yml 配置里边是不行的。

刚开始我就按照惯例配置在 _config.shoka.yml 内,结果发现没什么效果 —— 点按钮也没反应,查了代码发现发现实际是取的 config 配置。

const config = hexo.config;
const theme = hexo.theme.config;
if(config.algolia) {
  siteConfig.search = {
    appID    : config.algolia.appId,
    apiKey   : config.algolia.apiKey,
    indexName: config.algolia.indexName,
    hits     : theme.search.hits
  }
}

改到全局配置里就可以了。

# 总结

本来只是想:

  1. 为旧主题加个目录
  2. 想让目录 (目录同级头像不变) 可以跟随页面移动]
  3. 研究如何处理手机端兼容性问题
  4. 感觉主题不好看
  5. 换哪个?
  6. 研究 ing
  7. 换 Shoka 吧
  8. 研究 ing
  9. 自定义
  10. 研究功能如何实现的
  11. 解决问题
  12. 终于完成了,周末都过去了......

何况之前就早在看,只是周末才开始在实际动手而已,本来打算搞完周末补一下 Unity 内存知识的,现在已经是周日晚上 10 点多 周一 周二了!

花了这么多时间,算是对主题基本框架都有了个认识... 比如:

  • 作者预留了很多自定义的口子: source/_data/ 目录创建对应 yml 或 styl 就可以覆盖主题本来配置和样式 (images.yml 配置也可以在这覆盖)
  • 动态加载功能使用 pjax 实现,自己加脚本想页面初始化执行需要绑对应事件
  • 测试音乐播放器注意歌单缓存问题
  • 引用自定义脚本

虽然自信下一次想改什么肯定能更快找到该改哪里,但忙活这么久,乍一看似乎像是又没改到啥的样子,还是感觉有点惨了。

处理一下收尾该睡觉了,本来总结还打算再写点什么的,算了算了。

弄完搜索和拖动问题,再看了下页面,突然又想把 cover 图片也加上一点蒙版,不过肯定需要降低透明度 —— 虽然 CSS 自带透明度调整,但是一开始我竟然意图通过编辑图片来控制...(ノД`),不熟悉的东西就是这样。另外发现使用 background-attachment css 属性会导致在 cover 上 repeat 不正常,去掉就好了。

再看一眼老博客的样子:

最后,添加了最近评论及自定义脚本配置的: Shoka
添加解析 class='count_数量 ' 为最近评论数量功能的: MiniValine (只改了 *.min.js 那一个解析的地方)

后面再整理一下老文章的标签和分类,这次应该就真搞差不多了。

# 疑问

出现个疑问,而且没研究出来问题:指定随机的图片是如何实现的?

因为 Hexo 是静态的,只可能是后期修改。

然后却又没找到除了 engine.js 生成博客时调用 images.yml 随机图片的接口。也就是说,这个在生成时就应该已经被固定了,而且研究半天,发现动态刷新页面时查询相关接口的数据 (图片链接) 就已经变了。

Imgs 与之前的不一样了

所以要么这个功能是作者放在了其它的已合并的公共脚本中?但是又没发现哪里指定的 images.yml 中的配置图片值。

总不能由 Hexo 动态随机出来的吧?想不通。


经过多方面研究,破案了 —— 这东西还真是实时生成的!

测试方法是给 hexo.extend.helper 多绑一个计算时间的:

hexo.extend.helper.register('_date', function(language) {
  return new Date().toString();
});

然后在生成图片那给加上去:

<li class="item" data-background-image="{ image }" date="{ _date() }"></li>

运行时,这个时间一直在变化:

说明不是静态的!Hexo 什么时候支持这种动态执行生成页面的方法了?

然而我们生成后上传的东的确又是纯静态的东西... 想不明白。


上传的东西确实变成静态的了... 这个随机图片仅限于本地调试的时候。一旦上传就全部固定了...

原来如此,还以为一直随机呢... 花这么多时间来找原因,本地图片实时随机真是让人误会。