拥有一款属于自己的小程序之入门-天气小程序

5次阅读

共计 12360 个字符,预计需要花费 31 分钟才能阅读完成。

———— 润物无声,做一个有个格调的 coder 小程序、快应用现在可谓是家喻户晓,也更加密切的渗透入我们的生活中,笔者也算是个爱折腾的人,俗话说的好嘛,“不折腾,不前端“(当然是笔者自己的小心声)。于是在平日里忙碌的工作之余抽出来时间搞点事 (si) 情, 来写一个属于自己的贴身小天气。说时迟那时快,这就来了 …

经过两三年的发展,小程序的地位也步步高升,由腾讯领队的腾讯小程序,再到后来的支付宝,美团,头条等也都相应推出自家的小程序平台,都想顺着潮流抓住流量分一杯羹,可谓是兵家必争之地。大环境的改变,为了提高小程序的快速迭代和多人合作开发的效率,也使得各大厂商都开源了自己的小程序框架,mpvue、wepy、MINA、Taro 等相信大家也比较熟悉了。而小程序的社区也变得跟丰富健壮,也衍生出很多精美的 UI 框架。有兴趣的可以自行去相应的官网了解详情。
虽然上面介绍了那么多的框架,而本次笔者并没有使用框架,而是用原生的小程序来开发今天的主角,也希望能够用原始的方式来给那些和笔者一样的刚刚入门的小程序开发者一些帮助,也将自己所学的记录下来。毕竟原生才是最底层的基础,所有的框架都是在原生的基础上开花结果的,这样才能以不变应万变(吼吼~)。笔者水平有限,有错误或者解释不当的地方还望各位看官多多包涵。
蜗牛小天气 效果图
项目源码 润物无声 github

概况

在定位功能中,本程序用到腾讯地图的 api 相应的天气接口中,本程序用到的是和风天气提供的 api
两者都需要到官网中注册开发者账号,通过注册后得到的 appKey 来请求我们需要的数据,详细注册步骤请自行度娘由于需要用到定位功能,而小程序本身的 getLocation 方法获取到的是当前位置的坐标:

wx.getLocation({
type: ‘gcj02′, // 返回坐标的格式
success: res => {
// 此处只能获取到当前位置的经纬度(坐标)
},
})
所以需要利用腾讯地图 Api,通过坐标点反向获得该地点的详细信息。
基本配置
在 app.json 中是对整个小程序的一些基本配置
{
“pages”: [
“pages/index/index” // 当前小程序的主入口页面
],
// 主窗口的一些配置,如下,对背景颜色和小程序顶部的导航栏的样式进行了配置
“window”: {
“backgroundColor”: “#A7CAD3”,
“backgroundTextStyle”: “dark”,
“navigationBarBackgroundColor”: “#A7CAD3”,
“navigationBarTitleText”: “ 蜗牛天气 ”,
“navigationBarTextStyle”: “black”,
“navigationStyle”:”custom”
},
“permission”: {
“scope.userLocation”: {
“desc”: “ 蜗牛天气尝试获取您的位置信息 ” // 询问用户是否可以得到获取位置权限的提示文字
}
}
}
接下来,我们就来一步一步的实现这个小程序吧~~
1. 界面
由于没有 UI,再加上笔者扭曲的审美能力(坐在屏幕前开始愣神,陷入沉思 …), 所以还望各位看官多忍耐笔者又想又借鉴的界面成果 … 看来以后要多加强这方面的能力(haha~)
好了言归正传,首先,准备用一个页面来解决战斗,那就是各位看到以上的这个页面(都说了是‘小天气’嘛),页面一共分为五个部分,实时天气、24 小时内天气情况、未来一星期内天气情况、今天日落日出风向降雨等相关信息和天气的生活指数,这五个部分组成了整个页面,其对应的相应布局见一下代码
<view class=”container” style=’background: url({{bgImgUrl}}) center -200rpx / 100% no-repeat #62aadc;’>
<view style=’margin-top: -150rpx; padding-top: 150rpx;background: rgba(52,73,94, {{apl}})’>
<view class=’animation-view’>
<!– 当前定位信息显示 –>
<view class=’location’ bind:tap=”chooseLocation”>
<myicon class=”icon” type=”dingwei”></myicon>
<text class=’city’>{{position}}</text>
</view>
<!– 通过 canvas 画出当前天气情况动画 –>
<canvas canvas-id=’animation’ ></canvas>
<!– 实时天气情况 –>
<view class=”center-container”>

