掘金 后端 ( ) • 2024-05-12 18:05

趁着周末闲隙,又给自己的项目加个小小的功能✨✨,想着如果每个项目都要维护自己的一套公告系统,不如构建一个公共的管理体系,让不同子系统进行接入,感兴趣的小伙伴可以参考下述构建思路,试着搭建一套属于自己的通用公告系统体系。

项目介绍

构建通用动态公告系统,后端管理员统一维护公告信息,对外提供接口根据域名或者其他参数配置构建通告联系(关联通知对象,例如根据域名区分子系统等)。前台通过引入通用公告SDK组件(请求调用后台接口获取通知,封装弹窗组件获取公告信息),不同子系统接入只需要一行代码的形式即可完成接入。

项目源码

发布效果

管理员后台创建消息通知,前端(自定义页面)刷新页面触发消息通知弹窗

image-20240510094318549

模块设计

1.数据表设计

根据业务场景设计一个最基础的通知信息体(动态公告)

create table notification
(
    id         bigint                           comment 'id' primary key,
    title      varchar(255)                       not null comment '公告标题',
    content    varchar(2048)                      not null comment '公告内容',
    startTime  datetime                           null comment '开始时间',
    endTime    datetime                           null comment '结束时间',
    status     tinyint  default 0                 not null comment '0: 关闭,1: 启用',
    domain     varchar(255)                       null comment '域名',
    createTime datetime default CURRENT_TIMESTAMP null comment '创建时间',
    updateTime datetime default CURRENT_TIMESTAMP null on update CURRENT_TIMESTAMP,
    creater     bigint                            null comment '创建者',
    updater     bigint                            null comment '修改者',
    isDelete   tinyint  default 0                 not null comment '是否删除'
);

2.实现说明

动态公告核心构建思路:

  • 后台:对内维护公告信息,对外提供调用接口获取最新的公告信息
  • 前台:通过引入自定义SDK,调用弹窗信息

动态公告引入概念:构建一个通用的公告系统,可以为每个系统接入相应的通知体系(设定相应的域名或者引入租户概念区分子系统),通过指定域名等必要参数获取到相应通知信息。提供公告系统后台管理模块,进行公告信息维护。前台则通过构建前端SDK的模式提供给各个系统使用,子系统只需要接入相应的JS,触发页面刷新或者提供相应的组件按钮触发即可获取通知内容

  • 后台:通知/公告信息管理(按照现有项目模板构建公告的CRUD操作)

    • 实现公告信息管理(基础的CRUD操作)
    • 对外暴露一个接口用于获取通知信息(根据场景灵活适配),交由前端SDK进行调用触发
  • 前台:

    • 抽离公共的实现,前端SDK构建=》调用后台接口、获取通知弹窗

构建步骤

后台实现

后台管理部分按照数据表结构完成基础的CRUD操作,随后提供一个获取通知的接口(例如此处实现为根据domain获取最新的通知信息)

参考Mapper层的SQL实现:

        SELECT
            nt.id,
            nt.title,
            nt.content,
            nt.startTime,
            nt.creater,
            nt.updater,
            nt.domain,
            nt.isDelete,
            nt.createTime,
            nt.updateTime,
            cu.userName "createrUserName" ,
            cu.userAccount "createrUserAccount",
            uu.userName "updaterUserName",
            uu.userAccount "updaterUserAccount"
        FROM
            notification nt
                LEFT JOIN `user` cu ON cu.id = nt.creater
                LEFT JOIN `user` uu ON uu.id = nt.updater
        where nt.domain = #{domain}
        order by nt.updateTime desc limit 1

前台实现(构建前端SDK)

创建vite项目进行构建:npm create vite@latest 根据提示初始化项目,然后选择一个第三方弹窗库实现效果,参考网站BootCDN,可以选择一个体积比较小、样式精美的组件库,此处选用swaeetalert2

image-20240501154506034

npm create vite@latest,创建一个react项目,配置细节参考Vite官方文档

创建完成更新依赖并启动项目:npm installnpm run dev,启动项目初始化访问主页默认是如下页面

image-20240501155848426

项目启动没有问题,此处则可进一步构建自己的SDK,先导入所需要的弹窗依赖sweetalert2

导入sweetalert2

npm install sweetalert2

构建main.tsx(或者是main.jsx)具体看vite构建的时候选择基于的语法规则是什么

# 原有模板生成
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.tsx'
import './index.css'
​
ReactDOM.createRoot(document.getElementById('root')!).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
)

此处需注意,方法定义和方法触发,如果说默认加载js的时候触发弹窗,则在此处定义中调用一次方法进行弹窗显示alertNotification();,如果加载js后发现没有弹窗显示(F12刷新页面确认是否触发请求接口,如果没有请求则说明方法没有触发,进一步检查js定义)

此处在实现的时候本地将通知id+时间戳进行存储,用于避免一些通知重复弹窗。实现核心是在页面加载的时候从后端获取最新更新的通知信息,然后使用SweetAlert2进行弹窗显示,并将通知ID和更新时间存储到本地的localStrorage中,以避免重复提示。考虑到公告更新后localStrorage不会更新的问题,将key设置为id+时间戳(updateTIme)的格式,进而使得每次更新公告之后,js会请求校验然后更新key进而实现最新消息的弹窗提醒。参考实现如下:

