当前位置: 首页 > news >正文

Vue3 实战一个小项目(如 Todo List 或 天气查询),强制使用 Composition API

今天我们将把前面学到的所有知识(响应式、组件通信、Composables、Pinia、Router)融合在一起,构建一个功能完整、代码规范智能天气查询应用 (Smart Weather Dashboard)

相比 Todo List,天气应用更能体现异步请求、状态管理、动态路由和逻辑复用


🌤️ 项目目标:Smart Weather Dashboard

功能列表

  1. 搜索城市:输入城市名,获取实时天气。
  2. 历史记录:自动保存最近搜索的 5 个城市(使用 Pinia + localStorage)。
  3. 多页面
    • 首页 (/):显示当前天气和搜索框。
    • 历史页 (/history):显示搜索历史列表。
  4. 自定义 Hook:封装 useWeather 处理 API 请求逻辑。
  5. UI 风格:使用原生 CSS + 简单的响应式布局(无需额外 UI 库,保持轻量)。

⚠️ 强制要求:全程使用 <script setup>Composition API。禁止出现 export default { data() {} }


🛠️ 第一步:项目初始化

# 1. 创建项目 (选择 Vue + Router)
npm create vue@latest weather-app
cd weather-app
npm install# 2. 安装 Pinia 和 Axios (用于请求)
npm install pinia axios# 3. 启动
npm run dev

注:为了演示方便,我们将使用 Open-Meteo 免费 API,它不需要 API Key,直接可用。


🧠 第二步:状态管理 (Pinia)

我们需要一个 Store 来管理搜索历史,这样即使刷新页面,历史记录依然存在。

文件: src/stores/history.js

import { defineStore } from 'pinia'
import { ref, watch } from 'vue'export const useHistoryStore = defineStore('history', () => {// 初始化:从 localStorage 读取const saved = localStorage.getItem('weather_history')const cities = ref(saved ? JSON.parse(saved) : [])// 监听变化,自动同步到 localStoragewatch(cities, (newVal) => {localStorage.setItem('weather_history', JSON.stringify(newVal))}, { deep: true })// 添加城市到历史 (去重,并保持最新在前)function addCity(cityData) {const index = cities.value.findIndex(c => c.id === cityData.id)if (index !== -1) {// 如果已存在,移到最前面cities.value.splice(index, 1)}cities.value.unshift(cityData)// 只保留最近 5 个if (cities.value.length > 5) {cities.value.pop()}}function clearHistory() {cities.value = []}return { cities, addCity, clearHistory }
})

🔌 第三步:逻辑复用 (Composable)

这是 Vue 3 的精髓。我们将天气查询逻辑完全剥离到 useWeather.js 中,组件只负责展示。

文件: src/composables/useWeather.js

import { ref } from 'vue'
import axios from 'axios'export function useWeather() {const weatherData = ref(null)const loading = ref(false)const error = ref(null)// Open-Meteo 需要先通过地理编码 API 获取经纬度// 这里为了简化,我们假设用户输入的是标准城市名,实际项目中通常需要专门的 Geocoding API// 演示用:硬编码几个城市的坐标映射,或使用 open-meteo 的 geocoding 接口const fetchWeather = async (cityName) => {loading.value = trueerror.value = nullweatherData.value = nulltry {// 1. 先获取城市经纬度 (使用 Open-Meteo Geocoding API)const geoRes = await axios.get('https://geocoding-api.open-meteo.com/v1/search', {params: { name: cityName, count: 1, language: 'zh', format: 'json' }})if (!geoRes.data.results || geoRes.data.results.length === 0) {throw new Error('未找到该城市,请检查拼写')}const { latitude, longitude, name, country } = geoRes.data.results[0]// 2. 根据经纬度获取天气const weatherRes = await axios.get('https://api.open-meteo.com/v1/forecast', {params: {latitude,longitude,current_weather: true,timezone: 'auto'}})const current = weatherRes.data.current_weather// 组装数据weatherData.value = {city: name,country: country,temp: current.temperature,wind: current.windspeed,code: current.weathercode,time: current.time}return { id: `${latitude}-${longitude}`, name, country } // 返回用于存储历史的信息} catch (err) {error.value = err.message || '请求失败'return null} finally {loading.value = false}}return {weatherData,loading,error,fetchWeather}
}

🏠 第四步:核心组件开发

1. 首页组件 (src/views/HomeView.vue)

