侧边栏壁纸
博主头像
贯耳症博主等级

瓜虫冬匕ing……

  • 累计撰写 68 篇文章
  • 累计创建 49 个标签
  • 累计收到 892 条评论

目 录CONTENT

文章目录

Halo 博客改造备忘

贯耳症
2023-02-15 / 1 评论 / 6 点赞 / 2,494 阅读 / 4,738 字
温馨提示:
本网站有 CDN 缓存,一般刷新 3 次左右即可获取最新页面。

也不知自己写过多少个博客系统了,长时间不打理就忘了该怎么继续打理了。之前用的 Halo 倒还挺好用,可惜自己调整的代码都丢了,只能重新添加了。这次吸取教训,把需要的东西都记录下来,防止再忘掉。

首先创建本地分支 halobobo,用于自己对博客系统的个性化改造。同时在 Halo 有新版本时能够正常更新,同时不影响自己修改的代码(待验证)。

本地开发时直接通过 IDE 启动项目,用户信息采用推荐配置。

key value
用户名 test
昵称 test
邮箱 test@test.com
密码 opentest

数据库初始化完成后,打开首页,会报主题未找到的错误,需要手动下载主题。

在管理页面找到 外观 - 主题 - 安装主题,选择远程下载,输入https://github.com/qinhua/halo-theme-joe2.0.git后开始下载。

若下载速度过慢,可尝试https://gitee.com/duider/halo-theme-joe2.0.git

主题内容较多,初次下载大概用时两分钟。安装完成后点击启用,就可以正常打开首页了。

这个主题有黑暗模式,但不会自动切换,抽时间得给它解决了!

日志改造:支持微信公众号发文

日志本来就是随时随地记录自己心情的地方,结果每次想写点什么,还要登录后台,实在是毁心情。所以能直接在微信公众号操作,是我觉得最理想的记录方式了。

首先锁定JournalController类,这是日志记录的 API 接口,后续计划新建个接收微信消息的接口,然后做一下日志的记录,连接以及上传图床的工作。

公众号写日志方式:提供两个菜单按钮,叫写日志和写完了。同时后台要配置可以记录日志的 openid,在用户发来消息时判断,非指定用户则返回最新一条消息(去 markdown 格式),指定用户则开启记录模式,用户上次发送消息 30 分钟内的都存在一条日志里,直到 30 分钟过期或用户发送写完了的指令。

同时要支持发送图片、视频、音频,并自动上传至图床,并按照特定格式保存(主题读取到后可转换为自己使用的文字,即需要对主题进行改造)。

通过 gradle 引入依赖 implementation "com.github.binarywang:weixin-java-mp:$weixinJavaVersion",然后根据 wiki 接入微信公众平台。

MacMini 中,通过命令行进入/Users/zhengbo/Workspace/env/frp_0.36.2_darwin_arm64路径,输入./frpc -c frpc.ini开启内网穿透。

打开 finder,按Control+Shift+.显示隐藏文件,找到/Users/zhengbo/.halo//db/halo目录,新建application.yml并配置:

wx:
  mp:
    useRedis: false
    redisConfig:
      host: 127.0.0.1
      port: 6379
      timeout: 2000
    configs:
      - appId: 12ss
        secret: jjjjjj
        token: ffff

然后根据https://gitee.com/binary/weixin-java-mp-demo-springboot的示例项目编写消息接收接口(基本将示例项目所有类都 Copy 过来了)。

在将 WxMenuController 复制过来后,浏览器访问http://mapi.frp.aqzscn.cn/wx/menu/wxc65b34467bd05206/create即可创建默认公众号菜单。

joe2.0 主题采用了按需加载的方式,如果想要开启音乐播放器组件,需要在主题配置中同步开启 侧边栏、音乐播放器和歌单 id(这个个人认为没必要,导致文章界面想用都用不了)。

经过一番忙活,发送给公众号的文字、图片、声音和视频都可以被保存并正常显示了,基本算是实现了随时随地发动态的功能。但在打包项目到发布的过程中,还是遇到了一些问题,需要特殊处理才行。

首先是 checkStyle 插件会阻断打包,所以需要调整代码风格,但有些风格我想和 Idea 默认生成的代码保持一致,所以就需要删掉一些 checkStyle.xml 中的配置,最后保证不会被 ERROR 阻断即可,WARN 则可以忽略,因为 halo 自己的代码就有近一千个 WARN。

