2026年除夕夜,我像往常一样刷着Gitee,改着我的bug,突然发现我做的FileVibe被gitee推荐了。

我一个高二学生写的小工具,居然被推荐了,真是个奇迹哈。

年关已过,回望一下过去,总是有些说不尽的话。

恰好FileVibe全攻略专栏也该结束了,在此请允许我敞开心扉,在此说说我的心里话。

在这里插入图片描述


先聊聊开源这件事

我为什么选择开源?

做FileVibe的初衷特别简单:我需要一个能加密、能预览、还能AI解读的文件管理工具,市面上找不到满意的,那就自己写一个。

写完第一个能跑的版本时,我面临一个选择:

  • 自己用,谁也不告诉
  • 发到Gitee上,让别人也能用

我选了后者。

因为我自己就是开源的受益者。带我初入计算机大门的老师是做开源的;我学JavaScript的时候,看的都是开源项目的代码;在写Node.js的时候,用的全是开源的包;就连现在写文章用的Markdown编辑器,也是开源的。

用了别人那么多,总觉得该还点什么。


开源在我眼里是什么?

三个词:包容、交流、合作

包容:代码写得不好没关系。

我的第一版代码现在回头看,自己都想笑——变量命名随意、函数写得太长、连注释都没有。但我还是发出去了。

因为只有发出来,才能被人看到;只有被人看到,才能收到反馈;只有收到反馈,才能进步。

交流:有人给建议,有人给想法,项目才能变好。

FileVibe刚发出去的那几天,其实没什么人看。但有一个网友给了个试用反馈,说“幻灯片播放时切图会闪一下”。我看了半天,发现是动画没做好,我很快完成了修复。

就这一句话,我觉得发出来值了。

合作:一个人写不完,一群人才能写好。

我美术还行,但在UI设计领域并不算好。FileVibe的界面是我照着Tailwind文档硬凑出来的,说不上多好看,只能说“能看”,只是后来借助了Trae,我终于把UI给做好看了

如果有人愿意帮我改改CSS,让界面更漂亮,我会特别开心。

如果有人愿意加新功能,比如PDF预览、视频截图,我会更开心。

如果有人发现了bug,提个PR帮我修了,我会开心到睡不着。

这就是开源:你帮我,我帮他,大家一起变好。


对CSDN上一些现象的看法

在CSDN上经常看到一种现象:有人把GitHub上的开源项目下载下来,打包成“资源”,标价积分或者收费下载。

我理解想赚积分的心情,也理解“整理资源”也是一种劳动。但总觉得有点可惜。

可惜在哪里?

一个开源项目,背后可能是作者无数个夜晚的心血:

  • 想需求想得失眠
  • 写代码写到凌晨
  • 修bug修到崩溃
  • 写文档写到想吐

结果被别人一键打包,换个封面就成了“资源”。

更可惜的是,那些真正想学东西的人,下载下来只会用,不会改。遇到问题找不到原作者,想加功能不知道从哪下手。

开源的精神是分享,不是搬运。

举个例子,我的另一个专栏《青简问对》也是在gitee进行开源并获得了gitee的推荐

Gitee 青简问对 开源项目地址

在这里插入图片描述
但是在CSDN上,我看到了 (大家千万不要去买,我的项目是免费的) 举报多次依旧无法封号

在这里插入图片描述
他人还怪好嘞,帮我写了这么大一段简介。我看到之后真的觉得很无语

甚至,还有一些人把我的专栏转载后打上原创的标签在下面光明正大地推销课程(甚至贴脸开大,让我去报课,然后把我的项目拿出来说这是优秀学员的作品)

(但是我毕竟还是个学生,报课的钱对我来说实在拿不出来,更何况我也不想拿)


所以我做这个系列的时候,每一篇文章都坚持:

  • 代码贴全:想直接用的,复制就能跑
  • 思路讲透:想学东西的,知道为什么这么写
  • 设计讲清:想改功能的,知道从哪下手

能帮到一个是一个。

哪怕只有一个人看完文章,学会了怎么用CustomEvent解耦,学会了怎么防XSS攻击,学会了怎么设计密码强度指示器——那我这个月就值了。


一、这个项目是怎么开始的?

1.1 起因:我自己想要什么?

先说背景:我是一名高二学生,移动硬盘里文件越来越多。

作业文档:有周测答案、有自己写的作文、有从老师那儿拷的PPT。这些不想让别人看到——万一别人拿我硬盘,顺手打开看了,挺尴尬的。

旅游照片:去了贵州陕西深圳,拍了一千多张照片。回来整理时发现,很多地方叫什么已经忘了。要是能直接问AI“这张是哪儿”,该多好。