# 实现参考(直接替换掉main.tsx)
import Swal from "sweetalert2";
​
function alertNotification() {
    /**
     * 后端地址(本地)
     */
    // const BACKEND_HOST_LOCAL = "http://localhost:8101";
​
    /**
     * 后端地址(线上)
     */
    // const BACKEND_HOST_PROD = "http://xxx.xxx";
​
    function getNotificationVoUsingGet(params) {
        const url = `http://localhost:8101/api/admin/cms/notification/getNotificationVOByDomain`;
        return fetch(url + "?" + new URLSearchParams(params))
            .then((response) => {
                if (!response.ok) {
                    throw new Error("Network response was not ok");
                }
                return response.json();
            })
            .then((data) => data)
            .catch((error) => {
                // 处理错误
                console.error("Fetch request error:", error);
                throw error;
            });
    }
​
    const fetchNotification = function (domain) {
        // 发起请求获取通知信息的逻辑
        getNotificationVoUsingGet({ domain })
            .then((response) => {
                const data = response.data;
                const id = data.id;
                const updateTime = data.updateTime;
                if (
                    !localStorage.getItem(id + updateTime) &&
                    data.title &&
                    data.content
                ) {
                    // 使用 SweetAlert2 显示弹窗
                    Swal.fire({
                        title: data.title,
                        text: data.content,
                        icon: "info",
                        confirmButtonText: "知道了",
                    });
​
                    // 存储到 localStorage
                    localStorage.setItem(id + updateTime, "id");
                }
            })
            .catch((error) => {
                console.error("Fetch request error:", error);
            });
    };
​
    const url = new URL(location.href);
    const domain = url.hostname;
    fetchNotification(domain);
}
alertNotification();

SDK项目打包

通过npm run build方式打包项目,然后将生成的dist/assets/下的js文件进行上传(例如上传到腾讯云COS或者其他云存储进行测试)

PS:如果打包提示一些语法的问题,可能是eslint强校验导致,新手可以关闭强校验(修改tsconfig.json的strict:false进行关闭),或者严格规范代码编写的语法规则。打包成功之后可以看到生成js文件(dist/assets文件夹下的js),然后将其上传到云进行测试

子项目引用

方式1(全局配置):react项目,通过配置config/config.ts文件,在headScripts中加载js文件

image-20240509173906528

方式2(局部加载):在指定的页面,通过useEffect设定在页面初始化的时候加载js文件,或者通过按钮组件触发加载

useEffect(() => {
    const script = document.createElement('script');
    script.src = 'https://cos.holic-x.com/publish/index-D523rTEB.js';
    script.defer = true;
    document.head.appendChild(script);
  }, []);

方式3(触发加载)todo:例如定义一个获取通知的按钮,在页面自动触发加载节点信息(此处需要前端SDK配合封装,对外提供一个可触发的方法,目前现有的实现是直接加载js节点触发弹窗效果)

子项目启动访问

此处触发的条件是通过解析域名去获取参数信息,可以查看请求接口的内容,然后对照要实现的接口。例如此处前端请求js,访问后台接口(根据域名获取通知信息)

测试后台接口:http://localhost:8101/api/admin/cms/notification/getNotificationVOByDomain?domain=localhost

前端加载JS查看是否正常请求后端接口:确认接口返回信息是否正常

image-20240509175343384

通知弹窗确认

接口默认请求获取指定域名最近的一条通知记录(可以设定通知记录的开启/关闭状态,进而控制通知信息是否要触达用户前端),如果存在通知信息,则加载数据

前端SDK发布

目前实现是将sdk放到云存储上,但为了可以更方便地使用sdk,此处可以将sdk发布到npm,随后确认发布版本。

然后通过引入线上地址在浏览器中进行查看,也可在项目中直接引用发布的地址接入sdk

SDK项目发布

# 确认npm的镜像源
npm config get registry
​
# 切换默认镜像源
npm config set registry https://registry.npmjs.org/
​
# 登陆(输入npm login指令提示跳转页面,然后注册、登录账号即可)
npm login
​
# 注册成功返回到terminal,进入到要发布的项目目录
npm publish
​
# 确认发布信息,如果发布不成功则进一步确认提示

此处发布不成功,是因为package.json默认配置了私有,需要移除相关配置

image-20240509191209401

发布成功之后,登录官网查看发布信息(点击右侧头像=》查看packages)

image-20240509191310100

image-20240510084551805

SDK优化

基于上述的步骤,已经可以初步完成一个公共的通告系统框架核心,但是如果说这个前端SDK需要提供给外部使用,则要进一步优化SDK的构建方式,尽量在不影响基础功能的前提下,将打包体积进行压缩,提升用户体验。

方案1:引入Terser(强力压缩插件):JavaScript解析器、转换器和压缩工具,可以用于压缩、混淆、美化JavaScript代码。在项目中使用Terser一般是为了减小JavaScript体积,提高网页加载速度

# 安装Terser插件
npm install terser --save-dev
​
# 使用Terser进行压缩的配置参考

默认打包方式参考:77958字节(磁盘上的82KB),考虑第三方库本身比较大,所以打包后可能差距并不是特别的大

方案2:将引入方式调整为引入最小的组件库sweet2alert.all.min.js(保证基础功能的基础上构建项目)

方案3:尝试引入其他体积更小的组件库(例如sweetalert1,但是目前该库版本已经不再维护且版本非常少),但是可以尝试将其引入并构建测试(在不影响基础功能的基础上可以使用即可,相应要调整sweetalert的属性API的弹窗语法规则)