在一个周末内构建一个卡路里跟踪器网页应用
一个有用的卡路里跟踪器需要三个东西:一个食品搜索、一个日志列表和一个可解释的每份服务计算。
你可以用DietlyAPI和浏览器的本地存储作为单个客户端页面的后端,构建所有三个功能,且无需服务器和构建步骤。这份指南将指导你完成整个应用的构建,然后展示如何扩展它以满足用户的需求。
你正在构建什么
一个HTML文件。一个搜索框在用户输入时调用GET /search。点击一个结果将其添加到今天的日志中,一个可编辑的克数。一个运行总和计算每个条目使用相同的数学。每个条目都存储在localStorage中,所以刷新页面仍然保留今天的日志。
这是一个真正可用的跟踪器,它从未暴露一个特权密钥,因为读取端点是公开的。
1. 根据用户输入搜索食品
等待至少两个字符(API的最小值为q),防止每个按键都发出请求,然后渲染名称、品牌和每100克卡路里的值。请记住,响应是一个JSON数组。
const API = 'https://api.getdietly.com';
let timer;
function onType(e) {
clearTimeout(timer);
const query = e.target.value.trim();
if (query.length < 2) return;
timer = setTimeout(() => runSearch(query), 250);
}
async function runSearch(query) {
const url = new URL(API + '/search');
url.search = new URLSearchParams({ q: query, limit: '8' });
const res = await fetch(url);
if (!res.ok) {
showError(res.status);
return;
}
const foods = await res.json();
render(foods); // foods是空数组时没有匹配项
}
2. 存储日志条目,而不是搜索结果
当用户添加一个食品时,请保留ID、一个显示名称、克数和一个计算时的每100克卡路里值的快照。快照保留过去的一天,即使目录记录后来改变,也可以使用GET /food/{id}重新获取实时记录。
function addFood(food) {
const entries = load();
entries.push({
id: food.id,
name: food.name,
grams: 100,
kcal100: food.calories_kcal, // 每100克快照
protein100: food.protein_g,
});
save(entries);
draw();
}
const KEY = 'dietly-log';
const load = () => JSON.parse(localStorage.getItem(KEY) || '[]');
const save = (e) => localStorage.setItem(KEY, JSON.stringify(e));
3. 使总和可检查
使用一个克数到100克的公式计算每个条目,仅在显示时四舍五入,并让总和打开到产生它的条目。缺失的营养素不可用,不是零,所以在总和中跳过null值而不是强制转换它们。
function totals(entries) {
return entries.reduce((acc, e) => {
if (e.kcal100 != null) acc.kcal += e.kcal100 * e.grams / 100;
if (e.protein100 != null) acc.protein += e.protein100 * e.grams / 100;
return acc;
}, { kcal: 0, protein: 0 });
}
function draw() {
const entries = load();
const t = totals(entries);
document.querySelector('#total').textContent = `${Math.round(t.kcal)} kcal, ${Math.round(t.protein)} g protein`;
}
4. 允许用户编辑克数而不进行新搜索
编辑克数是任何跟踪器中最常用的动作。将其在原地改变并重新绘制。因为你已经存储了每100克快照,所以不需要网络调用。
function setGrams(index, grams) {
const entries = load();
entries[index].grams = Math.max(0, Number(grams) || 0);
save(entries);
draw();
}
5. 处理普通的失败状态
- 空的搜索数组是正常的无匹配状态,不是错误。显示“无匹配项”,而不是无限旋转。
- 对于429响应,尊重Retry-After标头并永远不重试紧密循环。
- 从/food/{id}获得404意味着记录丢失;优雅地丢弃条目。
- 解释给用户,让他们知道日志存储在浏览器中,清除站点数据会删除它。
6. 连接页面并发布它
整个应用都是一个静态文件:一个搜索输入框、一个结果列表和一个日志列表。因为没有构建步骤和服务器,所以你可以在任何静态主机(GitHub Pages、Cloudflare Pages、Netlify)上免费托管它,只需上传一个index.html。
以下标记是上述函数所需的所有结构:
<input id="q" oninput="onType(event)" placeholder="搜索食品">
<ul id="results"></ul>
<h2>今天</h2>
<ul id="log"></ul>
<p id="total">0 kcal</p>
<script src="app.js"></script>
将搜索结果渲染为按钮,调用addFood,将日志渲染为数字输入,绑定到setGrams。刷新页面时调用draw(),这样返回的访客就可以立即看到昨天的总和。将所有DOM更新放在一个draw()函数中;一个单一的渲染路径比散布的更新更容易理解,并且使总和和列表不易出现不一致。
一个可访问性的注意事项值得花两分钟:给搜索输入框一个真正的
下一步如何扩展它
这个原型是故意无密钥的和本地的。当你需要跨设备共享日志时,请添加账户和你的后端的服务器端存储,并将任何账户级别的Dietly调用移到后端,以便特权密钥永远不会在浏览器中分发。然后,在控制台中创建一个免费密钥,缓存热门查找,使用单独的卡路里计算器设置目标卡路里,而不是重新实现目标数学本身。
评论 (0)