Skip to content

综合案例:天气预报 ​

本页对应 1-10综合案例-天气预报,共 5 段视频,约 54 分钟。

学习目标 ​

  • 能够封装可复用的天气查询函数,接收城市编码作为参数并调用接口获取对应城市天气数据。
  • 能够根据接口返回数据,使用模板字符串、map 和 join 完成当前天气、今日天气及 7 日天气预报的页面渲染。
  • 能够为搜索框注册 input 事件,并结合防抖处理实现按输入关键字查询城市列表。
  • 能够将城市列表数据渲染为列表项,并通过 data-* 自定义属性保存城市编码。
  • 能够使用事件委托为动态生成的城市列表绑定点击事件,读取 dataset 中的城市编码并复用查询函数更新页面天气。

知识点 ​

项目目标:先让北京天气默认显示 ​

天气预报默认数据效果

这个案例的完整需求有三步:先获取北京市天气预报并展示;再根据搜索框查询城市列表并展示;最后点击城市,切换显示对应天气数据。本节先完成“默认数据”这条主线:封装查询函数、查询北京天气,并把拿到的数据设置到页面上。转写稿里也明确说,本节先做前两步,渲染逻辑紧接着完成;后面城市查询和点击切换,都是在这个查询函数上继续复用。

为什么需要:一打开页面就要看到北京天气,用户不必先搜索;同时还能检验前面封装的 myAxios 是否好用。
怎么想:查北京和查其他城市的逻辑几乎一样,只是城市编码不同,所以先把“查天气”封装成函数,页面初始化时调用一次。
怎么做:定义 searchWeather(city),内部调用 myAxios,按接口文档传 url、method: 'get'、params: { city }。
易错点:city 不能写死;北京传的是城市编码 110100,不是“北京”两个字;编码要按文档用字符串。

js
function searchWeather(city) {
  return myAxios({
    url: '天气预报接口地址',
    method: 'get',
    params: { city }
  })
}

searchWeather('110100').then(res => {
  console.log(res)
})

调用函数查询北京默认数据 ​

为什么需要:页面初始化时必须主动发起一次请求,否则页面不会自动出现北京天气。
怎么想:函数已经封装好,默认查询北京只需要在函数定义后调用,并传入北京编码。
怎么做:在控制台先打印 res,展开后看 data,确认里面有北京市、天气、温度等信息。转写稿中的调试过程就是:保存后切到页面,控制台出现“查询天气成功”,再展开 res.data 找到北京相关数据。
易错点:不要直接把整个 res 当天气数据用;真正要渲染的是 res.data。接口文档里城市参数名是 city,位置是 query,所以要放在 params 里。

js
searchWeather('110100').then(res => {
  const WD = res.data
  console.log(WD)
})

把数据渲染到页面:类名与属性名一致 ​

为什么需要:只查到数据还不够,用户要看的是页面上的城市名、日期、温度、天气、风向风力等。
怎么想:页面结构复杂时不能着急,要“一个一个来,写一个验证一个”。转写稿里特别强调:本案例提前做了准备——要使用数据的标签类名,和接口返回的属性名一致,因此选到标签后可以直接用同名属性替换。
怎么做:先用 document.querySelector 找到标签;文本用 innerText,带标签的结构用 innerHTML。例如城市名先选 .area,再赋值为 WD.area。日期区域可以先复制内部结构作为模板字符串,先改一个 1234 验证页面会变,再把 WD.date 等数据填进去。
易错点:不要复制最外层盒子,只复制内部结构;innerText 和 innerHTML 不要混用;模板字符串里替换数据时注意属性名和层级,别把 WD 写成别的对象。

js
const WD = res.data

document.querySelector('.area').innerText = WD.area

const dateStr = `页面日期区域的内部结构,用 ${WD.date} 等数据替换`
document.querySelector('.title').innerHTML = dateStr

七日预报:数组用 map + join ​

