全站加了个「天气预报」:接口免费,而且按访客 IP 定位

2244 字
11 分钟
全站加了个「天气预报」:接口免费,而且按访客 IP 定位
全站加了个「天气预报」:接口免费,而且按访客 IP 定位

刷参考站的时候看到他右栏挂着一张天气预报卡 —— 地点、大号温度、一屏指标,底下还有个「收起」按钮,看着挺舒服。

琢磨了一下,这东西对博客来说几乎是白捡的:不用服务器、不用注册 key、不用维护,访客点进来还能顺手瞄一眼自己那儿的天气。于是照着他的样子整了一个,全站都有。

参考站右栏那张天气预报卡
参考站右栏那张天气预报卡

参考站长什么样#

一张标准的侧栏卡片:标题「天气预报」左边一根主题色竖条,下面第一行是地点图标 + 「广州市 番禺区」,右上角一枚 3rem 的大天气图标;再往下是大号温度 29° 配一个「晴」字,接着一行「高温 32°C / 低温 25°C」—— 高温走红、低温走蓝;然后一条虚线分界,两列指标网格:风力、能见度、体感温度、湿度、气压、空气质量、云量、紫外线、降水量、降水概率、日出、日落,共 12 项,空气质量那格还带个「良」的等级标签;网格下面是全宽的「污染物浓度」,摊开 PM2.5 / PM10 / O₃ / NO₂ / SO₂ / CO 六个数值;卡片底部一行小字「9 分钟前发布」,最后是「收起」按钮。

我的版本结构上没怎么动 —— 参考站已经把信息密度调得挺合适了,我主要是换成了自己的注入方式,另外把展开/收起做成了默认展开。

整张卡片展开的样子
整张卡片展开的样子

我的做法:只动两个文件#

这类侧栏组件想插进上游主题,得改 SideBar.astro 的 componentMap(它写死了支持的组件类型),那是主题源文件,不能碰。所以还是走本站用惯了的页脚注入:

文件状态干什么
src/config/FooterConfig.html改(追加)全部样式 + PAGE_SCRIPTS 全站加载条目
public/assets/js/weather-card.js新增拼卡片 DOM、拉接口、渲染、挂载/搬运、折叠

跟「最近评论」那次不太一样的是,这个组件的 DOM 我没放在页脚 —— 卡片要拼几十个节点,几乎每个都得按接口字段回填,摆在页脚那堆 HTML 里反而更难维护,索性全交给脚本拼。页脚只留样式和一个加载条目:

{
// 「天气预报」:全站加载 —— 桌面挂右栏、移动端挂底部栏,挂载逻辑在 js 里
path: /.*/,
id: "ff-weather-js",
src: "/assets/js/weather-card.js?v=1",
},

git diff 对 FooterConfig.html 是 470 行纯新增、0 删除,主题里一个文件都没改。

核心机制#

数据:参考站用的是 uapis.cn 的天气接口,前端直连一次拿全:

GET https://uapis.cn/api/v1/misc/weather?extended=true&forecast=true
→ 省市 / 区县 / adcode / 天气 / weather_icon / 温度 / 风向风力 / 湿度
/ 高温低温 / 7 天预报 / 体感 / 能见度 / 气压 / 紫外线 / 降水量 / 云量
/ AQI + 等级 + 主要污染物 / PM2.5 PM10 O₃ NO₂ SO₂ CO / 发布时间

免费、不用注册 key,响应头带 Access-Control-Allow-Origin,浏览器直连不需要代理。中文描述现成(「晴」「多云」「东南风」都是接口直接给的),省得自己拿代码去翻。

按访客 IP 定位是这个接口最舒服的地方:不传参数时,它按请求方 IP 反查位置。也就是说每个访客看到的是自己所在地的天气,不是我这儿的。等于白捡一个「个性化」。想固定成站长常驻地也行,?city=广州 或 ?adcode=440105,脚本顶部留了个 FIXED_CITY 常量。

挂载点按视口分两套,跟主题自己的布局规则走:≥768px 进右栏 top 组件容器的末尾(站点信息之后),<768px 主题会把左右侧栏整块隐藏、改用页面底部的 #mobile-bottom-sidebar,组件就挂进里面的 #bottom-sidebar。两类容器都在 swup 替换范围之外,搬一次就永久有效:

function findMountPoint() {
if (isMobileNow()) {
var bar = document.getElementById("bottom-sidebar");
if (bar) return { parent: bar, before: null };
}
var sidebar = document.getElementById("right-sidebar");
if (!sidebar) return null;
var wrapper = sidebar.firstElementChild; // top 组件容器
if (!wrapper) return null;
return { parent: wrapper, before: null };
}

