用一个月的时间写一个自己的博客系统——Grtblog的技术介绍

grtsinry43
12/14/2024(更新于 1/28/2025
743 views
预计阅读时长 14 分钟

AI Summary

Powered By DeepSeek-R1

代码如星斗织梦,Bug 似迷雾缠身
终见破晓启新章,全栈之路始成真


项目总览

作者历时月余重构个人博客系统,完成首个全栈中型项目Github | 文档),融合复杂业务逻辑与用户体验优化。系统采用 Next.js 实现 SSG/SSR 混合渲染以平衡 SEO 与性能,后端基于 Spring Boot 生态(含微服务与权限管理),推荐算法通过 FastAPI + Word2Vec 实现,搜索依赖 ElasticSearch,中台管理选用 Umi.js + Ant Design Pro

技术亮点

  1. 实时在线统计:通过 socket.io 监听页面事件,Java 后端动态管理聊天室,TS 前端同步人数状态,后因 CDN 问题修复(2025-01-05 更新)。
  2. 动态标签云:结合 framer-motion 动画库与随机算法,实现流动标签背景效果,代码简洁却富有表现力。
  3. 架构设计:前后端分离,数据库暂用 MySQL(计划迁移至 PostgreSQL),注重缓存策略以降低服务器压力。

挑战与展望

  • 现存问题:WebSocket 性能波动、推荐逻辑待完善、移动端适配需优化。
  • 踩坑记录:水合不匹配、OAuth2 集成、Next.js 代理配置等难题已整理为后续更新内容。
  • 成长印记:从“玩具项目”到独立完成复杂系统,作者在技术深度与工程思维上实现跨越,未来志于深耕前端领域。

代码铸就星辰海,步履不停向光行
纵有千坑犹未惧,且将热血化长明

终于,历时一个多月的开发 bug 和测试,这个目前问题很多很不成熟很难用的系统终于上线了...也了结了我一直以来重构这个网站的小愿望,算是我的第一个全栈中小型项目,之前的感觉大多都是小型玩具项目,虽然也给学校学了好多项目,有几万用户和几千并发,但是这次算是略微复杂的业务逻辑,还要考虑用户体验,尤其是我还为了这个才学得怎么用 Next.js

