擁有一款屬於本身的小程序之入門-天氣小程序

———— 潤物無聲,作一個有個格調的coder

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

通過兩三年的發展,小程序的地位也步步高昇,由騰訊領隊的騰訊小程序,再到後來的支付寶,美團,頭條等也都相應推出自家的小程序平臺,都想順着潮流抓住流量分一杯羹,可謂是兵家必爭之地。大環境的改變,爲了提升小程序的快速迭代和多人合做開發的效率,也使得各大廠商都開源了本身的小程序框架,mpvuewepyMINATaro等相信你們也比較熟悉了。而小程序的社區也變得跟豐富健壯,也衍生出不少精美的UI框架。有興趣的能夠自行去相應的官網瞭解詳情。css

雖然上面介紹了那麼多的框架,而本次筆者並無使用框架,而是用原生的小程序來開發今天的主角,也但願可以用原始的方式來給那些和筆者同樣的剛剛入門的小程序開發者一些幫助,也將本身所學的記錄下來。畢竟原生纔是最底層的基礎,全部的框架都是在原生的基礎上開花結果的,這樣才能以不變應萬變(吼吼~)。筆者水平有限,有錯誤或者解釋不當的地方還望各位看官多多包涵。html

蝸牛小天氣 效果圖

項目源碼 潤物無聲github前端

圖片描述

概況

圖片描述

在定位功能中,本程序用到騰訊地圖的api
相應的天氣接口中,本程序用到的是和風天氣提供的apivue

二者都須要到官網中註冊開發者帳號,經過註冊後獲得的appKey來請求咱們須要的數據,詳細註冊步驟請自行度娘

因爲須要用到定位功能,而小程序自己的getLocation方法獲取到的是當前位置的座標:java

wx.getLocation({
      type: 'gcj02', // 返回座標的格式
      success: res => {
          // 此處只能獲取到當前位置的經緯度(座標)
      },
    })

因此須要利用騰訊地圖Api,經過座標點反向得到該地點的詳細信息。git

基本配置

在app.json中是對整個小程序的一些基本配置github

{
    "pages": [
        "pages/index/index" // 當前小程序的主入口頁面
    ],
    // 主窗口的一些配置,以下,對背景顏色和小程序頂部的導航欄的樣式進行了配置
    "window": {
        "backgroundColor": "#A7CAD3", 
        "backgroundTextStyle": "dark",
        "navigationBarBackgroundColor": "#A7CAD3",
        "navigationBarTitleText": "蝸牛天氣",
        "navigationBarTextStyle": "black",
        "navigationStyle":"custom"
    },
    "permission": {
        "scope.userLocation": {
        "desc": "蝸牛天氣嘗試獲取您的位置信息" // 詢問用戶是否能夠獲得獲取位置權限的提示文字
        }
    }
}

接下來,咱們就來一步一步的實現這個小程序吧~~json

1.界面

因爲沒有UI,再加上筆者扭曲的審美能力(坐在屏幕前開始愣神,陷入沉思...),因此還望各位看官多忍耐筆者又想又借鑑的界面成果...看來之後要多增強這方面的能力(haha~)canvas

好了言歸正傳,首先,準備用一個頁面來解決戰鬥,那就是各位看到以上的這個頁面(都說了是‘小天氣’嘛),頁面一共分爲五個部分,實時天氣、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

結束!!!


到此,蝸牛小天氣就開發完成了,但願對各位有幫助。但願在閱讀的同時還請看官別忘了給一個大大的贊👍,碼字不易...初來乍到,能耐有限,水平通常,只是想着用文字來記錄本身的學習過程。若是文中有錯誤之處,還請各位多多提意見,共同探討,也請各位多包涵。

相關文章
相關標籤/搜索