携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第1天,点击查看活动详情
前言
继「特 斯 拉 1.0」发布也有段时间了(问就是产后护理+参加摸爬滚打的弟弟去了),期间学习了下Redux以及React Hooks钩子函数的用法,并融入到了项目中,在完善Tesla1.0 的功能的同时增加了一些页面与功能,最后感谢提出建议的掘友和小伙伴,下面看看具体实现过程:
Redux
什么是Redux?
简单来说,就是一个专门用来做状态管理的 JS 库,具体可参考神三元的文章。在学习 Redux 前,设置状态都是用 useState
来完成,但随着项目地逐步扩大,组件会越来越多,所需要的状态也会越来越多。父子组件传值还好,但如果碰到跨组件传值,浙江卫视场屠杀(特斯拉1.0就是这样),这时候就轮到 Redux 登场了
Redux工作流程
用户想要修改状态 -->
ActionCreators 获取需求,封装 action(给予特定的 type) -->
派送dispatch(action) --(Store)-->
Reducer 通过 type 判断哪类状态需要修改,并返回新的状态 --(Store)-->
新的状态重新渲染到界面上,Store 相当于中间人,具体流程如下图:
注意:state(状态) 是只读的,唯一改变 state 的方法就是触发 action
BUT
这里要解释下,我使用 Redux 管理数据单纯是为了学习如何使用,要知道状态管理并不是必需品,当你的UI层比较简单、没有较多的交互去改变状态的场景下,使用状态管理方式反倒会弄巧成拙。正如 Redux 的发明者 Dan Abramov 所说:“只有遇到 React 实在解决不了的问题,你才需要 Redux。”
项目实现
首先来看下界面实现的效果图: 项目预览
Redux
这是一个繁琐的过程,开始前需要对状态分类,如果该状态很多组件需要用到那么放入仓库 Store 中,如果仅在当前组件使用,则使用 useState
设置状态即可。
- 先用 Provider 包裹,相当于给 App 根组件提供仓库服务
<Provider store={store}>
<HashRouter>
<App />
</HashRouter>
</Provider>
- 创建总仓库 Store
- applyMiddleware :提供中间件 thunk(除此之外还有 logger 等)
- composeEnhancers :将多个中间件合并在一起,方便调试。推荐使用
Redux DevTools
import { createStore, compose, applyMiddleware } from 'redux'
import thunk from 'redux-thunk'
import reducer from './reducer'
const composeEnhancers =
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(reducer,
composeEnhancers(
applyMiddleware(thunk)
)
)
export default store
总 Reducer ,集合了所有子仓库中 reducer
import { combineReducers } from "redux";
import { reducer as customReducer } from '@/pages/Custom/store/index'
import { reducer as footerReducer } from '@/components/Footer/store/index'
import { reducer as buyTeslaReducer } from "@/pages/BuyTesla/store/index";
export default combineReducers({
custom: customReducer,
footer: footerReducer,
buyTesla: buyTeslaReducer
})
- 组件连接仓库
// 读操作 相当于 useState 第一个参数
const mapStateToProps = (state) => {
return {
carParamsList: state.custom.carParamsList,
showEdition: state.custom.showEdition,
...
}
}
// 写操作 相当于 useState 第二个参数
const mapDispatchToProps = (dispatch) => {
return {
// 用户操作之后,获取 data 并 dispatch 给 actionCreators
getShowEditionDispatch(data) {
dispatch(actionCreators.getShowEdition(data))
}
...
}
}
// 连接仓库
export default connect(mapStateToProps, mapDispatchToProps)(React.memo(Custom))
- 组件内创建子仓库
index.js 文件,将所有数据交给总仓库
import reducer from "./reducer";
import * as actionCreators from './actionCreators'
export {
reducer,
actionCreators
}
actionCreators.js 文件
import { getCarParamsRequest } from "@/api/request";
import * as actionTypes from './constants'
// 修改汽车版本
// 给数据带上 type 就变成了action
// 无情的 action 制造机器
export const changeShowEdition = (data) => ({
type: actionTypes.CHANGE_SHOW_EDITION,
data
})
// 把数据给 action 制造机器,再 dispatch 给reducer
export const getShowEdition = (data) => {
return (dispatch) => {
dispatch(changeShowEdition(data))
}
}
...
reducer.js 文件,存放、修改状态
注意:返回的新状态,与旧状态引用地址不同,为了方便溯源,也可以用 Object.assign() 方法,效果一样
const defaultState = {
showEdition: '1',
}
export default (state = defaultState, action) => {
// 通过 type 判断修改哪个数据
switch (action.type) {
case actionTypes.CHANGE_SHOW_EDITION:
// 返回了新的状态,与旧状态引用地址不同,方便溯源
return {
...state,
showEdition: action.data
}
...
default:
return state
}
}
constants.js 文件,存放 type 类型配置
export const CHANGE_SHOW_EDITION = 'CHANGE_SHOW_EDITION'
...
Design组件
配置切换颜色、轮毂、内饰等需要的变量,并实时计算价格(通过图片配置中的数字判断价格),实现计算器功能(因为不严谨导致爆肝数小时...)
// 拼接 获得与配置中对应的图片序号
// 设置多个变量方便计算
let picNumber = showEdition + color + wheel;
let decNumber = showEdition + decoration;
let carMoney = showEdition == '1' ? 290988 : 367900;
let colorMoney = color == '1' ? 0 : 8000;
let wheelMoney = wheel == '1' ? 0 : wheel == '2' ? 6000 : 0;
let decorationMoney = decoration == '1' ? 0 : 8000;
// 计算加了几个装饰,用来判断是否有新能源汽车补贴,大于1则没有
let count = 0;
if (showEdition == '1') {
if (color != '1') count++;
if (wheel != '1') count++;
if (decoration != '1') count++;
}
// console.log(count, 'QAQ');
let total = carMoney + colorMoney + wheelMoney + decorationMoney;
// 换车轮或内饰则没有新能源汽车补贴
if (showEdition == '1' && count <= 1) {
total -= 11088;
}
let estimateMoney = total - 77500;
// 计算总价并转化为字符串,且在字符串中加“,”
total = total.toString().split('');
let tLength = total.length;
total.splice(tLength - 3, 0, ',')
total = total.join('')
// 估算价格,步骤同上
estimateMoney = estimateMoney.toString().split('');
let eLength = estimateMoney.length;
estimateMoney.splice(eLength - 3, 0, ',')
estimateMoney = estimateMoney.join('')
useEffect(() => {
getSumDispatch(total)
getEstimateDispatch(estimateMoney)
}, [total, estimateMoney])
实现颜色、轮毂、内饰切换(代码有省略),图片配置代码太多,这里就不放了
<div>
<div className='color_select_IMG'>
{/* 选中配置好的图片 */}
<img src={carPicture_color[picNumber]} alt="" />
</div>
<div className='color_select'>
<div className='color_select_title'>
<span>选择颜色</span>
</div>
<div className='fivecolor_wrapper'>
<div
onClick={() => getColorDispatch('1')}
className={color == '1' ? 'active' : ''}
>
<img src="https://static-assets.tesla.cn/share/tesla_design_studio_assets/MODEL3/UI/Paint_Black.png?version=v0028d202207140307" alt="" />
</div>
...
</div>
<div className='color_select_desc'>
<span>{color_desc[color]}</span>
<span>{color == '1' ? '包括' : '¥ 8,000'}</span>
</div>
</div>
</div>
实现
BuyTesla组件
为了实现显示、隐藏的切换过渡效果,本来打算交给 display:none || block
来实现的,但是这会使得transition 属性失效,所以想到传参给 Wrapper 组件,动态修改样式,styled-components (css in js)的好处
// 当页面刷新时,carParamsList 为空,因此需要跳转到主页
carParamsList.length != 0 ?
// 使用了 react 中的 CSSTransition 组件,实现页面切换动画效果
<CSSTransition
in={show1}
timeout={800}
appear={true}
classNames="fly"
unmountOnExit
onExit={() => {
navigate(-1)
}}
>
{/* 传参给Wrapper,动态修改样式,styled-components 的好处 */}
<Wrapper show={show} showEdition={showEdition} count={count}>
...
</Wrapper>
</CSSTransition>
: useEffect(() => (navigate("/")), [])
实现
Modal 组件内实现 swiper 效果
一开始纠结到底是用一个swiper包多个modal,还是用一个modal包多个swiper,前者尝试后失败了,只能用后者,采用了 antd-mobile
里的 Swiper 走马灯 组件
<Modal
visible={showModalCarDetail}
title=""
onClose={onModalClose}
>
<Swiper className='aaa' style={{
'--track-padding': ' 0 0 25px',
borderRadius: '16px'
}}>
<Swiper.Item style={{ width: '90%', margin: 'auto' }}>
<Swiper1
getShowModalCarDetailDispatch={getShowModalCarDetailDispatch}
getIsFixedDispatch={getIsFixedDispatch} />
</Swiper.Item>
...
</Swiper>
</Modal>
实现
优化
1. 配置baseUrl 及拦截器
// 配置请求对象
import axios from 'axios'
// 本地调试 dev 开发阶段
export const baseUrl = "https://www.fastmock.site/mock/7c2b4d662d21c3311479338632d3faec/tesla";
// product 阶段
// 设计模式
const axiosInstance = axios.create({
baseURL: baseUrl
})
// 响应拦截器
axiosInstance.interceptors.response.use(
res => res.data,
err => {
console.log(err, '网络错误~~')
}
)
export { axiosInstance }
2. 路由懒加载
import React, { lazy, Suspense } 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'
const BuyTesla = lazy(() => import('./pages/BuyTesla'))
function App() {
return (
<div className="App">
<Suspense>
<Header />
<Routes>
<Route path='/' element={<Custom />}></Route>
<Route path='/buy_tesla' element={<BuyTesla />}></Route>
</Routes>
<Footer />
</Suspense>
</div>
)
}
export default App
最后
以上就是 Tesla2.0 的分享,感谢各位看到最后,项目显然还存在很多不足等待我去完善,有任何建议欢迎在评论区告诉我,如有收获也请点个👍,非常感谢o( ̄▽ ̄)ブ♥
项目源码(github)
项目预览
我正在参与掘金技术社区创作者签约计划招募活动,点击链接报名投稿。