下载的歌曲:写作业时喜欢听歌,但每次都要打开音乐软件,还要看广告。要是能边看文件边听歌,多舒服。

代码片段:学JavaScript时写的demo,学Python时写的爬虫,东一个西一个。想预览一下,得用编辑器打开;想复制一段,得选中半天。

所以我想要一个工具,能同时做到:

  • 文件加密(不让别人看)
  • 图片解读(AI告诉我这是哪儿)
  • 音乐播放(边写作业边听)
  • 代码预览(打开就能看,还能一键复制)

市面上找了一圈:

工具 加密 AI解读 音乐播放 代码预览
百度网盘
有道云笔记
网易云音乐
什么都有

那就自己写一个吧。


1.2 动手:一行一行写出来

2025年10月的一个周末,我打开了VS Code,新建了一个文件夹,取名filevibe

第一行代码是:

const express = require('express');
const app = express();

那时候根本不知道这个项目能做成什么样。只是想:先跑起来再说。

第一个能跑的版本,只有一个/api/list接口,能列出当前目录的文件。页面丑得要命,就是一个ul列表。

然后是加密。为了搞懂AES,我把Node.js文档翻了好几遍,看了七八篇博客,写了十几个测试文件。

然后是解密。为了调试,我写了个脚本,加密一个文件,再解密,比对内容是不是一样。这个过程重复了几十次。

然后是AI。为了接模力方舟的API,我注册账号、申请密钥、看文档、写测试。第一次成功返回时,那个“你好”出现在屏幕上,我激动得拍了一下桌子。

然后是音乐播放器。然后是幻灯片。然后是API设置。然后是关于我们……

三个月周末,七万行代码(其实主要是文章字数),FileVibe慢慢长成了现在的样子。

获取FileVibe源代码


二、前六篇文章到底讲了什么?

回头看,六篇文章刚好对应一个项目的六个维度。

(一)SHA-256哈希加密

核心问题:用户输入的密码是“123456”,但AES加密需要32字节的密钥。怎么转换?

第一反应:直接把密码当密钥用?不行,长度不够,而且太简单。

再想:用哈希?MD5输出16字节,也不够。SHA-256输出32字节,正好。

最终代码

function generateKey(password) {
  return crypto.createHash('sha256').update(password).digest('base64').substring(0, 32);
}

思维点

  • 不是直接用密码加密,而是先哈希——这样即使密钥泄露,也推不出密码
  • SHA-256是单向的,从密钥反推密码不可能
  • 密码稍微变一点,密钥完全不一样(雪崩效应)

这教会我:安全不是用最牛的算法,而是把每个环节都做对。


(二)密码管理模块

核心问题:密码存在哪儿?存明文等于没加密。

错误示范:存数据库,字段叫password,值是123456——黑客一拖库,全完了。

正确做法:存哈希。但光哈希不够,还要加盐。

bcrypt加盐

const bcrypt = require('bcrypt');
const saltRounds = 10;

bcrypt.hash(password, saltRounds, (err, hash) => {
  // 存hash
});

暴力破解防护

let errorCount = 0;
let lockUntil = 0;

function verifyPassword(plainPwd) {
  if (Date.now() < lockUntil) {
    throw new Error('错误次数太多,锁定10分钟');
  }
  
  const result = bcrypt.compare(plainPwd, savedHash);
  
  if (!result) {
    errorCount++;
    if (errorCount >= 5) {
      lockUntil = Date.now() + 10 * 60 * 1000;
      errorCount = 0;
    }
  }
  
  return result;
}

思维点

  • 加盐让相同的密码产生不同的哈希值,防彩虹表
  • 故意设计成“慢哈希”,拖慢暴力破解
  • 连续失败5次就锁定,让破解成本指数级上升

这教会我:安全是一层层垒起来的,少一层都不行。


(三)AES加密解密

核心问题:文件怎么加密?解密时怎么知道用哪个IV?

加密

function encryptBuffer(buffer, password) {
  const key = generateKey(password);
  const iv = crypto.randomBytes(16);
  const cipher = crypto.createCipheriv('aes-256-cbc', Buffer.from(key), iv);
  const encrypted = Buffer.concat([cipher.update(buffer), cipher.final()]);
  return Buffer.concat([iv, encrypted]);  // IV和密文拼一起
}

解密

function decryptBuffer(buffer, key) {
  const finalKey = key.length === 32 ? key : generateKey(key);
  const iv = buffer.slice(0, 16);
  const encryptedText = buffer.slice(16);
  const decipher = crypto.createDecipheriv('aes-256-cbc', Buffer.from(finalKey), iv);
  const decrypted = Buffer.concat([decipher.update(encryptedText), decipher.final()]);
  return decrypted;
}