</view>
<!– 24 小时内天气情况 –>
<view class=”all-day-list”>
<scroll-view class=”scroll-view_x” scroll-x>
<view class=”all-day-list-item” wx:for=”{{everyHourData}}” wx:key=”item.time”>
<view class=”day-list-item”>{{item.time}}点 </view>
<view class=”day-list-item”>
<myicon type=”{{item.iconType}}”></myicon>
</view>
<view class=”day-list-item”>{{item.tmp}}°</view>
</view>
</scroll-view>
</view>
</view>
<!– 一星期内天气 –>
<view class=”one-week-list”>
<view class=”one-week-list-item” wx:for=”{{everyWeekData}}” wx:key=”item.weekday”>
<view class=”week-list-item”>
<view>{{item.weekday}}</view>
<view>{{item.date}}</view>
</view>
<view class=”week-list-item”>{{item.cond_txt_d}}</view>
<view class=”week-list-item”>
<myicon type=”{{item.iconTypeBai}}”></myicon>
</view>
<view class=”week-list-item”>{{item.tmp_min}}~{{item.tmp_max}}°</view>
<view class=”week-list-item”>
<myicon type=”{{item.iconTypeYe}}”></myicon>
</view>
<view class=”week-list-item”>{{item.cond_txt_n}}</view>
<view class=”week-list-item” style=”font-size: 28rpx”>
<view>{{item.wind_dir == ‘ 无持续风向 ’ ? ‘ 无 ’ : item.wind_dir}}</view>
<view>{{item.wind_sc}}级 </view>
</view>
</view>

</view>
<!– 日出日落风向降雨概率等 –>
<view class=’live-index-view’>

</view>
<!– 生活指数 –>
<view class=’last-view’>
<view class=’last-view-item’ wx:for=”{{lifeStyle}}” wx:key=”item.type”>
<view class=’last-view-item-top’>{{lifeEnum[item.type]}}</view>
<view class=’last-view-item-bottom’>{{item.brf}}</view>
</view>
</view>
</view>
</view>

具体 css 样式,详见 蜗牛小天气 源码

注意:(笔者入坑,一开始使用的纵向的 scroll-view,后来无奈的用了原来页面的滚动)scroll-view: 具体属性参考小程序官方文档在小程序中,内部为我们提供了 scroll-view 这个页面滚动的组件,对性能进行了一些优化,方便我们的使用。与此同时,也会有一些小坑

在使用 scroll-view 是,如果是纵向(Y 轴)滚动,scroll- y 属性,则必须为此 scroll-view 设置一个固定 (明确) 的高
请勿在 scroll-view 组件标签内使用 textarea、map、canvas、video 等组件
在使用了 scroll-view 组件时会阻止页面的回弹效果,也就是在 scroll-view 中滚动,无法触发 onPullDownRefresh 方法
如果想使用原生的下拉刷新(非自定义)或者双击顶部页面回滚到页面顶部,请不要使用 scroll-view。

相信各位看官发现了以上代码中有一个 <myicon> 的标签,此标签为一个图标组件,因为蜗牛天气中用到的图标也比较多。接下来我们说明下有关组件的封装事宜。为了提高代码的复用性和易维护性,以及多人合作开发的效率,组件化似乎是一个很好的解决办法,在微信小程序中,每个页面由四个文件组成

