Skip to content

实战篇 4:天气页面数据获取和交互实现

天气预报页面主要有两个重要的流程:获取地理位置和获取天气数据。本节重点介绍这两部分的代码实现。

定位和逆地址查询

获取地理位置功能可以拆成两个步骤:

  1. 使用微信 wx.getLocation 方法获取用户当前位置的经纬度
  2. 使用拿到的经纬度请求腾讯地图的逆地址解析接口,获取省市县和详细地址

下面来逐一介绍。

获取用户当前位置经纬度

小程序可以使用 wx.getLocation 方法获取用户的当前位置经纬度。wx.getLocation 默认获取的是 wgs84 坐标系,即 GPS 的坐标系,而国内地图(除百度地图外)一般用的都是 GCJ02(国测局坐标,又称为“火星坐标系”)的坐标系,所以需要传入 type 来指定坐标系统。

wx.getLocation({
  type: 'gcj02',
  success: this.updateLocation,
  fail: (e) => {
    // console.log(e)
    this.openLocation()
  }
})

Tips: 出于国家安全考虑,国内使用的坐标系都是经过国测局统一加密偏移后的坐标,所以我们拿到的坐标并不是真正的坐标,国测局统一加密后的坐标系就是 GCJ02。国测局规定:国内地图产品需要用 GCJ02 的坐标系,或者在 GCJ02 基础上再次加密的坐标系,百度自有的 BD09 坐标系就是在 GCJ02 基础上再次加密的坐标系。

GPS 定位拿到的是 WGS84 坐标系,直接在国内地图上使用或者调用国内地图的位置 API 服务都会计算有偏移。因为我们实战中用的逆地址解码服务是腾讯地图服务,腾讯地图的坐标系是 GCJ02,所以 wx.getLocation 的参数 type 应该是 gcj02,而不是默认的 wgs84。如果调用 wx.getLocation 不传入 type 则获取的 wgs84 坐标会有偏移。同理,使用 wx.openLocation 也要注意坐标系问题。

根据经纬度获取当前地址

wx.getLocation 方法返回有当前用户的经纬度,具体返回的数据有:

参数 说明
latitude 纬度,浮点数,范围为 -90~90,负数表示南纬
longitude 经度,浮点数,范围为 -180~180,负数表示西经
speed 速度,浮点数,单位 m/s
accuracy 位置的精确度
altitude 高度,单位 m
verticalAccuracy 垂直精度,单位 m(Android 无法获取,返回 0)
horizontalAccuracy 水平精度,单位 m

经纬度信息要转化成具体的地理位置,需要使用地图的逆地址查询接口,笔者在这里使用了腾讯地图的 API

首先在腾讯地图开放平台注册账号,注册后登录,在「我的控制台 -> 密钥(key)管理」中可以添加密钥。密钥是我们使用 API 必须传递的一个参数,每个不同的应用对应不同的密钥。

在小程序内,腾讯地图 API 的调用方式主要有两种:

  • 通过腾讯地图提供的小程序 SDK
  • WebService API 方式,即直接调用 API 的接口

这里笔者选择了 WebService API,原因如下:

  1. 小程序 SDK 和 WebService API 都很方便,不需要加密数据,密钥都是对外暴露的
  2. WebService API 使用微信 wx.request 方法可以直接发送请求,对代码无侵入性
  3. 小程序 SDK 本质上还是用 wx.request 封装的 WebService API

根据逆地址查询接口的文档,只需要传入密钥和 location (经纬度组合)信息即可:

// lib/api.js
const QQ_MAP_KEY = 'ZVXBZ-xxxxxx-xxxxx-xxxxxK-LQFU6'
/**
 *  逆地址查询
 * @param {*} lat
 * @param {*} lon
 */
export const geocoder = (lat, lon, success = () => {}, fail = () => {}) => {
  return wx.request({
    url: 'https://apis.map.qq.com/ws/geocoder/v1/',
    data: {
      location: `${lat},${lon}`,
      key: QQ_MAP_KEY,
      get_poi: 0
    },
    success,
    fail
  })
}

完整获取地址的流程

