這一篇會記錄串接 API 的過程,還有參考「大量」排版的藝術 🤣。我會先把完整的程式碼顯示出來,再一點一點的說明過程。

Weather 元件

首先,因為我有使用 vue-router 所以要顯示的元件都必須要放在相對的路徑上,路徑的設定可以到 ./src/index.ts 來調整。

./src/components 裡建立 Weather.vue 元件,並把它加到 ./src/views/HomeView.vue 裡:

<script setup lang="ts">
import Weather from '../components/Weather.vue';
</script>

<template>
  <Weather />
</template>

接下來,開始建立 Weather 元件,它會長得像:

vue-project-weather.png

程式碼的部分是:

<template>
  <WeatherCard>
    <Title :location="currentWeather.locationName" :description="currentWeather.description" />
    <div class="flex justify-center gap-10 items-center">
      <CurrentWeather :temperature="currentWeather.temperature" />
      <WeatherIcon :currentWeatherCode="currentWeather.weatherCode" :dayStatus="currentWeather.dayStatus" />
    </div>
    <div class="flex justify-start items-center gap-5 mt-5 px-5">
      <Airflow :wind="currentWeather.windSpeed" />
      <Rain :rain="percentageFormat" />
    </div>

    <div class="flex justify-end items-end mt-5 gap-3 text-right">
      <p class="text-mg">最後觀測時間: {{ dateFormat() }}</p>
      <button>
        <i class="fa-solid fa-rotate-right text-lg"></i>
      </button>
    </div>
  </WeatherCard>
</template>

外觀部分的 Tailwind 程式碼就不特別細說了,我也是根據以前的 CSS 經驗與查詢 TailwindCSS Docs 來套用的。

API Key

父元件(Weather.vue) 會獲取 API 的資料並傳給子元件們(./components/weather)。

首先,要先有 API Key 才能跟 氣象資料開放平台 請求 API。有 API Key 後我會在 root 底下建立一個 .env 檔案並把它放進去:

  • ./.env
VITE_API_KEY=<Your API Key>
  • 引入到 Weather.vue
<script setup lang="ts">
  const API_KEY = import.meta.env.VITE_API_KEY;
</script>

變數的前方一定要有 VITE_ 這樣 vite 才能拿到 (Env Variables and Modes)。

⚠️ 將機敏資料分開存放是很重要的

決定要抓取的資料

中央氣象局開放資料平臺之資料擷取API 來找尋需要的資料:

天氣觀測報告 說明檔

vue-project-data-A0003.png

一般天氣預報 說明檔 vue-project-data-C0032.png

使用說明檔觀看欄位裡面的意義來決定要獲取哪些資料,並使用上面所提供的 Swagger 來預先設定資料型別與預設值:

interface IWeather {
  observationTime: string;
  locationName: string;
  description: string;
  temperature: number;
  windSpeed: number;
  humid: number;
  weatherCode: number;
  rainPossibility: number;
  comfortability: string;
  dayStatus: string;
}

// 預設資料
const currentWeather: IWeather = reactive({
  observationTime: '2023-6-05 12:10:00',
  locationName: 'NaN',
  description: 'NaN',
  temperature: 27.5,
  windSpeed: 0.3,
  humid: 0.88,
  weatherCode: 1,
  rainPossibility: 10,
  comfortability: '悶熱',
  dayStatus: 'night',
});

抓取資料

使用 axios 來獲取觀測資料:

const fetchCurrentWeather = async () => {
  // 這裡會先暫填高雄
  const url = `https://opendata.cwb.gov.tw/api/v1/rest/datastore/O-A0003-001?Authorization=${API_KEY}&limit=5&format=JSON&locationName=高雄`;

  try {
    const weatherData = await axios.get(url);
    
    // 取出資料
    const locationData = weatherData.data.records.location[0];
    console.log(locationData);

    // 將 WDSD(風速)、TEMP(溫度)、HUMD(相對濕度) 取出
    const weatherElements = locationData.weatherElement.reduce((neededElements: any, item: any) => {
      if (['WDSD', 'TEMP', 'HUMD'].includes(item.elementName)) {
        neededElements[item.elementName] = item.elementValue;
      }
      return neededElements;
    }, {});

    // 設定變數
    currentWeather.observationTime = locationData.time.obsTime;
    currentWeather.temperature = weatherElements.TEMP;
    currentWeather.windSpeed = weatherElements.WDSD;
    currentWeather.humid = weatherElements.HUMD;
  } catch (err) {
    console.log(err);
  }
};

我並沒有使用所有獲取的參數,只是先把它取好名字等需要的時候再添加即可。目前回傳的資料裡有使用到的是:

  • 觀測時間 (observationTime)
  • 溫度 (temperature)
  • 風量 (windSpeed)

使用下一個 API 來獲取其他剩餘的資料:

// 獲得預測天氣 API
const fetchWeatherForecast = async () => {
  const url = `https://opendata.cwb.gov.tw/api/v1/rest/datastore/F-C0032-001?Authorization=${API_KEY}&format=JSON&locationName=高雄市`;

  try {
    const weatherForecastData = await axios.get(url);
    
    const locationData = weatherForecastData.data.records.location[0];
    
    const weatherElements = locationData.weatherElement.reduce((neededElements: any, item: any) => {
      if (['Wx', 'PoP', 'CI'].includes(item.elementName)) {
        neededElements[item.elementName] = item.time[0].parameter;
      }
      return neededElements;
    }, {});
    
		// 設定變數
    currentWeather.locationName = locationData.locationName;
    currentWeather.description = weatherElements.Wx.parameterName;
    currentWeather.weatherCode = weatherElements.Wx.parameterValue;
    currentWeather.rainPossibility = weatherElements.PoP.parameterName;
    currentWeather.comfortability = weatherElements.CI.parameterName;
  } catch (err) {
    console.log(err);
  }
};