*.wxml —- 文档结构 => 等同于 html
*.js —- 处理业务逻辑
*.json —- 当前页面或组件的一些配置和选项
*.wxss —- 样式文件 => 等同于 css

而对于本小程序中 <myicon> 组件来说
<!– 仅有一个 text 标签,通过 type 属性来改变字符图标的类型(多云,小雨 …)–>
<!– 字符图标通过 css 样式和自定义字体来实现,通过 class 来显示不同类型的图标字体 –>
<!– 具体自定义字体图标制作过程可参考此链接 https://blog.csdn.net/thatway_wp/article/details/79076023 –>
<text class=”icon icon-{{type}}”></text>
// 小程序中的组件,通过调用 Component 方法,将组件的逻辑处理部分,属性以及方法(生命周期)等一对象的方式传入 Component 方法中
Component({
properties: {
type: {
type: String, // type 属性的类型
value: ” // 默认值
}
}
});
使用了 Component 构造器,通过参数指定组件的属性,数据,方法以及生命周期中的一些方法,在此组件中定义了接受的 type 属性,类型为字符串,其默认值为空字符串。
{
“component”: true // 配置,当前为组件
}
// CSS 部分略过 …
就这样,一个简单的 icon 图标组件就封装好了,是不是很简单啊。封装是封装好了,那么我们怎么调用这个组件呢,是不是很类似于 Vue 呢,没错,只需要在你调用的页面中注册一下即可
// 当前想要调用的页面的 *.json 文件中,如下
{
“enablePullDownRefresh”: true, // 此项与组件无关,此项为是否用小程序本身的下拉刷新功能
“usingComponents”: {
“myicon”: “../../components/icon/index” // 调用,注册 icon 组件
}
}
2. 相关数据 API
一开始就说到了需要使用腾讯地图 API 的 appkey 还有和风天气 API 的 appkey,笔者是将 appkey 配置在了 config.js 中,看官只需将自己相应的 appkey 值替换即可,由于 appkey 是私密的,此处就不公开了,还望谅解。
// config.js
export default {
MAP_API_KEY: ‘XXXXX-XXXXX-XXXXX-XXXXX-XXXXX-XXXXX’, // 腾讯地图 key
WEATHER_API_KEY: ‘XXXXXXXXXXXXXXXX’ // 和风天气 key
}
所有数据的接口,都定义在了 api.js 文件中,此处没什么好说的,看官自行通过接口文档查询。接口均采用回调的方式,笔者并没有封装成 Promise 的方式,如果有兴趣可自行更改。
// 引入 config,为了后面的 key
import config from ‘../uitl/config’
// 地图 key
const mapKey = config.MAP_API_KEY
// 和风天气 key
const weatherKey = config.WEATHER_API_KEY
// map url
const locationUrl = ‘https://apis.map.qq.com/ws/geocoder/v1/’
// 天气 url
const weatherUrl = ‘https://free-api.heweather.net/s6/weather/forecast’
//24 小时内 每小时
const everyhoursUrl = ‘https://api.heweather.net/s6/weather/hourly’
// 一周内
const everyWeekUrl = ‘https://api.heweather.net/s6/weather/forecast’
// 空气质量
const airQualityUrl = ‘https://api.heweather.net/s6/air/now’
// 实况天气
const weatherLive = ‘https://api.heweather.net/s6/weather/now’
// 生活指数
const lifeStyle = ‘https://api.heweather.net/s6/weather/lifestyle’
// 根据当前位置的坐标反得到当前位置的详细信息
// lat,lon 为经纬度坐标
export const getPosition = (lat, lon, success = {}, fail = {}) => {
return wx.request({
url: locationUrl,
header: {
‘Content-Type’: ‘application/json’
},
data: {
location: `${lat},${lon}`,
key: mapKey,
get_poi: 0
},
success,
fail
})
}
// 根据 location 得到天气信息
// lat,lon 为经纬度坐标
export const getWeaterInfo = (lat, lon, success = {}, fail = {}) => {
return wx.request({
url: weatherUrl,
header: {
‘Content-Type’: ‘application/json’
},
data: {
location: `${lat},${lon}`,
lang: ‘zh’,
unit: ‘m’,
key: weatherKey
},
success,
fail
})
}
// 根据 location 信息得到 24 小逐小时天气情况
// lat,lon 为经纬度坐标
export const getEveryHoursWeather = (lat, lon, success = {}, fail = {}) => {
return wx.request({
url: everyhoursUrl,
header: {
‘Content-Type’: ‘application/json’
},
data: {
location: `${lat},${lon}`,
lang: ‘zh’,
unit: ‘m’,
key: weatherKey
},
success,
fail
})
}

