最渺小的作品:React 全家桶与 「特 斯 拉 2.0」

1,670 阅读6分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 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 相当于中间人,具体流程如下图:

1658382812882.jpg

注意:state(状态) 是只读的,唯一改变 state 的方法就是触发 action

BUT

这里要解释下,我使用 Redux 管理数据单纯是为了学习如何使用,要知道状态管理并不是必需品,当你的UI层比较简单、没有较多的交互去改变状态的场景下,使用状态管理方式反倒会弄巧成拙。正如 Redux 的发明者 Dan Abramov 所说:“只有遇到 React 实在解决不了的问题,你才需要 Redux。

项目实现

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

ezgif-4-cc4c66470f.gif

Redux

这是一个繁琐的过程,开始前需要对状态分类,如果该状态很多组件需要用到那么放入仓库 Store 中,如果仅在当前组件使用,则使用 useState 设置状态即可。

  1. 先用 Provider 包裹,相当于给 App 根组件提供仓库服务
<Provider store={store}>
    <HashRouter>
      <App />
    </HashRouter>
</Provider>
  1. 创建总仓库 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
})
  1. 组件连接仓库
// 读操作 相当于 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))
  1. 组件内创建子仓库

    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>

实现

3.gif

BuyTesla组件

为了实现显示、隐藏的切换过渡效果,本来打算交给 display:none || block 来实现的,但是这会使得transition 属性失效,所以想到传参给 Wrapper 组件,动态修改样式,styled-componentscss 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("/")), [])

实现

1.gif

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>

实现

2.gif

优化

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)
项目预览
我正在参与掘金技术社区创作者签约计划招募活动,点击链接报名投稿