擼一個微信天氣小程式

_潤物無聲_發表於2019-04-01

小程式、快應用現在可謂是家喻戶曉,也更加密切的滲透入我們的生活中,筆者也算是個愛折騰的人,俗話說的好嘛,“不折騰,不前端“(當然是筆者自己的小心聲)。於是在平日裡忙碌的工作之餘抽出來時間搞點事(si)情,來寫一個屬於自己的貼身小天氣。說時遲那時快,這就來了...

經過兩三年的發展,小程式的地位也步步高昇,由騰訊領隊的騰訊小程式,再到後來的支付寶,美團,頭條等也都相應推出自家的小程式平臺,都想順著潮流抓住流量分一杯羹,可謂是兵家必爭之地。大環境的改變,為了提高小程式的快速迭代和多人合作開發的效率,也使得各大廠商都開源了自己的小程式框架,mpvuewepyMINATaro等相信大家也比較熟悉了。而小程式的社群也變得跟豐富健壯,也衍生出很多精美的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
複製程式碼

結束!!!


到此,蝸牛小天氣就開發完成了,希望對各位有幫助。 希望在閱讀的同時還請看官臨走前留下一個大大的贊?,碼字不易... 初來乍到,能耐有限,水平一般,只是想著用文字來記錄自己的學習過程。如果文中有錯誤之處,還請各位多多提意見,共同探討,也請各位多包涵。

相關文章