完成了上面的获取经纬度和逆地址查询,就可以在 index.js 中进行整个地址获取的流程了,代码从 onLoad 调用 this.getLocation 开始,全部代码如下:

// weather/index.js
onLoad(){
  this.getLocation()
},
// 处理逆地址
getAddress(lat, lon, name) {
  wx.showLoading({
    title: '定位中',
    mask: true
  })
  let fail = (e) => {
    // console.log(e)
    this.setData({
      address: name || '北京市海淀区西二旗北路'
    })
    wx.hideLoading()

    this.getWeatherData()
  }
  geocoder(
    lat,
    lon,
    (res) => {
      wx.hideLoading()
      let result = (res.data || {}).result
      // console.log(1, res, result)

      if (res.statusCode === 200 && result && result.address) {
        let {address, formatted_addresses, address_component} = result
        if (formatted_addresses && (formatted_addresses.recommend || formatted_addresses.rough)) {
          address = formatted_addresses.recommend || formatted_addresses.rough
        }
        let {province, city, district: county} = address_component
        this.setData({
          province,
          county,
          city,
          address: name || address
        })
        this.getWeatherData()
      } else {
        //失败
        fail()
      }
    },
    fail
  )
},
// 更新 data 数据,调用 getAddress
updateLocation(res) {
  let {latitude: lat, longitude: lon, name} = res
  let data = {
    lat,
    lon
  }
  if (name) {
    data.address = name
  }
  this.setData(data)
  this.getAddress(lat, lon, name)
},
getLocation() {
  // 获取经纬度
  wx.getLocation({
    type: 'gcj02',
    success: this.updateLocation,
    fail: (e) => {
      // console.log(e)
      this.openLocation()
    }
  })
},
// 检测到失败,则提示用户打开位置权限
openLocation() {
  wx.showToast({
    title: '检测到您未授权使用位置权限,请先开启哦',
    icon: 'none',
    duration: 3000
  })
},

点击地址栏事件处理

在天气预报的顶部地址栏部分绑定了 tap 事件,点击地址栏则会打开地图让用户重新选择位置,这可以通过小程序提供的 wx.chooseLocation 方法来实现。

<view class="location" bindtap="chooseLocation">
  <icon type="dingwei" />
  <text>{{ address }}</text>
</view>

事件 chooseLocation 的代码如下:

chooseLocation() {
  wx.chooseLocation({
    success: (res) => {
      let {latitude, longitude} = res
      let {lat, lon} = this.data
      if (latitude == lat && lon == longitude) {
        this.getWeatherData()
      } else {
        this.updateLocation(res)
      }
    }
  })
}

和风天气数据获取

和风天气接口是提供三种调用方式的,不过对于 Web 端产品来说,只有普通 KEY 请求和签名请求两种方式,具体可以查看天气 API 接口说明

基于以下两点考虑,本小册中的天气服务采用了签名的认证方式:

  1. 为了提高安全性
  2. 练习云函数的使用方法

具体签名的算法可以参考加密签名认证,这个不是我们介绍的重点,笔者先把签名算法代码贴上:

const crypto = require('crypto')
const KEY = 'e8dd4902xxxxxxxxxxxxxxcb4df'
const USER_ID = 'HE11212121212121299'
function generateSignature(params) {
  params.username = USER_ID
  let data =
    Object.keys(params)
      .filter((key) => {
        return params[key] !== '' && key !== 'sign' && key !== 'key'
      })
      .sort()
      .map((key) => {
        return `${key}=${params[key]}`
      })
      .join('&') + KEY
  return crypto.createHash('md5').update(data).digest('base64')
}

有了认证的算法,就可以在云函数中发送数据请求。以获取天气集合接口为例,介绍下云函数的使用方法,获取空气质量相对简单且类似,具体代码中有详细注释:

// server/cloud-functions/he-weather
// 请求的地址
const API_URL = 'https://free-api.heweather.com/s6/weather'
// request 模块
const request = require('request')

// 引入云函数功能工具方法,跟空气质量公用
// gulp prod 打包的时候将公共 utils 库嵌入式引入
/*<jdists import="../../inline/utils.js" />*/

