Grtsinry43的前端札记 | 大三技术成长实录 & 学习笔记 | 「岁月漫长,值得等待」
文章
技术学习

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

2024年12月13日 12 分钟阅读 浏览 0 喜欢 0 评论 0

终于,历时一个多月的开发 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
@OnEvent("enterPage")
    public void onEnterPage(SocketIOClient client, String page) {
        UUID clientId = client.getSessionId();
        SocketAddress remoteAddress = client.getRemoteAddress();
        String pageName = pageMatcher.matchPath(page, remoteAddress);
        String previousPage = clientPageMap.put(clientId, pageName);

        if (previousPage != null && !previousPage.equals(pageName)) {
            Set<UUID> previousUsers = pageUserMap.getOrDefault(previousPage, ConcurrentHashMap.newKeySet());
            previousUsers.remove(clientId);
            if (previousUsers.isEmpty()) {
                pageUserMap.remove(previousPage);
            } else {
                pageUserMap.put(previousPage, previousUsers);
            }
            debounceUpdatePageViewCount(previousPage);
        }

        pageUserMap.computeIfAbsent(pageName, k -> ConcurrentHashMap.newKeySet()).add(clientId);
        debounceUpdatePageViewCount(pageName);
        debounceUpdateTotalOnlineCount();
    }

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

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

tsx
	const [socket, setSocket] = useState<Socket | null>(null); // Socket.IO 实例
    const [pageViewCount, setPageViewCount] = useState(0); // 当前页面在线人数
    const [totalOnlineCount, setTotalOnlineCount] = useState(0); // 总在线人数
    const param = usePathname();
    const dispatch = useAppDispatch();
    // 初始化 Socket.IO 连接
    useEffect(() => {
        const newSocket = io(url);
        setSocket(newSocket);
        getPageView().then((res) => {
            dispatch({
                type: "onlineCount/initPageView",
                payload: res
            })
        });
        // 监听总在线人数事件
        newSocket.on("totalOnlineCount", (count: number) => {
            setTotalOnlineCount(count);
            dispatch({type: "onlineCount/updateOnlineCount", payload: count});
        });
        // 监听页面在线人数事件
        newSocket.on("pageViewCount", (page, count) => {
            if (page === param) {
                setPageViewCount(count);
            }
            dispatch({
                type: "onlineCount/updatePageView", payload: {
                    name: page,
                    count: count
                }
            });
        });
        // 在组件卸载时关闭连接
        return () => {
            newSocket.disconnect();
        };
    }, [param, dispatch, totalOnlineCount, pageViewCount]);

    // 发送当前页面信息
    useEffect(() => {
        if (socket) {
            socket.emit("enterPage", param);
        }
    }, [socket, param]);

标签云实现

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

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

单个项目就简单设计就好

tsx
const TagItem: React.FC<TagItemProps> = ({ icon: Icon, text, isLeft, delay }) => (
  <motion.div
    initial={{ opacity: 0, x: isLeft ? 100 : -100 }}
    animate={{ opacity: [0, 0.8, 0], x: isLeft ? [-100, 0, 100] : [100, 0, -100] }}
    transition={{ duration: 5, delay, repeat: Infinity, repeatType: 'loop', ease: 'linear' }}
  >
    <div
      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)}>
      <Icon size={14} />
      <span>{text}</span>
    </div>
  </motion.div>
);

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

tsx
const icons: LucideIcon[] = [Book, Bookmark, FileText, Hash, Link, Paperclip, Tag, Type];

const getRandomIcon = (): LucideIcon => icons[Math.floor(Math.random() * icons.length)];
const getRandomTag = (tags: string[]): string => tags[Math.floor(Math.random() * tags.length)];

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

tsx
const TagRow: React.FC<TagRowProps> = ({ isLeft, rowIndex, tags }) => {
  const rowTags = Array.from({ length: 8 }, (_, i) => ({
    icon: getRandomIcon(),
    text: getRandomTag(tags),
    delay: i * 0.5 + rowIndex * 0.2,
  }));

  return (
    <div className={`flex ${isLeft ? 'justify-start' : 'justify-end'} space-x-4 my-8`}>
      {rowTags.map((tag, index) => (
        <TagItem key={index} {...tag} isLeft={isLeft} />
      ))}
    </div>
  );
};

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

tsx
export default function TagCloudBackground({ tags }: { tags: string[] }) {
  const [rows, setRows] = useState<Row[]>([]);

  useEffect(() => {
    const numberOfRows = Math.ceil(window.innerHeight / 50); // Approximate row height
    setRows(Array.from({ length: numberOfRows }, (_, i) => ({ isLeft: i % 2 === 0, index: i })));
  }, []);

  return (
    <div className="absolute inset-1 h-[85vh] overflow-hidden pointer-events-none z-0">
      {rows.map((row, index) => (
        <TagRow key={index} isLeft={row.isLeft} rowIndex={row.index} tags={tags} />
      ))}
    </div>
  );
}

效果是这样的

简要总结与挖坑

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

包括但不限于

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

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

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

分享此文
评论区在赶来的路上...