实践总结|前端架构设计的一点考究
▐ 为什么会有这一篇文章?
在日常/大促业务的“敏捷”开发过程中逐渐产生的几个疑惑,尝试地做出思考并想得到一些解决思路和方案。
总的来说,在前端开发和实践过程中,梳理了一些简单设计方案可以缓解当时让我 “头疼” 的几个敏捷迭代问题,并实践在项目迭代中。
▐ 因此个人对这篇文章有三个小目的:
-
梳理清楚个人真正疑惑开发迭代的问题在哪,解决的核心是什么,温故而知新。
-
提供前端架构设计的思考&方案,来缓解日常/大促敏捷迭代问题,希望可以得到一些拍砖~
-
能让项目协同的同学能初步理解个人对于前端结构设计,方便他人理解这样搞的原因背景,快速磨平协同上的一些理解和开发成本 💰。
-
在业务需求的敏捷迭代中,单看逻辑和视觉的变更都不难,为何结合起来迭代如此花费成本? -
敏捷业务迭代中,我们能找出什么是敏捷在变,什么是敏捷不变? -
面向视图开发,还是面向数据开发? -
React 定位“是一个用于渲染用户界面 (UI) 的 JavaScript 库”,那么 UI 和逻辑怎么更好地设计结合?
示例
先假设一个业务需求:核心关于【账户信息】
import { useEffect, useState } from 'react';import styles from './index.module.less';const Account = () => {// 账户金额const [account, setAccount] = useState(0);useEffect(() => {// 模拟接口数据setTimeout(() => {setAccount(12.34);}, 1000)}, [])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} /></div></div>);};export default Account;
2.相关样式
{width: 750rpx;height: 104rpx;display: flex;: center;: center;position: absolute;left: 0;top: 0;{width: 308rpx;height: 70rpx;background: center / contain no-repeat://gw.alicdn.com/imgextra/i4/O1CN01harLZI1kECtyvhAPh_!!6000000004651-2-tps-308-70.png);display: flex;: center;: center;{width: 37rpx;height: 46rpx;background: center / contain no-repeat url("https://gw.alicdn.com/imgextra/i4/O1CN01RQ3Gzj1ZJjv6MlKoD_!!6000000003174-2-tps-74-92.png");}{: 10rpx;display: flex;: column;: flex-start;height: 100%;{: 6rpx;: "Alibaba Sans 102";: 42rpx;: bold;display: flex;: baseline;color: #bc2b15;height: 60rpx;{: 22rpx;: 2rpx;}}}{: 10rpx;background: center / contain no-repeat://img.alicdn.com/imgextra/i4/O1CN01teiAeS1tZZvwjzqx9_!!6000000005916-2-tps-129-63.png");width: 86rpx;height: 42rpx;}}}
总结:基操~ 业务仍在高速迭代中... 很快需求来了~ 🚄 ✈️ ✈️ ✈️ ✈️
▐ 需求二:互动效果
需求二:业务希望权益氛围感增强,在金额变化的同时,有金币飞入红包的氛围效果
import { CSSProperties, FC, useRef, useEffect, useCallback } from 'react';import anime from 'animejs';import styles from './index.module.less';interface ICoinsFly {style?: CSSProperties;onEnd: () => void;}/*** 金币飞动画组件*/const CoinsFly: FC<ICoinsFly> = (props) => {const { style, onEnd } = props;const wrapRef = useRef<HTMLDivElement>(null);const rpx2px = useCallback((rpxNum: number) => (rpxNum / 750) * window.screen.width, []);useEffect(() => {// 金币动画anime({targets: wrapRef.current?.childNodes,delay: anime.stagger(90),translateY: [{ value: 0 },{value: -rpx2px(334),easing: 'linear',},],translateX: [{ value: 0 },{value: -rpx2px(98),easing: 'cubicBezier(.05,.9,.8,1.5)',},],scale: [{ value: 1 },{value: 0.5,easing: 'linear',},],opacity: [{ value: 1 },{value: 0,easing: 'cubicBezier(1,0,1,0)',},],duration: 900,complete: () => {onEnd();},});}, []);return (<div className={styles.container} style={style} ref={wrapRef}>{[1, 2, 3, 4, 5, 6, 7, 8].map((item) => (<div key={item} className={styles.coin} />))}</div>);};export default CoinsFly;
{position: absolute;top: 100rpx;left: 100rpx;: rgba(255, 255, 255, 0.6);{width: 106rpx;height: 106rpx;: url("https://gw.alicdn.com/imgextra/i4/O1CN01hVWasj25i4dZdV9sS_!!6000000007559-2-tps-160-160.png");: center;: contain;: no-repeat;position: absolute;top: 0;left: 0;}}
2.账户组件引入金币飞入组件 && 状态控制
import { useEffect, useState } from 'react';import CoinsFly from '../../components/CoinsFly';import styles from './demo1.module.less';const Account = () => {// 账户金额const [account, setAccount] = useState(0);// 金币飞入动画const [showCoinsFly, setShowCoinsFly] = useState(false);useEffect(() => {// 模拟接口数据setTimeout(() => {setAccount(12.34);setShowCoinsFly(true);}, 1000)}, [])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} /></div>{showCoinsFly && (<CoinsFlystyle={{top: '322rpx',left: '316rpx',zIndex: 1,}}onEnd={() => {setShowCoinsFly(false);}}/>)}</div>);};export default Account;
总结:🎈🎈简简单单搞定~ 代码写得清晰明了~ 还沉淀了一个金币飞入的组件 当然很快需求又来了 ✈️
▐ 需求三:权益承接
import styles from './index.module.less';export interface DialogData {a: string;/** 标题 */b: string;/** 金额 */c: string;d: string;e: string;}// 定义Props类型interface IPopupProps {onClose?: () => void;/** 弹窗信息 */data: DialogData;}// 提现弹窗const WithdrawDialog = (props: IPopupProps) => {const { onClose, data } = props;const {a,b,c,d} = data || {};// 关闭弹窗const handleClose = () => {typeof onClose === 'function' && onClose();};return (<div className={styles.popup}><div className={styles.content}>{/* 头部提示 */}<div className={styles.header}><div className={styles.icon} /><div className={styles.title}>{a}</div></div><div className={styles.body}>{/* 金额 */}<div className={styles.amountCon}><div className={styles.amount}>{b || ''}</div><div className={styles.unit}>元</div></div><div className={styles.dividing} />{/* 账户内容 */}<div className={styles.userContent}><div className={styles.userItem}><div className={styles.title}>提现账户</div><div className={styles.userText}>{c || ''}</div></div><div className={styles.userItem}><div className={styles.title}>打款方式</div><div className={styles.userText}>{d || ''}</div></div></div>{/* 按钮 */}<divclassName={styles.btn}onClick={() => handleClose()}>开心收下</div></div></div></div >);};export default WithdrawDialog;
{position: fixed;top: 0;left: 0;width: 100%;height: 100%;: rgba(0, 0, 0, 0.7);display: flex;: column;: center;: center;{display: none;}{position: relative;background: center / contain no-repeat url("https://gw.alicdn.com/imgextra/i3/O1CN01vlcfgm1xFCpji3rv7_!!6000000006413-2-tps-596-786.png");: 54rpx;width: 590rpx;height: 780rpx;display: flex;: column;{display: flex;: column;: center;: 66rpx;{width: 90rpx;height: 90rpx;background: center / contain no-repeat://gw.alicdn.com/imgextra/i4/O1CN01KSkat11aHHShz5JqV_!!6000000003304-2-tps-90-90.png");}{: 700;: 30rpx;: PingFangSC-Medium;: 32rpx;color: #1677ff;}}{display: flex;: column;: center;: 40rpx;{display: flex;: baseline;color: #ff0746;{: AlibabaSans102-Bd;: 120rpx;}{position: relative;top: -4rpx;: 60rpx;}}{: 40rpx;width: 506rpx;height: 2rpx;: #ccc;}{: 22rpx;width: 506rpx;height: 100%;{: 20rpx;width: 100%;display: flex;: space-between;{: PingFangSC-Regular;: 26rpx;color: #666;}{: PingFangSC-Medium;: 26rpx;color: #111;}}}}{: 38rpx;display: flex;: column;: center;{: 700;: 14rpx;: PingFangSC-Semibold;: 48rpx;color: #111;}{: 32rpx;: 44rpx;color: #363636;{color: #ff0d40;}}{: 18rpx;width: 300rpx;height: 384rpx;background: center / contain no-repeat url("https://gw.alicdn.com/imgextra/i3/O1CN01uMPIk91nUBd1MjN9v_!!6000000005092-2-tps-300-384.png");}}}}{position: absolute;left: 50%;transform: translateX(-50%);bottom: 42rpx;width: 518rpx;height: 96rpx;: linear-gradient(100deg, #f54ea1 0%, #ff0040 100%);: 52rpx;: center;: 96rpx;color: #fff;: PingFangSC-Medium;: 38rpx;}
Account 组件除了账户信息还添加了弹窗的信息内容,为了 Account 组件干净以及后续更好迭代,进行业务 Hook 逻辑抽象。
2.抽成账户刷新&金币的状态逻辑 Hooks
import { useCallback, useEffect, useState } from 'react';import { DialogData } from '../../components/WithdrawDialog';const useAccount = () => {// 账户金额const [account, setAccount] = useState(0);// 金币飞入动画const [showCoinsFly, setShowCoinsFly] = useState(false);// 弹窗展示const [showDialog, setShowDialog] = useState(false);const [dialogData, setDialogData] = useState<DialogData>();/** 模拟接口 => 刷新账户信息 */const refreshAccount = useCallback((account) => {setTimeout(() => {setAccount(account);setShowCoinsFly(true);}, 500)}, [])useEffect(() => {// 模拟初始化数据 => 接口数据refreshAccount(12.34)}, [])return {account,refreshAccount,showCoinsFly,setShowCoinsFly,showDialog,setShowDialog,dialogData,setDialogData}}export default useAccount;
import CoinsFly from '../../components/CoinsFly';import WithdrawDialog from '../../components/WithdrawDialog';import useAccount from './useAccount';import styles from './index.module.less';const Account = () => {// 账户业务逻辑const {account,refreshAccount,showCoinsFly,setShowCoinsFly,showDialog,setShowDialog,dialogData,setDialogData} = useAccount()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><divclassName={styles.withdraw}onClick={() => {setDialogData({a: "3000",b: "123456789123456789",c: "xxxx打款",d: "提现成功,预计2小时到账",e: "0.3",})setShowDialog(true);}}/></div>{/* 金币飞入 */}{showCoinsFly && (<CoinsFlystyle={{top: '322rpx',left: '316rpx',zIndex: 1,}}onEnd={() => {setShowCoinsFly(false);}}/>)}{/* 提现弹窗 */}{showDialog &&<WithdrawDialogdata={dialogData}onClose={() => {refreshAccount(12.04);setShowDialog(false);}}/>}</div>);};export default Account;
总结:
-
遵循着解耦以及内聚最小化的原则,将控制账户抽象为 hooks,后续可以在其他视图组件使用。
-
这里其实稍微暴露了让我难受的一点,因为视图需要与状态和方法做逻辑交互,一来二去 hooks 要将近乎所有的状态方法都抛出...
实际上开发也可以将 Account 和 Dialog 单独做状态和逻辑的封装 hook
然而需求不会仅仅局限于 Account 账户组件中,那么需求来啦 ✈️ ✈️ ✈️ ✈️
▐ 需求四:任务体系
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>{/* 按钮 */}<divclassName={styles.taskBtn}onClick={() => {}}>{TASK_INFO_MAP[state]?.btn || ''}</div></div>);};export default Task;
{position: relative;top: 200rpx;: 30rpx;padding: 25rpx 30rpx 25rpx 24rpx;background: #fae3e4;display: flex;: center;position: relative;{width: 92rpx;height: 92rpx;: url("https://gw.alicdn.com/imgextra/i4/O1CN01VqCtbK1vhU0PZUTzR_!!6000000006204-2-tps-80-80.png");: 100% 100%;position: relative;}{flex: 1;width: 370rpx;padding: 4rpx 0 2rpx;: 20rpx;display: flex;: column;: space-between;{: 20rpx;: bold;display: flex;: center;: 28rpx;color: #942703;}{color: #a07466;: 24rpx;height: 32rpx;display: flex;: center;overflow: hidden;{display: flex;: row;: center;: center;}}}{width: 144rpx;height: 68rpx;display: flex;: center;: center;: 34rpx;: linear-gradient(145deg, #ff5d83, #ff2929);: 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;
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 (<>账户信息 */}<Accountaccount={account}refreshAccount={refreshAccount}showCoinsFly={showCoinsFly}setShowCoinsFly={setShowCoinsFly}showDialog={showDialog}setShowDialog={setShowDialog}dialogData={dialogData}setDialogData={setDialogData}/>任务 */}<TasksetShowDialog={setShowDialog}setDialogData={setDialogData}/></>)};export default Architecture;
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 (className={styles.stickyAccountWrap}>className={styles.stickyAccount}>className={styles.stickyAccountGoldPocketPic} />className={styles.stickyAccountTitleContainer}>className={styles.stickyAccountTitle}><div>{account}</div>className={styles.unit}>元</div></div></div><divclassName={styles.withdraw}onClick={() => {setDialogData({a: "3000",b: "123456789123456789",c: "支付宝打款",d: "提现成功,预计2小时到账",e: "0.3",})setShowDialog(true);}}/></div>金币飞入 */}&& (<CoinsFlystyle={{top: '322rpx',left: '316rpx',zIndex: 1,}}onEnd={() => {setShowCoinsFly(false);}}/>)}提现弹窗 */}{showDialog &&<WithdrawDialogdata={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>{/* 按钮 */}<divclassName={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等),便于统一地状态分发以及观状态管理,这时候通常也会造成全局状态数据滥用的问题。
▐ 思考
需求...:简单往后举几个真实的业务例子
-
点击任务按钮 => 弹出抽屉面板进而选择权益&门槛 => 面板组件与账户交互
-
提现成功后后续权益的引导 => 账户刷新信息后 => 引导用户每日提现订阅 -
......
▐ 设计思路
由于前端 JS 语言的灵活性,导致代码实现路径【条条大路通罗马】,但没有一个良好地架构通常导致维护成本、理解成本线型巨增。
-
【干净架构】设计
-
分层标准:(内)抽象 ==> 具体(外) -
数据的依赖关系:(外)消费数据(内) ==> (内)不能消费(外) -
分层责任独立:(外)不能影响(内)
-
【实体/模型(Entities / Models)】:业务的实体/对象,封装了最通用和抽象的规则。当某些外部因素发生变化时,它们最不可能改变。
-
【用例(Cases / Server)】:特定的业务逻辑规则,也就是业务逻辑在这一层
-
【适配器(Adapters / Application)】:具体的逻辑与视图的控制器,通常是 MVC & MVVM 的 V & VM 角色,具体地处理 用例 层数据返回与视图 UI 的数据结构。
-
【框架和驱动程序(Frameworks and Drivers)】:具体的框架 / 工具等层面的内容。
例:React / Vue3 框架 & Mysql & Webpack 构建工具
-
【显式架构】设计

这篇文档从【应用架构系统】的角度进行设计,我梳理重点拆分了两个核心概念【应用核心代码】、【组件】。
1、应用核心代码:主要设计项目的核心代码如何通过 DDD 层来组织逻辑架构
在软件工程中,DDD(Domain-Driven Design,领域驱动设计)层指的是软件系统中的一个重要组成部分。旨在将领域逻辑(Domain Logic)与应用程序的其他部分分离开来,使得系统的设计更加易于理解、维护和扩展。
-
【应用层】:项目架构的第一层直接对接用户,可以直接触发一系列核心的业务流程。例如应用程序服务、命令处理程序、展开用例 / 业务流程的逻辑等。
- 操作领域库 - 查找/更改项目领域实体/模型的具体逻辑 - 操作领域层 - 实体/模型执行对应的领域逻辑 - 执行逻辑 - 具体执行业务实际逻辑,并处理额外的副作用
-
【领域层】:独立于应用层,是各个【业务领域】实体/模型对象包含数据和操作数据的逻辑。
这个层级细分了两个主要职责:1、领域服务 2、领域模型
-
【领域服务】:操作不同数据实体/模型的具体逻辑,独立于应用层且抽象可复用。 -
【领域模型】:代表业务领域具体实体/模型的数据,通常更新时会触发额外的逻辑操作。
- 声明具体领域实体/模型的组合逻辑 - 操作实体/模型具体的数据,独立且抽象
例如:任务组件 ComA 依赖于账户组件 ComB 组件,那么可以通过【任务应用层】来组合【账户领域层】的形式,共享账户的领域实体进而触发不同组件在业务逻辑上的副作用,消除组件直接的依赖耦合。
🌟结果:一个项目有着多个低耦合高内聚的抽象组件,可独立触发核心应用逻辑,并消费所需的业务模型数据。
▐ 设计思考
上述一顿库库输出后,大概率还是一脸懵,直接用可不行,得要梳理下适合开发实践的设计思路来才行。
-
抽象设计
先具体分析上述开发示例,回到我们最初的几个疑惑,尝试解答:
Q:React 定位是一个用于渲染用户界面 (UI) 的 JavaScript 库,那么数据状态和业务逻辑与视图和交互无关怎么更好地设计结合?
A:React 是一个 UI 组件库,作为 UI 组件应该只消费业务状态/数据和 UI 视图交互的逻辑。本质上不应该在组件内部关心业务的生产逻辑。
Q:敏捷业务迭代中,不断变化的部分导致开发成本,能找出什么是敏捷在变,什么是敏捷在不变?
A:以视图和逻辑来抽象拆分
-
视图
-
视图组件 (变)
-
视图交互 变
-
逻辑
-
业务逻辑 变
-
接口协议一般只增 不变
-
基础服务逻辑 不变
Q:在敏捷迭代中,单看逻辑和视觉的变更都不难,为何结合起来迭代如此花费成本?
A:独立的逻辑/视觉变更开发成本不大,但因为业务逻辑通常耦合在视图组件中,通常在迭代过程需花大量改造和兼容成本,修改组件与业务状态/数据的结构,甚至需重构整体项目,
Q:面向视图开发,还是面向数据开发?
A:一般在业务迭代过程中都面向视图开发,基本开发流程大概如下:
1.绘制 UI 结构 + 样式 => 前端组件
2.组件中初始化业务主逻辑 => 声明接口 data 数据 state
3.根据视图动态数据 => 消费 data 数据
4.根据视图交互事件 => 声明并绑定交互逻辑(新的控制 state 状态)
面向视图开发,通常会导致视图组件与数据逻辑强耦合。
例如上述示例的需求四
-
具体设计
基于上述的构架思路以及抽象开发过程中的关注点,在于如何将视图组件和数据逻辑解耦,这是一个老生常谈的问题,重点在于如何通过合理的架构模式将其优雅地分层。
1.定义清楚两者的关系,在视图组件中只做数据的消费者,而数据逻辑只作为视图组件的生产者。
2.数据逻辑需关注点分离出独立的业务领域,并按照分层约定项目目录,做好职责划分。
目录约定
pages├─UI // 视图组件dataArea // 数据领域├─newHome // newHome 页面挂钩的业务领域| ├─models // 数据模型 => 业务数据/状态模型 & 约定| | ├─Account // 🌰 账户业务领域| | | └index.tsx| ├─server // 数据服务 => 业务底层数据服务| | └Account.ts // 🌰 账户业务领域| ├─applications // 应用服务 => 上层业务逻辑| | └Account.ts // 🌰 账户业务领域├─common| ├─zustand // 统一的全局数据管理库| ├─mtop // 数据请求库
▐ 需求一:金额账户
需求一:展示账户信息
import { create } from 'zustand';interface IAccountModel {account: number;}/*** 账户模型*/const accountModel = create<IAccountModel>(() => ({account: undefined,}));export default accountModel;
import accountModel from '../models/account';/** 账户服务 */const accountServer = {/*** @des 获取数据*/getData: () => {setTimeout(() => {// 模拟接口请求accountModel.setState({ account: 12.34 });}, 1000);},};export default accountServer;
c.application 业务应用
import accountServer from '../server/account';/*** @des 账户应用*/const accountApplication = {/*** @des 初始化*/init: () => {accountServer.getData();},};export default accountApplication
import accountApplication from '@/dataArea/home/applications/account';import accountModel from '@/dataArea/home/models/account';import styles from './index.module.less';const Account = () => {// 账户应用初始化accountApplication.init();// 消费响应式数据const account = accountModel((state) => state.account);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} /></div></div>);};export default Account;
▐ 需求二:互动效果
需求二:【权益氛围感】金币飞入动效
import { CSSProperties, FC, useRef, useEffect, useCallback } from 'react';import anime from 'animejs';import styles from './index.module.less';interface ICoinsFly {style?: CSSProperties;onEnd: () => void;}/*** 金币飞动画组件*/const CoinsFly: FC<ICoinsFly> = (props) => {const { style, onEnd } = props;const wrapRef = useRef<HTMLDivElement>(null);const rpx2px = useCallback((rpxNum: number) => (rpxNum / 750) * window.screen.width, []);useEffect(() => {// 金币动画anime({targets: wrapRef.current?.childNodes,delay: anime.stagger(90),translateY: [{ value: 0 },{value: -rpx2px(334),easing: 'linear',},],translateX: [{ value: 0 },{value: -rpx2px(98),easing: 'cubicBezier(.05,.9,.8,1.5)',},],scale: [{ value: 1 },{value: 0.5,easing: 'linear',},],opacity: [{ value: 1 },{value: 0,easing: 'cubicBezier(1,0,1,0)',},],duration: 900,complete: () => {onEnd();},});}, []);return (<div className={styles.container} style={style} ref={wrapRef}>{[1, 2, 3, 4, 5, 6, 7, 8].map((item) => (<div key={item} className={styles.coin} />))}</div>);};export default CoinsFly;
{position: absolute;top: 100rpx;left: 100rpx;: rgba(255, 255, 255, 0.6);{width: 106rpx;height: 106rpx;: url("https://gw.alicdn.com/imgextra/i4/O1CN01hVWasj25i4dZdV9sS_!!6000000007559-2-tps-160-160.png");: center;: contain;: no-repeat;position: absolute;top: 0;left: 0;}}
import { useEffect, useState } from 'react';import CoinsFly from '../CoinsFly';import accountApplication from '@/dataArea/home/applications/account';import accountModel from '@/dataArea/home/models/account';import styles from './index.module.less';const Account = () => {// 账户应用初始化accountApplication.init();// 消费响应式数据const account = accountModel((state) => state.account);// 金币飞入动画const [showCoinsFly, setShowCoinsFly] = useState(false);// 响应式更新金币飞入动效useEffect(() => {account && setShowCoinsFly(true);}, [account]);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} /></div>{showCoinsFly && (<CoinsFlystyle={{top: '322rpx',left: '316rpx',zIndex: 1,}}onEnd={() => {setShowCoinsFly(false);}}/>)}</div>);};export default Account;
实现二:
▐ 需求三:权益承接
import { create } from 'zustand';interface IPopModel {show: boolean; // 是否展示popData?: Record<string, string | any>; // POP 数据onCloseCallback?: () => void;}/** 初始化数据 */export const DEFAULT_MODEL = {show: false,popData: undefined,onCloseCallback: () => {},};/*** pop 模型*/const popModel = create<IPopModel>(() => ({...DEFAULT_MODEL,}));export default popModel;
import popModel, { DEFAULT_MODEL } from '../models/pop';/** 弹窗服务 */const popServer = {setPopData: (data) => {popModel.setState({ popData: data });},openPop: () => {popModel.setState({ show: true });},closePop: () => {popModel.setState({ show: false });},/*** @des 重置数据*/resetModel: () => {popModel.setState(DEFAULT_MODEL);},};export default popServer;
import popServer from '../server/pop';import popModel from '../models/pop';/*** @des 弹窗应用*/const popApplication = {open: (data) => {popServer.setPopData(data);popServer.openPop();},/*** @des 关闭弹窗 => 触发自定义关闭回调 & 重置弹窗数据*/close: () => {popServer.closePop();popModel.getState().onCloseCallback?.();popServer.resetModel();},setCustomCloseCallback: (callback) => {popModel.setState({ onCloseCallback: callback });},};export default popApplication;
import popApplication from '@/dataArea/home/applications/pop';import popModel from '@/dataArea/home/models/pop';import styles from './index.module.less';// 弹窗const Dialog = () => {const { close } = popApplication;const show = popModel((state) => state.show);const popData = popModel((state) => state.popData);const {a,b,c,d,} = popData || {};if (!show) return null;return (<div className={styles.popup}><div className={styles.content}>{/* 头部提示 */}<div className={styles.header}><div className={styles.icon} /><div className={styles.title}>{a}</div></div><div className={styles.body}>{/* 金额 */}<div className={styles.amountCon}><div className={styles.amount}>{b || ''}</div><div className={styles.unit}>元</div></div><div className={styles.dividing} />{/* 账户内容 */}<div className={styles.userContent}><div className={styles.userItem}><div className={styles.title}>提现账户</div><div className={styles.userText}>{c || ''}</div></div><div className={styles.userItem}><div className={styles.title}>打款方式</div><div className={styles.userText}>{d || ''}</div></div></div>{/* 按钮 */}<divclassName={styles.btn}onClick={() => close()}>开心收下</div></div></div></div >);};export default Dialog;
import { useEffect, useState } from 'react';import CoinsFly from '../CoinsFly';import accountApplication from '@/dataArea/home/applications/account';import accountModel from '@/dataArea/home/models/account';import styles from './index.module.less';import popApplication from '@/dataArea/home/applications/pop';const Account = () => {// 账户应用初始化accountApplication.init();const { open: openWithdrawDialog } = popApplication;// 消费响应式数据const account = accountModel((state) => state.account);// 金币飞入动画const [showCoinsFly, setShowCoinsFly] = useState(false);// 响应式更新金币飞入动效useEffect(() => {account && setShowCoinsFly(true);}, [account]);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><divclassName={styles.withdraw}onClick={() => {openWithdrawDialog({a: '3000',b: '123456789123456789',c: '支付宝打款',d: '提现成功,预计2小时到账',e: '0.3',});}}/></div>{showCoinsFly && (<CoinsFlystyle={{top: '322rpx',left: '316rpx',zIndex: 1,}}onEnd={() => {setShowCoinsFly(false);}}/>)}</div>);};export default Account;

总结:至此,我们已经有了【account】和【pop】的业务领域数据逻辑,并且做到了【账户】和【弹窗】视图组件的完全解耦,而不需要内置 hook 状态来耦合视图组件。
▐ 需求四:任务体系
import accountModel from '../models/account';import accountServer from '../server/account';/*** @des 账户应用*/const accountApplication = {/*** @des 初始化*/init: () => {accountServer.getData();},reFreshData: (account) => {accountModel.setState({ account });},};export default accountApplication;
2.沉浸式写一个任务组件 Task
a.通过 pop 业务应用 open 更新数据并打开弹窗
b.关闭时调用 reFreshData 刷新账户信息
import { useCallback, useEffect, useState } from 'react';import popApplication from '@/dataArea/home/applications/pop';import accountApplication from '@/dataArea/home/applications/account';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 { open: openDialog, setCustomCloseCallback } = popApplication;const { reFreshData } = accountApplication;// 任务状态const [state, setState] = useState<TASK_STATUS>(TASK_STATUS.PROGRESS);const btnCallback = useCallback(() => {if (state === TASK_STATUS.COMPLETE) {openDialog({a: '3000',b: '123456789123456789',c: '支付宝打款',d: '提现成功,预计2小时到账',e: '0.3',});setCustomCloseCallback(() => {reFreshData(12.04);});}}, [openDialog, state]);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>{/* 按钮 */}<divclassName={styles.taskBtn}onClick={() => btnCallback()}>{TASK_INFO_MAP[state]?.btn || ''}</div></div>);};export default Task;
▐ 思考
整体的架构一定有设计者核心的逻辑和目的,而上层的设计只是过程中的关键路径/方法论。
现阶段我粗浅地认为核心的逻辑和目的在于:【找出业务迭代的关键因子,以最短路径 & 最小影响达到目的】
综上实践过程有几点关键路径/方法论可以梳理总结下:
1.以业务需求定义前端页面为导向,抽象定义其业务领域模型。
领域驱动设计(Domain-Driven Design):将软件系统的核心逻辑和业务规则放在领域模型中,通过领域模型来驱动项目设计和开发,领域驱动设计强调对业务领域的深入理解和模型化。
2.关注点分离,以业务领域模型来驱动通用服务 / 应用逻辑 / UI 组件等分层处理。
分层架构(Layered Architecture):将系统划分为多个层次,每个层次具有不同的关注点和责任,分层架构提供了松耦合、可测试和可维护的系统结构。
3.自内向外的通信处理,数据流和业务逻辑清晰可见。
完整架构分层:
pages // 视图组件├─components| ├─Account // 账户视图组件| ├─CoinsFly // 互动视图组件| ├─Dialog // 弹窗视图组件| ├─Task // 任务视图组件├─homedataArea // 数据领域├─newHome| ├─models| | └account.ts // 🌰 账户业务模型| | └common.ts // 🌰 基础能力模型(如关注、订阅、全局状态数据)| ├─server| | └account.ts // 🌰 账户业务服务| | └common.ts // 🌰 基础能力服务| ├─applications| | └account.ts // 🌰 账户业务应用| | └common.ts // 🌰 基础能力应用
如:预告页项目 - (项目注释很重要~ 👀 ) liveReplayModel:播回放模型 previewInfoModel:主播预告信息模型 1.主播 Header 信息、2.预告条信息、3.预告商品信息 commonModel:通用业务能力模型 1.全局配置、2.关注 / 分享 / 订阅
2.看主逻辑在哪调用,业务应用的初始化都做了什么~
一般来说会在项目入口处调用初始化的主逻辑:initApplication.init(data) 顺着主应用的初始化方法,顺着往下看就能知道具体的主逻辑了~
/*** @des 初始化应用*/const initApplication = {/*** @des 预告页初始化* - 初始化机型状态数值* - 兜底主接口 mtop(可选)* - 页面整体兜底状态(可选)* - 初始化业务模型数据* - 初始化回放 Feeds 无尽流服务(可选)* - ALive 服务* @param initData*/init: async (initData) => {const { isInitFlag } = commonModel.getState();if (isInitFlag) return;commonModel.setState({ statusBarHeight: initData?.statusBarHeight || getNavbarHeight().statusBarHeight });let data;if (initData) {data = initData;} else {// 兜底请求try {data = await initApplication.getData();} catch (error) {console.error('🔥 初始化主接口失败', error);data = null;}}const { broadCaster, preLives, modules, onlineLiveId } = data || {};const { liveReplay } = modules || {};/** 初始化业务模型数据 */commonApplication.setCommonData({ liveDetail: data, isInitFlag: true });previewInfoApplication.setData({onlineLiveId,anchorInfo: broadCaster,preLives,});liveReplayApplication.setData({hasMore: liveReplay?.hasMore,data: liveReplay?.data,});/** 更多回放,则初始化无尽流服务 */if (isTrue(liveReplay?.hasMore)) {liveReplayApplication.initFeedsServer();}/** 初始化 ALive 配置 */const aLiveConfigRes = await commonApplication.initAlive();commonApplication.setCommonData({ targetLandingDetail: aLiveConfigRes });},/** 页面销毁 */remove: () => {commonServer.resetData();liveReplayServer.resetData();previewInfoServer.resetData();},};export default initApplication;
可以看一下这一篇文章hhh,这就是写这个的一点 🤏 意义了。
▐ 架构理念
-
Yet another timer use React -
What I wish I knew about React | bitsofcode:https://bitsofco.de/what-i-wish-i-knew-about-react/ -
Clean Coder Blog:https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html -
DDD, Hexagonal, Onion, Clean, CQRS, … How I put it all together:https://herbertograca.com/2017/11/16/explicit-architecture-01-ddd-hexagonal-onion-clean-cqrs-how-i-put-it-all-together/ -
《我们是否对现代前端开发框架过于崇拜了》
▐ 架构实践
-
https://feature-sliced.design/ -
https://github.com/bespoyasov/refactor-like-a-superhero/tree/main
本文分享自微信公众号 - 大淘宝技术(AlibabaMTT)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。
关注公众号
低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
-
上一篇
LeetcodeJavaDebugEnhancer -- 一个用于Java的Leetcode算法题的本地调试增强器
LeetcodeJavaDebugEnhancer 🚀 一个用于Java的Leetcode算法题的本地调试增强器 🚀 [Github] 🎯 目标 提供方便快速的调试功能。 支持多样的输入源和输出源。 自动适配各种输入参数类型。 提供易维护、易拓展的API接口用于适配更多Leetcode算法调试场景。 🔧 下载与安装 下载 Maven <dependency> <groupId>io.github.jidcoo</groupId> <artifactId>leetcode-java-debug-enhancer</artifactId> <version>1.0.0</version> </dependency> Gradle implementation 'io.github.jidcoo:leetcode-java-debug-enhancer:1.0.0' Jar 资源 索引 托管仓库 点击这里浏览本项目的托管仓库 标准-Jar 点击这里直接下载(标准-Ja...
-
下一篇
Sermant热插拔能力在故障注入场景的实践
本文分享自华为云社区《Sermant热插拔能力在故障注入场景的实践》,作者:张豪鹏 华为云高级软件工程师 一、 前言 Sermant是基于Java字节码增强技术的无代理服务网格,采用Java字节码增强技术为宿主应用程序提供服务治理功能。从1.2.0版本开始,Sermant已经实现了在服务不停机状态下进行安装和卸载的热插拔功能,在上一篇文章《服务运行时动态挂载JavaAgent和插件——Sermant热插拔能力解析》中已经介绍了Sermant热插拔功能的实现原理。本篇文章将通过故障注入场景,来展示Sermant热插拔能力的应用价值。 二、 故障注入 1) 什么是故障注入? 故障注入是一种测试方法,它通过在系统中故意引入错误或故障,来测试系统对这些错误或故障的响应和恢复能力,并验证系统是否能够正常处理这些异常情况。下图是故障注入测试中的一些常见故障: 通过故障注入技术,测试人员可以在Java应用中模拟各种故障场景,以便评估应用的响应能力和恢复能力,还可以帮助提前拦截和发现Java应用潜在的可靠性问题,提升应用稳定性,避免现网出现重大质量事故。例如: 通过在指定方法中抛出自定义异常可以测试系...
相关文章
文章评论
共有0条评论来说两句吧...



微信收款码
支付宝收款码