思维点

  • IV必须随机,且不能重复。和密文一起存最方便,不会丢
  • 解密时先读前16字节当IV,剩下的才是密文
  • 密钥可能是32字节(session里存的)也可能是密码(批量脚本里传的),一行判断兼容两种场景

这教会我:好的设计是让调用者不用想太多,往里扔数据就行。


(四)事件通信

核心问题:list.js、preview.js、chat.js三个模块要通信,但又要保持独立。

错误做法:直接导入函数

import { openFile } from './preview.js';
import { updateCurrentFile } from './chat.js';

li.addEventListener('click', () => {
  openFile(rel, name);
  updateCurrentFile({ name, url });
});

问题:list.js知道了preview.js和chat.js的存在,以后加新模块还得回来改。

正确做法:用CustomEvent

// list.js只管广播
li.addEventListener('click', () => {
  document.dispatchEvent(new CustomEvent('open-file', { 
    detail: { rel, name } 
  }));
});

// preview.js自己监听
document.addEventListener('open-file', (e) => {
  openFile(e.detail.rel, e.detail.name);
});

// chat.js也自己监听
document.addEventListener('open-file', (e) => {
  prepareForAnalysis(e.detail.name);
});

思维点

  • 模块只说自己做了什么,不说别人该做什么
  • 新加功能只需监听事件,不用改原有代码
  • 所有事件通过document中转,没有直接依赖

这教会我:解耦不是技术问题,是设计问题。


(五)AI图片解读

核心问题:图片是加密的,怎么给AI看?AI生成长,用户等不了怎么办?

图片怎么给AI:后端解密,前端转Data URL

// preview.js
const imgUrl = data.isBinary 
  ? `data:${mimeType};base64,${data.contentBase64}` 
  : `data:${mimeType};base64,${btoa(data.content)}`;

document.dispatchEvent(new CustomEvent('file-selected', {
  detail: { name, type: mimeType, url: imgUrl }
}));

图文一起发:多模态API

body: JSON.stringify({
  messages: [
    {
      "role": "user",
      "content": [
        { "type": "image_url", "image_url": { "url": imageUrl } },
        { "type": "text", "text": userMessage }
      ]
    }
  ],
  stream: true
})

流式响应:逐块读取

const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';

while (true) {
  const { done, value } = await reader.read();
  if (done) break;
  
  buffer += decoder.decode(value, { stream: true });
  const lines = buffer.split('\n');
  buffer = lines.pop();
  
  for (const line of lines) {
    if (line.startsWith('data: ')) {
      const data = JSON.parse(line.substring(6));
      if (data.choices?.[0]?.delta?.content) {
        updateStreamingMessage(data.choices[0].delta.content);
      }
    }
  }
}

XSS防护:双层保险

// 接收时用textContent
streamingElement.textContent += content;

// 解析前先转义
const escapedText = escapeHtml(markdownText);
const htmlText = parseMarkdown(escapedText);
streamingElement.innerHTML = htmlText;

思维点

  • 等待5秒和逐字显示5秒,体验天差地别
  • 永远不要信任外部输入,先转义再插入
  • 流式响应不是炫技,是用户体验

这教会我:技术要为体验服务。

在这里插入图片描述


(六)前端界面设计

核心问题:用户第一眼看到什么?怎么让他用着舒服?

登陆页:背景动画缓解焦虑

@keyframes move {
  0% { transform: translate(0, 0) scale(1); }
  25% { transform: translate(100px, -100px) scale(1.2); }
  50% { transform: translate(50px, 100px) scale(1); }
  75% { transform: translate(-100px, -50px) scale(1.2); }
  100% { transform: translate(0, 0) scale(1); }
}

.animate-move {
  animation: move 12s ease-in-out infinite;
}

密码强度指示器:实时反馈

function checkPasswordStrength(password) {
  let strength = 0;
  if (password.length >= 6) strength++;
  if (password.length >= 8) strength++;
  if (/[A-Z]/.test(password)) strength++;
  if (/[0-9]/.test(password)) strength++;
  if (/[^A-Za-z0-9]/.test(password)) strength++;
  return Math.min(strength, 4);
}

在这里插入图片描述

三栏布局:左右固定,中间自适应

<div class="flex h-[calc(100vh-64px)]">
  <aside class="w-80">...</aside>  <!-- 左侧固定 -->
  <main class="flex-1">...</main>   <!-- 中间自适应 -->
  <aside class="w-80">...</aside>  <!-- 右侧固定 -->
</div>

文件列表:悬停反馈、文件名截断

