前言
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 请求,实现前后端分离。
项目实现
首先来看下界面实现的效果图: 项目预览
设计思路
通过观察Tesla 官网,可以了解到我们需要实现以下功能:
(该项目仅为1.0版本,更多功能后续后慢慢添加)
- 基础界面开发:完成静态页面编写(需要有一定html+css基础)
- 模态框:点击查看详情按钮、底栏箭头以及中间部分,弹出模态框
- tab 切换:底栏弹出的模态框内实现切换效果
- 弹出层:点击CLTC综合工况按钮、底栏的对话图标,从底部出现弹出层
下面来看看项目文件结构:
- 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 />
组件,同时引入propTypes对props
中数据类型进行检测,判断是否为数组,且必需传值
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)
项目预览
我正在参与掘金技术社区创作者签约计划招募活动,点击链接报名投稿。