再挂一个 matchMedia("(max-width: 767px)") 的 change 监听,跨断点时把节点搬到另一侧。实测 swup 无刷新切 /archive/ → /about/ → 回首页三跳之后,节点还是同一个、折叠状态也保持,而且接口一次都没重新请求。

折叠没去用上游 WidgetLayout.astro 那套 <widget-layout>。那个组件的展开逻辑写在自定义元素的 constructor 里,靠 querySelector 取自己的子节点 —— 我这边是脚本动态插进去的,构造时机不好控,赌浏览器在子节点就位前先把 constructor 跑掉就白搭。自己写反而简单,一个 class 切换加两行文案:

function applyCollapsed(next) {
collapsed = next;
root.classList.toggle("is-collapsed", collapsed);
label.textContent = collapsed ? "展开" : "收起";
sessionStorage.setItem(VIEW_KEY, collapsed ? "1" : "0");
}

默认展开(参考站是收起的,只露温度,我觉得天气卡的信息量就是它的卖点,没必要先藏起来),点一下压到 9.5rem —— 正好露出地点、温度和那条高低温;状态记在 sessionStorage 里,同一次会话内切页不会弹回默认。

点「收起」之后只剩地点和温度
点「收起」之后只剩地点和温度

缓存给到 30 分钟,原因不是性能,是配额。这接口的响应头里有 Ratelimit-Policy:visitor-quota 1500 credits / 31 天、ip-daily 2000 / 天,每次请求扣 1 credit。个人博客的流量按 30 分钟的本地缓存算,一个访客半小时内翻多少页都只请求一次,撑得住;真哪天被跑爆了,就得在 CF Worker 上再加一层共享缓存。

两个坑#

一、图标不能照抄 <use>。 参考站那些图标是 Astro 用 astro-icon 在构建期按需渲染出来的 <symbol>,符号表只包含当页真正用到的几枚。我在脚本里动态插入节点,既不在构建期的渲染树里、页面上的符号表也没有这些 id,写 <use href="#ai:material-symbols:air"> 只会渲染成一片空白。

解法是把 path 内联进来。从 iconify 的 material-symbols 集合把用到的 34 枚图标的 path 全拉下来,塞进脚本当常量:

var ICONS = {
"air": '<path fill="currentColor" d="M11.5 20q-1.25 0-2.125-.875T8.5 17h2q..."/>',
// ... 33 枚
};
function ico(name, cls) {
return '<svg class="ff-w-ico ' + cls + '" viewBox="0 0 24 24">' + ICONS[name] + "</svg>";
}

代价是脚本从 18.7 KB 长到 31.4 KB(图标常量独占 12.7 KB)—— 换来的是一份完全自包含、不依赖任何运行环境的图标表,值。顺便把天气代码到图标的映射也补全了:接口给的是三位数代码(100 晴 / 101 多云 / 104 阴 / 3xx 雨 / 4xx 雪 / 5xx 雾霾),先查精确码、落空再按百位兜底,以后接口新增代码也不会显示成空白。

二、auto-fit 在窄栏只排得出一列。 指标网格一开始写的是自适应的老配方:

/* 想「宽度够就两列,不够就一列」 */
grid-template-columns: repeat(auto-fit, minmax(8.5rem, 1fr));

8.5rem 是 136px,当时觉得 280px 的右栏怎么也能塞两列。结果算下来要 136×2 + 8(gap) = 280px,而卡片内边距还要吃掉 32px,可用宽度只有 248px —— 一格都放不下第二个,12 项全竖着排,整张卡片被顶到 925px 高。

改成写死两列就完事,每列约 124px:

grid-template-columns: repeat(2, minmax(0, 1fr));

卡片高度从 925px 落到 613px,差不多砍掉三分之一。教训是「自适应」在窄容器里经常不如硬编码 —— 右栏就这么宽,没什么好自适应的。

