最近我花了两周时间把一个拖了很久的想法落地了,项目起名就叫skills。它不是什么颠覆性平台,也不是一个在线课表,而是一个完全给自己用的个人技能管理工具。我做它的原因很朴素:作为一个什么都在学、什么都想沾一点的开发者,我发现自己根本没有一张“清楚的技术能力地图”。简历上写的技能是模糊的,脑子里的知识是碎片化的,每隔一阵子都要花很长时间回忆最近到底学了什么、哪些已经生疏、下一步该补什么。真正让我下决心动手的,是一次面试前的自我梳理,我打开一个空白文档准备列技能清单,结果写了两行就卡住了。
这个场景我相信很多人经历过。市面上的学习工具要么是课程播放器,要么是打卡软件,能把你学过的内容、熟练度、计划、复盘放在一起,并且用数据可视化的方式让你一眼看到差距的工具,几乎没有。所以我没有再等现成方案,而是自己动手做了一个。这个项目的核心就是四个字:技能盘点。它解决的是“我到底会什么、不会什么、接下来学什么”这个问题,适合正在积累技术栈的开发新人、想转行的学习者,以及任何被“知识焦虑”困扰但不知道从哪开始的人。下面我把这个项目的设计和实现过程完整拆开,包括我怎么定需求、怎么设计数据表、怎么写后端、怎么画图表,以及踩过哪些坑。
1. 为什么我会亲手写一个“skills”系统
1.1 需求源头:技能太多,盘点不过来
我给自己做过很多次年度学习计划,但基本没坚持过。原因是计划里的“学会 Vue”“精通 Java”这种条目,本身就没有可衡量性。三个月后回头看,我根本不知道自己是“会”还是“不太会”,更不知道投入的时间是否对得起产出。我想象中的技能管理,应该是像游戏角色面板那样:每个技能有等级、有经验条、有树状依赖,学到什么程度清清楚楚。现实是,大部分人的技能都被塞在一堆文件夹、书签、收藏夹和笔记里,没有结构。
后来我换了个思路:不用追求一劳永逸的庞大技能树,先做一个能快速录入、评估、查看差距的最小系统。我给自己定下第一个目标——每次打开这个系统,我能在三十秒内回答三个问题:我现在有哪些技能?每个技能处于什么水平?我最近应该在哪个方向上投入时间?
1.2 现有方案到底缺什么
市面上不是没有类似产品。有些在线学习平台有技能图谱,但那是平台定义的,不是针对个人的。有些简历工具有技能标签,但只面向求职,没有成长视角。还有些笔记软件支持标签和关系图,但维护成本太高,记着记着就变成了蹩脚的数据库。我真正需要的,是一个足够简单的数据库:技能主表、评估记录、学习投入记录,再配上几个直观的图表。这个需求听起来很简单,但通用工具为了兼容所有人,往往把流程做复杂了,反而让我不想打开。
另外我还想要一个“不看广告、不搞社交、没有课程推销”的纯粹工具。自己写的好处就是所有功能都按自己的习惯来,坏处是得自己维护。但作为一个开发者,这不正好是练手的机会吗?我把它看作一个长期演进的个人项目,而不是一定要求多完善。
1.3 我确认的核心需求清单
动工前,我列了一个优先级列表,原则是“先做能做出来的,再做想做的”。
- 技能管理:支持新增、编辑、删除技能,每个技能属于一个分类,分类如前端、后端、数据库、工程效能等。
- 等级评估:给每个技能设定一个当前等级和目标等级,等级定义要具体,不能含糊。
- 学习记录:记录每次投入的时间、内容和心得,让“学过”有迹可循。
- 数据可视化:用雷达图展示多维能力对比,用热力图展示近期投入,用条形图展示当前和目标差距。
- 数据导入导出:支持 JSON 备份,避免平台锁定。
至于提醒、多人协作、简历生成,我放到后面版本再做。第一个版本做得足够简单,才能让我坚持使用下去。
2. 设计一套不过度的数据模型
2.1 技能分类:从“技能树”到“技能森林”
最初我参考了很多技能树图谱,比如把某个语言细分为异步编程、内存管理、多线程等,树可以很深。但真正用起来会发现,超过三层的树维护成本极高。如果你今天学了个小众工具,你很难给它定位到准确的二级分类下,结果就是到处找地方放,最后干脆不录。我最终采用了扁平分类加标签的方案:技能只属于一个分类,但可以打多个标签。分类数量控制在十五个以内,例如:前端、后端、数据库、运维、算法、力、项目管理、软技能等。标签则自由得多,比如“面试高频”“近期在用”“兴趣方向”。
这样设计的好处是,录入技能时的认知负担很小。新增一条技能记录,只需要填名称、选分类、添加标签,整个过程不到十秒。同时查询和筛选也很方便:按分类看,按标签看,或者直接搜关键词。树结构虽然看起来性感,但对个人盘点来说,森林比树更合适:每棵小树代表一个领域,技能之间是松散关联,而不是严格的父子依赖。
2.2 等级评估:把自己当作一个“角色”
技能等级是主观的,所以必须定义清楚,否则自己和自己都容易产生分歧。我参考了很多游戏和评估体系的描述,最终定了一个六级量表:
| 等级 | 名称 | 行为特征 |
|---|---|---|
| 0 | 未接触 | 看过名字,不知道具体内容 |
| 1 | 了解 | 知道基本概念,能说清是什么,但没做过项目 |
| 2 | 入门 | 做过简单的练习或小项目,能照着文档写 |
| 3 | 熟练 | 独立完成中等规模任务,能解决常见问题 |
| 4 | 精通 | 能深入原理,能优化方案,能指导别人 |
| 5 | 专家 | 能定义最佳实践,能推动团队/社区认知 |
对每一项技能,我要求自己写一句“达到该等级的证据”,比如达到 Level 3 的 Vue,证据是“独立开发过企业内部中后台系统,并通过代码评审”。这句话很重要,因为人很容易高估自己,只有把证据写下来,等级才经得起回顾。半年后你再来看,如果证据站不住,那就是真的退步了。
除了当前等级和目标等级,我还加了一个字段“最近使用频率”,选择值包括:每天、每周、每月、偶尔、几乎不用。这个字段用于筛选“生疏技能”,也用于决定优先级:高频使用但等级不高,说明有实战机会,应该继续深入;低频但目标等级高,说明缺少场景,需要刻意安排。
2.3 学习投入与状态机
技能表只是静态快照,真正让系统“活”起来的是学习记录。我专门建了一张表,每次学习不超过一条记录,字段包括:技能 ID、日期、时长分钟、学习内容简述、状态类型。这里的状态类型我设计成一个简单的枚举:理论学习、动手实践、复盘总结、没学但复习。四种状态基本能覆盖日常学习行为。为什么需要这个状态?因为“学两个小时”和“练两个小时”的含金量完全不同。复盘的时候,我可以用图表看自己有多少时间真正花在敲代码和调试上,而不是只看视频。
我最初还想给技能加一个“掌握度百分比”,后来发现百分比和等级是重复的。于是把百分比改成进度条的动态值,它由当前等级除以目标等级计算得出。比如当前等级 2,目标等级 4,进度就是 50%。这样就不用手动维护两个数值,也避免了主观上的自欺欺人。
3. 从零实现:后端、前端与数据可视化
3.1 技术栈选择和初始化
个人项目最忌讳过度架构。我没有用 Kubernetes,也没拆微服务,更没上消息队列。最终选型是:后端用 Node.js 和 Express,数据库用 SQLite,前端用 Vue 3 和 Element Plus,图表用 ECharts。选这些主要是图省事,而且生态成熟。Node.js 写起来轻快,SQLite 不需要单独部署,适合个人项目。Vue 3 的组合式 API 写起来顺手,Element Plus 组件库能快速把表单和表格搭建起来。ECharts 则是目前最灵活的开源图表库,雷达图、热力图、柱状图都有成熟示例。
初始化项目时,我分成了两个目录:server和web。server只负责 API,web负责页面展示。开发时用 Vite 做代理,把/api的请求转发到后端。这里有个小坑:如果你用的是 Windows,SQLite 的 native 模块可能需要重新编译,建议直接用better-sqlite3,它是同步 API,性能足够,而且安装简单,基本不存在编译问题。
3.2 后端API最小闭环
我设计的第一版 API 非常简单,只有五组:
GET /api/skills:获取技能列表,支持分类、标签、关键词筛选POST /api/skills:新增技能PUT /api/skills/:id:更新技能DELETE /api/skills/:id:删除技能POST /api/records:新增学习记录GET /api/stats/radar:获取雷达图数据
后端代码结构分成routes、controllers、services三层。虽然只有几组接口,但分层能让自己后续扩展时不用重写。下面是一个简化版的新增技能接口示例:
// routes/skills.js const express = require('express'); const router = express.Router(); const skillService = require('../services/skillService'); router.post('/', async (req, res) => { try { const { name, category, tags, level, targetLevel, evidence, frequency } = req.body; if (!name || !category) { return res.status(400).json({ error: 'name 和 category 不能为空' }); } const skill = await skillService.createSkill({ name, category, tags, level, targetLevel, evidence, frequency }); res.status(201).json(skill); } catch (err) { res.status(500).json({ error: err.message }); } }); module.exports = router;这段代码朴实无华,但很实用。我在写的时候特别注意了字段校验,因为个人项目很容易忽略入参合法性,结果就是前端报错时难以排查。tags字段我记得存成 JSON 字符串,在数据库里是TEXT类型,读取时再解析,这样避免建关联表带来的复杂度。
数据库表结构我用 SQL 初始化的方式,没有引入 ORM。核心表两条:
CREATE TABLE IF NOT EXISTS skills ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, category TEXT NOT NULL, tags TEXT DEFAULT '[]', level INTEGER DEFAULT 1, target_level INTEGER DEFAULT 3, evidence TEXT DEFAULT '', frequency TEXT DEFAULT '偶尔', created_at TEXT DEFAULT CURRENT_TIMESTAMP, updated_at TEXT DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE IF NOT EXISTS learning_records ( id INTEGER PRIMARY KEY AUTOINCREMENT, skill_id INTEGER NOT NULL, date TEXT NOT NULL, duration INTEGER DEFAULT 0, content TEXT DEFAULT '', type TEXT DEFAULT '理论学习', created_at TEXT DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (skill_id) REFERENCES skills(id) );用 ORM 能节省一些代码,但个人项目用 SQL 直接操作反而更可控。尤其是当你需要写统计查询,比如按月份分组算总时长时,一段 SQL 比 ORM 链式调用清晰得多。
3.3 前端页面和交互细节
前端我做了三个页面:技能总览、技能详情、数据仪表盘。技能总览是一个表格,默认按分类排序,每行显示技能名、分类、标签、当前等级、目标等级、进度条,以及最后学习时间。每行右边有一个“记一笔”按钮,点击会弹出对话框,录入学习时长和内容。这个交互是使用频率最高的,所以一定要减少操作步骤:从点击按钮到保存成功,我控制在五次点击以内。
技能详情页展示单个技能的所有信息,包括历史学习记录,以及一个小的趋势图。趋势图是按月统计的学习时长柱状图,可以直观看到这个技能是否被冷落。详情页还有一个编辑区域,可以修改等级、目标和证据。每次修改后,我会提醒自己更新证据,否则等级容易失真。
写前端的时候有几个值得注意的体验细节。第一,技能名称输入框要做重复校验,后端也要查重,否则技能列表里会出现两个“Docker”,统计就会乱。第二,分类建议从预设下拉框里选,同时允许输入新分类,但新增的分类要提示用户确认,防止手滑造出一堆同义分类。第三,进度条颜色可以按照剩余差距来区分:差距小于等于 1 显示绿色,等于 2 显示橙色,大于 2 显示红色。这样一眼就能看出哪些技能是短板。
3.4 数据可视化:雷达图与热力图
仪表盘是整个项目最让我上瘾的部分。雷达图用来对比分类维度的能力值,而不是单个技能。因为单个技能比较太难且情绪化,但“后端能力”“前端能力”“云原生能力”这些维度对比起来很有启发。我写了一个统计函数:取某个分类下所有技能当前等级的平均值,作为该维度的得分,然后输出到雷达图。目标等级也做一份,同图叠加,差距一目了然。
举个例子,我把技能分成六个维度:前端、后端、数据库、DevOps、算法、软技能。雷达图上,当前等级围成的多边形跟目标等级围成的多边形之间会产生缺口,哪边缺口大,哪边就是最近的发力点。这个视觉冲击比任何表格都强。原本我总觉得自己算法还行,一上图发现算法维度只有 1.5 分,而目标 3.5 分,直接就排进了下个月的优先级首位。
热力图我用来展示最近九十天的学习活跃度。横轴是星期,纵轴是第几周,色块深浅表示当天的学习总时长。这个设计参考了 GitHub 的贡献图,但用 ECharts 里的 calendar 坐标系实现。每天学习不足三十分钟的格子是浅灰色,超过一个小时是深蓝色。热力图非常诚实,一旦连续两周都是浅灰色,看着就慌,于是会逼着自己至少保持一点学习节奏。
4. 实操中的问题与避坑盘点
4.1 不要把技能树做成无限深的树
我最初一版设计过parent_id,想做技能之间的依赖关系,比如“JavaScript”依赖“HTML/CSS”,“Promise”依赖“异步编程”。结果录技能的时候,我发现自己要先想清楚这个技能挂在哪个父节点下,成本太高,录了几条就烦了。后来我删掉了父节点字段,只保留分类和标签。依赖关系不是不重要,而是不适合作为日常输入模型。如果你想表达“需要先会 X 才能学 Y”,可以在技能详情里加一个“前置技能”文本字段,输入技能名即可,系统不强校验。这既能保留参考信息,又不影响录入效率。
4.2 自评等级别搞得太复杂
我一开始设计过十级的量表,后来又想过用百分比,但实际体验是:等级多了,人在评估时反而更纠结。你很难说清楚自己是“7级”还是“8级”,但“熟练(Level 3)”和“精通(Level 4)”之间就比较好判断。所以最终固定六级,并且强制填写行为特征作为证据。实操中我发现,很多人的技能等级评定不是“学得不够”,而是“定义不清楚”。所以这里有个小经验:每次调整等级时,一定要同步调整证据描述。如果证据已经支撑不了这个等级,就把等级降下来,这反而让人更踏实地看到自己真实水平。
4.3 数据备份和同步要提前想
个人项目最容易忽视的是数据安全。我用 SQLite 数据库,默认就是单文件,备份起来很简单。我的方案是每天定时把data/skills.db复制一份到备份目录,同时每周导出一份 JSON 文件存到云盘。这里的坑是:SQLite 的 WAL 模式下,直接复制文件可能漏掉尚未落盘的数据。我在备份前会执行一次sqlite3.db.exec('PRAGMA wal_checkpoint(FULL)'),确保把 WAL 日志合并到主文件里。你要是也打算用 SQLite,这一点值得留意。
4.4 冷启动后的坚持问题
工具做得再好,不坚持使用也白搭。我前两周的使用频率很高,第三周开始下降,原因是没有形成回看数据的习惯。后来我给仪表盘加了一个“上周学习总时长”的数字,放在首页最顶部,让自己每次打开先看到这个数字。这看起来逼格不高,但极其有效。当你的目标是“每周不低于三小时”,看到一个 2.5 小时就会不由自主想补一点。现在我已经连续两个月每天都打开一次,不是为了打卡,而是因为想看数据变化。
5. 这只是开始:skills 还能怎么延伸
5.1 和简历生成打通
技能数据积累到一定程度,最自然的落地场景就是简历。我现在已经在想,能不能把技能列表按“当前等级大于等于 3”筛选,自动生成一段“个人技能”描述。再加一个“项目经历”模块,把学习记录里面 type 为“动手实践”的记录汇总成项目描述。这样简历不再是临阵磨枪,而是平时数据的副产品。虽然自动化写的句子有点死板,但至少能作为初稿。
5.2 加入目标拆解和提醒
另一个明显的改进是目标管理。现在每个技能只有目标等级,但没有下一步行动。我计划给每个技能增加一个“下一步行动”字段,并支持设置计划日期。系统每天启动时,把今天要做的技能学习任务列出来。这其实是把项目管理系统里的 OKR 思路移植过来:目标定等级,拆解到具体技能,再落到每一个学习动作上。
5.3 多端同步与分享
目前我只在电脑上使用,但很多时候在地铁上想记一笔,没有移动端就不方便。我的计划是加一个 Telegram Bot 或者极简的移动端 PWA,实现快速录入。另外,我还想做一个分享页,可以把雷达图和技能列表以只读的方式分享给朋友或同学,接受他们的建议。这样既不会公开自己太弱,又能获得外部反馈。
最后再分享一个我这段实操里体会最深的事:个人技能管理系统不是一个“做完了就结束”的项目,它更像一个长期的数字花园。你要不断调整它的结构,让它适配自己的真实习惯,而不是反过来让生活去适应工具。工具本身的价值,不在代码多炫、界面多漂亮,而在于你每一次打开它,都能看到自己在哪个方向是空转,哪个方向在加速。对我来说,skills 已经从一个练手项目,变成了日常学习的基础设施。如果你也有类似的信息焦虑,不妨试着从一张技能清单开始,不用写代码,先用表格记录一个月,你会发现自己真正需要深入的领域,可能跟你想象的不太一样。