要想查看报错类型是 WARN 还是 ERROR,通过生成的网页是做不到的,需要调整build.gradle的配置,将showViolations打开,处理完错误后再关闭。

checkstyle {
    toolVersion = "8.39"
    showViolations = true
    ignoreFailures = false
}

除此之外,打包时进行的单元测试会占用很长时间,且不一定能成功,所以就直接在build.gradle中注掉了这几行:

//test {
//    useJUnitPlatform()
//}

此时再次双击build打包,就会在build/libs下生成两个 jar 包,其中不带-plain的就是可以运行的 jar 包。

此时虽然可以打包并运行,但在安装主题时会报错版本过低,主要原因就是gradle.properties里的版本号里有SNAPSHOT,导致以数字比较版本号的逻辑失效了,所以需要去掉SNAPSHOT后重新打包。

小插曲 1 AMR 转 MP3

微信公众号发来的语音是 AMR 格式的,前端无法直接播放,需要转为 MP3,工具包用的是implementation "com.github.dadiyang:jave:$javeVersion",但奇怪的是1.0.6版本并没有发布到 maven 仓库,所以只能用1.0.5版本了,还好能够正常转换,相关代码如下:

file = weixinService.getMaterialService().mediaDownload(wxMpXmlMessage.getMediaId());
// 微信语音是amr格式,前端无法直接播放 需转换为mp3存储
if (file.getName().endsWith(".amr")) {
    File temp = File.createTempFile(file.getName().substring(0, file.getName().length() - 4), ".mp3");
    AudioUtils.amrToMp3(file, temp);
    this.logger.info("covert {} to {} successfully", file.getName(), temp.getName());
    attachment = attachmentService.upload(FileUtils.getMultipartFile(temp, MediaType.MULTIPART_FORM_DATA_VALUE));
} else {
    attachment = attachmentService.upload(FileUtils.getMultipartFile(file, MediaType.MULTIPART_FORM_DATA_VALUE));
}

另外就是 Halo 里负责附件上传的是AttachmentService而不是StaticStorageService,上传时接收的文件类型是MultipartFile,这是 Spring 定义的接口,需要转换为具体的实现类,网上提供了不少思路,用MockMultipartFile应该比较简单,可惜build.gradle里把spring-test定义到测试依赖中去了,所以只能利用CommonsMultipartFile进行转换了,以下是我选择使用的工具方法,更多方法可参考java 中 File 转为 MultipartFile 的四种方式

/**
 * File 转 MultipartFile
 *
 * @param file 文件
 */
public static MultipartFile getMultipartFile(File file, String mediaType) {
    FileItem item = new DiskFileItemFactory().createItem("file",
            mediaType, true, file.getName());
    try (InputStream input = new FileInputStream(file);
         OutputStream os = item.getOutputStream()) {
        // 流转移
        IOUtils.copy(input, os);
    } catch (Exception e) {
        throw new IllegalArgumentException("Invalid file: " + e, e);
    }

    return new CommonsMultipartFile(item);
}

新增功能:定时更新背景图片

joe2.0主题可以设置背景图片,且可分别设置浅色和暗色主题的背景,我又不想每天打开网站都是同样的背景,所以需要利用定时任务,随机从 upsplash 或其他图片网站下载图片,存储到固定的链接上。

以上是全部修改内容,关键是WallpaperRefreshTask这个类,里面是更新壁纸的主要逻辑。

首先需要到Unsplash注册自己的账号,并创建一个应用。

然后打开开发者文档,里面有各个 API 的使用方式,这次要用的就是Get a random photo

这个接口属于公共请求,因此授权方式比较简单,直接在链接后面加上client_id这个参数就可以了,参数值是刚刚创建的 APP 的 AccessKey。

接着请求https://api.unsplash.com/photos/random?client_id=XXX这个地址,会随机得到一个图片的信息,其中urls节点里就是我们需要的地址,里面有各种分辨率,我觉得full的清晰度已经能满足要求了,大小大概在 3M 左右。

接下来获取这个图片文件,并通过AttachmentService上传到配置好的存储服务中。

这里有一个坑就是我需要上传后的文件名是固定的,但到最后总会在我指定的文件名后面加上一串随机数字。一开始我以为是MultipartFile这个类搞的鬼,因为前面在做公众号资源上传的时候也是有随机数字出现的,当时恰好需要这个功能所以就没细究。

最后经过一番倒腾之后才发现,是创建临时文件File.createTempFile()时就有的随机文件名,知道了原因后就比较好改了。

