Vue.js中如何在API返回满足条件时显示组件?解决v-if报错及Enter触发问题
解决Vue中API未加载完成时的
Cannot read property '0' of undefined错误 我完全懂你碰到的问题——初始状态下weather是空对象,Vue尝试渲染组件时,weather.weather还没被API赋值,自然是undefined,再去访问[0]就会抛出类型错误。而且你希望只有按下Enter获取数据后才显示对应动画组件,而不是一开始就加载所有组件。
下面是具体的修复方案:
步骤1:添加状态标记是否已获取天气数据
在data中新增一个布尔值属性,用来标记是否已经通过Enter键触发了API请求并获取到数据:
data () { return { api_key: '08f1525958fbc6584f628b6dac25a906', url_base: 'https://api.openweathermap.org/data/2.5/', query: '', weather: {}, hasFetchedWeather: false // 新增这个状态 } },
步骤2:获取数据后更新状态
在setResults方法中,把hasFetchedWeather设为true,表示已经获取到天气数据,可以开始判断动画组件了:
setResults (results) { this.weather = results; this.hasFetchedWeather = true; // 新增这行 },
步骤3:修改动画组件的v-if条件
给每个动画组件的v-if添加两层防御:先判断hasFetchedWeather(确保已经触发过API请求),再判断weather.weather是否存在,最后才判断具体的天气类型。这样就能避免未加载完成时的undefined错误:
<CloudsAnimation v-if="hasFetchedWeather && weather.weather && weather.weather[0].main=='Clouds'"></CloudsAnimation> <SunAnimation v-else-if="hasFetchedWeather && weather.weather && weather.weather[0].main=='Clear'"></SunAnimation> <NoAnimation v-else-if="hasFetchedWeather"></NoAnimation>
如果你的项目使用Vue 3,或者Vue 2配合了可选链语法的支持(比如通过Babel插件),可以简化条件为:
<CloudsAnimation v-if="hasFetchedWeather && weather.weather?.[0]?.main=='Clouds'"></CloudsAnimation> <SunAnimation v-else-if="hasFetchedWeather && weather.weather?.[0]?.main=='Clear'"></SunAnimation> <NoAnimation v-else-if="hasFetchedWeather"></NoAnimation>
完整修改后的代码
<template> <div id="container" :class="containerTemperature"> <h1>Better<br>Weather</h1> <main id="app" :class="appTemperature"> <div class="search-box"> <input type="text" class="search-bar" placeholder="Search..." v-model="query" @keypress="fetchWeather" /> </div> <!-- 修改后的动画组件条件 --> <CloudsAnimation v-if="hasFetchedWeather && weather.weather && weather.weather[0].main=='Clouds'"></CloudsAnimation> <SunAnimation v-else-if="hasFetchedWeather && weather.weather && weather.weather[0].main=='Clear'"></SunAnimation> <NoAnimation v-else-if="hasFetchedWeather"></NoAnimation> <div class="weather-wrap" v-if="typeof weather.main != 'undefined'"> <div class="location-box"> <div class="location">{{ weather.name }}, {{ weather.sys.country }}</div> <div class="date">{{ dateBuilder() }}</div> </div> <div class="weather-box"> <div class="temp">{{ Math.round(weather.main.temp) }}°c</div> <div class="weather">{{ weather.weather[0].main }}</div> </div> </div> </main> </div> </template> <script> import CloudsAnimation from "./components/CloudsAnimation" import SunAnimation from "./components/SunAnimation" import NoAnimation from "./components/NoAnimation" export default { name: 'App', components: { CloudsAnimation, SunAnimation, NoAnimation }, data () { return { api_key: '08f1525958fbc6584f628b6dac25a906', url_base: 'https://api.openweathermap.org/data/2.5/', query: '', weather: {}, hasFetchedWeather: false // 新增状态 } }, computed: { containerTemperature: function () { return { 'warm-container': typeof this.weather.main != 'undefined' && this.weather.main.temp > 20, 'cold-container': typeof this.weather.main != 'undefined' && this.weather.main.temp < 9 } }, appTemperature: function () { return { 'warm': typeof this.weather.main != 'undefined' && this.weather.main.temp > 20, 'cold': typeof this.weather.main != 'undefined' && this.weather.main.temp < 9 } } }, methods: { fetchWeather (e) { if (e.key == "Enter") { fetch(`${this.url_base}weather?q=${this.query}&units=metric&APPID=${this.api_key}`) .then(res => { return res.json(); }) .then(this.setResults) .then(() => this.query = ""); // 修复this指向问题 } }, setResults (results) { this.weather = results; this.hasFetchedWeather = true; // 更新状态 }, dateBuilder () { let d = new Date(); let months = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]; // 补全缺失的月份 let days = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]; let day = days[d.getDay()]; let date = d.getDate(); let month = months[d.getMonth()]; let year = d.getFullYear(); return `${day} ${date} ${month} ${year}`; } } } </script> <style lang="scss"> @import url('https://fonts.googleapis.com/css2?family=Montserrat:wght@400&display=swap'); @import url('https://fonts.googleapis.com/css2?family=Rajdhani:wght@300&display=swap'); * { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: 'Montserrat', sans-serif; } #container { position: relative; display: flex; justify-content: center; align-items: center; flex-direction: column; height: 100vh; background: rgb(167,207,242); background: linear-gradient(to bottom, #BDF2F2 0%, #34A6BF 50%, #074E8C 100%); h1 { position: absolute; top: 4vh; color: white; text-shadow: 1px 3px 2px rgba(0, 0, 0, 0.25); font-style: oblique; text-align: center; font-family: 'Rajdhani' } #app { background-image: url('./assets/cold.jpg'); background-size: cover; background-position: bottom; transition: 0.4s; width: 350px; height: 500px; border-radius: 20px; box-shadow: 0px 0px 16px rgba(0, 0, 0, 0.25); position: relative; overflow: hidden; &.warm { background-image: url('./assets/warm.jpg') } &.cold { background-image: url('./assets/coldd.jpg') } main { height: 100vh; padding: 25px; background-image: linear-gradient(to bottom, rgba(0,0,0,0.25), rgba(0,0,0,0.75)) } .search-box { width: 100%; margin-bottom: 30px; .search-bar { display: block; width: 100%; padding: 15px; color: #313131; font-size: 20px; appearance: none; border: none; outline: none; background: none; box-shadow: 0px 0px 8px rgba(0, 0, 0, 0.25); background-color: rgba(255, 255, 255, 0.5); border-radius: 0px 16px 0px 16px; transition: 0.4s; &:focus { box-shadow: 0px 0px 16px rgba(0, 0, 0, 0.25); background-color: rgba(255, 255, 255, 0.75); border-radius: 16px 0px 16px 0px; } } } .location-box { .location { color: white; font-size: 32px; font-weight: 500; text-align: center; text-shadow: 1px 2px 2px rgba(0, 0, 0, 0.4); } .date { color: white; font-size: 20px; font-weight: 300; font-style: italic; text-align: center; text-shadow: 1px 2px 2px rgba(0, 0, 0, 0.4); } } .weather-box { text-align: center; .temp { display: inline-block; padding: 10px 25px; color: white; font-size: 82px; font-weight: 900; text-shadow: 3px 6px rgba(0, 0, 0, 0.4); background-color: rgba(255, 255, 255, 0.4); border-radius: 16px; margin: 30px 0px; box-shadow: 3px 6px rgba(0, 0, 0, 0.4); } .weather { color: white; font-size: 48px; font-weight: 700; font-style: italic; text-shadow: 3px 6px 1px rgba(0, 0, 0, 0.25) } } } &.warm-container{ background: rgb(242,160,160); background: linear-gradient(to bottom right, rgba(242,160,160,1) 0%, rgba(242,82,170,1) 50%, rgba(34,32,89,1) 100%); } &.cold-container { background: #D9D9D9; background: linear-gradient(to bottom left, #f0efef 0%, #B4B7BF 50%, #909197 100%); } } </style>
另外我还修复了你代码里的两个小问题:
fetchWeather方法里的then(this.query = "")改成了箭头函数,避免this指向错误;dateBuilder里的months数组缺少了April到July,已经补全。
这样修改后,只有当你按下Enter获取到天气数据后,才会根据天气类型显示对应的动画组件,初始状态下不会渲染任何动画组件,也就不会出现undefined的错误了。
内容的提问来源于stack exchange,提问作者Mikado94




