未経験がOpenWeatherMapを使って天気アプリを作ってNetlifyにデプロイする

今回VueでAPI使う場合の勉強を兼ねてOpenWeatherMapを使って天気アプリを作ってNetlifyにデプロイしたので記録しておきます。

#APIキーを取得する
元々楽天会員なので[Rakuten RapidAPI](https://api.rakuten.net/)から取得しました。

#dockerを導入する
dockerを勉強中の身なので導入してみました。

```docker:Dockerfile
FROM node:14.15.1

WORKDIR /app

ENV LANG C.UTF-8
ENV TZ Asia/Tokyo

RUN apt-get update -y && \
    apt-get upgrade -y && \
    yarn global add @vue/cli@4.5.9

ADD . /app
```

```yaml:docker-compose.yml
version: "3"

services:
  web:
    container_name: web
    build: ./web/
    image: web
    volumes:
      - ./web:/app
    ports:
      - 8080:8080
    privileged: true

    command: "yarn run serve"
```

#vue-cliでvueをインストールします。
webフォルダを作成してその中にインストールしました。

# 天気情報を取得する
```vue:WeatherHome.vue
<template>
  <section id="weatherHome">
    <input
      v-model="getWeatherQuery"
      placeholder="調べたい場所を入力してください"
    />
    <button @click="getWeather">天気を調べる</button>
    <div v-if="hasGetWeatherData" id="weatherCard">
      <h1 class="weatherCardHeader">{{ getWeatherResult.name }}</h1>
      <div class="weatherCardDescription">
        {{ getWeatherInfo.description }}
      </div>
      <div class="weatherCardWeatherData">
        <img :src="getWeatherImageUrl" class="weatherCardImage" />
        <p class="weatherCardTemp">
          <v-icon name="thermometer-half" scale="2" />
          {{ getWeatherResult.main.temp }} ℃
        </p>
      </div>
      <div class="weatherCardWeatherOtherData">
        <div class="weatherCardWindSpeed">
          <v-icon name="wind" scale="2" />
          {{ getWeatherResult.wind.speed + "m/s" }}
        </div>
        <div class="weatherCardHumidity">
          <v-icon name="tint" scale="2" />
          {{ getWeatherResult.main.humidity + "%" }}
        </div>
      </div>
      風速:{{ getWeatherResult.wind.speed }} 湿度:{{
        getWeatherResult.main.humidity
      }}
    </div>
  </section>
</template>

<script>
import axios from "axios";
export default {
  name: "WeatherHome",
  data() {
    return {
      getWeatherQuery: "",
      getWeatherResult: {},
      getWeatherInfo: {},
      getWeatherImageUrl: "",
      hasGetWeatherData: false,
    };
  },
  methods: {
    getWeather() {
      let apiURL = "https://community-open-weather-map.p.rapidapi.com/weather";
      axios
        .get(apiURL, {
          headers: {
            "content-type": "application/octet-stream",
            "x-rapidapi-host": process.env.VUE_APP_OPEN_WEATHER_MAP_API_HOST,
            "x-rapidapi-key": process.env.VUE_APP_OPEN_WEATHER_MAP_API_KEY,
            useQueryString: true,
          },
          params: {
            id: "2172797",
            lang: "ja",
            units: "metric",
            q: this.getWeatherQuery,
          },
        })
        .then((response) => {
          this.getWeatherResult = response.data;
          this.getWeatherInfo = Object.assign(...this.getWeatherResult.weather);
          this.getWeatherImageUrl =
            "http://openweathermap.org/img/wn/" +
            this.getWeatherInfo.icon +
            "@4x.png";
          this.hasGetWeatherData = true;
          console.log(response);
        })
        .catch((error) => {
          console.log(error);
        });
    },
  },
};
</script>

<style>
#weatherCard {
  background-color: #fff;
  box-shadow: 0 0 8px rgba(0, 0, 0, 0.16);
  color: #212121;
  text-decoration: none;
  height: 400px;
  width: 400px;
  margin: auto;
}
.weatherCardWeatherData {
  height: 50%;
  display: flex;
  align-items: center;
}
.weatherCardHeader {
  height: 10%;
  text-align: center;
  padding-top: 2%;
}
.weatherCardDescription {
  height: 5%;
  font-size: 150%;
  text-align: center;
  padding: 0.5% 1%;
}
.weatherCardImage {
  margin-left: auto;
}
.weatherCardTemp {
  font-size: 120%;
  margin-right: 15px;
  padding: 1% 1%;
}
.weatherCardWeatherOtherData {
  height: 15%;
  position: relative;
}
.weatherCardWindSpeed {
  font-size: 150%;
  position: absolute;
  bottom: 0;
  left: 70px;
}
.weatherCardHumidity {
  font-size: 150%;
  position: absolute;
  bottom: 0;
  right: 70px;
}
</style>

```
env.localのAPIキーを使いaxiosで情報を取得して各情報を表示しvue-awesomeを使って少し見栄えを良くしています。

# Vuexを導入する
雨が降っていたら雨のアニメーションを表示するなど少し凝って見たくなったのでWeatherAnime.vueを作成したのですが、
そういえばVuexを使ったことがなかったと思い現在コンポーネントが2つあるので導入してみました。
＊今思うとpropsとか使ったほうが良かったと思います。

```javascript:store.js
import Vue from "vue";
import Vuex from "vuex";
import axios from "axios";

Vue.use(Vuex);

const store = new Vuex.Store({
  state: {
    apiKey: process.env.VUE_APP_OPEN_WEATHER_MAP_API_KEY,
    apiHost: "https://community-open-weather-map.p.rapidapi.com/weather",
    getWeatherQuery: "",
    getWeatherResult: {},
    hasGetWeatherData: false,
  },
  mutations: {
    setGetWeatherQuery(state, val) {
      state.getWeatherQuery = val;
    },
    setGetWeatherResult(state, data) {
      state.getWeatherResult = data;
    },
    setHasGetWeatherData(state, data) {
      state.hasGetWeatherData = data;
    },
  },
  getters: {
    getWeatherResult: (state) => {
      return String(state.getWeatherResult);
    },
    hasGetWeatherData: (state) => {
      return Boolean(state.hasGetWeatherData);
    },
  },
  actions: {
    async getWeather({ commit, state }, getWeatherQuery) {
      try {
        commit("setGetWeatherQuery", getWeatherQuery);

        const response = await axios.get(state.apiHost, {
          headers: {
            "content-type": "application/octet-stream",
            "x-rapidapi-host": process.env.VUE_APP_OPEN_WEATHER_MAP_API_HOST,
            "x-rapidapi-key": process.env.VUE_APP_OPEN_WEATHER_MAP_API_KEY,
            useQueryString: true,
          },
          params: {
            lang: "ja",
            units: "metric",
            q: state.getWeatherQuery,
          },
        });

        const newWeatherData = {
          name: response.data.name,
          temp: response.data.main.temp,
          description: response.data.weather[0].description,
          icon: response.data.weather[0].icon,
          info: response.data.weather[0].main,
          wind: response.data.wind.speed,
          humidity: response.data.main.humidity,
        };

        commit("setGetWeatherResult", newWeatherData);
        commit("setHasGetWeatherData", true);
      } catch (error) {
        console.log(error);
      }
    },
  },
});

export default store;
```

```vue:WeatherHome.vue
<template>
  <section id="weatherHome">
    <input v-model.trim="search" placeholder="調べたい場所を入力してください" />
    <button @click="getData">天気を調べる</button>
    <div v-if="this.hasGetWeatherData" id="weatherCard">
      <h1 class="weatherCardHeader">
        {{ this.getWeatherResult.name }}
      </h1>
      <div class="weatherCardDescription">
        {{ this.getWeatherResult.description }}
      </div>
      <div class="weatherCardWeatherData">
        <img
          :src="
            `http://openweathermap.org/img/wn/${this.getWeatherResult.icon}@4x.png`
          "
          class="weatherCardImage"
        />
        <p class="weatherCardTemp">
          <v-icon name="thermometer-half" scale="2" />
          {{ this.getWeatherResult.temp }} ℃
        </p>
      </div>
      <div class="weatherCardWeatherOtherData">
        <div class="weatherCardWindSpeed">
          <v-icon name="wind" scale="2" />
          {{ this.getWeatherResult.wind + "m/s" }}
        </div>
        <div class="weatherCardHumidity">
          <v-icon name="tint" scale="2" />
          {{ this.getWeatherResult.humidity + "%" }}
        </div>
      </div>
    </div>
  </section>
</template>

<script>
import { mapActions, mapState } from "vuex";
export default {
  name: "WeatherHome",
  data() {
    return {
      search: "",
    };
  },
  computed: {
    weatherQuery: {
      get() {
        return this.$store.state.getWeatherQuery;
      },
      set(value) {
        this.$store.commit("setGetWeatherQuery", value);
      },
    },
    ...mapState(["getWeatherResult", "hasGetWeatherData"]),
  },
  methods: {
    ...mapActions(["getWeather"]),
    getData() {
      this.getWeather(this.search);
    },
  },
};
</script>

<style>
#weatherCard {
  background-color: #fff;
  box-shadow: 0 0 8px rgba(0, 0, 0, 0.16);
  color: #212121;
  text-decoration: none;
  height: 400px;
  width: 400px;
  margin: auto;
}
.weatherCardWeatherData {
  height: 50%;
  display: flex;
  align-items: center;
}
.weatherCardHeader {
  height: 10%;
  text-align: center;
  padding-top: 2%;
}
.weatherCardDescription {
  height: 5%;
  font-size: 150%;
  text-align: center;
  padding: 0.5% 1%;
}
.weatherCardImage {
  margin-left: auto;
}
.weatherCardTemp {
  font-size: 120%;
  margin-right: 15px;
  padding: 1% 1%;
}
.weatherCardWeatherOtherData {
  height: 15%;
  position: relative;
}
.weatherCardWindSpeed {
  font-size: 150%;
  position: absolute;
  bottom: 0;
  left: 70px;
}
.weatherCardHumidity {
  font-size: 150%;
  position: absolute;
  bottom: 0;
  right: 70px;
}
</style>