… // 其他接口类似

}
3. 实现逻辑(业务代码)
3.1 流程
首先,当初次加载页面时,大体流程为首先通过定位获取位置,然后通过位置信息去得到我们需要的每一项天气信息,最后将天气信息渲染后页面中相应的位置具体流程:

获取位置经纬度
通过经纬度逆向获得位置信息

通过位置信息获取天气信息

获取实时天气信息

判断是否为白天和晚上(改变页面背景)– 该小程序中定义早上 6 点到晚上 18 点为浅色背景,其他时间为深色背景
判断当前天气的情况(雨或雪的大小), 在实况天气界面中通过 canvas 模拟雨雪的动画

获取 24 小时天气信息
获取一星期的天气信息
获取生活指数信息

当通过手动改变位置信息时,按顺序重复执行以上步骤
3.2 获取经纬度以及逆向出位置信息
通过 wx.getLocation 原生方法获取经纬度信息,在经过腾讯地图 api 通过经纬度逆向获取到相应的位置信息,对于这个项目来说获取位置信息是最重要的信息,故我们希望在页面一加载的时候就执行方法获取,然后『onLoad』方法可以帮助我们解决,这个方法就是小程序的生命周期函数 – 监听页面加载,此方法会在页面刚加载的时候(document 文档结构渲染完成后)执行。
小程序页面(Page)的生命周期函数:

name
type
functional

onLoad
函数
监听页面加载

onReady
函数
监听页面初次渲染完成

onShow
函数
监听页面显示

onHide
函数
监听页面隐藏

onUnload
函数
监听页面卸载

以下为获取位置信息代码:
// onLoad
onLoad: function () {


this.getPositon() // 调用获取位置信息
}
// 原生方法获取经纬度信息
getPosition: function () {
wx.getLocation({
type: ‘gcj02’,
success: this.updateLocation, // 成功会掉 updataLocation 方法为更新位置
fail: err => {
console.log(err)
}
})
}
// 更新位置信息
updateLocation: function(res) {


let {latitude: x,longitude: y,name} = res;
let data = {
location: {
x,
y,
name: name || ‘ 北京市 ’
},


};
this.setData(data); // 设置 page 中 data 对象中的属性
// 通过经纬度逆向获得位置信息
this.getLocation(x, y, name);
}
// 逆向获取位置信息
getLocation: function(lat, lon, name) {
wx.showLoading({
title: “ 定位中 ”,
mask: true
})
// 腾讯地图 api 接口
getPosition(lat, lon, (res) => {
if (res.statusCode == 200) {
let response = res.data.result
let addr = response.formatted_addresses.recommend || response.rough
this.setData({
position: addr // 赋值给 data 对象中的相应属性
})
wx.hideLoading()
this.getData(lat, lon);
}
}, (err => {
console.log(err)
wx.hideLoading()
}))
},
// 当用户点击显示定位处的 view 时,会调用原生的 chooseLocation 方法,内部调用选择位置页面
chooseLocation: function() {
wx.chooseLocation({
success: res => {
let {latitude,longitude} = res
let {x,y} = this.data.location
if (latitude == x && longitude == y) {

} else {
this.updateLocation(res)
}
}
})
},
setData 方法
上面代码中两次用到了 setData 方法,该方法接受一个对象,对象中的属性为需要改变的数据,同时接受一个 callback 函数,用于通过改变数据更新页面渲染完成之后的回调。我们来看看 data 的作用。
page({
data: {
backgroundColor:’red’,
fontSize: ’20’,


}
})
在 page 中,data 中的属性是连接逻辑层和视图层的一个桥梁,也就是说我们可以通过 js 代码的逻辑来控制 data 中的属性的值,而页面中的一部分显示内容是根据 data 中的属性的值而变化。这也就是我们所说的 mvvm 模型,我们只需把重心放在 js 逻辑层,而无需去频繁的手动的操作视图层。了解了 data 的作用,再来说 setData,setData 就是在 js 逻辑层中去改变和设置 data 中的属性的值,从而使页面得到响应。