li:hover {
  background-color: #374151;  /* hover变亮 */
}

.truncate {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

复制按钮:成功反馈

btn.addEventListener('click', () => {
  navigator.clipboard.writeText(content);
  btn.textContent = '已复制!';
  setTimeout(() => btn.textContent = '复制', 1500);
});

思维点

  • 每个细节都在回答“用户此刻需要什么”
  • 好的UI设计,用户用的时候感觉不到设计的存在
  • 但仔细一想,每个细节都有人在背后替他想过

这一篇教会我:代码写完只完成了一半,另一半是让用的人觉得舒服。

在这里插入图片描述


三、工程思维到底是什么?

很多人问我:你写代码的时候脑子里在想什么?

其实就四个字:解决问题

3.1 第一步:理解需求(别急着写代码)

错误示范

用户要加密 → 找个加密库装上 → 完事

正确姿势

用户要加密,为什么?→ 怕别人看见

谁可能看见?→ 别人用我电脑时、硬盘被偷时

加密能解决吗?→ 能,但密码不能太复杂,不然我自己也忘了

所以需要:强加密 + 好记的密码 → AES + 密码派生密钥

这就是(一)和(三)的来源——不是“我要用SHA”,而是“用户需要好记的密码,但AES需要32字节密钥,所以用SHA把密码转成密钥”。

3.2 第二步:拆解问题(大事化小)

需求:让AI看懂图片

拆开看

  • 图片从哪来?→ 文件列表点击
  • 图片是加密的,AI怎么收?→ 后端解密,前端转Data URL
  • 怎么传给AI?→ 多模态API,content数组同时传图片和文字
  • AI生成长,用户等不了 → 流式响应,边生成边显示
  • AI返回有格式 → 解析Markdown,转成HTML
  • AI返回不安全 → XSS防护,先转义后插入

每个小问题都有对应的解法,合起来就是(五)。

3.3 第三步:选择工具(够用就好)

问题:模块间需要通信

选项

方案 优点 缺点
全局变量 简单 污染、乱
Event Bus 灵活 要自己写
Redux 强大 重型
CustomEvent 原生 功能简单

选CustomEvent。为什么?

因为FileVibe的通信场景就这么几种:list通知preview、preview通知chat。用不着Redux那种全家桶。

原则:用最轻量的工具解决当前问题,不为“以后可能用到”的功能提前复杂化。

3.4 第四步:接受不完美(先跑起来)

回头看代码,很多地方可以优化:

问题 现状 可以优化
大文件预览 一次性读内存 流式读取
长文件列表 全部渲染 虚拟滚动
错误提示 alert弹窗 toast飘一下
播放进度 不保存 localStorage存状态

先跑起来,再跑得快

第一版能跑,第二版优化,第三版重构——这才是做项目的节奏。

如果一开始就想写完美的代码,可能到现在还在想,一行都没写出来。


四、现在,邀请你一起参与

FileVibe还很新,才发布几天。目前:

  • ✅ 核心功能都跑通了
  • ✅ 能加密、能预览、能AI解读
  • ✅ 能听音乐、能放幻灯片

但还有很多可以做的事:

方向 可以做什么
性能 大文件流式读取、虚拟列表
体验 toast提示、状态保存
移动端 适配手机屏幕
新功能 PDF预览、视频截图
文档 写更详细的README

如果你也对文件管理感兴趣,欢迎:

  • 点个Star —— 让更多人看到这个项目
  • 🐛 提Issue —— 发现bug告诉我,或者提你想要的功能
  • 🔧 提PR —— 直接写代码,我们一起改
  • 💬 提建议 —— 任何想法都可以,哪怕只是“这个按钮不好看”

一个人写不完,一群人才能写好。


五、最后想说

六篇文章,三个月周末,从一行代码到一个完整的项目。

最大的收获不是Gitee推荐,而是把脑子里的想法变成了能用的东西

如果你也是学生,也想自己写点什么——别等,现在就开始

不需要等学会了再写,边写边学才是最快的。

写的过程中会遇到无数问题:这个报错什么意思?这个库怎么用?这个算法怎么实现?

没关系。百度、谷歌、查文档、看源码、问朋友——所有问题都有答案。

FileVibe还会继续改,但这个系列就停在这里了。

希望这些文章和代码,能帮到一个是一个。

哪怕只有一个人看完文章,学会了怎么用CustomEvent解耦,学会了怎么防XSS攻击,学会了怎么设计密码强度指示器——那我这三个月就值了。

谢谢读到这里的你。


(本专栏到此结束,欢迎点赞收藏评论,更多技术内容请关注 凌云拓界)

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