```

# 背景画像を変更する
アニメーションを作りたかったのですがgithubで色々なソースを読んでググってもわからなかったので取り急ぎ背景画像を変更する仕様にしました。
＊今思うとちゃんと分かるまで調べたほうが良かったと思います。

```vue:WeatherAnime.vue
<template>
  <div
    class="weather-anime"
    :class="[
      { 'weather-rain': getWeatherResult.info === 'Rain' },
      { 'weather-snow': getWeatherResult.info == 'Snow' },
      { 'weather-clear': getWeatherResult.info == 'Clear' },
      { 'weather-clouds': getWeatherResult.info == 'Clouds' },
    ]"
  ></div>
</template>

<script>
import { mapState } from "vuex";
export default {
  computed: {
    ...mapState(["getWeatherResult"]),
  },
};
</script>

<style scoped>
.weather-anime {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background-size: cover;
  background-repeat: no-repeat;
  background-size: cover;
  background-position: center;
}
.weather-rain {
  background-image: url("../assets/images/rain.jpg");
}
.weather-clouds {
  background-image: url("../assets/images/clouds.jpg");
}
.weather-clear {
  background-image: url("../assets/images/clear.jpg");
}
</style>
```

# 少し修正する
ボタンのデザイン変更と初期データに東京を指定しました。

```vue:WeatherHome.vue
<div class="weatherSearch">
<input
  v-model.trim="search"
  placeholder="調べたい場所を入力してください"
/>
<button @click="getData" class="weatherCardButton">天気を調べる</button>
</div>

<script>
created: function() {
  this.getWeather("Tokyo");
},
</script>

<style>
.weatherCardButton {
  border: 2px solid transparent;
}
.weatherCardButton:hover {
  background: transparent;
  border-color: white;
}
</style>
```

# Netlifyにデプロイする
```toml:netlify.toml
[build]
  base = "/web"
[build.environment]
  YARN_VERSION = "1.22.5"
  YARN_FLAGS = "--no-ignore-optional"
  NODE_VERSION = "14.15.3"
```

# 終わりに
一応完成はしましたがGithub上にあるOpenWeatherMapを使ったコードを読んでみるといかに自分のコードのレベルが低いか痛感させられます。
コードは[Github](https://github.com/usumarin/Weather_Vue_App)に保存してあるので見たい方がいらしたらどうぞ。
改善するために最近はコードリーディングや写経して作り方のパターンを覚えたほうがいいのではと思案中です。