(更新于2025-01-05,socket果然是cdn的问题,已经能正常用了

写在前面

还是挺多次提交的

总之就是,它还是如愿成功上线了,也证明这个我曾经天马行空的想法是可以实现的。目前还仍旧有许多问题,比如 WebSocket 似乎由于性能问题无法正常工作(在启用 ES 的第二天就出问题了,感觉是性能问题),点赞和推荐的逻辑还没有想好怎么实现,推荐只是留了接口还没有正式应用...开发的过程中遇到的问题还是很多的,接下来考虑慢慢更新一些,如果你也遇到了相关问题不妨看看我帮你踩好的坑。

技术选择

这是一个开源项目,地址在 Github

,我会长期进行维护。

项目地址

文档在grtblog

文档网站

它采用了前后端分离的方式进行开发,

为了平衡 SSG 和 SSR 的优势,我选择了 Next.js 框架用于构建用户界面。这样可以在构建初期对于一些不频繁变更的内容生成静态页面,当然也可以增量生成。对于动态内容较多的页面也会采取 SSR 来保证内容网站的 SEO 完整,此外,构建页面时的请求结构能够被缓存,这样既能加速构建,也能减轻服务器压力。

后端方面采用了生态极佳性能也比较优秀的 Spring Boot,权限采用 Spring Security,使用 Spring Cloud 调用周边微服务等等,数据库选择了 mysql,后期计划迁移到我更偏爱的 postgresql

对于用户推荐部分,我用 word2vector 简单弄了一个推荐算法,使用 FastAPI,由主框架使用 API 调用;搜索则使用 ElasticSearch 实现,由主框架调用。

中台管理为了方便目前选择的阿里的 Umi.js + Ant Design Pro

一些细节

实时在线人数

悲,首先这个不知道为什么用不了了

这个采用了socket.io实现,原理就是加入聊天室传递信息,每次链接会发送消息,用定时任务防抖一下就好

其后端核心代码是这样,在用户进入页面发送enterpage事件,根据其传递的URI匹配页面并创建聊天室

JAVA
1@OnEvent("enterPage")
2    public void onEnterPage(SocketIOClient client, String page) {
3        UUID clientId = client.getSessionId();
4        SocketAddress remoteAddress = client.getRemoteAddress();
5        String pageName = pageMatcher.matchPath(page, remoteAddress);
6        String previousPage = clientPageMap.put(clientId, pageName);
7
8        if (previousPage != null && !previousPage.equals(pageName)) {
9            Set<UUID> previousUsers = pageUserMap.getOrDefault(previousPage, ConcurrentHashMap.newKeySet());
10            previousUsers.remove(clientId);
11            if (previousUsers.isEmpty()) {
12                pageUserMap.remove(previousPage);
13            } else {
14                pageUserMap.put(previousPage, previousUsers);
15            }
16            debounceUpdatePageViewCount(previousPage);
17        }
18
19        pageUserMap.computeIfAbsent(pageName, k -> ConcurrentHashMap.newKeySet()).add(clientId);
20        debounceUpdatePageViewCount(pageName);
21        debounceUpdateTotalOnlineCount();
22    }

而前端部分就是加入并获取信息咯

借助socketio的自定义事件就可以传递页面和人数的信息

TSX
1	const [socket, setSocket] = useState<Socket | null>(null); // Socket.IO 实例
2    const [pageViewCount, setPageViewCount] = useState(0); // 当前页面在线人数
3    const [totalOnlineCount, setTotalOnlineCount] = useState(0); // 总在线人数
4    const param = usePathname();
5    const dispatch = useAppDispatch();
6    // 初始化 Socket.IO 连接
7    useEffect(() => {
8        const newSocket = io(url);
9        setSocket(newSocket);
10        getPageView().then((res) => {
11            dispatch({
12                type: "onlineCount/initPageView",
13                payload: res
14            })
15        });
16        // 监听总在线人数事件
17        newSocket.on("totalOnlineCount", (count: number) => {
18            setTotalOnlineCount(count);
19            dispatch({type: "onlineCount/updateOnlineCount", payload: count});
20        });
21        // 监听页面在线人数事件
22        newSocket.on("pageViewCount", (page, count) => {
23            if (page === param) {
24                setPageViewCount(count);
25            }
26            dispatch({
27                type: "onlineCount/updatePageView", payload: {
28                    name: page,
29                    count: count
30                }
31            });
32        });
33        // 在组件卸载时关闭连接
34        return () => {
35            newSocket.disconnect();
36        };
37    }, [param, dispatch, totalOnlineCount, pageViewCount]);
38
39    // 发送当前页面信息
40    useEffect(() => {
41        if (socket) {
42            socket.emit("enterPage", param);
43        }
44    }, [socket, param]);

标签云实现

说实话这个还挺好玩的,利用的是framer-motion这个库实现的动画,然后排版一下实现的效果

它的组成就是单个项目形成一行,反复渲染每一行直到填满区域

单个项目就简单设计就好

TSX
1const TagItem: React.FC<TagItemProps> = ({ icon: Icon, text, isLeft, delay }) => (
2  <motion.div
3    initial={{ opacity: 0, x: isLeft ? 100 : -100 }}
4    animate={{ opacity: [0, 0.8, 0], x: isLeft ? [-100, 0, 100] : [100, 0, -100] }}
5    transition={{ duration: 5, delay, repeat: Infinity, repeatType: 'loop', ease: 'linear' }}
6  >
7    <div
8      className={clsx('inline-flex items-center space-x-2 rounded-full bg-gray-100 dark:bg-gray-800 text-gray-400 dark:text-gray-400 px-3 py-1 text-sm shadow-sm', article_font.className)}>
9      <Icon size={14} />
10      <span>{text}</span>
11    </div>
12  </motion.div>
13);

然后我们为其传递随机的图标和标签不够随机来凑的标签内容

TSX
1const icons: LucideIcon[] = [Book, Bookmark, FileText, Hash, Link, Paperclip, Tag, Type];
2
3const getRandomIcon = (): LucideIcon => icons[Math.floor(Math.random() * icons.length)];
4const getRandomTag = (tags: string[]): string => tags[Math.floor(Math.random() * tags.length)];

对于左右半的内容使用一个isLeft来区分

TSX
1const TagRow: React.FC<TagRowProps> = ({ isLeft, rowIndex, tags }) => {
2  const rowTags = Array.from({ length: 8 }, (_, i) => ({
3    icon: getRandomIcon(),
4    text: getRandomTag(tags),
5    delay: i * 0.5 + rowIndex * 0.2,
6  }));
7
8  return (
9    <div className={`flex ${isLeft ? 'justify-start' : 'justify-end'} space-x-4 my-8`}>
10      {rowTags.map((tag, index) => (
11        <TagItem key={index} {...tag} isLeft={isLeft} />
12      ))}
13    </div>
14  );
15};

最后利用数组方法来传递参数,渲染满区域~

TSX
1export default function TagCloudBackground({ tags }: { tags: string[] }) {
2  const [rows, setRows] = useState<Row[]>([]);
3
4  useEffect(() => {
5    const numberOfRows = Math.ceil(window.innerHeight / 50); // Approximate row height
6    setRows(Array.from({ length: numberOfRows }, (_, i) => ({ isLeft: i % 2 === 0, index: i })));
7  }, []);
8
9  return (
10    <div className="absolute inset-1 h-[85vh] overflow-hidden pointer-events-none z-0">
11      {rows.map((row, index) => (
12        <TagRow key={index} isLeft={row.isLeft} rowIndex={row.index} tags={tags} />
13      ))}
14    </div>
15  );
16}

效果是这样的

简要总结与挖坑

这个项目可能还是入门项目吧..但是从设计到独立完成花费我的精力还是蛮多的,接下来会连续更新讲讲我这个项目的过程遇到的一系列问题

包括但不限于

水合不匹配问题,OAauth2实现,RSC的主题切换问题,移动端适配实现,推荐算法的简单实现,Nextjs的痛苦开发代理服务器问题,反代 301优化SEO问题,静态资源路径问题

一份学习一分收获吧,虽然现在我还很菜,但是还是不断学习不断进步

希望自己能成为独当一面的前端工程师,在热爱的领域闪闪发光,仅此而已

相关推荐

六月初至七月中旬|前端学习简要总结,生活的小回顾

在这段时间里我通过完成多个项目,深入学习了前端和全栈开发的技术。主要使用了 Vue3、C++ W...

grtsinry43
7/12/2024
74
1
1

学习分享|跨域解决、安卓开发探索、油猴脚本探索

最近学习的一些内容,包括跨域问题及其解决方案,安卓开发的简单探索,OpenAI的api做了个小插...

grtsinry43
6/10/2024
65
2
2

学习分享|原生三件套的网页效果

这是学习前端基础,用原生三件套尤其是js实现效果一些学习过程的记录,首先是静态的小米商城,之后实...

grtsinry43
6/10/2024
46
0
0

折腾记录|使用 Nuxt.js 重写个人主页,使用 SSR 优化 SEO ,实现一些期待已久的效果

在 22 年刚创建个人主页的时候,由于我的技术水平不够,只能用一些 wordpress type...

grtsinry43
9/19/2024
165
0
0

利用Vue自定义指令(directives)实现全站动画效果

最近一直在学习 React 高阶知识,因此对于主页的开发再一次停滞了,主要也是一段时间内没有找到什...

grtsinry43
10/13/2024
95
0
1
COMMENT 7273377343744380928

发表评论

来这里畅所欲言吧!
支持 Markdown 语法 0 / 3000

在风雨飘摇之中

本站已运行了

一路走来,感谢陪伴与支持

愿我们不负热爱,继续前行

全站通知
更新通知