实战篇 4:天气页面数据获取和交互实现¶
天气预报页面主要有两个重要的流程:获取地理位置和获取天气数据。本节重点介绍这两部分的代码实现。
定位和逆地址查询¶
获取地理位置功能可以拆成两个步骤:
- 使用微信
wx.getLocation方法获取用户当前位置的经纬度 - 使用拿到的经纬度请求腾讯地图的逆地址解析接口,获取省市县和详细地址
下面来逐一介绍。
获取用户当前位置经纬度¶
小程序可以使用 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,原因如下:
- 小程序 SDK 和 WebService API 都很方便,不需要加密数据,密钥都是对外暴露的
- WebService API 使用微信
wx.request方法可以直接发送请求,对代码无侵入性 - 小程序 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 接口说明。
基于以下两点考虑,本小册中的天气服务采用了签名的认证方式:
- 为了提高安全性
- 练习云函数的使用方法
具体签名的算法可以参考加密签名认证,这个不是我们介绍的重点,笔者先把签名算法代码贴上:
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,天气数据则使用云函数来获取数据。小节的最后介绍了下拉刷新和配置分享文案的实现。