520看到女神这么热,我不禁用React写了个夏日空调

我正在参加「初夏创意投稿大赛」详情请看:初夏创意投稿大赛

摘要

当520来临而又恰逢夏日时节,暗恋女神多日的我决定今晚把她约出去看电影,一想到晚上的甜蜜时光,我直接拿起手机直接给女神发消息,掘友们等着吃狗粮吧,管饱。

image.png

既然女神这么热,我身为一个react使者怎能甘心呢,不行,要给女神写个空调,说不定今晚可以出去二人世界了呢。要写个夏日空调可真难,倒不是代码繁琐,只是这夏日空调得蘸上四分春风,三分月色,两分微醺,还有一分她的眉眼才好。咱们话不多说,上号。

image.png

创建项目

> create-react-app air-condition
复制代码

组件划分

无论写react还是vue,最重要的一点永远都有如何确定组件分工,组件就是分而治之的思想,它就像一个备胎,哪里需要哪里搬!因为这个React夏日空调并不算复杂,甚至有点简单,所以html、css部分不多赘述,我们直接来看怎么划分组件。如下图所示,App组件为根组件,包裹Panel、CopyWriting、Button、Audio组件,Panel组件中又包含Temp组件,我们将状态(空调当前温度)、行为(调节按钮、声音的播放与暂停)统统放在App组件中,因为Panel、Button、Audio组件之间需要共享数据,而最方便的方法无疑是将共有数据存放在其共同的父组件中,这种方式React称之为状态提升

image.png

组件分工

index

import React from 'react'
import ReactDOM from 'react-dom/client'

import App from './App'

const root = ReactDOM.createRoot(document.querySelector('#root'))
root.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
)
复制代码

App

import { useState, useRef, useCallback } from 'react'

import Panel from './component/Panel'
import CopyWriting from './component/CopyWriting'
import Button from './component/Button'
import Audio from './component/Audio'

const App = () => {
  // 使用useRef得到Audio组件的实例
  const ref = useRef()
  // temp为存储的空调温度,默认为26
  const [temp, setTemp] = useState(26)
  // 调高温度
  const up = useCallback(() => {
    const result = temp + 1
    ref.current.playTip()
    // 温度最高为31
    if (result > 31) { return false }
    setTemp(result)
  }, [temp])
  // 调低温度
  const down = useCallback(() => {
    const result = temp - 1
    ref.current.playTip()
    // 温度最低为16
    if (result < 16) { return false }
    setTemp(result)
  }, [temp])
  // 开关按钮
  const toSwitch = useCallback(() => {
    ref.current.playTip()
    ref.current.playRun()
  }, [temp])

  return (
    <>
      <section className="title">
        <h1 style={{ fontWeight: 400 }}>夏日空调</h1>
      </section>
      <section className="panel">
        <Panel temp={temp} />
      </section>
      <section className="copywriting">
        <CopyWriting />
      </section>
      <section className="button">
        <Button up={up} down={down} toSwitch={toSwitch} />
      </section>
      <section style={{ display: 'none' }}>
        <Audio ref={ref} />
      </section>
    </>
  )
}

export default App
复制代码

App组件中我们使用useRef继而得到子组件Audio中的方法。up、down、toSwitch方法通过props传递给Button组件,为了引起Button组件的不必要渲染,我们使用useCallback包裹住传递的方法,同时Button组件自身使用React.memo进行包裹。

Panel

import Temp from '../Temp'

// 生成div
const productDiv = length => Array.from({ length }).map((_, i) => <div key={i}></div>)

const Panel = ({ temp }) => {
    return (
        <div className="air-condition">
            <div className="a-tag">
                <div className="a-t-header">
                    {productDiv(6)}
                </div>
                <div className="a-t-content-1">
                    <div className="c-l">
                        <div className="c-l-1"></div>
                        <div className="c-l-2"></div>
                        <div className="c-l-3"></div>
                    </div>
                    <div className="c-r">
                        <p className="c-r-grade"></p>
                    </div>
                </div>
                <div className="a-t-content-2">
                    <div className="c-2-title">
                        {productDiv(9)}
                    </div>
                    <div className="c-2">
                        <div className="c-2-1">
                            {productDiv(6)}
                        </div>
                        <div className="c-2-2">
                            {productDiv(6)}
                        </div>
                    </div>
                </div>
                <div className="a-t-footer">
                    {productDiv(6)}
                </div>
            </div>
            <div className="a-screen">
                <div className="s-icon">
                    <img src={snowflake} className="img-100" alt="" />
                </div>
                <div className="s-temp">
                    <Temp temp={temp} />
                </div>
            </div>
            <div className="a-line"></div>
            <div className="a-wind">
                <img src={wind} alt="failed" className="img-l" />
                <img src={wind} alt="failed" className="img-c" />
                <img src={wind} alt="failed" className="img-r" />
            </div>
        </div>
    )
}

export default Panel
复制代码

我们也可以选择不使用Temp组件,而是将温度直接显示在Panel组件中,这种方式同样可以实现效果,但是为了数据和页面更好的分离,我们的Panel组件只负责接收数据,Temp组件只负责显示数据,这样如果后期需要对Panel组件添加一些复杂交互、功能,我们只需要对Panel组件进行修改,如果需要对空调温度的显示界面进行修改时,我们只需要修改Temp组件即可。

Temp

import React from 'react'

const Temp = ({ temp }) => {
    return (
        <>
            <span>{temp}</span>
            <span className="s-t-symbol"></span>
            <span>c</span>
        </>
    )
}

export default React.memo(Temp)
复制代码