this.setData({
backgroundColor: ‘green’ // 改变背景颜色属性,视图中以来此属性的会将颜色变成绿色
})

注意:

直接修改 this.data 的值,而不是通过调用 this.setData()方法,是无法成功改变页面的状态的
仅仅支持 JSON 化的数据(key:value)
单词设置的值不能超过 1024K,所以使用的时候尽量不要一次设置过多的数据
不要把 data 中的任何一项 value 值设置成 undefined,否则这一项将不能被设置,也可能会有其他问题
不要频繁的去调用 this.setData()方法去设置同一个值或者多个值,比如通过在循环中调用 this.setData(), 这样会导致性能损耗

3.3 获取天气
通过上面获得到的位置信息,用来调用相应的接口获得当前位置的天气。方法接口已在前面封装好,直接调用然后通过对 response 进行过滤或者重组等来满足当前的应用,最后通过 this.setData()方法去更新数据是页面得到响应。

getWeather(lat, lon) // lat, lon 为当前位置的经纬度
getAir(lat, lon)
getHourWeather(lat, lon)
getWeatherForWeek(lat, lon)
getLifeIndex(lat, lon)

以上方法不在一一列举其中数据处理的过程,可自行查看源码 详见 蜗牛小天气 源码
关于 canvas 画出模拟雨和雪的粒子动画效果
粒子动画在现在越来越多的项目中被用到。从静态到动态最后再到仿真效果更好的视觉体验,也是人们在视觉上追求极致的体验。我们通过粒子,也就是通过点和线,来模拟出雨和雪的效果。通过小程序中的 canvas 画布来画出我们想要的效果。实现原理:

首先我们通过点和线来模拟雨滴下落和雪花飘落
我们通过在同一时间同一块区域 (也就是此小程序页面中实况天气的区域) 中雨滴或雪花的多少来表示大小
构造一个总的 Weather 基类,来设置画布的 width,height,以及雨滴或雪花的数量,同时会有两个 Start 和 Stop 方法(也就是开始和停止方法)
构造一个 Rain 类和一个 Snow 类,都继承自 Weather 类,Rain 和 Snow 都有自己私有的 _init(初始化), _drawing(画), _update(更新画布)三个方法来控制 Rain 和 Snow 的动作

Weather 类
Weather 类是一个基类,主要处理画布的一些信息,例如 width,height,定时器,以及当前动画的状态(status)等
const STOP_ANIMATION = ‘stop’
const START_ANIMATION = ‘start’

class Weather {
constructor(context, width, height, option = {}) {
this.opt = option || {}
this.context = context
this.timer = null
this.status = STOP_ANIMATION
this.width = width
this.height = height
this._init()
}
// 实例调用此方法,开始在画布上画
start() {
if(this.status !== START_ANIMATION) {
this.status = START_ANIMATION
this.timer = setInterval(() => {
this._drawing()
}, 30)
return this
}
}
stop() {
this.status = STOP_ANIMATION
clearInterval(this.timer)
this.timer = null
return this
}
}

