本文分享一个多端工时记录工具的完整技术实现,涵盖纯原生前端架构、日历组件自研、离线数据持久化、大规模JSON静态化、SEO工程化等实践。项目地址:https://xiaoshigongjizhang.com
一、项目背景与技术选型
在开发这个工具之前,我调研了市面上现有的工时记录类 App,发现大多数产品要么功能臃肿、广告满天飞,要么只支持单一平台、数据无法同步。对于真正的目标用户------小时工、兼职人员、临时工来说,他们需要的只是一个打开就能用、多端能同步、没有广告干扰的轻量工具。
基于此,我梳理了核心诉求:
- 多端覆盖:微信小程序、iOS、Android、Windows、Web 五端统一体验
- 离线优先:网络不稳定场景下(如工厂车间、外卖骑手路上)也能正常记录
- 零后端依赖:个人项目,不想维护服务器和数据库,降低长期运营成本
- SEO 友好:Web 版需要被搜索引擎收录,让更多有需求的用户自然发现
基于以上约束,技术选型非常明确:
| 端 | 技术方案 | 理由 |
|---|---|---|
| Web | 纯 HTML/CSS/JS,无框架 | 极致轻量、首屏快、SEO 可控 |
| 小程序 | 原生微信小程序 | 用户获取成本低 |
| iOS/Android | 原生/跨平台方案 | 体验接近原生 |
| Windows | UWP / PWA | 微软商店分发 |
| 数据存储 | localStorage + 单条 JSON | 零后端、按需加载 |
为什么不选 Vue/React?
这个项目 Web 版的核心逻辑并不复杂(日历渲染 + 表单 + 数据列表),引入框架反而会增加包体积和首屏加载时间。对于 SEO 场景,SSR 方案又需要额外的构建和部署成本。最终采用了"原生 JS + 模块化思维"的写法,把代码按功能拆成独立的函数块,保持可维护性。
二、核心功能实现
2.1 日历组件------纯 JS 实现
日历是整个项目最核心的交互组件。没有借助任何日期库(如 moment.js、dayjs),全部用原生 Date API 实现,最终压缩后代码不到 3KB。
日期计算核心逻辑:
javascript
function renderCalendar() {
const year = currentDate.getFullYear();
const month = currentDate.getMonth();
const firstDay = new Date(year, month, 1);
const lastDay = new Date(year, month + 1, 0);
const startDay = firstDay.getDay(); // 本月1号是星期几
const daysInMonth = lastDay.getDate(); // 本月总天数
// 渲染日历网格:前补空位 + 本月日期 + 后补空位
let html = '';
// 星期标题
const weekdays = ['日', '一', '二', '三', '四', '五', '六'];
weekdays.forEach(d => {
html += `<div class="weekday-header">${d}</div>`;
});
// 前置空位
for (let i = 0; i < startDay; i++) {
html += `<div class="calendar-day other-month"></div>`;
}
// 本月日期
const records = getRecords();
const monthKey = `${year}-${String(month + 1).padStart(2, '0')}`;
for (let day = 1; day <= daysInMonth; day++) {
const dateKey = `${monthKey}-${String(day).padStart(2, '0')}`;
const record = records[dateKey];
const hasRecord = record && (record.hours || record.income);
html += `
<div class="calendar-day ${hasRecord ? 'has-record' : ''}"
onclick="openModal('${dateKey}')">
<div class="day-number">${day}</div>
${hasRecord ? `
<div class="day-hours">${record.hours}h</div>
<div class="day-income">${record.income}元</div>
` : ''}
</div>
`;
}
document.getElementById('calendarGrid').innerHTML = html;
}
这里有两个工程细节值得注意:
- 日期 Key 规范 :统一用
YYYY-MM-DD字符串作为 localStorage 的键,避免时区问题。toISOString().split('T')[0]是最简单的方案。 - 性能优化:日历渲染是 O(n) 操作,240 条数据以内完全无感。如果数据量更大,可以考虑虚拟滚动,但当前场景不需要。
2.2 数据层------localStorage 的工程化封装
localStorage 虽然简单,但直接裸用会带来几个问题:异常数据导致 JSON 解析失败、并发写入冲突、存储空间限制(5MB)。
我的封装策略:
javascript
const STORAGE_KEY = 'hourly_worker_records';
function getRecords() {
try {
return JSON.parse(localStorage.getItem(STORAGE_KEY) || '{}');
} catch {
// 数据损坏时自动回退,避免页面崩溃
return {};
}
}
function saveRecords(records) {
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(records));
} catch (e) {
if (e.name === 'QuotaExceededError') {
showToast('存储空间不足,请清理历史记录');
}
}
}
数据结构设计:
json
{
"2024-05-01": { "hours": 8, "income": 200 },
"2024-05-02": { "hours": 6.5, "income": 162.5 }
}
用日期字符串做 Key,而不是数组,是为了 O(1) 查询和更新。每个日期的数据量极小(两个数字),即使记录 10 年的数据,JSON 字符串也远不到 1MB,完全在 localStorage 的安全范围内。
实际使用中,很多用户会在月末回顾整月数据。为此在日历上方增加了月度统计面板(总工时、总收入、记录天数),用一次遍历即可算出,复杂度 O(n),240 条数据以内无感。
2.3 劳动法数据静态化------240 条 JSON 的工程实践
项目中有一个比较特殊的模块:劳动者权益保护法律手册。数据来源于一本 Word 文档,包含《劳动法》《劳动合同法》等 240 条法律条文。
数据拆分策略:
最初考虑过把所有数据塞进一个 laws.json,但 240 条条文 + 解析 + 案例,体积会很大,且用户通常只看其中几条。最终采用了"索引集中 + 单条拆分"的方案:
data/
├── index.json # 240 条索引(id/title/category/section/summary/tags)
└── law/
├── law_001.json # 单条完整数据
├── law_002.json
└── ...
索引结构设计:
json
{
"id": "law_001",
"title": "第一条",
"category": "劳动法",
"section": "第一章 总则",
"article": "第一条",
"summary": "第一条 为了保护劳动者的合法权益...",
"tags": ["劳动法"]
}
单条数据结构:
json
{
"id": "law_001",
"title": "第一条",
"category": "劳动法",
"section": "第一章 总则",
"article": "第一条",
"content": "第一条 为了保护劳动者的合法权益...",
"summary": "...",
"tags": ["劳动法"],
"analysis": "【条文解析】...",
"case": "【典型案例】..."
}
这种设计的优势:
- 列表页 只加载
index.json(约 200KB),前端内存过滤,无需后端 - 详情页按需加载单条 JSON,平均 2-3KB,首屏极快
- SEO 友好 :每个详情页都有独立 URL(
detail.html?id=law_001),可被搜索引擎单独收录 - 部署简单:纯静态文件,直接扔 nginx 即可
值得一提的是,这个法律手册模块上线后收到了不少用户正向反馈。很多小时工朋友表示,以前遇到拖欠工资、强制加班的情况不知道该怎么办,现在可以直接在工具里查到对应的法律条文和维权案例。这也让我意识到:技术工具除了解决效率问题,还可以承担一定的社会责任。
批量处理脚本:
从 Word 到 JSON 的转换用 Python 脚本自动化处理,核心逻辑:
python
import json, re, os
def parse_docx_to_entries(path):
# 用 python-docx 解析文档,按段落拆分
# 正则匹配"第X条"作为分割点
...
def split_to_single_json(entries):
for i, entry in enumerate(entries, 1):
filename = f"law_{i:03d}.json"
with open(f"data/law/{filename}", 'w', encoding='utf-8') as f:
json.dump(entry, f, ensure_ascii=False, indent=2)
后续还为每条补充了 analysis(条文解析)和 case(典型案例),用批量 Python 脚本写入,避免手动编辑 240 个文件。
三、搜索与目录------纯前端过滤的实现
法律手册页面需要支持搜索和分类目录。240 条数据量下,纯前端过滤完全够用。
搜索逻辑:
javascript
function applyFilter() {
const keywords = searchQuery.trim().toLowerCase().split(/\s+/).filter(k => k);
filteredData = allData.filter(item => {
if (currentCat && item.category !== currentCat) return false;
if (currentSec && item.section !== currentSec) return false;
if (keywords.length === 0) return true;
const text = [
item.title, item.summary, item.article,
(item.tags || []).join(' ')
].join(' ').toLowerCase();
return keywords.every(k => text.includes(k));
});
}
支持多关键词 AND 搜索(空格分隔),支持分类联动过滤。实测在 240 条数据上,过滤延迟 < 5ms,无需 debounce 也能流畅,但还是加了 300ms debounce 防止输入过快时频繁渲染。
目录分组:
按 category → section 两级分组,用原生 JS 构建侧边栏目录:
javascript
catalogMap = {
"劳动法": {
"第一章 总则": 9,
"第二章 促进就业": 3,
...
},
"劳动合同法": { ... }
};
点击章节后,右侧列表自动筛选并滚动定位。URL 同步 ?cat=劳动法&sec=第一章 总则,刷新后状态保留。
四、SEO 工程化------静态站的搜索优化
既然是面向公众的 Web 站点,SEO 必须认真做。以下是几个关键实践:
4.1 结构化数据
首页注入了 4 组 Schema:
html
<!-- SoftwareApplication -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "SoftwareApplication",
"name": "小时工记账",
"applicationCategory": "FinanceApplication",
"offers": { "@type": "Offer", "price": "0", "priceCurrency": "CNY" },
"aggregateRating": { "@type": "AggregateRating", "ratingValue": "4.8", "ratingCount": "10000" }
}
</script>
<!-- FAQPage -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "FAQPage",
"mainEntity": [ ... ]
}
</script>
FAQPage 能让搜索引擎在结果页直接展示问答摘要,显著提升点击率。
4.2 动态 Meta 标签
详情页的内容是 JS 异步加载的,但搜索引擎爬虫不一定执行 JS。为此做了两层保障:
- 基础 Meta:HTML 中预置通用的 title/description/keywords
- 动态更新 :JS 加载数据后,通过
document.querySelector('meta[name="description"]').setAttribute('content', ...)替换为精准内容
同时注入 BreadcrumbList Schema,强化页面层级关系:
javascript
const breadcrumblist = {
'@context': 'https://schema.org',
'@type': 'BreadcrumbList',
itemListElement: [
{ '@type': 'ListItem', position: 1, name: '首页', item: 'https://xiaoshigongjizhang.com/' },
{ '@type': 'ListItem', position: 2, name: '劳动法', item: 'https://xiaoshigongjizhang.com/law.html?cat=劳动法' },
{ '@type': 'ListItem', position: 3, name: '第一条', item: 'https://xiaoshigongjizhang.com/detail.html?id=law_001' }
]
};
4.3 Sitemap 与 Robots
生成包含 243 个 URL 的 sitemap.xml(首页 + 法律手册 + 240 条详情页),配合 robots.txt 引导爬虫:
text
User-agent: *
Allow: /
Sitemap: https://xiaoshigongjizhang.com/sitemap.xml
五、响应式与性能优化
5.1 响应式策略
采用 Mobile-First 的 CSS 写法,核心断点 768px:
css
/* 桌面端:侧边栏固定 */
.law-layout { display: flex; gap: 20px; }
.sidebar { width: 260px; position: sticky; top: 20px; }
/* 移动端:侧边栏折叠 */
@media (max-width: 768px) {
.law-layout { flex-direction: column; }
.sidebar { display: none; width: 100%; position: static; }
.sidebar.show { display: block; }
}
日历组件在小屏幕上做了自适应:网格间距缩小、日期单元格最小高度降低、字体缩小,保证在 375px 宽度的手机上也能正常显示。
5.2 性能指标
| 指标 | 数值 | 说明 |
|---|---|---|
| 首屏 HTML | ~15KB | 纯静态,无构建产物 |
| index.json | ~200KB | 法律索引,一次性加载 |
| 单条 law JSON | ~2-3KB | 按需加载 |
| 首屏请求数 | 3 | HTML + CSS(inline) + index.json |
| Lighthouse 性能 | 95+ | 无框架、无大体积依赖 |
六、踩坑记录
6.1 Windows 终端编码陷阱
在 Windows PowerShell 下用 Python 处理中文 JSON 时,终端默认 GBK 编码会导致输出乱码。解决方案:所有文件读写显式指定 encoding='utf-8',避免依赖终端编码。
python
with open(path, 'r', encoding='utf-8') as f:
data = json.load(f)
6.2 localStorage 并发写入
虽然 localStorage 是同步 API,但在极端情况下(用户快速连续点击保存),可能出现竞态。解决方案:每次写入前先读取最新数据,合并后再写入,而不是基于内存中的旧数据写入。
6.3 单页应用的 SEO 困境
detail.html?id=xxx 这种 URL 对搜索引擎不够友好。目前的缓解方案:
- 确保每个 id 对应的页面都有独立的 title、description、canonical
- 生成完整的 sitemap.xml 提交给搜索引擎
- 未来可以考虑预渲染(prerender.io)或 SSR,把关键内容直接输出到 HTML 中
七、项目成果与数据
截至目前的几个关键数据:
- 多平台上线:微信小程序、iOS App Store、微软商店、Android(百度网盘)、Web 网页版
- 法律数据覆盖:240 条完整条文,全部补充了条文解析和典型案例
- Web 性能:Lighthouse 性能评分 95+,首屏加载 < 1s
- SEO 收录:sitemap 提交后,详情页陆续被主流搜索引擎收录
对于个人开发者来说,这个项目的最大价值在于验证了"零后端 + 纯静态"架构的可行性。不需要买服务器、不需要配数据库、不需要处理并发,就能把一个有实际用户价值的工具做出来并持续运营。
八、总结
这个项目从 2023 年开始持续迭代,目前已在微信小程序、iOS App Store、微软商店等渠道上线,累计服务了数万名小时工和兼职人员。最有成就感的反馈来自用户:"终于找到一个没有广告、打开就能记工时的工具了"。
作为一个个人维护的项目,它整体践行了"够用就好"的工程哲学:
- 不需要框架时,坚决不用框架------减少依赖、降低维护成本
- 不需要后端时,用 localStorage + 静态 JSON 解决------零服务器费用
- 数据量不大时,纯前端过滤比接入 ElasticSearch 更轻量
- SEO 不需要 SSR 时,用结构化数据 + sitemap 也能拿到不错的收录效果
技术栈虽然简单,但每个决策都经过了场景匹配度 和长期维护成本的双重考量。对于个人项目或小型工具类站点,这种"原生技术栈 + 工程化思维"的组合,往往比盲目追新框架更务实。
如果你也在做类似的个人工具项目,欢迎参考这个项目的实现思路。当然,如果你就是小时工、兼职人员,或者身边有这样的朋友,也可以把这个工具推荐给他们------让每一分努力都有迹可循。
在线体验 :https://xiaoshigongjizhang.com(网页版无需下载,打开即用)
技术交流:欢迎在评论区讨论前端工程化、SEO 优化、数据静态化、跨端开发等话题。