首先通过HaloProperties拿到系统配置保存路径,然后在里面创建文件夹temp,并使用真实文件保存输入流即可,关键方法如下:

private MultipartFile downloadImage(String imageUrl, String targetFilename) throws IOException {
    ResponseEntity<Resource> responseEntity = restTemplate.getForEntity(imageUrl, Resource.class);
    if (responseEntity.getStatusCode().is2xxSuccessful() && responseEntity.getBody() != null) {
        // 使用临时文件会有随机数字后缀,因此需创建真实文件
        File temp = new File(haloProperties.getWorkDir() + "/temp/" + targetFilename);

        InputStream inputStream = responseEntity.getBody().getInputStream();
        org.apache.commons.io.FileUtils.copyInputStreamToFile(inputStream, temp);
        return FileUtils.getMultipartFile(temp,
                Objects.requireNonNull(responseEntity.getHeaders().getContentType()).toString());
    }
    return null;
}

最后就可以看到上传到 OSS 的两张背景图了,从文件大小来看似乎有点大,但毕竟是通过 OSS 访问的,速度应该不会太慢,后面看看是否会影响博客的整体加载速度吧,可能得加上图片压缩的流程。

新增功能:动态图片预处理

因为这个博客系统的日志并不是类似朋友圈的那种一段文字加几张图片的固定格式,而是可以无限续写的 Markdown 格式。

这个 joe2.0 主题又给这个页面加入了超出指定高度自动折叠的功能,比如手机拍的照片,根本无法完全显示就被折叠了,还是很影响使用体验的。

所以一方面要把指定高度设置得高一点,另一方面需要调整一下图片大小。

高度可以在主题中进行配置,图片缩放本想用代码实现的,但想到我使用的阿里云是有图片处理功能的,于是乎去捣鼓了一番,配置好图片处理规则后,命名为journal,然后在公众号上传图片之后给返回的图片 URL 加上?x-oss-process=style/journal参数,就可以得到处理过后的图片,而且原图还在 OSS 上保存着,想看更清晰的图片也是没问题的。

这次代码的修改量并不大,仅仅在配置文件增加了一个参数,然后在上传图片的访问地址处加上了这个参数值。

修复 BUG 生成的图片无法覆盖原文件

20220606

本打算今天看看新背景什么样子的,结果发现还是昨天的壁纸,到 OSS 一看,发现昨晚获取的图片都被加上时间戳了,而且时间戳前还多了一岗一杠,那就应该不是临时文件名了。

因此猜想是 Halo 系统预设了重名文件自动加后缀的功能,今天回来一看果然如此,就在AliOssFileHandler这个类里:

FilePathDescriptor uploadFilePath = new FilePathDescriptor.Builder()
    .setBasePath(basePath.toString())
    .setSubPath(source)
    // 就在这里开启自动重命名
    .setAutomaticRename(true)
    // 这个是检查文件名是否重复的方法
    .setRenamePredicate(relativePath - >
        attachmentRepository
        .countByFileKeyAndType(relativePath, AttachmentType.ALIOSS) > 0)
    .setOriginalName(file.getOriginalFilename())
    .build();

通过以上代码可以看出文件名重复的判断是通过查询数据库获得的,所以要想不让它加后缀,就在上传文件前先删掉对应的数据库记录就可以了.

以下是代码修改部分:

// AttachmentRepository
Attachment findAttachmentByFileKeyAndType(@NonNull String fileKey, @NonNull AttachmentType type);

// WallpaperRefreshTask
private AttachmentType getAttachmentType() {
    return Objects.requireNonNull(optionService
        .getEnumByPropertyOrDefault(AttachmentProperties.ATTACHMENT_TYPE, AttachmentType.class,
            AttachmentType.LOCAL));
}

// 如果附件已经存在,则先删除后再上传
String source =
    optionService.getByPropertyOrDefault(AliOssProperties.OSS_SOURCE, String.class, "");
Attachment ath = attachmentRepository.findAttachmentByFileKeyAndType(source + "/" + targetFilename, getAttachmentType());
if (ath != null) {
    attachmentService.remove(ath);
    log.info("{} has been removed", ath.getPath());
}
Attachment attachment = attachmentService.upload(multipartFile);

导航条标题配置

导航条此处的文字内容是可以调整的,但不是在主题设置中,而是在菜单设置中。

除了可以调整文字之外,图标也是可以调整的,详见图标列表

