1. 项目概述一个面向开发者的技能管理工具最近在GitHub上看到一个挺有意思的项目叫fightZy/simple-skills。乍一看名字你可能会觉得这是个关于“简单技能”的什么教程或者清单。但点进去之后我发现它的定位其实更偏向于一个个人技能管理工具或者说是一个帮助开发者尤其是程序员系统化梳理、追踪和展示自己技术栈的轻量级解决方案。我自己在团队里带过新人也面试过不少人一个很深的感触是很多开发者对自己的技能认知是模糊的、碎片化的。简历上写着“精通Java”但具体精通到什么程度是能熟练使用Spring Boot全家桶做微服务还是仅仅会写基础的CRUD面对一个新技术学习路径是怎样的当前掌握到了哪个阶段这些问题往往缺乏一个清晰的记录和可视化工具。simple-skills项目瞄准的就是这个痛点。它试图用结构化的方式比如YAML或JSON来定义技能树然后通过一个简单的静态站点生成器或脚本将这些定义转化为可视化的技能图谱或清单方便个人复盘和对外展示。这个项目的核心价值在于“管理”而非“教学”。它不教你如何学会Python而是帮你回答“我的Python现在处于什么水平”以及“我接下来该往哪个方向深入”。对于渴望清晰职业规划、希望系统性提升自己的开发者来说这样一个工具能带来意想不到的秩序感。接下来我就结合对这个项目思路的拆解以及如何构建这样一个工具的实际经验来详细聊聊。2. 核心设计思路与方案选型2.1 为什么需要技能管理在深入技术细节前我们先聊聊“为什么”。技能管理听起来有点“务虚”但对于技术从业者尤其是需要不断学习的程序员来说它非常务实。首先对抗遗忘与碎片化。技术栈更新快今天学了Docker下个月可能就要接触K8s。如果没有记录很容易学了就忘或者只记得一些模糊的概念。通过技能管理你可以为每个技能点打上“标签”记录学习时间、掌握程度、关联项目形成个人知识库。其次明确学习路径与目标。看着一个庞大的技术生态比如前端领域的Vue、React、构建工具、状态管理新手很容易迷茫。一个结构化的技能树可以充当学习地图告诉你从基础到进阶需要攻克哪些关卡当前处在什么位置。再者用于求职与个人品牌建设。一份动态的、可视化的技能图谱比千篇一律的文字简历更能直观地展示你的技术轮廓和深度。你可以针对不同的职位要求快速生成侧重点不同的技能视图。simple-skills项目正是基于这些需求诞生的。它的设计目标应该是轻量、可定制、开发者友好。这意味着它很可能选择用配置文件如YAML来定义技能用静态站点来展示从而避免复杂的后端和数据库依赖让每个开发者都能轻松部署和维护自己的技能页面。2.2 技术栈选型背后的逻辑基于“轻量、静态、可定制”的目标我们可以推断出项目可能采用或适合采用的技术方案技能定义层数据源YAML极有可能的选择。YAML格式清晰、易读易写非常适合用来定义结构化的数据比如技能的分类、名称、熟练度、描述等。相比JSONYAML支持注释对于需要大量备注的技能描述来说更友好。JSON另一种可选方案更通用但可读性稍逊。如果项目考虑未来通过程序动态生成技能数据JSON可能是更好的选择。Markdown也可以考虑用Markdown文件来管理每个技能的详细描述但结构化数据提取会更复杂一些。处理与生成层引擎静态站点生成器SSG这是最自然的路径。例如Hugo、Jekyll、VuePress或Docusaurus。这些工具本身就是为了将文本/标记文件转换为网站而设计。我们可以编写一个模板读取YAML技能数据渲染成HTML页面。simple-skills可能自己实现了一个简单的生成脚本也可能直接基于某个SSG进行定制。纯前端渲染另一种思路是将技能数据YAML/JSON放在仓库里写一个简单的HTML页面用JavaScript比如Vue.js或React在浏览器端动态加载并渲染这些数据。这样连生成步骤都省了但不利于SEO。自定义脚本用Python、Node.js写一个脚本读取YAML按照模板生成静态HTML。这种方式最灵活但需要自己处理模板引擎、资源打包等琐事。可视化与UI层技能图谱如何直观展示技能间的关联和掌握程度可能会用到图表库如ECharts或D3.js来绘制力导向图、雷达图或树状图。例如用雷达图展示不同领域如前端、后端、运维的技能评分。技能清单更简单的方式是分类列表用进度条、星级或标签如“了解”、“熟悉”、“精通”来标识熟练度。这用基本的HTML/CSS配合一点JavaScript就能实现。部署与托管GitHub Pages / GitLab Pages这是此类项目的绝配。将生成好的静态文件推送到特定分支即可自动发布。完全免费且与代码仓库无缝集成。Vercel / Netlify提供更强大的自动化部署、预览和自定义域名支持。如果项目使用Node.js脚本或Next.js等框架这些平台是更优选择。注意以上是基于项目名称和常见实践的分析。一个优秀的simple-skills实现应该让用户只需关注技能数据的编辑YAML文件而无需关心复杂的构建和部署流程真正做到“简单”。3. 技能数据结构设计与实操3.1 定义你的技能模型这是整个项目的基石。一个好的数据模型应该能充分描述一个技能同时保持扩展性。我们设计一个YAML结构作为示例# skills.yaml skills: - category: 编程语言 items: - name: Python level: 4 # 假设1-5级5为最高 description: 主要用于后端开发和数据分析熟悉Flask和Django框架。 since: 2018 tags: [后端, 自动化, 爬虫] projects: [个人博客系统, 数据监控平台] learning_goal: 深入理解异步编程asyncio学习FastAPI。 - name: JavaScript level: 5 description: 熟练掌握ES6熟悉Vue.js和React生态。 since: 2017 tags: [前端, 全栈] projects: [管理后台, 可视化大屏] learning_goal: 探索Vue 3组合式API和TypeScript深度结合。 - category: 基础设施与运维 items: - name: Docker level: 3 description: 能够编写Dockerfile使用docker-compose编排多容器应用。 since: 2020 tags: [容器化, 部署] projects: [微服务本地开发环境] learning_goal: 学习Kubernetes基础概念与编排。 - name: Linux level: 4 description: 熟练使用常用命令可进行服务器基础运维和Shell脚本编写。 since: 2016 tags: [操作系统, 运维] projects: [] learning_goal: 深入理解系统性能调优和网络配置。字段解析与设计理由category: 技能分类帮助宏观梳理知识结构。name: 技能名称。level:核心字段。量化掌握程度。这里用1-5数字表示也可以使用“入门”、“熟悉”、“精通”等枚举值。数字的好处是便于计算和生成图表。description: 简要描述可以写具体应用场景、掌握的库/框架。since: 开始接触时间用于计算“工龄”或生成时间线。tags: 标签用于多维度的筛选和聚合。比如你可以快速找出所有带“后端”标签的技能。projects: 关联项目技能最好的证明是实践。这里可以写项目名称或链接。learning_goal:进阶字段。记录下一步学习目标让技能树是“生长中”的而非静止的。3.2 从数据到页面的生成策略有了数据下一步是如何把它变成网页。假设我们选择用Node.js脚本 模板引擎的方案因为它足够轻量和可控。1. 项目初始化与依赖安装mkdir my-simple-skills cd my-simple-skills npm init -y npm install --save-dev ejs # 选择EJS作为模板引擎语法简单 npm install --save-dev yaml # 用于解析YAML文件 npm install --save-dev chokidar # 可选用于开发时监听文件变化2. 核心生成脚本 (generate.js)这个脚本负责读取YAML渲染模板输出HTML。const fs require(fs); const path require(path); const yaml require(yaml); const ejs require(ejs); // 1. 读取技能数据 const skillsYaml fs.readFileSync(path.join(__dirname, data/skills.yaml), utf8); const skillsData yaml.parse(skillsYaml); // 2. 准备模板数据 // 可以在这里对数据进行预处理例如计算每个分类的平均等级等 const templateData { skills: skillsData.skills, generatedAt: new Date().toLocaleDateString(), // 可以添加一个计算总体技能水平的功能 overallScore: calculateOverallScore(skillsData.skills) }; // 3. 读取EJS模板 const templateStr fs.readFileSync(path.join(__dirname, templates/index.ejs), utf8); // 4. 渲染模板 const htmlOutput ejs.render(templateStr, templateData); // 5. 输出到静态文件 const outputDir path.join(__dirname, dist); if (!fs.existsSync(outputDir)) { fs.mkdirSync(outputDir, { recursive: true }); } fs.writeFileSync(path.join(outputDir, index.html), htmlOutput); console.log(技能页面生成完毕); // 辅助函数计算总体技能分示例可按需调整 function calculateOverallScore(skills) { let totalLevel 0; let totalCount 0; skills.forEach(category { category.items.forEach(item { totalLevel item.level; totalCount; }); }); return totalCount 0 ? (totalLevel / totalCount).toFixed(1) : 0; }3. 模板文件 (templates/index.ejs)这是一个简单的EJS模板示例展示如何遍历数据。!DOCTYPE html html langzh-CN head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 title我的技能图谱/title link relstylesheet hrefhttps://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css style /* 这里可以放入你的CSS样式 */ .skill-category { margin-bottom: 2rem; } .skill-item { background: #f5f5f5; margin: 0.5rem 0; padding: 1rem; border-radius: 8px; } .skill-level { display: inline-block; width: 100px; height: 10px; background: #ddd; border-radius: 5px; overflow: hidden; } .skill-level-bar { height: 100%; background: #4CAF50; width: % (item.level / 5) * 100 %%; } /style /head body header h1我的技术技能库/h1 p最后更新于% generatedAt % | 综合技能指数% overallScore %/5.0/p /header main % skills.forEach(function(category){ % section classskill-category h2i classfas fa-folder/i % category.category %/h2 div classskill-list % category.items.forEach(function(item){ % div classskill-item h3% item.name % span classskill-level span classskill-level-bar title等级 % item.level %/span /span /h3 p% item.description %/p div classskill-meta small接触于 % item.since % 年/small % if(item.tags item.tags.length 0){ % div % item.tags.forEach(function(tag){ % span classtag% tag %/span % }); % /div % } % % if(item.learning_goal){ % div classlearning-goal strong下一步/strong % item.learning_goal % /div % } % /div /div % }); % /div /section % }); % /main script // 这里可以加入简单的交互比如点击技能项展开详情等 /script /body /html4. 运行与部署在package.json中添加脚本命令{ scripts: { build: node generate.js, dev: node watch.js // 如果实现了监听热更新 } }运行npm run build就会在dist目录生成index.html。将整个dist目录的内容部署到 GitHub Pages 或任何静态托管服务即可。实操心得在定义YAML结构时不要追求一步到位。先从最核心的name,level,description开始用起来之后再根据实际需要添加tags、projects等字段。数据文件应该易于手动编辑这是工具能被长期使用的关键。4. 高级功能与可视化扩展基础列表展示虽然清晰但缺乏冲击力。我们可以引入一些可视化图表让技能展示更生动。4.1 集成雷达图展示技能分布雷达图非常适合展示你在不同技术领域的均衡性。我们可以使用ECharts这个强大的图表库。首先在模板的head中引入 EChartsscript srchttps://cdn.jsdelivr.net/npm/echarts5.4.3/dist/echarts.min.js/script然后在数据预处理阶段generate.js中我们需要将技能数据聚合为雷达图需要的格式。假设我们想按category来展示平均技能水平// 在generate.js的templateData准备阶段添加 function prepareRadarData(skills) { const indicator []; // 雷达图指标维度 const value []; // 对应维度的值 skills.forEach(category { // 计算该分类下所有技能的平均等级 const avgLevel category.items.reduce((sum, item) sum item.level, 0) / category.items.length; indicator.push({ name: category.category, max: 5 }); // 指标名称和最大值 value.push(avgLevel.toFixed(2)); // 平均值 }); return { indicator, data: [{ value, name: 技能掌握度 }] }; } templateData.radarData prepareRadarData(skillsData.skills);接着在模板的合适位置比如首页顶部添加一个容器并渲染图表!-- 在body内添加 -- div idradarChart stylewidth: 100%; height: 400px;/div script // 确保DOM加载后执行 document.addEventListener(DOMContentLoaded, function() { const chartDom document.getElementById(radarChart); const myChart echarts.init(chartDom); const option { title: { text: 技能分布雷达图, left: center }, tooltip: {}, radar: { indicator: %- JSON.stringify(radarData.indicator) % }, series: [{ name: 技能 vs 平均值, type: radar, data: %- JSON.stringify(radarData.data) % }] }; myChart.setOption(option); // 响应窗口大小变化 window.addEventListener(resize, function() { myChart.resize(); }); }); /script这里使用了%- %来输出未转义的JSON字符串因为ECharts需要的是JavaScript对象。4.2 实现技能时间线展示技能的学习历程可以增加故事的感染力。我们可以用简单的CSS时间线来实现。在模板中添加时间线部分并遍历所有技能按since年份排序section h2i classfas fa-timeline/i 技能学习时间线/h2 div classtimeline % // 首先收集所有技能项并过滤出有since字段的 const allItemsWithYear []; skills.forEach(cat { cat.items.forEach(item { if(item.since) { allItemsWithYear.push({...item, category: cat.category}); } }); }); // 按年份排序 allItemsWithYear.sort((a,b) a.since - b.since); // 按年份分组 const itemsByYear {}; allItemsWithYear.forEach(item { if(!itemsByYear[item.since]) itemsByYear[item.since] []; itemsByYear[item.since].push(item); }); % % Object.keys(itemsByYear).sort().forEach(year { % div classtimeline-year h3% year % 年/h3 div classtimeline-items % itemsByYear[year].forEach(item { % div classtimeline-item span classskill-name% item.name %/span span classskill-cat-tag% item.category %/span span classskill-level-badgeLv.% item.level %/span /div % }); % /div /div % }); % /div /section配合一些CSS样式就能形成一个清晰直观的时间线。注意事项可视化功能是“锦上添花”核心永远是数据本身。不要为了复杂的图表而让数据准备过程变得繁琐。确保添加新技能、更新等级这些基础操作依然简单快捷。如果图表配置太复杂可以考虑将图表生成也放在构建脚本中生成图片或SVG嵌入而不是依赖浏览器端渲染这样对SEO更友好。5. 自动化、部署与持续维护一个工具只有用起来才有价值。如何让技能管理变得像更新日记一样自然是关键。5.1 实现自动化构建与部署我们使用GitHub Actions来实现“提交即发布”。在项目根目录创建.github/workflows/deploy.ymlname: Deploy to GitHub Pages on: push: branches: [ main ] # 在推送到main分支时触发 workflow_dispatch: # 允许手动触发 jobs: build-and-deploy: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkoutv3 - name: Setup Node.js uses: actions/setup-nodev3 with: node-version: 18 cache: npm - name: Install Dependencies run: npm ci # 使用ci命令确保依赖锁一致 - name: Build run: npm run build # 运行我们之前定义的生成脚本 - name: Deploy to GitHub Pages uses: peaceiris/actions-gh-pagesv3 with: github_token: ${{ secrets.GITHUB_TOKEN }} publish_dir: ./dist # 将dist目录的内容部署到gh-pages分支配置好后每次你修改skills.yaml文件并推送到GitHubActions会自动运行npm run build生成最新的静态页面并部署到GitHub Pages。你只需要访问https://[你的用户名].github.io/[仓库名]就能看到实时更新的技能页。5.2 设计便捷的数据维护流程降低维护成本才能坚持使用。这里有几个建议使用VS Code等编辑器的代码片段为YAML技能项创建代码片段这样输入几个关键字就能快速生成一个结构完整的技能条目。开发一个简单的本地CLI工具可选进阶可以创建一个交互式命令行工具通过问答方式引导你添加或更新技能然后自动写入YAML文件。例如npx my-skill-cli add # 交互式提问技能名分类等级描述 # 自动追加到 skills.yaml定期回顾与更新在日历中设置一个季度提醒花15分钟回顾一下技能树。更新已有技能的level添加新学的技能规划下一季度的learning_goal。5.3 个性化定制与主题切换为了让页面更符合个人品味可以引入主题系统。一个简单的方法是使用CSS变量。在模板的CSS部分定义两套主题变量:root { --primary-color: #3498db; --bg-color: #ffffff; --text-color: #333333; --card-bg: #f5f5f5; /* ...其他变量 */ } [data-themedark] { --primary-color: #1abc9c; --bg-color: #1a1a1a; --text-color: #e0e0e0; --card-bg: #2d2d2d; } body { background-color: var(--bg-color); color: var(--text-color); transition: background-color 0.3s ease; } .skill-item { background-color: var(--card-bg); }然后在页面中添加一个切换按钮通过JavaScript修改html标签的>