export default Weather
Rain 类
Rain 类继承自 Weather 类,通过_init 方法和父类中画布参数,以及 option 参数中的 counts(雨滴数量)来初始化。
import Weather from ‘./Weather.js’
class Rain extends Weather {
// 初始化
_init() {
this.context.setLineWidth(2)
this.context.setLineCap(’round’)
let height = this.height
let width = this.width
let counts = this.opt.counts || 100
let speedCoefficient = this.opt.speedCoefficient
let speed = speedCoefficient * height
this.animationArray = []
let arr = this.animationArray

for (let i = 0; i < counts; i++) {
let d = {
x: Math.random() * width,
y: Math.random() * height,
len: 2 * Math.random(),
xs: -1,
ys: 10 * Math.random() + speed,
color: ‘rgba(255,255,255,0.1)’
}
arr.push(d)
}
}
// 开始画
_drawing() {
let arr = this.animationArray
let ctx = this.context
ctx.clearRect(0, 0, this.width, this.height)
for (let i = 0; i < arr.length; i++) {
let s = arr[i]
ctx.beginPath()
ctx.moveTo(s.x, s.y)
ctx.lineTo(s.x + s.len * s.xs, s.y + s.len * s.ys)
ctx.setStrokeStyle(s.color)
ctx.stroke()
}
ctx.draw()
return this.update()
}
// 更新画布
update() {
let width = this.width
let height = this.height
let arr = this.animationArray
for (let i = 0; i < arr.length; i++) {
let s = arr[i]
s.x = s.x + s.xs
s.y = s.y + s.ys
if (s.x > width || s.y > height) {
s.x = Math.random() * width
s.y = -10
}
}
}
}

export default Rain
Snow 类
Snow 类继承自 Weather 类,通过_init 方法和父类中画布参数,以及 option 参数中的 counts(雪花数量)来初始化。
import Weather from ‘./Weather.js’
class Snow extends Weather {
// 初始化
_init() {
let {
width,
height
} = this
console.log(width)
let colors = this.opt.colors || [‘#ccc’, ‘#eee’, ‘#fff’, ‘#ddd’]
let counts = this.opt.counts || 100

let speedCoefficient = this.opt.speedCoefficient || 0.03
let speed = speedCoefficient * height * 0.15

let radius = this.opt.radius || 2
this.animationArray = []
let arr = this.animationArray

for (let i = 0; i < counts; i++) {
arr.push({
x: Math.random() * width,
y: Math.random() * height,
ox: Math.random() * width,
ys: Math.random() + speed,
r: Math.floor(Math.random() * (radius + 0.5) + 0.5),
color: colors[Math.floor(Math.random() * colors.length)],
rs: Math.random() * 80
})
}
console.log(arr)
}
// 开始画
_drawing() {
let arr = this.animationArray
let context = this.context
context.clearRect(0, 0, this.width, this.height)
for (let i = 0; i < arr.length; i++) {
let {
x,
y,
r,
color
} = arr[i]
context.beginPath()
context.arc(x, y, r, 0, Math.PI * 2, false)
context.setFillStyle(color)
context.fill()
context.closePath()
}

context.draw()
this._update()
}
// 更新画布
_update() {
let {
width,
height
} = this
let arr = this.animationArray
let v = this.opt.speedCoefficient / 10
for (let i = 0; i < arr.length; i++) {
let p = arr[i]
let {
ox,
ys
} = p
p.rs += v
p.x = ox + Math.cos(p.rs) * width / 2
p.y += ys
if (p.x > width || p.y > height) {
p.x = Math.random() * width
p.y = -10
}
}
}
}

export default Snow
结束!!!

到此,蜗牛小天气就开发完成了,希望对各位有帮助。希望在阅读的同时还请看官别忘了给一个大大的赞????,码字不易 … 初来乍到,能耐有限,水平一般,只是想着用文字来记录自己的学习过程。如果文中有错误之处,还请各位多多提意见,共同探讨,也请各位多包涵。

正文完
 0