Cache home location and weather across page navigation
ci-cd / verify (push) Successful in 6s
ci-cd / deploy (push) Skipped

This commit is contained in:
2026-10-05 11:08:11 +08:00
parent 42aea10101
commit f0ee63e642
5 changed files with 205 additions and 57 deletions
+28 -54
View File
@@ -1,5 +1,6 @@
const { secure, toast } = require("../../utils/api"); const { secure, toast } = require("../../utils/api");
const { localDate } = require("../../utils/date"); const { localDate } = require("../../utils/date");
const environment = require("../../utils/environment");
function dateLabel() { function dateLabel() {
const now = new Date(); const now = new Date();
return `${now.getMonth() + 1}月${now.getDate()}日 周${"日一二三四五六"[now.getDay()]}`; return `${now.getMonth() + 1}月${now.getDate()}日 周${"日一二三四五六"[now.getDay()]}`;
@@ -19,7 +20,7 @@ Page({
error: "", error: "",
}, },
onShow() { onShow() {
this.updateEnvironmentLabel(); this.applyEnvironment(environment.cached());
this.load(); this.load();
}, },
onUnload() { onUnload() {
@@ -31,62 +32,40 @@ Page({
dateLabel() + " · " + (this.weatherLabel || "轻触获取天气"), dateLabel() + " · " + (this.weatherLabel || "轻触获取天气"),
}); });
}, },
applyEnvironment(value) {
if (this.disposed) return;
if (value) {
this.weatherLabel = value.weatherLabel;
this.setData({ locationName: value.locationName });
}
this.updateEnvironmentLabel();
},
async refreshEnvironment(event) { async refreshEnvironment(event) {
if (this.environmentBusy) return; if (this.environmentBusy) return;
this.environmentBusy = true; this.environmentBusy = true;
const tapped = !!event; const tapped = !!event;
let positioned = false; const cached = environment.cached();
try { if (!cached) {
const settings = await new Promise((resolve, reject) =>
wx.getSetting({ success: resolve, fail: reject }),
);
if (settings.authSetting["scope.userLocation"] === false) {
if (!tapped) throw new Error("location-denied");
const enabled = await new Promise((resolve, reject) =>
wx.openSetting({ success: resolve, fail: reject }),
);
if (!enabled.authSetting["scope.userLocation"])
throw new Error("location-denied");
}
this.weatherLabel = "正在获取天气"; this.weatherLabel = "正在获取天气";
this.updateEnvironmentLabel(); this.updateEnvironmentLabel();
const point = await new Promise((resolve, reject) => }
wx.getLocation({ try {
type: "gcj02", this.applyEnvironment(await environment.refresh(tapped));
success: resolve,
fail: reject,
}),
);
positioned = true;
const result = await secure("/environment", "GET", {
latitude: point.latitude,
longitude: point.longitude,
});
this.setData({ locationName: result.location_name });
this.weatherLabel = result.weather
? `${result.weather.description} ${result.weather.temperature}°C`
: "天气暂不可用";
this.environmentAt = result.weather ? Date.now() : 0;
} catch (error) { } catch (error) {
const denied = /denied|deny|auth/.test( if (this.disposed) return;
error.errMsg || error.message || "", if (cached) {
); this.applyEnvironment(cached);
this.setData({ } else {
locationName: positioned const denied = /denied|deny|auth/.test(
? "位置暂不可用" error.errMsg || error.message || "",
: denied );
? "开启定位" this.setData({ locationName: denied ? "开启定位" : "定位暂不可用" });
: "定位暂不可用", this.weatherLabel = denied ? "轻触开启定位" : "轻触重试";
}); this.updateEnvironmentLabel();
this.weatherLabel = denied ? "轻触开启定位" : "轻触重试"; }
if (tapped && !denied) if (tapped) toast(error);
wx.showToast({
title: positioned ? "位置和天气获取失败,请重试" : "定位失败,请重试",
icon: "none",
});
} finally { } finally {
this.environmentBusy = false; this.environmentBusy = false;
this.updateEnvironmentLabel();
} }
}, },
async load() { async load() {
@@ -129,12 +108,7 @@ Page({
phase: "ready", phase: "ready",
}); });
// Location permission and weather never hold up the first usable screen. // Location permission and weather never hold up the first usable screen.
if ( this.refreshEnvironment();
!this.environmentAt ||
Date.now() - this.environmentAt > 15 * 60 * 1000
) {
this.refreshEnvironment();
}
} catch (error) { } catch (error) {
if (this.disposed) return; if (this.disposed) return;
this.setData({ this.setData({
+85
View File
@@ -0,0 +1,85 @@
const { secure } = require("./api");
const STORAGE_KEY = "pet_environment_v1";
let value;
let attempted = false;
let pending;
let failure;
function cached() {
if (value === undefined) {
const saved = wx.getStorageSync(STORAGE_KEY);
value =
saved &&
typeof saved.locationName === "string" &&
typeof saved.weatherLabel === "string" &&
Number.isFinite(saved.updatedAt)
? saved
: null;
}
return value;
}
async function fetchEnvironment(tapped) {
const settings = await new Promise((resolve, reject) =>
wx.getSetting({ success: resolve, fail: reject }),
);
if (settings.authSetting["scope.userLocation"] === false) {
if (!tapped) throw new Error("location-denied");
const enabled = await new Promise((resolve, reject) =>
wx.openSetting({ success: resolve, fail: reject }),
);
if (!enabled.authSetting["scope.userLocation"])
throw new Error("location-denied");
}
const point = await new Promise((resolve, reject) =>
wx.getLocation({ type: "gcj02", success: resolve, fail: reject }),
);
const result = await secure("/environment", "GET", {
latitude: point.latitude,
longitude: point.longitude,
});
if (!result || !result.location_name)
throw new Error("位置和天气获取失败,请重试");
const previous = cached();
value = {
locationName: result.location_name,
weatherLabel: result.weather
? `${result.weather.description} ${result.weather.temperature}°C`
: previous && previous.locationName === result.location_name
? previous.weatherLabel
: "天气暂不可用",
updatedAt: Date.now(),
};
// Store display data only; precise coordinates are not persisted here.
try {
wx.setStorageSync(STORAGE_KEY, value);
} catch (_) {}
return value;
}
function refresh(tapped = false) {
if (pending) return pending;
// Module lifetime follows the app, including recreated pages and warm resumes.
// Failed automatic attempts also wait for a tap or the next cold start.
if (attempted && !tapped)
return failure ? Promise.reject(failure) : Promise.resolve(cached());
attempted = true;
pending = fetchEnvironment(tapped)
.then(
(result) => {
failure = null;
return result;
},
(error) => {
failure = error;
throw error;
},
)
.finally(() => {
pending = null;
});
return pending;
}
module.exports = { cached, refresh };
+84
View File
@@ -0,0 +1,84 @@
const test = require("node:test");
const assert = require("node:assert/strict");
const fs = require("node:fs");
const vm = require("node:vm");
function runtime(storage = new Map()) {
const counts = { location: 0, request: 0 };
let fail = false;
let release;
const module = { exports: {} };
vm.runInNewContext(fs.readFileSync("pets-fe/utils/environment.js", "utf8"), {
module,
wx: {
getStorageSync: (key) => storage.get(key),
setStorageSync: (key, value) => storage.set(key, value),
getSetting: (opts) => opts.success({ authSetting: {} }),
getLocation: (opts) => {
counts.location++;
opts.success({ latitude: 25, longitude: 102 });
},
},
require: () => ({
secure: async () => {
counts.request++;
if (fail) throw new Error("offline");
if (release) await release;
return {
location_name: "昆明市 · 五华区",
weather: { description: "晴", temperature: "22" },
};
},
}),
});
return {
api: module.exports,
counts,
storage,
fail: () => {
fail = true;
},
hold: (promise) => {
release = promise;
},
};
}
test("页面重建与返回复用同一轮天气请求,点击可以刷新", async () => {
const r = runtime();
let finish;
r.hold(
new Promise((resolve) => {
finish = resolve;
}),
);
const first = r.api.refresh();
assert.equal(r.api.refresh(), first);
finish();
await first;
await r.api.refresh();
await r.api.refresh();
assert.equal(r.counts.location, 1);
assert.equal(r.counts.request, 1);
await r.api.refresh(true);
assert.equal(r.counts.request, 2);
assert.equal(r.api.cached().weatherLabel, "晴 22°C");
const saved = r.storage.get("pet_environment_v1");
assert.equal("latitude" in saved, false);
assert.equal("longitude" in saved, false);
});
test("冷启动先读取上次缓存,再请求一次;失败保留缓存且不会返回就重试", async () => {
const previous = runtime();
await previous.api.refresh();
const next = runtime(previous.storage);
assert.equal(next.api.cached().locationName, "昆明市 · 五华区");
assert.equal(next.counts.request, 0);
next.fail();
await assert.rejects(next.api.refresh(), /offline/);
await assert.rejects(next.api.refresh(), /offline/);
assert.equal(next.counts.request, 1);
assert.equal(next.api.cached().weatherLabel, "晴 22°C");
await assert.rejects(next.api.refresh(true), /offline/);
assert.equal(next.counts.request, 2);
});
+3
View File
@@ -5,6 +5,9 @@ const path = require("node:path");
const apiPath = require.resolve("../pets-fe/utils/api"); const apiPath = require.resolve("../pets-fe/utils/api");
function page(name, secure, wx = {}) { function page(name, secure, wx = {}) {
// Each test represents a fresh app session; page navigation within a test
// still shares the environment module.
delete require.cache[require.resolve("../pets-fe/utils/environment")];
global.wx = { global.wx = {
getStorageSync: () => [], getStorageSync: () => [],
setStorageSync: () => {}, setStorageSync: () => {},
+5 -3
View File
@@ -99,9 +99,11 @@ function home(login, secure) {
definition = value; definition = value;
}, },
require: (name) => require: (name) =>
name.includes("date") name.includes("environment")
? { localDate: () => "2026-10-05" } ? { cached: () => null }
: { secure, toast: () => {} }, : name.includes("date")
? { localDate: () => "2026-10-05" }
: { secure, toast: () => {} },
}); });
const page = { const page = {
...definition, ...definition,