为什么需要:下半部分是未来七天,每天结构相同,不能手写七遍。
怎么想:接口里七日天气是数组,数组生成多个 HTML 结构,应该用 map 映射,再用 join('') 拼成一个字符串。转写稿中先确认 data.dayForecast 里正好 7 条数据,再复制一个 li 的结构作为模板。
怎么做:WD.dayForecast.map(v => 模板字符串).join(''),最后把结果赋给对应 ul 的 innerHTML。替换时用 v.dateFormat、v.weather、温度、风向风力等字段。
易错点:数据层级别取错,是 WD.dayForecast;join 要传空字符串,避免多余符号;复制模板时不要多带外层 ul;先 console.log 拼好的字符串,确认无误再渲染。

js
const weekStr = WD.dayForecast.map(v => {
  return `<li>用 v.dateFormat、v.weather 等字段替换后的结构</li>`
}).join('')

document.querySelector('.week').innerHTML = weekStr

小结与易错点 ​

本节的核心不是“背代码”,而是建立默认数据的处理顺序:封装查询函数 → 调用函数拿北京数据 → 从 res.data 取数据 → 按页面结构逐块渲染 → 七日预报用 map + join。转写稿中的调试方法很值得保留:每替换一块就看一次页面,控制台先打印数据再渲染;复杂结构不要一次性写完。常见同音字也要纠正:myAxios 不是“黑马ex”,innerHTML 不是“in the mail”,innerText 不是“in the test”,lodash 不是“low大学”。本节先把北京默认天气跑通,后面城市查询和点击切换才有稳定基础。

本节要做什么 ​

这一节完成天气预报案例中的“搜索城市列表”:在输入框里输入关键字后,根据关键字请求城市信息,并把匹配的城市渲染成列表。效果如下:

城市搜索列表效果图

笔记里把步骤概括为两步:绑定 input 事件获取关键字;获取并展示城市列表数据。转写稿补出了中间关键推导:input 会随着每次输入触发,不能每次都直接请求接口,否则浪费性能,所以要先做函数防抖,再请求、再渲染。

为什么监听 input 事件 ​

为什么需要:用户输入或删除内容时都要重新查询城市,input 事件正好能在输入框内容变化时触发。
怎么想:先拿到输入框元素,注册 input,在事件处理函数里通过 this.value 取关键字。这里用普通函数而不是箭头函数,是为了让 this 指向输入框。
怎么做:

js
document.querySelector('.search-input').addEventListener('input', function () {
  console.log(this.value)
})

易错点:不要写成箭头函数后还期待 this 是输入框;调试时先在控制台确认每次输入都能打印值,再继续往下写。

为什么先防抖再请求 ​

为什么需要:转写稿里明确提醒,用户连续打字时 input 会一直触发,如果里面直接调接口,会造成性能浪费。
怎么想:用户停止输入后稍等一会儿再查,这就是函数防抖。课程里用 lodash 的 _.debounce 实现(转写稿里的“楼大学”是 lodash,“抵bs”是 debounce)。
怎么做:把真正要执行的函数包成防抖函数,再绑定给 input:

js
const searchCity = _.debounce(function () {
  // 这里再请求城市接口
  console.log(this.value)
}, 1000)

document.querySelector('.search-input').addEventListener('input', searchCity)

易错点:防抖后的返回值要重新用于 addEventListener;延迟时间不要设得太长,转写稿中一开始设 2000 毫秒,后来调成 1000 毫秒更合适;防抖函数内部如果要用 this.value,仍要注意 this 的指向。

怎么请求并渲染城市列表 ​

为什么需要:关键字只是输入,真正要展示的是服务器返回的城市数组。
怎么想:调用课程封装好的请求函数,传查询参数 city,拿到 res.data 后,用 map 生成多个 li,再用 join('') 拼成字符串,最后通过 innerHTML 放到列表容器里。转写稿调试时输入“北”,控制台能看到北京、河北、北辰、北戴河;网络面板也能确认请求参数是 city=北。
怎么做:核心结构如下(请求函数名按你课程里实际封装的名称替换):

