在一个周末内构建一个卡路里跟踪器网页应用

一个有用的卡路里跟踪器需要三个东西:一个食品搜索、一个日志列表和一个可解释的每份服务计算。

你可以用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调用移到后端,以便特权密钥永远不会在浏览器中分发。然后,在控制台中创建一个免费密钥,缓存热门查找,使用单独的卡路里计算器设置目标卡路里,而不是重新实现目标数学本身。

构建合同:使用OpenAPI规范确保字段准确,集成指南了解速率限制和归因,假设每种营养素都可能为null。