上手步骤#

  • 换数据源:改 weather-card.js 顶部的 API_URL(字段名要跟着 render() 一起改)。
  • 固定城市:把 FIXED_CITY 填成城市名或 adcode,留空就是按访客 IP 定位。
  • 改默认展开 / 收起:DEFAULT_COLLAPSED 改成 true。
  • 改外观:样式全在 FooterConfig.html 末尾的 .ff-w-* 段,类名统一带前缀,不会跟别的组件撞。
  • 改了 JS 记得升 ?v=:vercel.json 给 /assets/* 配了 30 天强缓存,不加版本号老访客拿到的还是旧文件。

验证走的是老一套:astro dev 起服务,headless 浏览器分别看亮 / 暗主题、1440 与 1280 两种右栏宽度、390px 移动端挂载点,再叠一次 swup 往返(三跳之后节点还是同一个、接口一次都没重新请求)。错误路径也单独试了:把接口请求掐断再重载,卡片会切到错误态并给一个「重试」按钮;重试时接口还挂着就继续停在错误态,不会白屏也不会卡在「加载中」;接口恢复后再点一下才回到内容。

暗色主题下颜色都是跟着主题变量走的,文字和图标不用单独调:

暗色主题下的同一张卡
暗色主题下的同一张卡

移动端挂在页面底部的组件栏里,宽度拉满,指标网格还是两列,污染物那行三列:

移动端底部组件栏里的天气预报
移动端底部组件栏里的天气预报

卡片在右栏用的是和其它侧栏组件同一套 card-base,圆角、描边和暗色适配都跟着主题走,不用单独调。

支持与分享

如果这篇文章对你有帮助,欢迎分享给更多人或打赏支持!

打赏
全站加了个「天气预报」:接口免费,而且按访客 IP 定位
https://www.9ll.uk/posts/blog/weathercard/
作者
HY
发布于
2026-10-16
许可协议
CC BY-NC-SA 4.0
相关文章智能推荐
1
全站加了个「最近评论」:数据直连 Waline,主题文件一个没改
博客在参考站的右栏看到「最近评论」组件,想在自己站上也挂一个,而且要求全站都有。评论数据就在自己的 Waline 后端里,直接调它的公开接口;DOM 和样式走页脚注入,脚本全站加载,桌面挂右栏、移动端挂底部组件栏。踩了三个坑:Waline 的 orig 字段是渲染前的 HTML、类名和外包装撞车吃掉 32px 内边距、窄栏 flex 布局把昵称挤成一个字。
2
全站鼠标指针换成《俺妹》主题:亮色桐乃、深色黑猫
博客这次更新把整站的鼠标指针换了一遍:亮色主题配高坂桐乃、深色主题配五更琉璃(黑猫),各 28 枚共 56 枚,正常选择、链接、文本、禁用、拖拽这些场景全部覆盖。素材来自两张总览图,切图时发现它们不是普通白底图 —— 人物衣服和箭头内部都是纯白,跟我页面的白同色,按我处理贴纸那套「全局去白底」会把图形整个掏空。CSS 那边我把「用哪张图 + 热点坐标」全塞进了自定义属性,28 条规则只写一遍,切换主题只换一组变量。
3
首页加了一条会滚的动态:参考楠枝小筑,一个主题文件都没改
博客在楠枝小筑首页看到一条会一直横滚的「瞬间」条,想在自己站上也挂一个。他用的是 Ethereal 主题,代码没法照抄,所以只搬机制:Memos 数据在客户端拉,滚动靠 CSS 把轨道复制成两份,DOM 走页脚注入再加一点点搬运。最后改动落在两个文件里,主题源码一个没碰。
4
评论区回复默认展开 2 条,长串收在「展开更多」后面
博客友链页有条评论下面挂了 13 条回复,往下翻要滑过整屏;可全收起来又太狠,访客第一眼什么都看不到。最后定成默认展示 2 条,多出来的才收着,还有剩就给个「展开更多」,摊完能收回去。做法照旧只加文件不动上游。踩坑集中在一点上:Waline 是 Vue 渲染的,DOM 会重绘、会随 swup 换页被整体替换,所以折叠状态不能只靠一次性的 DOM 操作。
5
回复带上引用:别再让读者猜「@昵称」在回哪一句
博客Waline 的二级回复只渲染一行「@昵称:」,谁在回谁很清楚,回的是哪一句全靠猜。翻 DOM 发现它其实留了线索 —— 回复里那个 @昵称 的链接 href 就是被回复评论的 objectId。顺着锚点把目标评论捞回来,摘成引用块插在下面,点它还能滚过去高亮。踩了个坑:剥 @ 前缀时忘了 clone,把页面上那行整个删了。
随机文章随机推荐

评论区

Profile Image of the Author
HY
Hello, I'm HY.
公告
迷茫.... 为了访问速度,本站将会在11月上旬至中旬迁移到0dm.cn并完成备案
分类
标签
最新动态
站点统计
文章
74
分类
8
标签
192
总字数
161,979
运行时长
0 天
最后活动
0 天前
站点信息
构建平台
Vercel
博客版本
Firefly v6.16.8
文章许可
CC BY-NC-SA 4.0
文章目录