日志为例,在图标列表中切换到Font class选项卡,找到的图标名为.joe-icon-riji,但不能直接这样复制粘贴过去,而是需要在图标栏类似这样去填:joe-font joe-icon-riji,即要先指明要使用的字体图标是哪个。

这就是配置之后的效果。

CDN 加速

因为自己的服务器基本就自己一个人用,所以配置就很低(配置高的也买不起),导致干啥速度都不快。

但毕竟是有了一个看着还过得去的门面了,咋说也得拯救一下它的访问速度,这就要靠 CDN 的助力了。

CDN 其实以前也用过,但最多就是用一些公共 js 库的 CDN,没有对自己的项目加速过。

CDN 加速的原理也简单,就是用户发起请求时,由离用户最近的节点来响应数据。数据其实还是这个节点从源站获取的,但这个节点会做一个缓存,在缓存时间内就不需要再向源站请求资源了。所以配置 CDN 后不仅能提升用户的访问速度,还能缓解源站的资源压力。

我这小破站有个屁的资源压力。。。

既然云服务器都是在阿里云搞的,CDN 自然也在阿里云搞会方便点。因为图片资源大部分已经由 OSS 接管了,所以 CDN 加速的也就是一些静态的 HTML 和 JS 了,一年也不会用多少的,所以选择了 100G/20 块/年 的资源包。

如果自己的网站是 HTTPS 加密的,开启 HTTPS 加密还得花钱,但好像按请求次数是 1 万次 0.0028 元吧,我只能说让他恰让他恰了。

资源包购买之后,进入 CDN 管理控制台,分别添加三个域名,其中两个是为了让主页可以正常访问的,另外一个是自己的内网穿透服务的泛域名。

*.aqzscn.cn这个泛域名为例,需要在基本配置中添加源站信息,端口号填写源站监听的端口,一般都是 80.

除此之外,还需要在域名解析中删掉原来泛域名的 A 记录,改成 CNAME 记录。

等待一段时间后,CDN 检测到域名解析生效后,就可以在缓存配置中设置要缓存的资源和缓存时间了。

为了进一步提升性能,还可以在性能优化中开启静态资源优化和文件压缩功能。

在 HTTPS 证书配置中,我没有采用阿里云的服务,而是使用OHTTPS的服务,一是免费,二是证书过期后可以自动更新。在 Let’Encrypt 支持泛域名证书之后,证书管理就更加方便了,现在我只需要配置管理两个泛域名证书就可以了。

目前自己网站首页基本上 3 秒内就能加载完毕,而且 CDN 资源包的消耗量也不大。

如果源站已经开启 HTTPS 的话,最好给关掉,只暴露 80 端口就可以了。

日志封存

虽然自己的小破站至今没什么人气,但万一哪天真会有很多人来看呢?那自己以前写的很多日志被翻个底朝天,说不定翻到我说的啥见不得人的话,那该有多难堪?

所以我想着需要设定一个门槛,既能不让别人看,又能在自己想看的时候轻松看到。

可是这个博客系统设计之初就是单用户设计,没办法通过用户权限来拦截。那就没什么好办法了,只好设置一个访问密码来控制了。

2.0 版本的 Halo 虽然是多用户设计,但开发目标变成了建站系统,感觉有点宽泛了,至今都还没把博客系统的所有功能补充完。现在我自己改造的 1.6 版本我用得也挺好,没什么升级的动力,所以还是就在现有代码上缝缝补补接着用吧!

经过一番对 Halo 开发文档的翻阅,最后决定用纯前端控制的方式,这样做改动量是最小的。不过也很容易被懂代码的人给破解,没办法,防君子不防同行~

首先把halo-theme-joe2.0的源码 fork 并下载下来,放到开发环境的~.halo/templates/themes目录下面,用 VSCode 打开,安装推荐的两个插件后,修改settings.yaml,新增两个主题配置:

打开开发环境的管理后台,在主题配置的日志页即可看到新增的两个配置:

改为自己需要的值后,分别修改以下几个文件:

  • journals.ftl
  • source/css/global.less
  • source/js/journals.js
  • template/macro/lock.ftl

经过一番捣鼓后,就实现了这样的效果:

时光回溯

虽然不想让人看到所有的日志,但又不想完全不给别人看,就是这种矛盾的心理,催生了这个仿照 QQ 空间的功能——那年今日。

这个功能不算复杂,就是每天在后台随机缓存一篇公开日志,显示在日志的第一页中。

最终效果如下:

虽然简陋,但甚得我心。

6

评论区