js
const res = await hmAxios({
  url: '城市信息接口地址',
  params: {
    city: this.value
  }
})

const str = res.data.map(v => {
  return `<li class="city-item" data-code="${v.code}">${v.name}</li>`
}).join('')

document.querySelector('.search-list').innerHTML = str

易错点:数据在 res.data 里,不要直接拿整个响应;渲染容器是 ul.search-list,不是输入框;列表默认可能被样式隐藏,调试时可在元素面板临时取消 display: none 观察;每个 li 要顺手存 data-code,因为后面点击城市查天气时要用城市编码,而不是城市名字。

调试顺序与易错点 ​

转写稿的调试过程很值得照做:先只打印 this.value,确认 input 绑定成功;再加防抖,确认连续输入时只在停止后打印一次;再请求接口,确认参数和返回数据;最后渲染,先在控制台打印拼好的字符串,再赋值给 innerHTML。常见易错点还有:把 dataset 写成 data-set;判断点击目标时用 e.target.classList.contains('city-item'),而不是直接比较整个 className;自定义属性读取用 e.target.dataset.code。本节先把搜索列表跑通,后面点击城市查天气时,直接取这里存下的 code 即可。

小结 ​

监听输入框实时改变的事件是 input 事件。实现“搜索城市列表”时,推荐顺序是:绑定 input 获取关键字 → 用 _.debounce 做防抖 → 按关键字请求城市数据 → 用 map + join 渲染列表,并保存城市 code 供后续使用。

本节要完成的效果 ​

本节要完成的是:在搜索框输入城市名后,页面会展示城市列表;点击列表中的某个城市,就切换为该城市的天气数据。效果示意如下:

城市天气切换效果示意

从转写稿的演示看,默认页面先展示北京天气;在右上角搜索框输入内容后,会出现城市候选列表;点击“广州”“武汉”等城市项,页面天气随之切换。核心步骤只有两步:检测搜索列表的点击事件并获取城市 code,然后复用前面已经封装好的查询并展示城市天气的函数。

知识点一:给动态城市列表绑定点击事件 ​

为什么需要:城市列表不是页面一开始就写死的,而是根据搜索关键字动态生成的。动态生成的 li 如果逐个绑定点击事件,代码麻烦,而且列表重新渲染后还要重新绑定。因此更适合用事件委托:把点击事件绑在始终存在的父容器 ul 上,再判断真正点击的是不是城市项。

怎么想:点击“谁”,就查“谁”的天气。父容器 ul 一直存在,所以事件绑在 ul 上;事件触发后,通过事件对象 e.target 找到实际触发元素,再判断它是否包含城市项的类名。

怎么做:在转写稿中,先找到搜索列表的 ul,注册 click 事件,并在回调里判断 e.target 的类名:

js
document.querySelector('搜索列表类名').addEventListener('click', (e) => {
  if (e.target.classList.contains('城市项类名')) {
    console.log('点击了 li 标签')
  }
})

调试时,转写稿先输入“北”得到城市列表,点击城市项,控制台能打印;再点击列表外区域,不打印,说明事件范围限制正确。

易错点:不要给动态生成的每个 li 直接绑事件;判断类名时不要漏掉 classList.contains;如果城市项内部还有子元素,e.target 可能不是 li 本身,需要按实际结构确认判断目标。

知识点二:从被点击项取出城市 code ​

为什么需要:查询天气时,后端要求传递的不一定是城市名字,而可能是城市编码。转写稿中查询北京天气传的是 110100 这样的数字字符串;上一节渲染城市列表时,已经把每个城市的 code 存进了 li 的自定义属性 data-code。所以点击后要把这个 code 取出来。

怎么想:点击事件里已经能拿到被点击的 li,而 li 上有 data-code,就可以通过 dataset 读取。转写稿中强调:上一节存了 data-code,这一节才能拿到;不存就拿不到。

怎么做:在点击判断内部解构出 code:

js
const { code } = e.target.dataset
console.log(code)

