3.4 需求四:任务体系
需求四:业务方开始做用户运营了~ 新增一个提现任务模块,用户经过一系列裂变或变现的操作完成任务后,点击完成任务弹出提现成功弹窗,收下弹窗金币飞入再金额刷新。解决四:很常规的需求,不同功能的视图组件做交互进而强调用户的交互路径1.写一个任务组件 Task
import { useEffect, useState } from 'react'; import styles from './index.module.less'; // 任务状态枚举 enum TASK_STATUS { PROGRESS = 'progress', COMPLETE = 'complete', } // 任务信息 const TASK_INFO_MAP = { [TASK_STATUS.PROGRESS]: { btn: '进行中', }, [TASK_STATUS.COMPLETE]: { btn: '已完成', } } /** 任务组件 */ const Task = () => { // 任务状态 const [state, setState] = useState<TASK_STATUS>(TASK_STATUS.PROGRESS) useEffect(() => { setTimeout(() => { alert('完成任务'); setState(TASK_STATUS.COMPLETE); }, 3000); }, []) return ( <div className={styles.taskWrap}> {/* icon */} <div className={styles.taskImg} /> {/* 详情 */} <div className={styles.taskDesc}> <div className={styles.action}> 完成任务节即可提现 </div> <div className={styles.detailText}> <div className={styles.detailTextDetail}>完成后可提现 0.6 元</div> </div> </div> {/* 按钮 */} <div className={styles.taskBtn} onClick={() => {}} > {TASK_INFO_MAP[state]?.btn || ''} </div> </div> ); }; export default Task;
.taskWrap { position: relative; top: 200rpx; border-radius: 30rpx; padding: 25rpx 30rpx 25rpx 24rpx; background: #fae3e4; display: flex; align-items: center; position: relative; .taskImg { width: 92rpx; height: 92rpx; background-image: url("https://gw.alicdn.com/imgextra/i4/O1CN01VqCtbK1vhU0PZUTzR_!!6000000006204-2-tps-80-80.png"); background-size: 100% 100%; position: relative; } .taskDesc { flex: 1; width: 370rpx; padding: 4rpx 0 2rpx; margin-left: 20rpx; display: flex; flex-direction: column; justify-content: space-between; .action { margin-bottom: 20rpx; font-weight: bold; display: flex; align-items: center; font-size: 28rpx; color: #942703; } .detailText { color: #a07466; font-size: 24rpx; height: 32rpx; display: flex; align-items: center; overflow: hidden; .detailTextDetail { display: flex; flex-direction: row; justify-content: center; align-items: center; } } } .taskBtn { width: 144rpx; height: 68rpx; display: flex; justify-content: center; align-items: center; border-radius: 34rpx; background-image: linear-gradient(145deg, #ff5d83, #ff2929); font-size: 28rpx; color: #fff; } }
因为 Task 任务组件与 Account 账户信息组件在业务是同层关系,需要将抽象到视图组件外层 Architecture 中。这里是成本不大~ 单纯 UI 层面的处理。
2.将 Dialog 和 Task 组件提到组件 Architecture最外层
import Account from "./Account"; import Task from "./Task"; /** 主入口 */ const Architecture = () => { return ( <> {/* 账户信息 */} <Account /> {/* 任务 */} <Task /> </> ) }; export default Architecture;
🔥 前面为了业务逻辑整洁,封装账户信息 hooks。但这时候在 Task 组件中,在完成任务后也要处理弹窗数据以及展示提现弹窗,也就是 setDialogData 和 setShowDialog。
3.将 useAccount 账户信息逻辑抽象到 Architecture外,并改造 Account和 Task 逻辑。
import Account from "./Account"; import Task from "./Task"; import useAccount from "./Account/useAccount"; /** 主入口 */ const Architecture = () => { // 账户业务逻辑 const { account, refreshAccount, showCoinsFly, setShowCoinsFly, showDialog, setShowDialog, dialogData, setDialogData } = useAccount() return ( <> {/* 账户信息 */} <Account account={account} refreshAccount={refreshAccount} showCoinsFly={showCoinsFly} setShowCoinsFly={setShowCoinsFly} showDialog={showDialog} setShowDialog={setShowDialog} dialogData={dialogData} setDialogData={setDialogData} /> {/* 任务 */} <Task setShowDialog={setShowDialog} setDialogData={setDialogData} /> </> ) }; export default Architecture;
4.Account和 Task 组件改造数据状态分发逻辑
import CoinsFly from '../components/CoinsFly'; import WithdrawDialog from '../components/WithdrawDialog'; import { DialogData } from '../components/WithdrawDialog'; import styles from './index.module.less'; interface IAccount { account: number; showCoinsFly: boolean; showDialog: boolean; dialogData: DialogData; setDialogData: React.Dispatch<React.SetStateAction<DialogData>>; setShowDialog: React.Dispatch<React.SetStateAction<boolean>>; setShowCoinsFly: React.Dispatch<React.SetStateAction<boolean>>; refreshAccount: (account: number) => void; } /** 账户组件 */ const Account = (props: IAccount) => { const { account, refreshAccount, showCoinsFly, setShowCoinsFly, showDialog, setShowDialog, dialogData, setDialogData } = props; return ( <div className={styles.stickyAccountWrap}> <div className={styles.stickyAccount}> <div className={styles.stickyAccountGoldPocketPic} /> <div className={styles.stickyAccountTitleContainer}> <div className={styles.stickyAccountTitle}> <div>{account}</div> <div className={styles.unit}>元</div> </div> </div> <div className={styles.withdraw} onClick={() => { setDialogData({ a: "3000", b: "123456789123456789", c: "支付宝打款", d: "提现成功,预计2小时到账", e: "0.3", }) setShowDialog(true); }} /> </div> {/* 金币飞入 */} {showCoinsFly && ( <CoinsFly style={{ top: '322rpx', left: '316rpx', zIndex: 1, }} onEnd={() => { setShowCoinsFly(false); }} /> )} {/* 提现弹窗 */} { showDialog && <WithdrawDialog data={dialogData} onClose={() => { refreshAccount(12.04); setShowDialog(false); }} /> } </div> ); }; export default Account;
import { useCallback, useEffect, useState } from 'react'; import { DialogData } from '../components/WithdrawDialog'; import styles from './index.module.less'; // 任务状态枚举 enum TASK_STATUS { PROGRESS = 'progress', COMPLETE = 'complete', } // 任务信息 const TASK_INFO_MAP = { [TASK_STATUS.PROGRESS]: { btn: '进行中', }, [TASK_STATUS.COMPLETE]: { btn: '已完成', } } interface ITask { setDialogData: React.Dispatch<React.SetStateAction<DialogData>>; setShowDialog: React.Dispatch<React.SetStateAction<boolean>>; } /** 任务组件 */ const Task = (props: ITask) => { const { setDialogData, setShowDialog } = props; // 任务状态 const [state, setState] = useState<TASK_STATUS>(TASK_STATUS.PROGRESS) useEffect(() => { setTimeout(() => { alert('完成任务'); setState(TASK_STATUS.COMPLETE); }, 3000); }, []) const btnCallback = useCallback(() => { if (state === TASK_STATUS.COMPLETE) { setDialogData({ a: "3000", b: "123456789123456789", c: "支付宝打款", d: "提现成功,预计2小时到账", e: "0.3", }) setShowDialog(true); } }, [state]) return ( <div className={styles.taskWrap}> {/* icon */} <div className={styles.taskImg} /> {/* 详情 */} <div className={styles.taskDesc}> <div className={styles.action}> 完成任务节即可提现 </div> <div className={styles.detailText}> <div className={styles.detailTextDetail}>完成后可提现 0.6 元</div> </div> </div> {/* 按钮 */} <div className={styles.taskBtn} onClick={() => btnCallback()} > {TASK_INFO_MAP[state]?.btn || ''} </div> </div> ); }; export default Task;
实现四:
总结:
逻辑成本:上述业务需求能稍微看出,其实业务主逻辑账户 useAccount 并没有改动,只是新增了一个 Task 任务玩法但就需要对账户主逻辑进行 1/3 的迁移改造。
深入思考下在业务迭代过程中,业务逻辑其实不跟视图挂钩。
我理解业务逻辑只是操作数据与状态,而视图只是业务逻辑的一种呈现以及交互。
但是因为前端架构设计的因素,只要视图稍微变化,就会在业务逻辑没有改动时不断地 "重构" 代码,开发成本也就随之产生了。
- Hooks:在通常业务迭代中,项目为了主逻辑的数据状态可以让所有组件可监听使用和操作, hooks 难免会暴露在最外层入口组件(Home)中。这时候隐喻会带来两个不好的地方:一个是全局重渲染,一个是逻辑与视图会越来越杂糅难以维护。
- 全局管理:复杂的业务会进行全局状态管理(redux/mobx/ustands/vuex等),便于统一地状态分发以及观状态管理,这时候通常也会造成全局状态数据滥用的问题。
🚀 🚀 🚀 以上只是日常任务中简单的需求迭代,而真实大促敏捷迭代中需求复杂 && 人员协同将造成更大成本。
思考
需求...:简单往后举几个真实的业务例子
新玩法:如任务面板玩法
- 点击任务按钮 => 弹出抽屉面板进而选择权益&门槛 => 面板组件与账户交互
新功能:如推挽订阅
- 提现成功后后续权益的引导 => 账户刷新信息后 => 引导用户每日提现订阅
- ......
解决业务敏捷迭代过程中,即便是视觉上无/小的改动,因业务逻辑的杂糅,我们都需不断地调整页面结构和业务逻辑&状态。
因此初步认为:复杂开发成本不在 新增/删除 独立的业务逻辑上,而在于 修改/调整 交集的业务逻辑上。
设计
4.1 设计思路
由于前端 JS 语言的灵活性,导致代码实现路径【条条大路通罗马】,但没有一个良好地架构通常导致维护成本、理解成本线型巨增。
在期间也有看到一些外界优秀的架构设计,结合业务实际开发作为参考设计~
- 【干净架构】设计
在【干净的架构】中的核心思路是两个:数据依赖规则 以及 合理领域分层。1、数据依赖规则从内向外的分层几个核心规则
- 分层标准:(内)抽象 ==> 具体(外)
- 数据的依赖关系:(外)消费数据(内) ==> (内)不能消费(外)
- 分层责任独立:(外)不能影响(内)
2、合理领域分层
- 【实体/模型(Entities / Models)】:业务的实体/对象,封装了最通用和抽象的规则。当某些外部因素发生变化时,它们最不可能改变。
例:业务定义好的数据接口结构以及数据的 CRUD。
- 【用例(Cases / Server)】:特定的业务逻辑规则,也就是业务逻辑在这一层
例:处理业务数据实体/模型的通用逻辑
- 【适配器(Adapters / Application)】:具体的逻辑与视图的控制器,通常是 MVC & MVVM 的 V & VM 角色,具体地处理 用例 层数据返回与视图 UI 的数据结构。
例:处理业务用例服务的数据,返回可直接被 UI 视图消费的具体数据 & 状态,文章举了 SQL 数据库的示例。
- 【框架和驱动程序(Frameworks and Drivers)】:具体的框架 / 工具等层面的内容。
例:React / Vue3 框架 & Mysql & Webpack 构建工具