惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

宝玉的分享
宝玉的分享
T
Threat Research - Cisco Blogs
H
Hacker News: Front Page
N
News and Events Feed by Topic
Know Your Adversary
Know Your Adversary
Cisco Talos Blog
Cisco Talos Blog
SecWiki News
SecWiki News
C
Cisco Blogs
D
Darknet – Hacking Tools, Hacker News & Cyber Security
T
Tor Project blog
K
Kaspersky official blog
Forbes - Security
Forbes - Security
Webroot Blog
Webroot Blog
Schneier on Security
Schneier on Security
P
Privacy & Cybersecurity Law Blog
H
Heimdal Security Blog
Y
Y Combinator Blog
The GitHub Blog
The GitHub Blog
S
SegmentFault 最新的问题
V
Vulnerabilities – Threatpost
T
Tenable Blog
T
Tailwind CSS Blog
P
Privacy International News Feed
WordPress大学
WordPress大学
大猫的无限游戏
大猫的无限游戏
小众软件
小众软件
博客园 - Franky
Hacker News: Ask HN
Hacker News: Ask HN
Jina AI
Jina AI
C
Cybersecurity and Infrastructure Security Agency CISA
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
雷峰网
雷峰网
Vercel News
Vercel News
A
About on SuperTechFans
爱范儿
爱范儿
Simon Willison's Weblog
Simon Willison's Weblog
AWS News Blog
AWS News Blog
The Last Watchdog
The Last Watchdog
Engineering at Meta
Engineering at Meta
Spread Privacy
Spread Privacy
Security Archives - TechRepublic
Security Archives - TechRepublic
博客园 - 司徒正美
量子位
博客园 - 三生石上(FineUI控件)
J
Java Code Geeks
Hacker News - Newest:
Hacker News - Newest: "LLM"
Recorded Future
Recorded Future
H
Hackread – Cybersecurity News, Data Breaches, AI and More
Martin Fowler
Martin Fowler
Project Zero
Project Zero

微风静语

用 codebase-memory-mcp 给 AI 编程加一层“代码记忆” EchoCraft - 专为独立开发者和小型团队打造的产品官网主题 Halo 站长必备:外链安全与流量分析神器——“链接工具箱”重磅升级! Halo插件开发:java17旧项目迁移至java21项目记录 Halo插件|一个面向创作者的多功能媒体处理工具集 分布式事务详解:从入门到精通 让你的网站初步适配PWA Halo主题 - 微浸:以轻量构建美学,用细节定义体验 深入理解Python爬虫技术:原理、实现与实践 Halo-2.20 新特性:自定义登录页面模版 vue3 中父子组件数据传输踩坑 将你的服务运维面板从宝塔替换为1panel Python实战项目:外星人入侵(源码分享) theme-hao主题适配友链自助提交插件 plugin-artalk 插件保姆级使用教程 plugin-blog-hao 插件部分功能演示 Thymeleaf 模版引擎语法浅记 信息洪流中的自我救赎 mac 系统里如何管理多个jdk版本 memso API 不完全指南 基础语法:从CPU角度看变量、数组、类型、运算、跳转、函数等语法 halo-theme-hao主题:artalk评论区美化 衡量个人成长 halo-theme-hao 主题魔改教程 分享一个魔改的 Artalk 评论系统邮件模版 halo-theme-hao 主题标签记录 开发一个 Halo2.0 的安全条跳转中台插件! Mac 软件推荐:使用 Mac 一年半来,我一直在用的软件! TypeScript教程---面向对象编程 软件版本命名规范 TypeScript教程---基础语法及编译设置 2024年家乡春季的第一场大雪 提问的智慧 基础篇:容器化部署技术 -—docker,从此摆脱多环境配置的苦恼! 普通本科的四年大学教育,给我带来了什么? 因果与平衡 程序的本质:代码是如何被执行的 软件开发者应该具备的基本提问素质 如何编写 Restful 风格的接口 探索力 Springboot入门基础篇 springboot中如何使用注解来实现aop Springboot项目生成接口文档方法 Java 实体代码生成器 lombok 的使用 深入 Java 泛型 分布式版本控制工具 git 的基本使用 如何搭建前后端分离的项目 初识Vue3--令人焕然一新的使用逻辑和代码组织方式! 浅谈前端发展史 为什么要学习数据结构和算法 关于富文本编辑器 wangeditor 在 vue2 项目中的使用
halo 插件开发踩坑记录(一)
webjing · 2024-07-10 · via 微风静语