// 普通 mock server 的代码直接将 utils 库当模块引入
/*<remove>*/
const $ = require('../../inline/utils')
/*</remove>*/

// 按照云函数的规定,必须导出 main 函数
exports.main = async (event) => {
  const {lat,lon} = event
  let location = `${lat},${lon}`
  let params = {
    location,
    t: Math.floor(Date.now() / 1e3),
    unit: 'm'
  }
  // 生成签名
  params.sign = $.generateSignature(params)
  let query = []
  for (let i in params) {
    query.push(`${i}=${encodeURIComponent(params[i])}`)
  }
  let url = API_URL + '?' + query.join('&')
  // 将 request.get 方法改造成 promise 方式
  return new Promise((resolve, reject) => {
    request.get(url, (error, response, body) => {
      if (error || response.statusCode !== 200) {
        reject(error)
      } else {
        try {
          // 统一处理接口返回的数据
          let rs = $.handlerData(JSON.parse(body))
          resolve(rs)
        } catch (e) {
          reject(e)
        }
      }
    })
  })
}

完成获取天气集合数据,天气预报页面还用到了空气质量相关的接口,这个免费接口使用的参数不是经纬度而是城市名称,具体获取数据的代码跟天气数据接口类似,这里直接贴出代码:

// server/cloud-functions/he-air
// const path = require('path')
const API_URL = 'https://free-api.heweather.com/s6/air/now'
const request = require('request')
/*<jdists import="../../inline/utils.js" />*/

/*<remove>*/
const $ = require('../../inline/utils')
/*</remove>*/

exports.main = async (event) => {
  let location = event.city
  let params = {
    location,
    t: Math.floor(Date.now() / 1e3),
    unit: 'm'
  }
  // 生成签名
  params.sign = $.generateSignature(params)
  let query = []
  for (let i in params) {
    query.push(`${i}=${encodeURIComponent(params[i])}`)
  }
  let url = API_URL + '?' + query.join('&')
  // console.log(url)
  return new Promise((resolve, reject) => {
    // console.log(url)
    request.get(url, (error, response, body) => {
      if (error || response.statusCode !== 200) {
        reject(error)
      } else {
        try {
          let data = JSON.parse(body)
          // console.log(data)
          if (data && data.HeWeather6 && data.HeWeather6[0].air_now_city) {
            let {aqi, qlty} = data.HeWeather6[0].air_now_city
            resolve({
              status: 0,
              aqi,
              color: $.airBackgroundColor(aqi),
              name: qlty
            })
          } else {
            resolve({
              status: 500
            })
          }
          // resolve(rs)
        } catch (e) {
          reject(e)
        }
      }
    })
  })
}

天气预报页面流程图

下拉刷新

天气预报页面是支持下拉刷新的,在页面配置中 index.json 已经将 enablePullDownRefresh 设置为 true,我们还需要在 index.js 中增加下拉事件监听,监听下拉刷新操作,然后重新获取定位和天气数据,最后更新页面:

// weather/index.js
onPullDownRefresh() {
  this.getWeatherData(() => {
    wx.stopPullDownRefresh()
  })
}

配置分享文案

为了提升我们小程序的分享体验,笔者设计了分享文案,通过监听 onShareAppMessage 事件,用户分享小程序的时候增加当前定位和天气相关的文案,onShareAppMessage 只需要返回一个分享文案和路径的对象即可:

// weather/index.js
onShareAppMessage() {
  // 如果获取数据失败,则没有位置和天气信息,那么需要个默认文案
  if (!isUpdate) {
    return {
      title: '我发现一个好玩的天气小程序,分享给你看看!',
      path: '/pages/weather/index'
    }
  } else {
    // 如果有天气信息,那么需要给 path 加上天气信息
    const {lat, lon, address, province, city, county} = this.data
    let url = `/pages/weather/index?lat=${lat}&lon=${lon}&address=${address}&province=${province}&city=${city}&county=${county}`

    return {
      title: `「${address}」现在天气情况,快打开看看吧!`,
      path: url
    }
  }
},

