阶段二:核心服务与状态管理
1. 教学目标
- 理解服务分层:掌握将不同职责的逻辑(如定位、API请求、状态管理、工具函数)拆分到独立
utils文件中的重要性。 - 掌握异步流程:能够熟练运用
Promise和async/await封装uni-app的原生异步 API。 - 学习简易状态管理:理解如何使用 Vue 3 的
reactiveAPI 结合uni.setStorageSync构建一个轻量、持久化的全局状态管理器。 - 建立真实数据流:将阶段一的静态首页与真实的服务层连接,建立起清晰的数据流动闭环:页面加载 → 定位 → 获取数据 → 更新状态 → 渲染UI。
- 数据驱动开发:实现首页所有数据均由真实 API 动态驱动,替换掉所有硬编码的静态数据。
2. 真实数据流图 (Mermaid)
此图精确展示了本项目中从应用启动到首页渲染完成的完整数据流。
3. 服务层构建详解
在阶段一,我们的页面是“死”的。现在,我们要通过建立服务层,为它注入“灵魂”——真实的数据。这个服务层由一系列各司其职的JS文件构成。
🔗 步骤一:封装定位服务 (utils/location.js)
设计流程
目标:我们需要一个可靠的方式来获取应用的起始城市信息。这个过程比单一的API调用要复杂,它包含多个步骤:首先通过IP地址大致定位城市,然后用获取到的城市名去查询更精确的 locationId(天气API需要这个ID)。
设计决策:将这个复杂、涉及多个异步调用的流程封装到一个名为 getCurrentCity 的函数中。这样,页面层只需调用这一个函数,就能得到所有需要的位置信息,大大简化了页面的逻辑。
编码流程
- 创建
/utils/location.js文件。 - 在文件中,定义
getCurrentCity函数,并标记为async。 - 在函数内部,使用
new Promise包装第一个uni.request,用于通过高德IP定位API获取城市名和经纬度。提供fail回调,以便在失败时返回一个默认城市(如北京)。 - 使用
await等待上一步的结果。 - 拿到城市名后,再次
await调用fetchCityId函数(同样是Promise封装的uni.request),去和风天气API查询该城市名的locationId。 - 将所有获取到的信息(城市名、经纬度、
locationId)组合成一个对象并return。
点击展开/折叠 utils/location.js 源代码
/**
* 定位工具函数
* 1、获取天气情况的api中,需要用到loaction(城市id),需要先获取城市id,
* 2、获取城市的id,可以通过城市名称来获取(调用和风天气api)
* 3、城市名称,可以根据ip地址获取,通过ip地址获取城市名称和经纬度信息。
* * 步骤一、根据ip地址获取城市名称和经纬度信息(用高德地图api)
* 步骤二、根据城市名称获取城市id(用和风天气api)
* * 错误处理:失败的话,都默认返回北京id、城市名、经纬度信息
*/
// 和风天气API Key
import UrlConfig from '../datas/url.json';
const QWEATHER_KEY = UrlConfig.getCityIdUrl.key;
const AMAP_URL = UrlConfig.getCurrentCityUrl.AMAP_URL;
const AMAP_KEY = UrlConfig.getCurrentCityUrl.AMAP_KEY;
// const BAIDU_URL = UrlConfig.getCurrentCityUrl.BAIDU_URL;
// const BAIDU_KEY = UrlConfig.getCurrentCityUrl.BAIDU_KEY;
/**
* 获取当前位置信息
* @returns {Promise} - 城市信息对象,包含city、longitude、latitude、locationId
*/
export const getCurrentCity = async () => {
// 第一步:获取IP对应的城市
const ipData = await new Promise((resolve) => {
uni.request({
// // url: '[http://ip-api.com/json/?lang=zh-CN](http://ip-api.com/json/?lang=zh-CN)'
url: AMAP_URL, //高德 /百度
data: {
key: AMAP_KEY //高德
// ak: "BAIDU_KEY" , coor:"bd09ll" //百度
},
method: 'GET',
success: (res) => {
const [lng, lat] = res.data.rectangle.split(';')[0].split(',');
resolve({
city: res.data.city,
longitude: parseFloat(lng).toFixed(2), //经度
latitude: parseFloat(lat).toFixed(2) //纬度
});
console.log("success", res.data);
},
fail: (err) => {
resolve({
city: '北京',
longitude: 116.41,//经度
latitude: 39.92 //纬度
});
console.log("fail");
}
});
});
const cityName = ipData.city || '北京';
// 第二步:获取城市对应的locationId
const cityIdData = await fetchCityId(cityName);
const locationId = cityIdData?.location?.[0]?.id || '101010100';
// 创建城市信息对象
const cityInfo = {
city: cityName,
longitude: ipData.longitude,
latitude: ipData.latitude,
locationId: locationId
};
// 返回城市信息对象
return cityInfo;
};
/**
* 根据城市名获取和风天气locationId
* @param {string} cityName - 城市名称
* @returns {Promise} - 包含locationId的响应数据
*/
export const fetchCityId = (cityName) => {
return new Promise((resolve) => {
uni.request({
url: UrlConfig.getCityIdUrl.url,
method: 'GET',
data: {
location: cityName,
key: QWEATHER_KEY
},
success: (res) => {
resolve(res.data);
},
fail: () => {
// 失败时返回默认数据
resolve({ location: [{ id: '101010100' }] });
}
});
});
};
/**
* 根据城市名更新城市信息
* @param {string} cityName - 城市名称
* @returns {Promise} - 城市信息对象,包含city、longitude、latitude、locationId
*/
export const updateCityInfoByCity = async (cityName) => {
// 获取城市对应的locationId
const cityIdData = await fetchCityId(cityName);
const locationId = cityIdData?.location?.[0]?.id || '101010100';
// 获取经纬度(如果cityIdData中包含)
const longitude = cityIdData?.location?.[0]?.lon || 116.41;
const latitude = cityIdData?.location?.[0]?.lat || 39.92;
// 创建城市信息对象
const cityInfo = {
city: cityName,
longitude: longitude,
latitude: latitude,
locationId: locationId
};
// 返回城市信息对象
return cityInfo;
};🗄️ 步骤二:创建简易状态管理器 (utils/store.js)
设计流程
目标:我们需要一个地方来存放全局共享的数据,例如当前城市信息。这个数据需要在多个页面(首页、城市页)之间同步,并且在小程序关闭后能够被记住。
设计决策:不引入大型状态管理库(如Pinia),而是利用 Vue 3 自带的 reactive API 创建一个轻量级的 store 对象。通过导出 set 和 get 方法来规范化对状态的读写。在每次 set 操作时,都同步使用 uni.setStorageSync 将数据写入本地缓存,实现持久化。在应用首次加载时,通过 initStore 函数从缓存中恢复数据。
编码流程
- 创建
/utils/store.js文件。 - 从
vue导入reactive。 - 创建一个名为
store的reactive对象,并定义初始状态,如cityInfo。 - 编写
setCityInfo(cityInfo)函数。函数内部首先更新store.cityInfo,然后立即调用uni.setStorageSync('CityInfo', cityInfo)。 - 编写
getCityInfo()函数,直接返回store.cityInfo。 - 编写
initStore()函数,在文件首次被导入时执行。它尝试用uni.getStorageSync('CityInfo')读取数据,如果存在,则用它初始化store.cityInfo。
点击展开/折叠 utils/store.js 源代码
import { reactive } from 'vue';
// 创建全局状态
const store = reactive({
// 当前城市信息
cityInfo: {
city: '北京',
longitude: 116.41,
latitude: 39.92,
locationId: '101010100'
},
// 我的城市列表
myCities: ['北京', '上海', '广州'],
// 热门城市列表
hotCities: ['北京', '上海', '广州', '深圳', '杭州', '成都', '武汉', '西安']
});
/**
* 设置当前城市信息
* @param {Object} cityInfo - 城市信息对象,包含city、longitude、latitude、locationId
*/
export const setCityInfo = (cityInfo) => {
store.cityInfo = cityInfo;
// 保存到本地存储,使用单个键"CityInfo"
uni.setStorageSync('CityInfo', cityInfo);
};
/**
* 获取当前城市信息
* @returns {Object} - 当前城市信息对象
*/
export const getCityInfo = () => {
return store.cityInfo;
};
/**
* 添加城市到我的城市列表
* @param {string} city - 城市名称
*/
export const addMyCity = (city) => {
if (!store.myCities.includes(city)) {
store.myCities.push(city);
// 保存到本地存储
uni.setStorageSync('myCities', store.myCities);
}
};
/**
* 从我的城市列表中删除城市
* @param {string|number} city - 城市名称或索引
*/
export const removeMyCity = (city) => {
let index;
if (typeof city === 'string') {
index = store.myCities.indexOf(city);
} else {
index = city;
}
if (index !== -1) {
store.myCities.splice(index, 1);
// 保存到本地存储
uni.setStorageSync('myCities', store.myCities);
}
};
/**
* 获取我的城市列表
* @returns {Array} - 我的城市列表
*/
export const getMyCities = () => {
return store.myCities;
};
/**
* 获取热门城市列表
* @returns {Array} - 热门城市列表
*/
export const getHotCities = () => {
return store.hotCities;
};
// 初始化时从本地存储加载数据
const initStore = () => {
// 加载城市信息
const cityInfo = uni.getStorageSync('CityInfo');
if (cityInfo) {
store.cityInfo = cityInfo;
}
// 加载我的城市列表
const cities = uni.getStorageSync('myCities');
if (cities && Array.isArray(cities)) {
store.myCities = cities;
}
};
initStore();
export default store;🔗 步骤三:封装统一API服务 (utils/api.js)
设计流程
目标:将所有与第三方天气API的直接通信都集中到一个地方。这样做可以避免在多个页面中重复编写 uni.request 代码,并方便未来统一处理如更换API供应商、修改API参数、添加通用错误处理等需求。
设计决策:为每个API端点(如获取实时天气、获取7日预报)创建一个独立的、async 的导出函数。在函数内部,从 store(通过本地缓存)读取需要的 locationId,然后执行 uni.request。最重要的是,在 success 回调中,对API返回的原始数据进行“清洗”和“格式化”,将其转换为UI组件真正需要的、干净整洁的 Object 或 Array 格式再返回。
编码流程
- 创建
/utils/api.js文件。 - 为每个API(如
getCurrentWeather)创建一个async函数。 - 在函数内部,通过
uni.getStorageSync('CityInfo')获取locationId。 - 使用
new Promise包装uni.request,并在其中配置好URL、Key和locationId等参数。 - 在
success回调中,从res.data中提取需要的数据(如res.data.now),然后构建一个新的、结构清晰的对象返回。例如,将res.data.now.temp映射为data.temp。 - 为所有API函数重复此过程。
点击展开/折叠 utils/api.js 源代码
// 引入API地址配置
// 引入API地址配置
import UrlConfig from '../datas/url.json';
/**
* 天气文本映射表
* 将天气文本映射到组件使用的icon名称
*/
const WEATHER_TEXT_MAP = {
'晴': 'star',
'多云': 'cloud-download',
'阴': 'cloud-upload',
'小雨': 'download',
'中雨': 'download',
'大雨': 'download',
'暴雨': 'download',
'阵雨': 'download',
'雷阵雨': 'download',
'小雪': 'cloud-download',
'中雪': 'cloud-download',
'大雪': 'cloud-download',
'暴雪': 'cloud-download',
'雨夹雪': 'cloud-download',
'雾': 'info',
'霾': 'info',
'沙尘暴': 'info'
};
/**
* 获取实时天气数据
* @param {string} city - 城市名称
* @returns {object} - 实时天气数据
*/
export const getCurrentWeather = async (city) => {
// 从CityInfo对象中获取locationId
const cityInfo = uni.getStorageSync('CityInfo') || {};
const locationId = cityInfo.locationId || UrlConfig.CurrentWeatherUrl.data.location;
// 使用异步请求获取实时天气数据
const result = await new Promise((resolve) => {
uni.request({
url: UrlConfig.CurrentWeatherUrl.url,
method: 'GET',
data: {
key: UrlConfig.CurrentWeatherUrl.data.key,
location: locationId
},
dataType: 'json',
success: (res) => resolve(res.data),
fail: () => resolve({ code: '500' })
});
});
if (result.code === '200') {
const nowData = result.now;
return {
code: '200',
data: {
city: city,
temp: nowData.temp,
weather: nowData.text,
humidity: `${nowData.humidity}%`,
wind: `${nowData.windDir} ${nowData.windScale}级`,
pressure: `${nowData.pressure} hPa`,
visibility: `${nowData.vis} km`,
icon: WEATHER_TEXT_MAP[nowData.text] || 'star'
}
};
} else {
// 请求失败或数据错误时返回默认数据
return {
code: '500',
data: {
city: city,
temp: '25',
weather: '晴',
humidity: '45%',
wind: '东北风 3级',
visibility: '10 km',
icon: 'star'
}
};
}
};
/**
* 获取空气质量数据
* @param {string} city - 城市经纬度,格式为"经度longitude,纬度latitude"
* @returns {object} - 空气质量数据
*/
export const getAirQuality = async (city) => {
// 直接导入getCurrentCity
const { getCurrentCity } = await import('./location.js');
// 从CityInfo对象中获取经纬度
const cityInfo = uni.getStorageSync('CityInfo') || {};
const longitude = cityInfo.longitude || 116.41;
const latitude = cityInfo.latitude || 39.92;
// 构建空气质量URL,使用获取到的经纬度
const baseUrl = UrlConfig.CurrentAirQualityUrl.url;
const url = `${baseUrl}/${latitude}/${longitude}`;
// 使用异步请求获取空气质量数据
const result = await new Promise((resolve) => {
uni.request({
url: url,
method: 'GET',
data: UrlConfig.CurrentAirQualityUrl.data,
dataType: 'json',
success: (res) => resolve(res.data),
fail: () => resolve({ code: '500' })
});
});
if (result.indexes && result.pollutants) {
const aqiData = result.indexes[0];
const pollutants = result.pollutants;
// 从pollutants中提取各项污染物数据
const pollutantMap = {};
pollutants.forEach(pollutant => {
pollutantMap[pollutant.code] = pollutant.concentration.value;
});
// 根据AQI值计算颜色
const getAirQualityColor = (aqi) => {
if (aqi <= 50) return '#00E400'; // 优
if (aqi <= 100) return '#FFFF00'; // 良
if (aqi <= 150) return '#FF7E00'; // 轻度污染
if (aqi <= 200) return '#FF0000'; // 中度污染
if (aqi <= 300) return '#99004C'; // 重度污染
return '#7E0023'; // 严重污染
};
return {
code: '200',
data: {
aqi: aqiData.aqi,
level: aqiData.category,
pm10: pollutantMap['pm10'] || 0,
no2: pollutantMap['no2'] || 0,
o3: pollutantMap['o3'] || 0,
color: getAirQualityColor(aqiData.aqi)
}
};
} else {
// 请求失败或数据错误时返回默认数据
return {
code: '500',
data: {
aqi: 75,
level: '良',
pm25: 35,
pm10: 60,
no2: 25,
o3: 80,
color: '#FFFF00'
}
};
}
};
/**
* 获取24小时天气预报
* @param {string} city - 城市名称
* @returns {object} - 24小时天气预报数据
*/
export const get24hForecast = async (city) => {
// 从CityInfo对象中获取locationId
const cityInfo = uni.getStorageSync('CityInfo') || {};
const locationId = cityInfo.locationId || UrlConfig.Weather24hUrl.data.location;
// 使用异步请求获取24小时天气预报数据
const result = await new Promise((resolve) => {
uni.request({
url: UrlConfig.Weather24hUrl.url,
method: 'GET',
data: {
key: UrlConfig.Weather24hUrl.data.key,
location: locationId
},
dataType: 'json',
success: (res) => resolve(res.data),
fail: () => resolve({ code: '500' })
});
});
if (result.code === '200') {
const hourlyData = result.hourly;
const forecast = hourlyData.map(item => {
// 从fxTime中提取小时,格式为HH:00
const hour = new Date(item.fxTime).getHours().toString().padStart(2, '0');
return {
time: `${hour}:00`,
temp: item.temp,
weather: item.text,
wind: `${item.windDir} ${item.windScale}级`,
icon: WEATHER_TEXT_MAP[item.text] || 'star'
};
});
return {
code: '200',
data: forecast
};
} else {
// 请求失败或数据错误时返回默认数据
return {
code: '500',
data: []
};
}
};
/**
* 获取7天天气预报
* @param {string} city - 城市名称
* @returns {object} - 7天天气预报数据
*/
export const get7dForecast = async (city) => {
// 1、从CityInfo对象中获取locationId
// 2、使用异步请求获取7天天气预报数据
// 3、处理返回数据:对日期数据进行格式化、提取需要的字段(需要返回date、day、tempMax、tempMin、weather、wind、icon)
// 4、如果请求成功,返回格式化后的数据;如果失败,返回默认数据(code: '500', data: [])
};
/**
* 获取生活指数数据
* @param {string} city - 城市名称
* @returns {object} - 生活指数数据
*/
export const getLifeIndex = async (city) => {
// 1、从CityInfo对象中获取locationId
// 2、使用异步请求获取生活指数数据
// 3、 处理返回数据:提取需要的字段(需要返回{type、name、value、icon、color、desc})(type类型,参考和风天气api中天气指数-天气指数预报的接口文档)
// 4、如果请求成功,返回格式化后的数据;如果失败,返回默认数据(code: '500', data: [])
};🗄️ 步骤四:编写辅助工具 (utils/weather.js 暂不写)
设计流程
目标:项目中的一些业务逻辑并不直接依赖于外部 API 或全局状态,而是对数据进行纯粹的计算或转换,例如根据AQI数值判断空气质量等级。将这类纯计算逻辑独立出来,可以提高代码的可测试性、可维护性和复用性。
设计决策:创建一个专门存放这类辅助函数的 weather.js 文件。这些函数应该接收输入、返回输出,不产生副作用,也即“纯函数”。
编码流程
- 创建
/src/utils/weather.js文件。 - 定义
getAirQualityLevel(aqi)函数,它接收一个AQI数值作为参数。 - 在函数内部,根据不同的AQI范围,返回包含
level,color,desc的对象。 - 将
getAirQualityLevel函数export导出,以便其他模块可以导入并使用。
点击展开/折叠 utils/weather.js 源代码
/**
* 天气工具函数
*/
/**
* 根据AQI值获取空气质量等级
* @param {number} aqi - AQI值
* @returns {object} - 空气质量等级信息
*/
export const getAirQualityLevel = (aqi) => {
if (aqi <= 50) {
return {
level: '优',
color: '#00E400',
desc: '空气质量令人满意,基本无空气污染'
};
} else if (aqi <= 100) {
return {
level: '良',
color: '#FFFF00',
desc: '空气质量可接受,但某些污染物可能对极少数异常敏感人群健康有较弱影响'
};
} else if (aqi <= 150) {
return {
level: '轻度污染',
color: '#FF7E00',
desc: '易感人群症状有轻度加剧,健康人群出现刺激症状'
};
} else if (aqi <= 200) {
return {
level: '中度污染',
color: '#FF0000',
desc: '进一步加剧易感人群症状,可能对健康人群心脏、呼吸系统有影响'
};
} else if (aqi <= 300) {
return {
level: '重度污染',
color: '#99004C',
desc: '心脏病和肺病患者症状显著加剧,运动耐受力降低,健康人群普遍出现症状'
};
} else {
return {
level: '严重污染',
color: '#7E0023',
desc: '健康人群运动耐受力降低,有明显强烈症状,提前出现某些疾病'
};
}
};🔗 步骤五:连接服务与视图 (index.vue)
设计流程
目标:让 index.vue 页面(父组件)成为一个“总指挥”,负责调用前面创建的各种服务,获取所有数据,然后分发给各个子组件进行展示。
设计决策:我们将在 onMounted 生命周期钩子(页面首次加载时触发)中执行一个名为 initData 的总初始化函数。initData 首先调用 location.js 中的 getCurrentCity 获取位置,然后调用 store.js 中的 setCityInfo 将位置信息存入全局状态。随后,调用 updateWeather 函数,此函数内将并行调用 api.js 中的多个接口,并将获取到的数据分别更新到 index.vue 中定义的各个 ref 变量上。Vue的响应式系统会自动将这些更新后的数据传递给子组件,从而刷新UI。
编码流程
- 打开
pages/index/index.vue文件。 - 在
<script setup>顶部,从utils目录import所有需要的服务函数。 - 使用
ref()为每个子组件所需的数据创建响应式变量,如currentWeather = ref({})。 - 创建
async函数updateWeather,在其中await调用api.js中的各个数据获取函数,并将返回的数据赋值给对应的ref变量。 - 创建
async函数initData,在其中await调用getCurrentCity和setCityInfo,然后await调用updateWeather。 - 在
onMounted钩子中调用initData()。
点击展开/折叠 index.vue 脚本区源代码
<script setup>
import {ref, onMounted, computed} from 'vue';
// 1. 导入所有组件和服务
import CurrentEnvironment from '../../components/CurrentEnvironment.vue';
import HourlyForecast from '../../components/HourlyForecast.vue';
import DailyForecast from '../../components/DailyForecast.vue';
import LifeIndex from '../../components/LifeIndex.vue';
import {getCurrentWeather, get24hForecast, get7dForecast, getAirQuality, getLifeIndex} from '../../utils/api';
import {getCityInfo, setCityInfo} from '../../utils/store';
import {getCurrentCity} from '../../utils/location';
// ...
const currentCity = computed(() => getCityInfo().city);
// 2. 定义 ref 变量
const currentWeather = ref({});
const airQuality = ref({});
const hourlyForecast = ref([]);
// dailyForecast 、 lifeIndices
// 3. 核心数据更新函数
const updateWeather = async () => {
const city = getCityInfo().city;
// 获取当前天气情况 currentWeather
const weatherRes = await getCurrentWeather(city);
if (weatherRes.code === '200') {
currentWeather.value = weatherRes.data;
}
// 获取当前空气质量 airQuality
const airQualityRes = await getAirQuality(city);
if (airQualityRes.code === '200') {
airQuality.value = airQualityRes.data;
}
// ...获取其他天气数据... hourlyForecast 、 dailyForecast 、 lifeIndices
};
// 4. 初始化函数
const initData = async () => {
const currentLocation = await getCurrentCity();
setCityInfo(currentLocation);
await updateWeather();
};
// 5. 在页面加载时触发所有逻辑
onMounted(() => {
initData();
});
</script>