halo 插件的开发虽然和一般的 Springboot 项目一样,但版本确是 springboot 3.x 的,Web 层不再使用 Servlet 技术,而是充分向异步和非阻塞的反应式编程靠拢,使用 Netty 作为 Web 服务器,使用 Reactor 作为异步编程框架,使用 R2DBC 作为数据库访问框架,使用 WebFlux 作为 Web 层框架。所以好多以前的开发习惯都已经不在适用 halo 插件的开发了,于是在没有专门学习这些新技术的前提下,去开发 halo 插件踩了很多坑,一些特殊语法都是参照已有的 halo 源码和其他插件源码以及 Springboot3.x 文档进行开发的,过程稍微有点一波三折,在此记录一些我开发过程中的踩坑记录

一、插件配置数据的获取

24 年七月初,halo 更新了 2.17.0 版本,对于内容管理方面进行了全面升级,随即导致好多插件不兼容,无法正常访问前台,主要的一个原因就是错误地在 Reactor 中调用 block 方法。而 halo 提供了两个对象获取插件的配置数据,分别是阻塞式的 SettingFetcher 和非阻塞式的 ReactiveSettingFetcher ,一个是同步获取,一个是异步获取。由于有些插件使用 SettingFetcher 获取插件配置数据,所以会导致程序阻塞。因为 Reactor 是基于非阻塞的响应式编程模型,而 block 方法会将异步操作转换为同步操作,从而破坏其非阻塞特性。为了避免这种情况,应该尽量避免在 WebFlux 中使用 block 方法,而是使用其他非阻塞的操作,如 flatMap、map,所以,获取插件配置数据使用 ReactiveSettingFetcher 最好,示例如下:

@Service
@RequiredArgsConstructor
public class SeoService {

    private final ReactiveSettingFetcher settingFetcher;

    public Mono<Void> getBaseConf() {
        return settingFetcher.fetch(SeoSetting.GROUP, SeoSetting.class)
                .doOnNext(baseConfig -> {
                    if (baseConfig.isEnable()) {
                        // do something
                    }
                })
                .then();
    }
}

如果需要将配置数据作为成员变量使用,可以通过订阅消费的方式去使用,也可以使用blockOptional 获取配置对象:

  • 订阅消费的方式使用

    @Service
    @RequiredArgsConstructor
    public class SeoService {
    
        private final ReactiveSettingFetcher settingFetcher;
    
        private BaseConifg baseConfig;
    
        public Mono<Void> getPluginConfig() {
            return settingFetcher.fetch(SeoSetting.GROUP, SeoSetting.class)
                    .subscribe(baseConf -> {
                         baseConfig = baseConf
                    });
        }
    }
    
  • block

    @RequiredArgsConstructor
    @Component
    public class ArtalkComment implements CommentWidget {
    
        private final SettingConfigGetter settingConfigGetter;
    
        @Override
        public void render(ITemplateContext context, IProcessableElementTag elementTag, IElementTagStructureHandler iElementTagStructureHandler) {
    
            var siteConfig = settingConfigGetter.getBasicConfig().blockOptional().orElseThrow();
    
            String siteTitle = String.valueOf(siteConfig.getSiteTitle());
            String artalkUrl = String.valueOf(siteConfig.getArtalkUrl());
    
            // do something 
        }
    }
    // 其中 SettingConfigGetter 是自己定义的抓去配置的方法
    @Component
    @RequiredArgsConstructor
    public class SettingConfigGetter {
    
        private final ReactiveSettingFetcher settingFetcher;
    
        public Mono<Settings> getBasicConfig() {
            return settingFetcher.fetch(Settings.GROUP, Settings.class)
                .defaultIfEmpty(new Settings());
        }
    }
    

建议通过 map、flatMap、doOnNext 等方法操作配置数据。这样以后维护插件的成本比较小。

二、WebClient 对象的使用

在 halo 插件开发中,如果后端需要通过 http 请求去获取一些数据,请不要使用 RestTemplate 去请求,否则在生产环境会导致程序阻塞,具体原因我目前还不是很确定,所以在之后的开发中,我都是使用了 Spring boot 3 中新增的 WebClient 对象模拟客户端进行 API 请求。

WebClient对象是一个非阻塞的、基于响应式编程的网络客户端,用于发送HTTP请求和接收HTTP响应。它是基于 Project Reactor 库的,可以与Spring WebFlux一起使用,以实现异步和非阻塞的网络通信。WebClient 提供了一种简洁的方式来构建 HTTP 请求,并支持多种请求方法(如GET、POST、PUT、DELETE等)。它还支持请求参数、请求头、请求体等的配置,以及响应的处理。下面是一个简单的示例,其中每个方法的作用请参考代码里的注释,其他方法和 API 请参考官方文档。