调试时,点击“上海”会打印一个对象,里面有 code 属性;再点其他城市,code 值会变化,说明取到的是对应城市编码。

易错点:data-code 对应的是 dataset.code,不要写成 dataset.dataCode;要确认上一节渲染列表时确实写入了 data-code;不要误以为传城市中文名一定可以,笔记小结明确说:看后端要求传递什么,就传什么。

知识点三:复用查询天气函数切换天气 ​

为什么需要:查询北京天气和查询其他城市天气,逻辑基本一致:调接口、拿数据、渲染页面。前面已经封装了 searchWeather,这里不应重新写一套查询逻辑,否则重复且难维护。

怎么想:点击城市项拿到 code 后,直接调用已有函数,把 code 传进去。这样“点谁查谁”,简单高效。

怎么做:

js
const { code } = e.target.dataset
searchWeather(code)

转写稿中验证:搜索“广州”并点击,广州天气被查出;再搜索“武汉”并点击,武汉天气也切换成功。说明复用函数有效。

易错点:不要传城市名字代替 code;不要忘记调用函数;要保证 searchWeather 的参数就是后端要求的城市编码;点击列表项后如果页面没变,先检查 code 是否取到、函数是否被调用。

小结 ​

本节的关键是“事件委托 + 自定义属性 + 复用函数”。动态列表用事件委托绑定点击;点击后通过 dataset.code 取出上一节存下的城市编码;最后调用已封装的 searchWeather(code) 切换天气。至于传给后端的是城市名字还是 code,以接口文档和后端要求为准。

课件图示 ​

课件第31页:默认查询北京天气 封装查询函数 查询北京天气 渲染数据

课件第34页:1. 默认查询北京天气 2. 城市查询 3. 点击查询城市天气 需求

课件第35页:点击查询城市天气 绑定点击事件 获取城市编码 查询城市天气

代码示例 ​

