实践总结|前端架构设计的一点考究(中)

简介: 本文总结了作者在日常/大促业务的“敏捷”开发过程中产生的疑惑,并尝试做出思考得到一些解决思路和方案。在前端开发和实践过程中,梳理了一些简单设计方案可以缓解当时 “头疼” 的几个敏捷迭代问题,并实践在项目迭代中。

实践总结|前端架构设计的一点考究(上)


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.DialogTask 组件提到组件 Architecture最外层



import Account from "./Account";
import Task from "./Task";

/** 主入口 */
const Architecture = () => {
  
  return (
    <>
      {/* 账户信息 */}
      <Account />
      
      {/* 任务 */}
      <Task />
    </>
  )
};

export default Architecture;

🔥 前面为了业务逻辑整洁,封装账户信息 hooks。但这时候在 Task 组件中,在完成任务后也要处理弹窗数据以及展示提现弹窗,也就是 setDialogDatasetShowDialog
3.useAccount 账户信息逻辑抽象到 Architecture外,并改造 AccountTask 逻辑。


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.AccountTask 组件改造数据状态分发逻辑



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 的迁移改造。


深入思考下在业务迭代过程中,业务逻辑其实不跟视图挂钩。


我理解业务逻辑只是操作数据与状态,而视图只是业务逻辑的一种呈现以及交互

但是因为前端架构设计的因素,只要视图稍微变化,就会在业务逻辑没有改动时不断地 "重构" 代码,开发成本也就随之产生了。

  1. Hooks:在通常业务迭代中,项目为了主逻辑的数据状态可以让所有组件可监听使用操作, hooks 难免会暴露在最外层入口组件(Home)中。这时候隐喻会带来两个不好的地方:一个是全局重渲染,一个是逻辑与视图会越来越杂糅难以维护
  2. 全局管理:复杂的业务会进行全局状态管理(redux/mobx/ustands/vuex等),便于统一地状态分发以及观状态管理,这时候通常也会造成全局状态数据滥用的问题。

🚀 🚀 🚀 以上只是日常任务中简单的需求迭代,而真实大促敏捷迭代中需求复杂 && 人员协同将造成更大成本。


思考


需求...:简单往后举几个真实的业务例子


新玩法:如任务面板玩法

  • 点击任务按钮 => 弹出抽屉面板进而选择权益&门槛 => 面板组件与账户交互

新功能:如推挽订阅

  • 提现成功后后续权益的引导 => 账户刷新信息后 => 引导用户每日提现订阅
  • ......


解决业务敏捷迭代过程中,即便是视觉上无/小的改动,因业务逻辑的杂糅,我们都需不断地调整页面结构和业务逻辑&状态。

因此初步认为:复杂开发成本不在 新增/删除 独立的业务逻辑上,而在于 修改/调整 交集的业务逻辑上。

image.png

设计

4.1 设计思路


由于前端 JS 语言的灵活性,导致代码实现路径【条条大路通罗马】,但没有一个良好地架构通常导致维护成本、理解成本线型巨增


在期间也有看到一些外界优秀的架构设计,结合业务实际开发作为参考设计~

  • 【干净架构】设计


【干净的架构】中的核心思路是两个:数据依赖规则 以及 合理领域分层。1、数据依赖规则从内向外的分层几个核心规则

  • 分层标准:(内)抽象 ==> 具体(外)
  • 数据的依赖关系:(外)消费数据(内) ==> (内)不能消费(外)
  • 分层责任独立:(外)不能影响(内)

2、合理领域分层

  • 【实体/模型(Entities / Models)】:业务的实体/对象,封装了最通用和抽象的规则。当某些外部因素发生变化时,它们最不可能改变。

例:业务定义好的数据接口结构以及数据的 CRUD。

  • 【用例(Cases / Server)】:特定的业务逻辑规则,也就是业务逻辑在这一层

例:处理业务数据实体/模型的通用逻辑

  • 【适配器(Adapters / Application)】:具体的逻辑与视图的控制器,通常是 MVC & MVVMV & VM 角色,具体地处理 用例 层数据返回与视图 UI 的数据结构。

例:处理业务用例服务的数据,返回可直接被 UI 视图消费的具体数据 & 状态,文章举了 SQL 数据库的示例。

  • 【框架和驱动程序(Frameworks and Drivers)】:具体的框架 / 工具等层面的内容。

例:React / Vue3 框架 & Mysql & Webpack 构建工具



实践总结|前端架构设计的一点考究(下)

作者介绍
目录