<script setup>
import { ref } from 'vue'
import { useWeather } from '../composables/useWeather'
import { useHistoryStore } from '../stores/history'
import { useRouter } from 'vue-router'const cityInput = ref('')
const { weatherData, loading, error, fetchWeather } = useWeather()
const historyStore = useHistoryStore()
const router = useRouter()const handleSearch = async () => {if (!cityInput.value.trim()) returnconst result = await fetchWeather(cityInput.value.trim())if (result) {// 搜索成功,添加到历史historyStore.addCity(result)}
}
</script><template><div class="home-container"><h1>🌤️ 智能天气查询</h1><!-- 搜索框 --><div class="search-box"><input v-model="cityInput" @keyup.enter="handleSearch"placeholder="输入城市名 (如: Beijing, Shanghai, London)" /><button @click="handleSearch" :disabled="loading">{{ loading ? '查询中...' : '搜索' }}</button></div><!-- 错误提示 --><div v-if="error" class="error-msg">❌ {{ error }}</div><!-- 天气展示卡片 --><div v-if="weatherData" class="weather-card"><h2>{{ weatherData.city }}, {{ weatherData.country }}</h2><div class="temp">{{ weatherData.temp }}°C</div><div class="details"><span>🍃 风速: {{ weatherData.wind }} km/h</span><span>🕒 更新时间: {{ new Date(weatherData.time).toLocaleString() }}</span></div><p class="hint">天气代码: {{ weatherData.code }}</p></div><!-- 空状态 --><div v-if="!loading && !weatherData && !error" class="empty-state">👆 输入城市开始查询</div><div class="nav-link"><router-link to="/history">查看搜索历史 →</router-link></div></div>
</template><style scoped>
.home-container { max-width: 600px; margin: 40px auto; text-align: center; }
.search-box { display: flex; gap: 10px; margin-bottom: 20px; }
input { flex: 1; padding: 10px; border-radius: 6px; border: 1px solid #ddd; font-size: 16px; }
button { padding: 10px 20px; background: #42b983; color: white; border: none; border-radius: 6px; cursor: pointer; font-size: 16px; }
button:disabled { background: #ccc; }
.weather-card { background: #f9f9f9; padding: 20px; border-radius: 12px; box-shadow: 0 4px 12px rgba(0,0,0,0.1); }
.temp { font-size: 48px; font-weight: bold; color: #42b983; margin: 10px 0; }
.details { display: flex; justify-content: space-around; color: #666; }
.error-msg { color: #ff4d4f; margin-bottom: 20px; }
.nav-link { margin-top: 30px; }
a { color: #42b983; text-decoration: none; }
</style>

2. 历史页组件 (src/views/HistoryView.vue)

<script setup>
import { useHistoryStore } from '../stores/history'
import { useRouter } from 'vue-router'const historyStore = useHistoryStore()
const router = useRouter()const goToCity = (cityName) => {// 编程式导航回首页,并可以通过 query 传递参数(可选)// 这里简单跳转,用户在首页再次输入或我们可以优化 useWeather 支持直接加载router.push('/')// 实际项目中可以触发一个全局事件或直接调用 store 动作来预加载alert(`即将查询:${cityName} (请在首页输入)`); 
}const clearAll = () => {if(confirm('确定清空历史吗?')) {historyStore.clearHistory()}
}
</script><template><div class="history-container"><h1>📜 搜索历史</h1><div v-if="historyStore.cities.length === 0" class="empty">暂无历史记录</div><ul v-else class="history-list"><li v-for="(city, index) in historyStore.cities" :key="index" @click="goToCity(city.name)"><span class="name">{{ city.name }}</span><span class="country">{{ city.country }}</span></li></ul><button v-if="historyStore.cities.length > 0" @click="clearAll" class="clear-btn">清空历史</button><div class="nav-link"><router-link to="/">← 返回首页</router-link></div></div>
</template><style scoped>
.history-container { max-width: 600px; margin: 40px auto; }
.history-list { list-style: none; padding: 0; }
.history-list li {background: white; border: 1px solid #eee;padding: 15px; margin-bottom: 10px;border-radius: 8px; cursor: pointer;display: flex; justify-content: space-between;transition: transform 0.2s;
}
.history-list li:hover { transform: translateX(5px); border-color: #42b983; }
.clear-btn { background: #ff4d4f; color: white; border: none; padding: 10px 20px; border-radius: 6px; cursor: pointer; width: 100%; margin-top: 20px;}
.nav-link { margin-top: 20px; text-align: center; }
</style>

🔗 第五步:路由配置 (src/router/index.js)

确保路由已经配置好这两个视图。

import { createRouter, createWebHistory } from 'vue-router'
import HomeView from '../views/HomeView.vue'
import HistoryView from '../views/HistoryView.vue'const router = createRouter({history: createWebHistory(import.meta.env.BASE_URL),routes: [{ path: '/', name: 'home', component: HomeView },{ path: '/history', name: 'history', component: HistoryView }]
})export default router

🚀 运行与测试

  1. 运行 npm run dev
  2. 在首页输入 "Beijing",点击搜索。
    • 观察:Loading 状态 -> 显示天气卡片。
    • 检查:浏览器 LocalStorage 中是否多了 weather_history 字段。
  3. 点击“查看搜索历史”。
    • 观察:列表中出现 "Beijing"。
  4. 再搜索 "London"。
    • 观察:历史记录更新,London 排在第一位。
  5. 刷新页面。
    • 观察:历史记录依然存在(Pinia + localStorage 生效)。

💡 核心知识点复盘 (Checklist)

在这个小项目中,你强制使用了以下 Vue 3 特性:

  • <script setup>: 所有组件都使用了语法糖,代码简洁。
  • 响应式 API: ref (cityInput, weatherData), computed (虽未显式使用但逻辑中包含), watch (Pinia 中监听持久化)。
  • Composables: useWeather 完美封装了异步逻辑和状态,组件变得极其干净。
  • Pinia: useHistoryStore 实现了跨组件(虽然目前只有两个页面,但逻辑是解耦的)的状态共享和持久化。
  • Vue Router: 实现了页面切换和编程式导航。
  • 单向数据流: 搜索框修改本地 state -> 触发 action -> 更新 store -> 视图更新。

🎓 毕业总结

恭喜你!完成了 Vue 3 五日速成训练营

你已经从一个对 Vue 3 陌生的开发者,成长为能够:

  1. 理解并运用 Composition API 思维。
  2. 设计可复用的 Composables
  3. 构建基于 PiniaRouter 的企业级应用架构。
  4. 独立开发完整的 SPA 单页应用

接下来的进阶之路

  • TypeScript: 尝试将上面的 JS 代码全部重构为 TS,体验类型安全的快感。
  • UI 框架: 引入 Element Plus 或 Naive UI,快速美化界面。
  • 服务端渲染 (SSR): 学习 Nuxt 3,它是 Vue 3 的全能框架,解决 SEO 和首屏速度问题。
  • 测试: 学习 Vitest 和 Vue Test Utils,为你的组件编写单元测试。

Vue 的世界非常广阔,保持好奇,继续编码吧!💻🔥

http://www.jsqmd.com/news/428867/

相关文章:

  • 2026靠谱的商标管理系统公司推荐及选择指南 - 品牌排行榜
  • 2026哪些公司提供优质商标管理系统?行业实践案例参考 - 品牌排行榜
  • 2026商标管理系统开发公司推荐及选择参考 - 品牌排行榜
  • 2026年英联翻译公司(INLION Translation)官方联系方式公示,精准翻译合作便捷入口 - 讯息观点
  • 基于PLC的机械手控制系统设计
  • 如何在 DotNet 中使用类似 golang 的 vendor 的编译模式
  • 鸿蒙 HarmonyOS 6 | 多媒体 (02) 音视频 AVPlayer 播放器定制与音频并发策略
  • 2026年 发电机厂家推荐排行榜,柴油发电机/玉柴/康明斯/潍柴/通柴/上柴/高压/大功率发电机,实力品牌与高效动力深度解析 - 品牌企业推荐师(官方)
  • 基于PLC的机械臂控制系统设计
  • 安立MS2760A超便携频谱仪测试汽车毫米波雷达
  • 实测才敢推!MBA专属AI论文网站 —— 千笔·专业学术智能体
  • [AI智能体与提效-117] - OpenAI API用法:response = client.chat.completions.create的返回结果为啥是response,而不是会话的对象?
  • 2026/3/2操作记录
  • 【小白】一文读懂CLIP图文多模态模型
  • 微波器件产线应用选择VNA矢量网络分析仪的考虑因素
  • C++ Template 基础篇(一):函数模板
  • [AI智能体与提效-116] - OpenAI API用法:Completions创建聊天对话
  • 使用矢量网络分析仪(VNA)测试汽车保险杠与车标雷达透波性能
  • Vue3 整合 Pinia 和 Vue Router
  • 2026年 堵漏工程厂家实力推荐榜:专业解决地下室/隧道/大坝等各类防水堵漏难题,精选优质服务商 - 品牌企业推荐师(官方)
  • 锁相放大器SR865A与SR860选型指南
  • 2026年厂房、餐饮、店铺及多元商业空间装修专业选型指南:聚焦靓滔装饰与思嫒装潢 - 品牌推荐官
  • 2026年诚信型会议预约系统优质推荐榜:工位系统服务商/工位系统订做研发公司/访客系统订研发公司/选择指南 - 优质品牌商家
  • 2026年无锡网站建设与外贸推广服务商推荐榜:专业SEO优化、宣传片拍摄及小程序开发一站式解决方案 - 品牌企业推荐师(官方)
  • 矢量网络分析仪E5080B使用说明
  • 2026年靠谱装修公司选择指南:老房翻新/工装/高端别墅场景下的头部品牌测评与选型建议 - 博客万
  • 基于51单片机的声光控制开关设计
  • 2026 日本展台设计搭建公司甄选:和风科创筑展,精益适配点亮会展新场景 - 资讯焦点
  • 基于单片机的智能抢答器设计
  • 2026年篮球架厂家推荐:纽戈(上海)实业有限公司专业供应移动/箱式/悬挂式/成人/室外全系产品 - 品牌推荐官