回傳的資料裡有使用到的是:

  • 地點 (locationName)
  • 天氣描述 (description)
  • 天氣現象代碼 (weatherCode)
  • 降雨率 (rainPossibility)

特地說一下天氣現象代碼,它會根據所預測到的天氣現象來給一個代碼,這可以在一般天氣預報的說明檔找到每個代碼的意義,這個也就是變更天氣 Icon 所依循的機制。

說到天氣 Icon,我的 Weather Icons 又有分早上與晚上,所以這邊要再引入一個 API:日出日沒時刻 來知道今天的日出日落,以便算出目前的時間是白天還是晚上。

// 獲得日出日落時間 API
const fetchSunRiseSunSet = async () => {
  // 獲得目前日期
  let currentDate: string = new Date().toJSON().slice(0, 10);

  // 設定獲取日期的範圍:當前日期的後兩天
  let end: Date = new Date();
  end.setDate(end.getDate() + 2);
  let endDate = end.toJSON().slice(0, 10);

  // 從目前時間開始獲得日出日落的資料
  const url = `https://opendata.cwb.gov.tw/api/v1/rest/datastore/A-B0062-001?Authorization=${API_KEY}&format=JSON&CountyName=高雄市&timeFrom=${currentDate}&timeTo=${endDate}`;

  const sunAPIData = await axios.get(url);
  const sunRiseSunSetData = sunAPIData.data.records.locations.location[0];

  // 找尋資料內與現在日期一致的
  const locationDate = sunRiseSunSetData.time.find((time: any) => time.Date === currentDate);

  // 將日出、日落、現在時間,轉為 TimeStamp
  const sunriseTimestamp = new Date(`${locationDate.Date} ${locationDate.SunRiseTime}`).getTime();

  const sunsetTimestamp = new Date(`${locationDate.Date} ${locationDate.SunSetTime}`).getTime();

  const nowTimeStamp = new Date().getTime();

  // 如果目前的時間在日出與日落中間,那就是白天,其他時間為晚上
  currentWeather.dayStatus = sunriseTimestamp <= nowTimeStamp && nowTimeStamp <= sunsetTimestamp ? 'day' : 'night';
};

這樣就完成抓到所以我所需要的資料了,但是要如何讓它啟動獲取 API 的函數呢?

啟動抓取 API 函數

這裡有兩個狀況:

  1. 當畫面載入的時候自動抓取
  2. 當使用者按下重新整理圖式時抓取

第一個很簡單,使用 onMount() 即可:

<script setup lang="ts">
  import { onMounted } from 'vue';
  //...
  // 初始化
  onMounted(() => {
    fetchCurrentWeather();
    fetchWeatherForecast();
    fetchSunRiseSunSet();
  });
</script>

第二個就是在重整圖示按鈕上新增事件:

<div class="flex justify-end items-end mt-5 gap-3 text-right">
  <p class="text-mg">最後觀測時間: {{ dateFormat() }}</p>
  <button
	@click="
	  () => {
		fetchCurrentWeather();
		fetchWeatherForecast();
		fetchSunRiseSunSet();
		updateAPILimit();
	  }
	"
	:disabled="disabled.clicked">
	<i class="fa-solid fa-rotate-right text-lg"></i>
  </button>
</div>

既然有個按鈕,就必須要給他一些冷卻時間以防使用者一直點擊:

// 中央氣象局的更新頻率為 1 小時
const updateAPILimit = () => {
  disabled.clicked = true;

  setTimeout(() => {
    disabled.clicked = false;
  }, 2 * 60 * 1000);
};

處理資料傳給子組件

上面我已經把資料取出並序列化了,但是有些資料還是不能直接做於顯示使用的,例如:觀測時間 (observationTime)。為了能符合我所需要的,我把它調到我需要的格式:

// 優化 date
const dateFormat = () => {
  return new Intl.DateTimeFormat('zh-TW', {
    hour: 'numeric',
    minute: 'numeric',
  }).format(new Date(currentWeather.observationTime));
};

最後的成果會像:

<template>
  <WeatherCard>
    <Title :location="currentWeather.locationName" :description="currentWeather.description" />
    <div class="flex justify-center gap-10 items-center">
      <CurrentWeather :temperature="currentWeather.temperature" />
      <WeatherIcon :currentWeatherCode="currentWeather.weatherCode" :dayStatus="currentWeather.dayStatus" />
    </div>
    <div class="flex justify-start items-center gap-5 mt-5 px-5">
      <Airflow :wind="currentWeather.windSpeed" />
      <Rain :rain="currentWeather.rainPossibility" />
    </div>
    
    <div class="flex justify-end items-end mt-5 gap-3 text-right">
      <p class="text-mg">最後觀測時間: {{ dateFormat() }}</p>
      <button
        @click="
          () => {
            fetchCurrentWeather();
            fetchWeatherForecast();
            fetchSunRiseSunSet();
            updateAPILimit();
          }
        "
        :disabled="disabled.clicked"
      >
        <i class="fa-solid fa-rotate-right text-lg"></i>
      </button>
    </div>
  </WeatherCard>
</template>

程式碼部分同步在 et860525/Morning-things


結語

子組件沒有細講是因為它們只負責接收資料並顯示,只有 Weather Icon 這個子組件要判斷什麼時候顯示哪個 Icon,這也是下一篇要說的。