@Service
@Slf4j
public class ArtalkService {

    private WebClient webClient;

    private Settings pluginSetting;

    private static final String PAGE_COMMENT = "/api/v2/comments";

    private final SettingConfigGetter settingConfigGetter;

    public ArtalkService(SettingConfigGetter settingConfigGetter) {
        this.settingConfigGetter = settingConfigGetter;
        this.pluginSetting = this.settingConfigGetter.getBasicConfig().blockOptional().orElseThrow();   
         //初始化 webclient 对象,设置请求头,baseUrl
        this.webClient = WebClient.builder().baseUrl(pluginSetting.getArtalkUrl())
            .defaultHeaders( headers ->{
                    headers.set("Origin", this.pluginSetting.getAuthDomain());
                    headers.setBasicAuth("Content-Type","application/x-www-form-urlencoded");
            }).build();
    }


    /**
     * 获取指定页面的所有评论
     * @param pageKey 页面唯一标识
     * @return
     */
    public Mono<JsonNode> getSpecialPageComment(String pageKey){
        Mono<JsonNode> jsonNodeMono = webClient.get().uri(PAGE_COMMENT +
                    "?site_name={siteName}&page_key={page}&limit={size}",
                pluginSetting.getSiteTitle(), pageKey, 100) // get 请求参数传递方式
            .accept(MediaType.APPLICATION_JSON)
            .retrieve() //获取响应体
            .onStatus(HttpStatusCode::is4xxClientError, response ->
                response.bodyToMono(JsonNode.class).flatMap(error -> Mono.empty())) //处理客户端的错误
            .onStatus(HttpStatusCode::is5xxServerError, response ->
                response.bodyToMono(JsonNode.class).flatMap(error -> Mono.empty()))//处理服务端的500响应错误
            .onStatus(HttpStatusCode::is5xxServerError, response ->
                    response.bodyToMono(JsonNode.class).flatMap(error -> Mono.empty()))
            .bodyToMono(JsonNode.class); // 接受正确响应之后的数据
        return jsonNodeMono;
    }

}

三、表单定义

表单定义其实也没什么难度,就是根据 formkit 组件 将其转换为 yaml 语法进行使用,但是需要注意的是一些特殊的声明方式,否则就会造成转换异常,或者数据值无法正常映射的问题。

  • 动态显示隐藏相应的组件,使用 if: $get(changeVal).value 进行控制,比如下方的 customCss 字段是通过 enableCustomCss 进行显示隐藏的。

    - $formkit: radio
       name: enableCustomCss
       id: enableCustomCss
       key: enableCustomCss
       label: 是否自定css样式
       value: false
       options:
          - value: true
            label: 开启
          - value: false
            label: 关闭
    - $formkit: code
      name: customCss
      if: $get(enableCustomCss).value
      label: css样式
      value: a{display:inline-block;}
      language: css
      help: 支持css语法样式,无需包裹style标签
      height: 350px
    

$get(changeVal).value 也可以是字符串,比如 $get(changeVal).value == str 来显示隐藏,例如:

- $formkit: select
  name: enableLightDark
  id: enableLightDark
  key: enableLightDark
  label: 暗黑模式适配
  value: false
  options:
    - value: attribute
       label: 明暗切换属性选择器
    - value: single
      label: 独立切换
    - value: disable
      label: 禁用(明暗模式下的中性色,不是很美观,但内容清晰可见)
- $formkit: text
  if: $get(enableLightDark).value == attribute
  name: darkModeAttribute
  label: 选择器名称(严格按照说明的示例格式填写)
  value:
  • 如果要根据一个值的变化改变多组值的显示隐藏,请务必加上 key,负责会导致组件在切换的时候发生渲染错误和数据无法与正确映射的问题。例如这里需要根据 themeStyle 的值切换去渲染相应的组件,也就是 defaultTmplConfig,它下边有两个值,所以为了防止切换过程中和其他的组件发生渲染混乱的问题,要为其加上 key,标识其唯一性。注意不能在其 children 下边的值加上key,这样会导致数据无法正常更新!
     - $formkit: select
       name: themeStyle
       id: themeStyle
       key: themeStyle
       label: 跳转页面模版
       value: jumpGo
       options:
         - label: 默认展示页面
           value: jumpGo
         - label: 极客风格
           value: geekStyleGo
    
    - $formkit: group
      if: $get(themeStyle).value == jumpGo
      name: defaultTmplConfig
      key: defaultTmplConfig
      label: 默认模版参数配置
      value:
         staticBgImg: "https://files.codelife.cc/wallhaven/full/ey/wallhaven-eyzx5k.jpg"
         awaitTime: 6
      children:
         - $formkit: attachment
           name: staticBgImg
           label: 自定义背景
           value: https://files.codelife.cc/wallhaven/full/ey/wallhaven-eyzx5k.jpg
           help: 建议换成自己自建图床的图片url
         - $formkit: number
           name: awaitTime
           label: 倒计时时间,单位S
           value: 6
    

