从入门到入职(不是),新手React开发之——Tesla界面开发教程

10,159 阅读7分钟

前言

React是FaceBook于2013开源的项目,一经问世就在前端占据一席之地,随着近几年不断发展壮大,已成为前端主流框架。而它之所以能如此成功,主要是因为它的声明式组件化高效(虚拟DOM、Diff)便捷(React Native),下面通过项目实战详细地介绍组件的设计思路与流程,如发现各种问题,请看官巨佬们轻喷并指正~🙏

准备阶段

首先我们需要创建一个React项目,我使用的是 ViteJS 来初始化本项目,当然也可以使用官方脚手架 create-react-app 来初始化,前者相对来说更快,两者各有优缺点,大家可以根据喜好选择。在VSCode终端输入:

 npm init @vitejs/app

接着给项目命名为 Tesla --> 连续两次选择 react --> 最后根据提示进入创建的项目文件夹 --> 安装项目依赖 --> 运行下我们的项目

cd Tesla  
npm install
npm run dev

紧接着我们会在3000端口看到项目已经运行起来了,最后再来介绍下本项目依赖的包:

  • axios: 是一个基于 promise 的一个用于发送ajax请求的HTTP库,本质上是对AJAX的封装,用于前端获取数据。

  • styled-components:是一个针对React的 css in js 类库。

  • antd-mobile:提供可直接使用的前端模板,如本项目用到的Modal模态框、PopUp弹出层等。

  • iconfont:图标字体库。

  • propTypes:对props中数据类型进行检测及限制。 在终端命令行输入以下命令,将上面提到的包全部下载安装:

      npm i axios
      npm i styled-components
      npm i antd-mobile
      npm i prop-types
    

另外,项目的数据存储在fastmock,这是一个在线接口工具,可以让你在没有后端程序的情况下能真实地在线模拟 ajax 请求,实现前后端分离。

项目实现

首先来看下界面实现的效果图: 项目预览

ezgif.com-gif-maker.gif

设计思路

