为 Fuwari 侧边栏添加天气小组件,自动定位后显示当前所在地的温度、天气、湿度、风速等信息,点击设置按钮可手动搜索城市。
一、技术选型
1.1 天气 API:Open-Meteo
Open-Meteo 是完全免费的天气 API,无需 API Key,无需注册。两个接口:
| 接口 |
|---|
https://geocoding-api.open-meteo.com/v1/search?name={城市名}&count=5&language=zh |
https://api.open-meteo.com/v1/forecast?latitude={纬度}&longitude={经度}¤t=temperature_2m,relative_humidity_2m,weather_code,wind_speed_10m,apparent_temperature |
1.2 定位:浏览器 Geolocation API
优先使用浏览器原生定位,失败时降级为手动搜城市。定位得到经纬度后,再通过 Open-Meteo Geocoding API 反查城市名。
1.3 Swup 兼容性
Fuwari 使用 Swup 做页面切换。Swup 只替换两个容器的内容:#swup-container(主内容区)和 #toc(目录)。侧边栏在 Swup 容器之外,跨页面持久存在,不需要注册任何 Swup hooks。
这一点很重要:只要把天气组件放在侧边栏,它就不会被 Swup 替换,只需要初始化一次。
二、实现步骤
2.1 新建 Weather.astro
在 src/components/widget/ 下新建 Weather.astro,复用 Fuwari 已有的 WidgetLayout 组件(和 Tags、Categories 用法一致)。
组件结构分为三部分:
- 模板:Astro 模板,只负责渲染 HTML 骨架,数据由客户端 JS 填充
- SVG 图标:WMO 天气码到内联 SVG 的映射表,嵌入在
<script>里,不依赖外部 CDN - 客户端脚本:
<script is:inline>块,负责 API 调用、DOM 操作、缓存管理
模板部分
---import I18nKey from "../../i18n/i18nKey";import { i18n } from "../../i18n/translation";import WidgetLayout from "./WidgetLayout.astro";---
<WidgetLayout name={i18n(I18nKey.weather)} id="weather"> <div id="weather-content"> <div id="weather-loading">Loading...</div> <div id="weather-display" class="hidden"> <!-- 图标、温度、描述 --> <!-- 湿度、风速、体感温度 --> <!-- 城市名 + 设置按钮 --> </div> <div id="weather-settings" class="hidden"> <!-- 自动定位按钮 --> <!-- 城市搜索输入框 + 下拉建议 --> </div> </div></WidgetLayout>模板里没有任何动态数据——温度、图标、城市名全是占位符(--°C、--),由客户端脚本填充。
IMPORTANT必须用
<script is:inline>,不能用普通的<script>。普通<script>会被 Astro 打包成 ES module 注入<head>,但 Fuwari 配置了updateHead: false,Swup 切页时不会重新执行。is:inline让脚本直接写入 HTML,页面加载时同步执行——这样才能在首帧渲染前就把 localStorage 缓存的天气数据显示出来,避免页面加载后”闪一下”。
SVG 天气图标
Open-Meteo 返回的 weather_code 是 WMO 标准天气码(0 = 晴,2 = 多云,61 = 小雨,95 = 雷暴……)。需要一张映射表把天气码转成图标。
不用 Iconify CDN,也不装额外依赖——直接把 Material Symbols 的 SVG path 写在 JS 对象里:
var WMO_ICONS = { 0: '<svg viewBox="0 0 24 24" fill="currentColor" class="w-10 h-10"><path d="M12 7c-2.76 0-5..."/></svg>', 2: '<svg viewBox="0 0 24 24" fill="currentColor" class="w-10 h-10"><path d="M19.35 10.04A7.49..."/></svg>', // ... 其余 WMO 码};图标的 SVG 数据从 @iconify-json/material-symbols 包里提取,也可以直接去 Iconify 网站复制。天气描述同理,用一个 WMO_DESC 对象映射成中文。
客户端脚本核心逻辑
整个脚本包在一个 IIFE 里,用 var 避免污染全局(is:inline 脚本不做模块化处理)。
数据流:
页面加载 ↓检查 localStorage 缓存(30 分钟有效期) ↓ 有缓存 → 立即渲染,结束 ↓ 无缓存尝试 Geolocation API ↓ 成功 → 拿到经纬度 → 反查城市名 → 请求天气 → 渲染 + 存缓存 ↓ 失败 → 显示城市搜索面板,等用户手动输入localStorage 缓存结构:
{ "timestamp": 1714980000000, "temperature": 23, "humidity": 65, "weather_code": 2, "wind": 12, "feels_like": 21, "city": "上海", "lat": 31.23, "lon": 121.47}反向地理编码:
Geolocation API 只返回经纬度,不返回城市名。用 Open-Meteo Geocoding API 做反查:
async function reverseGeocode(lat, lon) { var url = GEOCODING_API + "?name=" + lat.toFixed(1) + "," + lon.toFixed(1) + "&count=1&language=zh"; var resp = await fetch(url); var json = await resp.json(); if (json.results && json.results.length > 0) return json.results[0].name; return lat.toFixed(2) + ", " + lon.toFixed(2);}城市搜索(带防抖):
输入框监听 input 事件,300ms 防抖后调用 Geocoding API,结果渲染为下拉列表:
cityInput.addEventListener("input", function() { clearTimeout(debounceTimer); var query = cityInput.value.trim(); if (query.length < 2) { suggestions.classList.add("hidden"); return; } debounceTimer = setTimeout(async function() { var results = await searchCity(query); // 渲染下拉建议,每项绑定 click → fetchWeather(r.latitude, r.longitude, r.name) }, 300);});点击建议项后清空输入框、隐藏下拉、请求天气数据。
2.2 注册到 SideBar.astro
编辑 src/components/widget/SideBar.astro,在 Tags 组件后面加上 Weather:
---// ... 已有 importimport Weather from "./Weather.astro";---
<!-- 在 #sidebar-sticky 容器内,Tags 后面 --><Tag class="onload-animation" style="animation-delay: 200ms"></Tag><Weather class="onload-animation" style="animation-delay: 250ms"></Weather>animation-delay: 250ms 保证入场动画在 Tags(200ms)之后,形成依次出现的视觉效果。
2.3 添加 i18n 翻译
在 src/i18n/i18nKey.ts 枚举里加一个键:
weather = "weather",然后在每个语言文件里添加对应翻译:
| 文件 | 翻译 |
|---|---|
zh_CN.ts | "天气" |
en.ts | "Weather" |
ja.ts | "天気" |
ko.ts | "날씨" |
es.ts | "Clima" |
| 其余语言文件 | 对应翻译 |
i18n 函数在 Astro 服务端渲染时调用,读的是 siteConfig.lang,不需要客户端参与。