Temp组件只是用来展示UI,不进行其它任何操作,无论Temp组件后期修改后多么花里胡哨,温度(temp)我们已经拿到了,无论怎么修改均不影响其它功能。同样,为了引起Temp组件的不必要渲染,我们依然使用React.memo对组件自身进行包裹。

CopyWriting

import { useState, useEffect } from "react"

// 获取文案
const getContent = async () => {
    try {
        const connect = await fetch('https://v1.hitokoto.cn?c=d')
        const data = await connect.json()
        return data
    } catch (e) { return false }
}

const CopyWriting = () => {
    // 存储获取到的文案
    const [text, setText] = useState({ hitokoto: '', quote: '' })
    useEffect(() => {
        const update = async () => {
            const data = await getContent()
            const from_who = data.from_who ? data.from_who : ''
            data.quote = `——${from_who}${data.from}》`
            setText(data)
        }
        const token = setInterval(update, 6000)
        update()
        // 组件卸载时清除定时器
        return () => clearInterval(token)
    }, [])
    return (
        <>
            <p>{text.hitokoto}</p>
            <p>{text.quote}</p>
        </>
    )
}

export default CopyWriting
复制代码

因为本项目相对较小,无需其它复杂功能,所以getContent采用fetch获取文案,如果非要使用xhr、axios倒显得有些冗余了。我们此处将获取到的文案存放在了useState中,还有一种方式,此处并未采用。即使用useRef来存储数据,当获取到数据后,使用useState刷新页面。例如

const Comp = () => {
    const ref = useRef(1)
    const [force, setForce] = useState()
    const handle = () => {
        const result = ref.current
        ref.current = result + 1
        setForce(result)
    }
    return (
        <>
            <span>{ref.current}</span>
            <button onClick={handle}>自增</button>
        </>
    )
}
复制代码

Button

import React, { useState } from 'react'

const Button = ({ up, down, toSwitch }) => {
    // 存储开关按钮的背景颜色
    const [bg, setBg] = useState({ open: '#f33531', off: '#43a047', flag: false })
    // 播放声音、切换状态
    const switchState = () => {
        toSwitch()
        setBg({ ...bg, flag: !bg.flag })
    }
    return (
        <>
            <div className="b-inc" onClick={() => up()}>
                <img src={upArrow} className="img-5" alt="failed" />
            </div>
            <div
                className="b-switch"
                onClick={() => switchState()}
                style={{ backgroundColor: bg.flag ? bg.open : bg.off }}
            >
                <img src={switchBtns} className="img-5" alt="failed" />
            </div>
            <div className="b-dec" onClick={() => down()}>
                <img src={downArrow} className="img-5" alt="failed" />
            </div>
        </>
    )
}

export default React.memo(Button)
复制代码

Button组件自身使用React.memo进行包裹,一般情况下,React.memo与useCallback、useMemo搭配使用,使用useCallback、useMemo的原因是无论父组件如何操作,始终保证传递给Button组件的props不变,使用React.memo是为了对props做比较,这样才不会引起Button组件的重新渲染。React.memo类似于类式组件的PureComponent。

Audio

import React, { useRef, useImperativeHandle } from 'react'

const Audio = (_, ref) => {
    const tipRef = useRef()
    const runRef1 = useRef()
    // 要传递给父组件的方法
    useImperativeHandle(ref, () => ({
        playTip: () => {
            tipRef.current.play()
        },
        playRun: () => {
            const flag = runRef1.current.paused
            if (flag) return setTimeout(() => runRef1.current.play(), 300);
            runRef1.current.pause()
        },
    }))
    return (
        <>
            <audio src={tip} ref={tipRef} ></audio>
            <audio src={run} ref={runRef1} loop></audio>
        </>
    )
}

// 使用React.forwardRef将子组件ref转发给父组件
export default React.forwardRef(Audio)
复制代码

Audio组件中,我们使用React.forwardRef与useImperativeHandle让父组件可以操作子组件。注意,类式组件并不需要这么做,只需要在使用子组件时直接ref即可,显然,hooks并不支持这种写法。

// son
class Son extends React.Component {
  constructor() {
    super()
    this.state = { value: 1 }
    this.inc = () => this.setState({ value: this.state.value + 1 })
  }
  render() {
    const { value } = this.state
    const { inc } = this
    return (
      <>
        <span>{value}</span>
        <button onClick={inc}>加1</button>
      </>
    )
  }
}
// father
// father可以是函数式组件也可以是类式组件
class Father extends React.Component {
  constructor() {
    super()
    this.ref = React.createRef()
    this.inc = () => this.ref.current.inc()
  }
  render() {
    const { ref, inc } = this
    return (
      <>
        <Son ref={ref} />
        <button onClick={inc}>加21</button>
      </>
    )
  }
}
复制代码

项目完成

> npm start
复制代码

image.png

在线演示地址

1.在线演示

2.码上掘金

项目到这里就已经大功告成了,另外给大家推荐一下我自己写的JS每日一题JavsScript每日一题专栏每天用2-3分钟做一做,我相信技术肯定会得到提升的。兄弟们回见了,我要去拿给女神看看了。

image.png

我心想,女神一定高兴坏了吧,今晚应该去看什么电影呢,听说520档期有《可不可以不要离开我》、《暗恋:橘生淮南》,我相信有她陪我,每一天都是不重复的电影。

image.png

女神为什么这样啊,难道我的空调不够好吗?思来想去,可能兔兔去睡觉了不希望被人打扰罢了。

猜你喜欢

转载自juejin.im/post/7100478900673150989