通过观察Tesla 官网,可以了解到我们需要实现以下功能:
该项目仅为1.0版本,更多功能后续后慢慢添加

  • 基础界面开发:完成静态页面编写(需要有一定html+css基础
  • 模态框:点击查看详情按钮、底栏箭头以及中间部分,弹出模态框
  • tab 切换:底栏弹出的模态框内实现切换效果
  • 弹出层:点击CLTC综合工况按钮、底栏的对话图标,从底部出现弹出层
    下面来看看项目文件结构:

2.png

  • api 目录:顾名思义,用来存放数据请求。
  • assets 资源目录:用来存放一些静态资源,比如:图片、字体、全局样式...
  • components 组件目录:存放通用组件,如顶栏、底栏,以及一些可复用的组件等。
  • page 页面目录:存放页面组件,如首页、定制等。 (由于本项目为初级阶段,页面级组件暂时只有Cuntom定制界面) 采用Layout布局,将<Header /><Footer />组件分别固定在头部与尾部,页面级别组件<Custom /> 放在中间,简化App.jsx根组件。

App 根组件

根组件负责调用api获取数据,并通过props向子组件传递参数。(由于只有一个页面组件,所以没有将路由分离到单独的文件,后期页面组件多了则需要分离)

import React, { useState, useEffect } from 'react'
import './App.css'
import { Routes, Route, Link } from 'react-router-dom'
import Custom from './pages/Custom'
import Header from './components/Header'
import Footer from './components/Footer'
import { getCarParams } from './api/request'


function App() {
  // 设置一个汽车参数状态并设初始值为空数组,carParams为读操作,setCarParams为写操作
  const [carParams, setCarParams] = useState([]);
  // 设置一个显示版本状态并设初始值为1,showEdition为读操作,setShowEdition为写操作
  const [showEdition, setShowEdition] = useState('1')
  // useEffect生命周期函数,它会在组件加载完成时运行,这里作用是异步(async + await)获取数据
  useEffect(() => {
    (async () => {
      let { data: carParamsData } = await getCarParams()
      setCarParams(carParamsData)
    })()
  }, [])

  return (
    <div className="App">
      <Header />
      <Routes>
        <Route path='/' element={<Custom carParams={carParams} showEdition={showEdition} setShowEdition={setShowEdition} />}></Route>
      </Routes>
      <Footer carParams={carParams} showEdition={showEdition} />
    </div>
  )
}

export default App

| api接口

引入axios 远程获取数据,这是采用了fastmock创建的伪接口,后续根组件通过引入并调用getCarParams方法拿到数据。

import axios from 'axios'

export const getCarParams = () =>
    axios.get('https://www.fastmock.site/mock/7c2b4d662d21c3311479338632d3faec/tesla/design')

Custom 定制组件

定制页面组件将根组件传来的参数props继续传给 Main.jsx 子组件。(由于时间有限,只写了主组件,定制界面的其他组件后期添加)

import React, { useState, useEffect } from 'react'
import { Wrapper } from './style'
import Main from './Main'

export default function Custom(props) {
    return (
        <Wrapper>
            <Main 
                carParams={props.carParams} 
                showEdition={props.showEdition} 
                setShowEdition={props.setShowEdition} />
        </Wrapper>
    )
}

Main 定制界面内的主组件

获取<Cuntom />组件传来的数据,通过判断当前组件是否选中,是则渲染数据,不是则不渲染,并引入<ModalCarDetail /> 组件,同时引入propTypesprops中数据类型进行检测,判断是否为数组,且必需传值

import propTypes from 'prop-types'
...
<Wrapper>
    {/* 汽车图片 */}
    ...
    {/* 汽车参数 */}
    {
        carParams.map(item => {
            return (
                {/* 通过判断当前组件是否选中,是则渲染数据,不是则不渲染 */}
                showEdition == item.id &&
                <div className="main__section2_param" key={item.id}>
                    ...
                </div>
            )
        })
    }
    {/* 电机驱动部分 */}
    ...
    {/* 模态框部分 */}
    <ModalCarDetail visible={visible} setVisible={setVisible} onModalClose={onModalClose} />
</Wrapper>
...
// 判断是否为数组,且必需传值
Main.propTypes = {
    carParams: propTypes.array.isRequired
}

Header 头组件

头组件只用来放置 Logo,通过定位,固定在头部。

import React from 'react'
import { Wrapper } from './style'
import IMG from '../../assets/img/tesla.png'
import { Link } from 'react-router-dom'

export default function Header() {
    return (
        <Wrapper>
            <Link to='/' className='logo'>
                <img src={IMG} alt="" />
            </Link>
        </Wrapper>
    )
}

Footer 尾组件

尾组件存放基础结构、<ModalCalculator />计算器弹层组件与<Dialogue />弹出层组件,通过定位,固定在尾部。

<Wrapper>
    <div className='calculator'>
        {/* 点击显示模态框 */}
        <div className='calculator-icon' onClick={() => setVisible(true)}>
            <i className='iconfont icon-xiangshang'></i>
        </div>
        <div className='calculator-desc' onClick={() => setVisible(true)}>
            <div className='calculator-desc-top'>
                {
                    carParams.map(item => {
                        return (
                            showEdition == item.id && <span key={item.id} className='calculator-price1'>¥ {item.price}</span>
                        )
                    })
                }
                <span>实际价格</span>
            </div>
            <div>
                {
                    carParams.map(item => {
                        return (
                            showEdition == item.id && <span key={item.id} className='calculator-price1'>¥ {item.another_price}</span>
                        )
                    })
                }
                <span>减去节省的燃油费</span>
            </div>
        </div>
    </div>
    {/* 对话框弹出层组件 */}
    <Dialogue />
    {/* 计算器弹层 */}
    <ModalCalculator visible={visible} setVisible={setVisible} onModalClose={onModalClose} />
</Wrapper >

Dialogue 对话框组件

引入了上面提到的antd-mobile 组件库中的Popup弹出层组件

// 引入 antd-mobile 组件库
import { Popup, Button } from 'antd-mobile'
...
<Wrapper>
    <Button className='btn-dialogue'
        onClick={() => {
            setVisible1(true)
        }}
    >
        <i className='iconfont icon-duihua'></i>
    </Button>

    <Popup
        visible={visible1}
        showCloseButton
        // 遮罩层样式
        maskStyle={{
            filter: 'blur(5px)',
            opacity: 0.5,
        }}
        // 主体样式
        bodyStyle={{
            borderTopLeftRadius: '12px',
            borderTopRightRadius: '12px',
            minHeight: '40vh',
        }}
        // 点击关闭图标关闭弹出层
        onClose={() => {
            setVisible1(false)
        }}
        // 点击遮罩层关闭弹出层
        onMaskClick={() => {
            setVisible1(false)
        }}
    >
        // 弹出层内容
        {mockContent()}
    </Popup>
</Wrapper>
...
const mockContent = () => {
    return (
        <Content>
            ...
        </Content>
    )
}

ModalCalculator 组件

引入了上面提到的antd-mobile 组件库中的Modal弹层组件,通过结构父组件传递的参数props,实现tab 切换。

import React from 'react'
import Modal from '@/components/common/Modal/ModalCalculator/modal'
import './index.css'
import { useState } from 'react'

export default function ModalCalculator(props) {
    // 将三个参数从父组件传递的参数props 中解构出来
    const { visible, setVisible, onModalClose } = props
    // 设置一个 active 状态,并设初始值为1,用来给下面tab 切换做准备
    const [active, setActive] = useState('1')
   
    return (
        <Modal
            visible={visible}
            title="付款计算器"
            onClose={onModalClose}
        >
            <div>
                <div className='btn-close' onClick={() => setVisible(false)}>
                    <i className='iconfont icon-guanbi'></i>
                </div>
                {* tab 切换实现 */}
                <div className='tab'>
                    <div className={active == '1' ? 'active' : ''} onClick={() => setActive('1')}>
                        <span>现金</span>
                    </div>
                    <div className={active == '2' ? 'active' : ''} onClick={() => setActive('2')}>
                        <span>合作金融机构贷款</span>
                    </div>
                    <div className={active == '3' ? 'active' : ''} onClick={() => setActive('3')}>
                        <span>特斯拉融资租赁</span>
                    </div>
                </div>
                {
                    active == '1' &&
                    <div className='tab-list'>
                        ...
                    </div>
                }
                {
                    active == '2' &&
                    <div className='tab-list'>
                        ...
                    </div>
                }
                {
                    active == '3' &&
                    <div className='tab-list'>
                        ...
                    </div>
                }
            </div>
        </Modal>
    )
}

Modal组件

由于样式不同,模态框无法复用 (应该是我的原因),导致<ModalCalculator />组件需要与<ModalCarDetail />组件分开写,但是即使分开,如果className相同,也会相互影响,所以我干脆 结构分离 + 类名分离,Modal组件的 {title}{children} 则对应<ModalCalculator />组件与<ModalCarDetail />组件的 标题内容<Modal />组件 (该组件对应<ModalCalculator />组件) 如下:

import React, { useState, useEffect } from "react";
import './modal.css'

const Modal = (props) => {
    const [visible, setVisible] = useState(false)
    const { visible: show, children, title, onClose } = props;

    useEffect(() => {
        setVisible(show)
    }, [show])

    const maskClick = () => {
        setVisible(false)
        onClose && onClose()
    }

    return (
        visible && <div className="modal-wrapper">
            <div className="modal">
                <div className="modal-title">{title}</div>
                <div className="modal-content">{children}</div>
            </div>
            <div className="mask" onClick={maskClick}></div>
        </div>
    )
}

export default Modal;

剩下的<ModalCarDetail />组件以及对应的<Modal />组件就不放图了,与上面类似,只是改了下类名。

总结

以上就是整个Tesla1.0界面开发过程,过程虽然曲折,遇到许多问题,但通过查看文献,基本都能得到解决。后续也会继续陆续添加新的功能,期待一个Tesla2.0~ 非常感谢各位大佬能看到最后,如有什么不对的地方可以批评指正出来,菜鸟作者一定立刻马上该,如有收获也请点个👍,非常感谢o( ̄▽ ̄)ブ♥
项目源码(github)
项目预览
我正在参与掘金技术社区创作者签约计划招募活动,点击链接报名投稿