示例 1:封装 searchWeather 并默认查询北京天气 ​

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8" />
  <title>天气预报 - 默认查询北京</title>
  <style>
    body { font-family: system-ui, sans-serif; padding: 24px; }
    .weather-box span { display: inline-block; margin-right: 16px; padding: 6px 10px;
                        background: #f2f6ff; border-radius: 6px; }
  </style>
</head>
<body>
  <h3>今日天气</h3>
  <div class="weather-box">
    <span class="area">--</span>          <!-- 城市名 -->
    <span class="date">--</span>          <!-- 日期 -->
    <span class="temperature">--</span>   <!-- 温度 -->
    <span class="weather">--</span>       <!-- 天气 -->
    <span class="pm25">--</span>          <!-- PM2.5 -->
  </div>

  <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
  <script>
    /* 1. 自己封装的 myAxios:返回后端返回的整个对象 { code, message, data } */
    async function myAxios(config) {
      const res = await axios(config);   // axios 的 res.data 才是后端数据
      return res.data;
    }

    /* 2. 接口地址(以接口文档为准) */
    const WEATHER_URL = 'https://hmajax.itheima.net/api/weather';

    /* 3. 封装查询天气函数:city 是形参,不写死,方便后面复用 */
    async function searchWeather(city) {
      const res = await myAxios({
        url: WEATHER_URL,
        method: 'get',                   // get 可以省略
        params: { city }                 // 查询参数叫 city,值是城市编码
      });
      console.log(res);

      const wData = res.data;            // 真正要用的数据在 res.data 里
      console.log(wData);

      /* 4. 渲染:类名与属性名一一对应,直接取值填充 */
      const fill = (selector, value) => {
        const el = document.querySelector(selector);
        if (el) el.innerHTML = value ?? '--';
      };
      fill('.area', wData.city);
      fill('.date', wData.date);
      fill('.temperature', wData.temperature);
      fill('.weather', wData.weather);
      fill('.pm25', wData.pm25);
    }

    /* 5. 页面一打开就查北京:传的是城市编码 110100,不是"北京"两个字 */
    searchWeather('110100');
  </script>
</body>
</html>

关键行与易错点

  • myAxios 封装返回的是 res.data(后端的整个响应体),所以函数里还要再取一层 res.data 才是业务数据,两层 data 别混。
  • searchWeather(city) 里 city 必须是形参,不能写死成 '110100',否则后面城市切换时无法复用。
  • 请求参数走 params(查询参数),不是 data(请求体),这是本项目最容易写错的地方。
  • 北京要传的是城市 code 110100(字符串),不是城市名;具体传名字还是 code 由接口文档决定,其他城市的 code 后面搜索列表接口会返回。
  • 先 console.log 打印确认数据结构,再写渲染;字段含义以接口文档为准。

示例 2:输入关键字搜索城市列表(input + 防抖 + map/join 渲染) ​

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8" />
  <title>天气预报 - 城市搜索列表</title>
  <style>
    body { font-family: system-ui, sans-serif; padding: 24px; }
    .search { width: 260px; padding: 8px 10px; border: 1px solid #ccc; border-radius: 6px; }
    .search-list { list-style: none; padding: 0; width: 260px; margin: 8px 0 0;
                   border: 1px solid #ddd; border-radius: 6px; overflow: hidden; }
    .search-list .item { padding: 8px 10px; cursor: pointer; }
    .search-list .item:hover { background: #f2f6ff; }
  </style>
</head>
<body>
  <input type="text" class="search" placeholder="请输入城市名,如:北" />
  <ul class="search-list"></ul>

  <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/lodash/lodash.min.js"></script>
  <script>
    async function myAxios(config) {
      const res = await axios(config);
      return res.data;
    }

    const CITY_URL = 'https://hmajax.itheima.net/api/city';   // 以接口文档为准
    const searchInput = document.querySelector('.search');
    const searchList  = document.querySelector('.search-list');

    /* 1. 真正的业务逻辑:查城市 + 渲染列表 */
    async function searchCity() {
      const res = await myAxios({
        url: CITY_URL,
        method: 'get',
        params: { city: this.value }      // this 是 input 元素,所以下面用普通函数
      });
      console.log(res);

      /* 2. 数组 → HTML 字符串:map 生成 + join 拼接 */
      const str = res.data.map((v) => {
        return `<li class="item" data-code="${v.code}">${v.name}</li>`;
      }).join('');

      searchList.innerHTML = str;
    }

    /* 3. 用 lodash 的 debounce 做函数防抖,避免边打字边发请求 */
    const debounceSearch = _.debounce(searchCity, 1000);

    /* 4. 监听 input 事件(输入、删除内容都会触发) */
    searchInput.addEventListener('input', debounceSearch);
  </script>
</body>
</html>

关键行与易错点

  • 监听的是 input 事件,不是 change——change 要等失焦才触发,做不到实时搜索。
  • 必须用普通函数 function () {} 而不是箭头函数,否则 this.value 拿不到输入框的值(lodash 的 debounce 会保留调用时的 this)。
  • 防抖必须包在 addEventListener 外面:_.debounce(fn, 1000) 返回的是新函数,addEventListener('input', _.debounce(...)) 每次也是新函数,虽然能跑但语义混乱,且无法复用。
  • 每个 <li> 上要偷偷存一份 data-code="${v.code}",用户看不见,但点击切换城市时全靠它。
  • map 只负责生成数组,必须 join('') 才能拼成字符串赋给 innerHTML,否则中间会出现逗号。
  • 数据里的属性名(name / code)以打印结果或接口文档为准,别凭印象写。

示例 3:点击城市列表切换天气(事件委托 + 复用查询函数) ​

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8" />
  <title>天气预报 - 点击切换城市</title>
  <style>
    body { font-family: system-ui, sans-serif; padding: 24px; }
    .search { width: 260px; padding: 8px 10px; border: 1px solid #ccc; border-radius: 6px; }
    .search-list { list-style: none; padding: 0; width: 260px; margin: 8px 0 0;
                   border: 1px solid #ddd; border-radius: 6px; overflow: hidden; }
    .search-list .item { padding: 8px 10px; cursor: pointer; }
    .search-list .item:hover { background: #f2f6ff; }
    .weather-box span { display: inline-block; margin: 12px 16px 0 0; padding: 6px 10px;
                        background: #f2f6ff; border-radius: 6px; }
  </style>
</head>
<body>
  <input type="text" class="search" placeholder="请输入城市名,如:广州" />
  <ul class="search-list"></ul>

  <div class="weather-box">
    <span class="area">--</span>
    <span class="temperature">--</span>
    <span class="weather">--</span>
  </div>

  <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/lodash/lodash.min.js"></script>
  <script>
    async function myAxios(config) {
      const res = await axios(config);
      return res.data;
    }

    const WEATHER_URL = 'https://hmajax.itheima.net/api/weather';
    const CITY_URL    = 'https://hmajax.itheima.net/api/city';

    const searchInput = document.querySelector('.search');
    const searchList  = document.querySelector('.search-list');

    /* ===== 1. 查询并渲染天气(示例 1 封装的函数,这里直接复用) ===== */
    async function searchWeather(city) {
      const res = await myAxios({ url: WEATHER_URL, method: 'get', params: { city } });
      const wData = res.data;
      const fill = (s, v) => {
        const el = document.querySelector(s);
        if (el) el.innerHTML = v ?? '--';
      };
      fill('.area', wData.city);
      fill('.temperature', wData.temperature);
      fill('.weather', wData.weather);
    }

    /* ===== 2. 查询城市列表 ===== */
    async function searchCity() {
      const res = await myAxios({ url: CITY_URL, method: 'get', params: { city: this.value } });
      searchList.innerHTML = res.data
        .map((v) => `<li class="item" data-code="${v.code}">${v.name}</li>`)
        .join('');
    }
    searchInput.addEventListener('input', _.debounce(searchCity, 1000));

    /* ===== 3. 事件委托:给 ul 绑点击,动态生成的 li 也能响应 ===== */
    searchList.addEventListener('click', (e) => {
      if (e.target.classList.contains('item')) {   // 限定只有点 li 才触发
        const { code } = e.target.dataset;         // 取出 data-code
        searchWeather(code);                       // 复用第一步的函数
        searchList.innerHTML = '';                 // 选完收起列表(可选)
      }
    });

    /* ===== 4. 页面打开先展示北京 ===== */
    searchWeather('110100');
  </script>
</body>
</html>

关键行与易错点

  • li 是动态渲染出来的,直接给它绑事件会失效或绑不到,必须用事件委托:绑在始终存在的 ul 上。
  • 一定要写 e.target.classList.contains('item') 做判断,否则点 ul 的空白区域、或点到别的元素也会触发查询。
  • e.target.dataset.code 读取的就是 <li data-code="440100"> 里存的编码,前提是示例 2 里真的存进去了,没存就取到 undefined。
  • 拿到 code 后直接调用 searchWeather(code),这就是前面"封装函数方便复用"的回报,不要再复制一遍请求逻辑。
  • 传进去的是 code 编码而不是城市名——和示例 1 保持一致;具体传什么由接口文档决定。

常见错误 ​

1. 请求成功,页面却全是 undefined

  • 现象:Network 里 200,控制台打印数据正常,但城市名、温度位置空白或显示 undefined。
  • 原因:模板字符串里的属性名写错(大小写、拼错),或者把「类名 = 属性名」的巧合当成规律,直接抄了类名却抄错层级。
  • 修法:先在控制台把 res.data 逐层展开,确认字段真实路径(如 data.todayWeather.temDay);再对照接口文档确认含义;每替换一个字段就保存看一次页面,别整段写完再调。

2. this.value 打印出 undefined

  • 现象:input 事件能触发,但拿不到输入框内容。
  • 原因:回调写成了箭头函数,this 指向 window 而非 input 元素。
  • 修法:回调改回普通 function,或干脆用 e.target.value,不依赖 this。

3. 城市列表点了没反应,或点空白处也触发

  • 现象:列表渲染正常,点击无响应;或者点到 ul 的缝隙也执行了查询。
  • 原因:把 click 直接绑在动态生成的 li 上(绑定时元素还不存在);或没有做目标元素判断。
  • 修法:事件委托绑在始终存在的 ul 上,回调里用 e.target.classList.contains('li类名') 过滤后再执行。

4. 点击城市后查到的还是北京 / 报错

  • 现象:点上海,页面仍是北京天气;或 searchWeather(undefined)。
  • 原因:渲染列表时漏写 data-code 自定义属性;或读取时写成 e.target.code、dataset.Code(大小写敏感)。
  • 修法:生成时写 data-code="${v.code}",读取用 e.target.dataset.code;先 console.log(e.target.dataset) 确认真的存进去了。

5. 输入「北京」发了十几次请求

  • 现象:Network 面板请求刷屏,接口偶发限流或报错。
  • 原因:没做防抖,input 每敲一个字符触发一次。
  • 修法:用 _.debounce(fn, 300~1000) 包一层,注意把返回值赋给新变量再注册,不能直接调原函数。

6. 拼接后列表项之间多出逗号 / 标签变成纯文本

  • 现象:页面上出现 </li>,<li> 的逗号;或整段 <li> 被当文字显示。
  • 原因:map 返回数组没 join('') 就赋值;或用了 innerText 而不是 innerHTML。
  • 修法:.map(...).join('');需要解析标签一律用 innerHTML。

7. 渲染后页面结构错乱、盒子套盒子

  • 现象:样式全崩,控制台元素里出现重复的外层容器。
  • 原因:复制模板时把最外层容器标签也一起复制进去了。
  • 修法:只复制容器内部结构,外层用 querySelector 找到后整体替换 innerHTML。

练习 ​

  1. 换默认城市:把页面打开时默认查询的城市从北京改成上海。要求先说清楚上海的 code 从哪里来,再动手改,并解释为什么不能直接写「上海」两个字。
  2. 验证防抖:把搜索防抖延迟设为 500ms,在 Network 面板连续快速输入「北京市」三个字,数一数实际发出了几次请求;再把延迟改成 0 对比一次,记录差异。
  3. 点击后收起列表:点击某个城市查出天气后,自动清空搜索框并让城市列表隐藏,同时保证再点输入框时列表能重新出现。
  4. 只渲染三天:7 日天气预报改为只显示前 3 天,其余不动。思考改哪一行最省事。
  5. 兜底显示:假设接口某天 weather 字段返回空字符串,页面上该显示 -- 而不是空白。请在不改动数据结构的前提下实现。

原视频对照 ​

#视频时长
1案例-天气预报-查询北京天气08:39
2案例-天气预报-渲染北京0111:07
3案例-天气预报-渲染北京0211:24
4案例-天气预报-城市查询14:20
5案例-天气预报-点击查询城市08:51

小结 ​

  1. 查询天气的逻辑一定要先封装成带参数的函数,后面「点谁查谁」才能一行调用,否则就是复制粘贴两遍代码。
  2. 传城市名还是传 code,完全由接口文档决定,不要凭直觉;code 不用背,查城市列表时服务器会一并返回。
  3. 渲染复杂页面没有捷径:一个字段一个字段来,改一处验一处,比一口气写完再调试快得多。
  4. 「类名 = 属性名」只是本案例特意做的便利设置,真实项目里往往对不上,对不上就以接口文档为准。
  5. 凡是动态生成的元素,事件一律用委托绑在始终存在的父级上,这是通用做法而不是本例的特殊技巧。
  6. 点击时才需要的数据(如 code),用 data-* 属性藏在 DOM 上,用户看不见、代码随时取。
  7. 实时搜索必须防抖,这不是优化项而是必做项;延迟时间取 300~1000ms 之间比较符合手感。
  8. 渲染列表的固定套路是「模板字符串 + map + join + innerHTML」,四步缺一不可。