最终的效果如上图所示,分享出去的小程序链接是带着当前天气的数据的,但是这时候如果有人打开了链接,那么还是根据自己的定位信息查看天气,不能看到分享人的位置和天气信息,所以还需要在 onLoad 中,获取页面的 URL 参数,然后使用分享的地理位置直接获取天气数据,对应的代码如下:

// weather/index.js
onLoad() {
  // ......
  const pages = getCurrentPages() //获取加载的页面
  const currentPage = pages[pages.length - 1] //获取当前页面的对象
  const query = currentPage.options
  // 如果有地址,经纬度信息
  if (query && query.address && query.lat && query.lon) {
    let {province, city, county, address, lat, lon} = query
    // 取出这些数据,设置页面data
    // 利用setData的callback,保证数据设置完成后,获取天气信息
    this.setData(
      {
        city,
        province,
        county,
        address,
        lat,
        lon
      },
      () => {
        this.getWeatherData()
      }
    )
  } else {
    // 否则,正常逻辑:先获取地址,再获取天气数据
    this.getLocation()
  }
},

Tips: 在获取分享的 URL 数据之后通过 setData 设置地理位置信息,不应该直接在 setData 之后获取天气信息,而应该在 setData 的回调函数中调用 getWeatherData 获取天气数据,这是因为 setData 是异步的,可以参考基础篇 3:小程序架构及其实现机制的相关介绍。

使用 Chart.js 绘制图表

笔者在七天天气模块使用了 Chart.js 来绘制一个温度走势图。Chart.js 是个 Canvas 版本的图表库,有人将其改造成了小程序版本。由于本实战中的温度走势图相对比较简单,所以 Chart.js 可以胜任需求,先看下效果图。

首先在 <view class="week"/> 最后增加走势图图表的 canvas 组件:

<view class="week">
  <view class="week-weather">
    ....
  </view>
  <view class="week-chart">
    <canvas canvas-id="chart" id="chart"></canvas>
  </view>
</view>

通过绝对定位的方式,将 week-chart 放置在对应的位置,并且设置 canvas 的宽度和高度:

.week-chart {
  position: absolute;
  left: 0;
  right: 0;
  height: 272rpx;
  top: 262rpx;
}
.week-chart canvas {
  width: 750rpx;
  height: 272rpx;
}

绘制走势图使用 Chart.js 是使用了折线图(line)方式,而温度数值的标注,是给 Chart 注册了个 afterDatasetsDraw 钩子(hook)内,将数据遍历一遍之后,直接绘制将温度写上,具体代码如下:

// weather/index.js
import {fixChart, getChartConfig, drawEffect} from '../../lib/utils'
import Chart from '../../lib/chartjs/chart'
// Page 中定义的 drawChart 函数
drawChart() {
  const {width, scale, weeklyData} = this.data
  let height = CHART_CANVAS_HEIGHT * scale
  let ctx = wx.createCanvasContext('chart')
  fixChart(ctx, width, height)

  // 添加温度
  Chart.pluginService.register({
    afterDatasetsDraw(e, t) {
      ctx.setTextAlign('center')
      ctx.setTextBaseline('middle')
      ctx.setFontSize(16)

      e.data.datasets.forEach((t, a) => {
        let r = e.getDatasetMeta(a)
        r.hidden ||
          r.data.forEach((e, r) => {
            // 昨天的数据发灰
            ctx.setFillStyle(r === 0 ? '#e0e0e0' : '#ffffff')
            // 增加温度符号
            let i = t.data[r].toString() + '\xb0'
            let o = e.tooltipPosition()
            // 计算文字位置
            0 == a ? ctx.fillText(i, o.x + 2, o.y - 8 - 10) : 1 == a && ctx.fillText(i, o.x + 2, o.y + 8 + 10)
          })
      })
    }
  })

  return new Chart(ctx, getChartConfig(weeklyData))
}

小结

本节介绍了天气页面数据获取和交互的实现,重点讲解了地理位置获取和天气数据获取,地理位置使用 wx.request 请求腾讯地图 API,天气数据则使用云函数来获取数据。小节的最后介绍了下拉刷新和配置分享文案的实现。