初识 react-native 实现天气预报

439 阅读1分钟

初识 react-native 实现天气预报

Simulator Screen Shot - iPhone SE (2nd generation) - 2022-01-28 at 00.50.18.png

Simulator Screen Shot - iPhone SE (2nd generation) - 2022-01-28 at 00.47.46.png

github blog

api 接口

项目需要用到 metaweather 提供的 api,直接请求存在跨域问题。

因为服务端没有跨域限制,所以我决定用 express 搭个服务器处理请求问题。

要想在 node 中使用 esm 的语法,需要在 pakage.json 添加"type": "module".

且文件后缀不能省略.js

import express from "express"
import cors from "cors"
import morgan from "morgan"

import { getLocation, getWeather } from "./controller.js"

const app = express()
app.use(cors())
app.use(morgan("dev"))
app.use(express.json())

app.get("/location", getLocation)
app.get("/weather", getWeather)

export default app
import axios from "axios"

async function getLocation(req, res) {
  const { city } = req.query
  const response = await axios.get(
    `https://www.metaweather.com/api/location/search/?query=${city}`
  )
  res.status(200).json(response.data)
}

async function getWeather(req, res) {
  const { woeid } = req.query
  const response = await axios.get(
    `https://www.metaweather.com/api/location/${woeid}`
  )

  res.status(200).json(response.data)
}

export { getLocation, getWeather }

react-native

与 react 心智模型基本一致。

开发环境的配置较为繁琐。

手机局域网请求本地接口要使用 ip 地址

const response = await fetch(
  `http://192.168.31.133:3000/weather?woeid=${woeid}`
)

还需要熟悉debug,core components以及样式的调整

import React, { useEffect, useState } from "react"
import {
  View,
  KeyboardAvoidingView,
  Text,
  StyleSheet,
  Platform,
  ImageBackground,
  ActivityIndicator,
  StatusBar,
} from "react-native"

import { SearchInput } from "./src/components/searchInput"
import useAsync from "./src/hooks/useAsync"
import { textSize } from "./src/utils/style"
import getImageForWeather from "./src/utils/getImageForWeather"
import { fetchLocationId, fetchWeather } from "./src/utils/api"

const App = () => {
  const [location, setLocation] = useState("Beijing")
  const { data, error, isLoading, run } = useAsync()

  const renderWeather = () => {
    const { location, weather, temperature } = data
    return (
      <>
        <Text style={[styles.largeText, styles.textStyle]}>{location}</Text>
        <Text style={[styles.smallText, styles.textStyle]}>{weather}</Text>
        <Text style={[styles.largeText, styles.textStyle]}>{`${Math.round(
          temperature
        )}°`}</Text>
      </>
    )
  }
  useEffect(() => {
    const promise = fetchLocationId(location).then((locationId) => {
      if (!locationId) {
        return Promise.reject("不支持的地区")
      } else {
        return Promise.resolve(fetchWeather(locationId))
      }
    })

    run(promise)
  }, [location])
  return (
    <KeyboardAvoidingView
      style={styles.container}
      behavior={Platform.OS === "ios" ? "padding" : "height"}
    >
      <StatusBar barStyle="light-content" />

      <ImageBackground
        source={data ? getImageForWeather(data.weather) : null}
        imageStyle={styles.image}
        style={styles.imageContainer}
        resizeMode="cover"
      >
        <View style={styles.detailsContainer}>
          <ActivityIndicator animating={isLoading} color="white" size="large" />
          {!isLoading && (
            <View style={styles.contentContainer}>
              {data && renderWeather()}
              {error && (
                <Text style={[styles.error, styles.smallText]}>{error}</Text>
              )}
              <SearchInput onSubmit={setLocation} />
            </View>
          )}
        </View>
      </ImageBackground>
    </KeyboardAvoidingView>
  )
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: "#34495E",
  },
  largeText: {
    fontSize: textSize.large,
  },
  smallText: {
    fontSize: textSize.small,
  },
  contentContainer: {
    flex: 1,
    justifyContent: "center",
    alignItems: "center",
  },
  imageContainer: {
    flex: 1,
  },
  image: {
    flex: 1,
    width: null,
    height: null,
    resizeMode: "cover",
  },
  error: {
    color: "red",
  },
  detailsContainer: {
    flex: 1,
    justifyContent: "center",
    backgroundColor: "rgba(0,0,0,0.2)",
    paddingHorizontal: 20,
  },
  textStyle: {
    textAlign: "center",
    fontFamily: Platform.OS === "ios" ? "AvenirNext-Regular" : "Roboto",
    color: "white",
  },
})

export default App
import { useState } from "react"
import { StyleSheet, TextInput, View } from "react-native"

export function SearchInput({ onSubmit }) {
  const [input, setInput] = useState("")
  const handleSubmit = (text) => {
    if (!input) return
    onSubmit(input)
    setInput("")
  }
  return (
    <View style={styles.container}>
      <TextInput
        style={styles.textInput}
        placeholder="select a city"
        value={input}
        onChangeText={setInput}
        onSubmitEditing={handleSubmit}
      />
    </View>
  )
}

const styles = StyleSheet.create({
  container: {
    width: 120,
    height: 40,
    marginTop: 20,
    backgroundColor: "#eee",
    marginHorizontal: 40,
    paddingHorizontal: 10,
    borderRadius: 5,
  },
  textInput: {
    flex: 1,
    color: "black",
  },
})