四、使用扩展点的注意事项

如果需要使用 halo 提供的扩展点,比如 TemplateHeadProcessorTemplateFooterProcessor 时,注入一些插件中获取的数据的时候,请先处理一下空数据,否则会有空指针异常导致的前台模版无法渲染问题,一般我都会在插件配置数据中提供一个默认值,或者在程序中进行判断。

对于评论区的扩展,在 2.17.0 版本之后,请声明一下自定义模型文件,否则用户在使用的时候是无法在后台的插件扩展点配置中选择评论系统,其他扩展点亦是如此。例如,声明一个类 ArtalkComment 对评论进行扩展,则需要下边的 yaml 文件进行声明。

@RequiredArgsConstructor
@Component
public class ArtalkComment implements CommentWidget {

    private final SettingConfigGetter settingConfigGetter;

    @Override
    public void render(ITemplateContext context, IProcessableElementTag elementTag, IElementTagStructureHandler iElementTagStructureHandler) {

        var siteConfig = settingConfigGetter.getBasicConfig().blockOptional().orElseThrow();

        String siteTitle = String.valueOf(siteConfig.getSiteTitle());
        String artalkUrl = String.valueOf(siteConfig.getArtalkUrl());

        iElementTagStructureHandler.replaceWith(moderateTemplateResolve(siteTitle, artalkUrl), false);
            }
        }
    }
}

扩展点声明文件:

apiVersion: plugin.halo.run/v1alpha1
kind: ExtensionDefinition
metadata:
  name: artalk-comment
spec:
  className: xin.wenjing.halo.artalk.ArtalkComment
  extensionPointName: comment-widget
  displayName: "artalk评论组件"
  description: "由作者 dreamChaser的小屋 提供的第三方评论组件"

五、插件内置模版路由的使用

首先,你需要在插件的 resources 目录下创建一个 templates 目录,然后在 templates 目录下提供你的模板,这个模版不仅可以使用你在渲染时提供的数据,而且可以使用全局变量 sitetheme ,参考文档:https://docs.halo.run/developer-guide/theme/global-variables#theme。例如,下边是一个更具配置参数动态切换模版文件和动态注入变量的一个示例:

@RequiredArgsConstructor
@Configuration(proxyBeanMethods = false)
public class LinkSecurityDetectRouter {

    private final TemplateNameResolver templateNameResolver;

    private final ReactiveSettingFetcher settingFetcher;

    private final PluginContext pluginContext;

    @Bean
    RouterFunction<ServerResponse> momentRouterFunction() {
        return RouterFunctions.route().GET("/jumpGo",this::renderTermialPage).build();
    }

    Mono<ServerResponse> renderTermialPage(ServerRequest request) {
        var model = new HashMap<String, Object>();
        Settings.getTipMsgConfig(this.settingFetcher).subscribe((tipMsgConfig)-> model.put("tipMessage", tipMsgConfig) );
        return this.settingFetcher.fetch(Settings.BaseConfig.GROUP, Settings.BaseConfig.class).flatMap( baseConfig -> {
            String themeStyle = baseConfig.getThemeStyle();
            model.put("version", pluginContext.getVersion());
            // 各个模版的独有变量注入
            if("jumpGo".equals(themeStyle)){
                model.put("backgroundImage", baseConfig.getDefaultTmplConfig().getStaticBgImg());
            }else if("wave".equals(themeStyle)){
                System.out.println(baseConfig.getWaveTmplConfig().getColorValOne()+ "=======" + baseConfig.getWaveTmplConfig().getColorValTwo());
                model.put("colorOne", baseConfig.getWaveTmplConfig().getColorValOne());
                model.put("colorTwo", baseConfig.getWaveTmplConfig().getColorValTwo());
            }
            model.put("browserTitle", baseConfig.getBroswerTitle());

            return templateNameResolver.resolveTemplateNameOrDefault(request.exchange(), themeStyle)
                .flatMap(templateName -> ServerResponse.ok().render(templateName, model));
        });
    }

}

暂时就记录这么多吧,还有一些平时踩过的坑忘了记录了,后续我会慢慢的在进行一个补充完善,希望可以帮助到初入 halo 插件的开发者们。