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

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

⛰️ 一、背景

为什么会有这一篇文章?

在日常/大促业务的“敏捷”开发过程中逐渐产生的几个疑惑,尝试地做出思考并想得到一些解决思路和方案。

总的来说,在前端开发和实践过程中,梳理了一些简单设计方案可以缓解当时让我 “头疼” 的几个敏捷迭代问题,并实践在项目迭代中。

因此个人对这篇文章有三个小目的:

  1. 梳理清楚个人真正疑惑开发迭代的问题在哪,解决的核心是什么,温故而知新
  2. 提供前端架构设计的思考&方案,来缓解日常/大促敏捷迭代问题,希望可以得到一些拍砖~
  3. 能让项目协同的同学能初步理解个人对于前端结构设计,方便他人理解这样搞的原因背景,快速磨平协同上的一些理解和开发成本 💰

👀 二、疑惑

先抛出主要的几点问题,在业务迭代过程中你为什么会疑惑(WHY),疑惑什么问题(WHAT),会以什么方式解决(HOW)?

  1. 在业务需求的敏捷迭代中,单看逻辑和视觉的变更都不难,为何结合起来迭代如此花费成本?
  2. 敏捷业务迭代中,我们能找出什么是敏捷在变,什么是敏捷不变?
  3. 面向视图开发,还是面向数据开发?
  4. React 定位“是一个用于渲染用户界面 (UI) 的 JavaScript 库”,那么 UI 和逻辑怎么更好地设计结合?

🌰 三、示例

为了更加有体感,以一个常规的业务例子,简单描述下个人开发的规范和经验的粗浅历程。

先假设一个业务需求:核心关于【账户信息】

3.1 需求一:金额账户

需求一:一开始需求很简单 —— 展示账户信息

解决一:咔咔写一个组件和 CSS 样式,在组件中定义数值并获取接口数据更新。

  1. Account 组件
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;
  1. 相关样式
.stickyAccountWrap {
  width: 750rpx;
  height: 104rpx;
  display: flex;
  align-items: center;
  justify-content: center;
  position: absolute;
  left: 0;
  top: 0;
  .stickyAccount {
    width: 308rpx;
    height: 70rpx;
    background: center / contain no-repeat
      url(https://gw.alicdn.com/imgextra/i4/O1CN01harLZI1kECtyvhAPh_!!6000000004651-2-tps-308-70.png);
    display: flex;
    align-items: center;
    justify-content: center;
    .stickyAccountGoldPocketPic {
      width: 37rpx;
      height: 46rpx;
      background: center / contain no-repeat url("https://gw.alicdn.com/imgextra/i4/O1CN01RQ3Gzj1ZJjv6MlKoD_!!6000000003174-2-tps-74-92.png");
    }
    .stickyAccountTitleContainer {
      margin-left: 10rpx;
      display: flex;
      flex-direction: column;
      justify-content: flex-start;
      height: 100%;
      .stickyAccountTitle {
        margin-top: 6rpx;
        font-family: "Alibaba Sans 102";
        font-size: 42rpx;
        font-weight: bold;
        display: flex;
        align-items: baseline;
        color: #bc2b15;
        height: 60rpx;
        .unit {
          font-size: 22rpx;
          margin-left: 2rpx;
        }
      }
    }
    .withdraw {
      margin-left: 10rpx;
      background: center / contain no-repeat
        url("https://img.alicdn.com/imgextra/i4/O1CN01teiAeS1tZZvwjzqx9_!!6000000005916-2-tps-129-63.png");
      width: 86rpx;
      height: 42rpx;
    }
  }
}

实现一:

总结:基操~ 业务仍在高速迭代中...  很快需求来了~ 🚄 ✈️ ✈️ ✈️ ✈️


3.2 需求二:互动效果

需求二:业务希望权益氛围感增强,在金额变化的同时,有金币飞入红包的氛围效果

解决二:简单 😁 ~ 在组件内写一个金币组件 + 飞入状态控制即可

  1. 金币飞入通用组件
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;
.container {
  position: absolute;
  top: 100rpx;
  left: 100rpx;
  background-color: rgba(255, 255, 255, 0.6);
  .coin {
    width: 106rpx;
    height: 106rpx;
    background-image: url("https://gw.alicdn.com/imgextra/i4/O1CN01hVWasj25i4dZdV9sS_!!6000000007559-2-tps-160-160.png");
    background-position: center;
    background-size: contain;
    background-repeat: no-repeat;
    position: absolute;
    top: 0;
    left: 0;
  }
}

  1. 账户组件引入金币飞入组件 && 状态控制
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, setShowConsFly] = useState(false);
    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 && (
        <CoinsFly
          style={{
            top: '322rpx',
            left: '316rpx',
            zIndex: 1,
          }}
          onEnd={() => {
            setShowCoinsFly(false);
          }}
        />
      )}
    </div>
  );
};
export default Account;

实现二:

总结:🎈🎈简简单单搞定~   代码写得清晰明了~  还沉淀了一个金币飞入的组件  当然很快需求又来了 ✈️


3.3 需求三:权益承接

需求三:业务方提出希望有一个弹窗承接,也就是点击提现按钮 => 提现成功弹窗收下 => 金币飞入再金额刷新

解决三:

  1. 写一个提现成功弹窗
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>
          {/* 按钮 */}
          <div
            className={styles.btn}
            onClick={() => handleClose()}
          >开心收下</div>
        </div>
      </div>
    </div >
  );
};
export default WithdrawDialog;
.popup {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background-color: rgba(0, 0, 0, 0.7);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  .hidden {
    display: none;
  }
  .content {
    position: relative;
    background: center / contain no-repeat url("https://gw.alicdn.com/imgextra/i3/O1CN01vlcfgm1xFCpji3rv7_!!6000000006413-2-tps-596-786.png");
    border-radius: 54rpx;
    width: 590rpx;
    height: 780rpx;
    display: flex;
    flex-direction: column;
    .header {
      display: flex;
      flex-direction: column;
      align-items: center;
      margin-top: 66rpx;
      .icon {
        width: 90rpx;
        height: 90rpx;
        background: center / contain no-repeat
          url("https://gw.alicdn.com/imgextra/i4/O1CN01KSkat11aHHShz5JqV_!!6000000003304-2-tps-90-90.png");
      }
      .title {
        font-weight: 700;
        margin-top: 30rpx;
        font-family: PingFangSC-Medium;
        font-size: 32rpx;
        color: #1677ff;
      }
    }
    .body {
      display: flex;
      flex-direction: column;
      align-items: center;
      margin-top: 40rpx;
      .amountCon {
        display: flex;
        align-items: baseline;
        color: #ff0746;
        
        .amount {
          font-family: AlibabaSans102-Bd;
          font-size: 120rpx;
        }
        .unit {
          position: relative;
          top: -4rpx;
          font-size: 60rpx;
        }
      }
      .dividing {
        margin-top: 40rpx;
        width: 506rpx;
        height: 2rpx;
        background-color: #ccc;
      }
      .userContent {
        margin-top: 22rpx;
        width: 506rpx;
        height: 100%;
        .userItem {
          margin-top: 20rpx;
          width: 100%;
          display: flex;
          justify-content: space-between;
          .title {
            font-family: PingFangSC-Regular;
            font-size: 26rpx;
            color: #666;
          }
          .userText {
            font-family: PingFangSC-Medium;
            font-size: 26rpx;
            color: #111;
          }
        }
      }
    }
    .errCon {
      margin-top: 38rpx;
      display: flex;
      flex-direction: column;
      align-items: center;
      .title {
        font-weight: 700;
        margin-bottom: 14rpx;
        font-family: PingFangSC-Semibold;
        font-size: 48rpx;
        color: #111;
      }
      .des {
        font-size: 32rpx;
        line-height: 44rpx;
        color: #363636;
        .redText {
          color: #ff0d40;
        }
      }
      .img {
        margin-top: 18rpx;
        width: 300rpx;
        height: 384rpx;
        background: center / contain no-repeat url("https://gw.alicdn.com/imgextra/i3/O1CN01uMPIk91nUBd1MjN9v_!!6000000005092-2-tps-300-384.png");
      }
    }
  }
}
.btn {
  position: absolute;
  left: 50%;
  transform: translateX(-50%);
  bottom: 42rpx;
  width: 518rpx;
  height: 96rpx;
  background-image: linear-gradient(100deg, #f54ea1 0%, #ff0040 100%);
  border-radius: 52rpx;
  text-align: center;
  line-height: 96rpx;
  color: #fff;
  font-family: PingFangSC-Medium;
  font-size: 38rpx;
}

Account 组件除了账户信息还添加了弹窗的信息内容,为了 Account 组件干净以及后续更好迭代,进行业务 Hook 逻辑抽象

  1. 抽成账户刷新&金币的状态逻辑 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;

  1. 抽象重构 Account 账户组件,引入提现成功弹窗
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>
        <div
          className={styles.withdraw}
          onClick={() => {
            setDialogData({
              a: "3000",
              b: "123456789123456789",
              c: "xxxx打款",
              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;

实现三:

总结:

  • 遵循着解耦以及内聚最小化的原则,将控制账户抽象为 hooks,后续可以在其他视图组件使用。
  • 这里其实稍微暴露了让我难受的一点,因为视图需要与状态和方法做逻辑交互,一来二去 hooks 要将近乎所有的状态方法都抛出...

实际上开发也可以将 Account 和 Dialog 单独做状态和逻辑的封装 hooks

然而需求不会仅仅局限于 Account 账户组件中,那么需求来啦 ✈️ ✈️ ✈️ ✈️


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 层面的处理。

  1. DialogTask 组件提到组件 Architecture最外层
import Account from "./Account";
import Task from "./Task";
/** 主入口 */
const Architecture = () => {
  
  return (
    <>
      {/* 账户信息 */}
      <Account />
      
      {/* 任务 */}
      <Task />
    </>
  )
};
export default Architecture;

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

  1. 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;

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

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

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

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

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

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


3.5 思考

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

🌰 新玩法:如任务面板玩法

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

🌰 新功能:如推挽订阅

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

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

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


🌟 四、设计

4.1 设计思路

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

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

4.1.1【干净架构】设计

在【干净的架构】中的核心思路是两个:数据依赖规则 以及 合理领域分层

1、数据依赖规则

从内向外的分层几个核心规则

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

2、合理领域分层

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

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

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

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

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

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

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

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


4.1.2【显式架构】设计

作者将【DDD、六边形、洋葱、清洁、CQRS】进行深入学习并梳理总结的一个前端架构设计,并且经历一定应用实践的考验。

这篇文档从【应用架构系统】的角度进行设计,我梳理重点拆分了两个核心概念【应用核心代码】、【组件】。

1、应用核心代码:主要设计项目的核心代码如何通过 DDD 层来组织逻辑架构

在软件工程中,DDD(Domain-Driven Design,领域驱动设计)层指的是软件系统中的一个重要组成部分。旨在将领域逻辑(Domain Logic)与应用程序的其他部分分离开来,使得系统的设计更加易于理解、维护和扩展。

  • 【应用层】:项目架构的第一层直接对接用户,可以直接触发一系列核心的业务流程。例如应用程序服务、命令处理程序、展开用例 / 业务流程的逻辑等。

🌰 应用场景

- 操作领域库 - 查找/更改项目领域实体/模型的具体逻辑

- 操作领域层 - 实体/模型执行对应的领域逻辑

- 执行逻辑 - 具体执行业务实际逻辑,并处理额外的副作用

  • 【领域层】:独立于应用层,是各个【业务领域】实体/模型对象包含数据和操作数据的逻辑。

这个层级细分了两个主要职责:1、领域服务 2、领域模型

  • 【领域服务】:操作不同数据实体/模型的具体逻辑,独立于应用层且抽象可复用。
  • 【领域模型】:代表业务领域具体实体/模型的数据,通常更新时会触发额外的逻辑操作。

🌰 应用场景

- 声明具体领域实体/模型的组合逻辑

- 操作实体/模型具体的数据,独立且抽象


上述【应用核心组织】以 DDD 的设计思路将代码进行的的隔离划分,而通常一个项目会有很多不同的领域。我们不可能将所有领域的代码按来进行设计隔离,也就是我们如何通过【组件】来抽象封装。


2、组件:以项目角度抽象封装上述不同层级的代码组织,最终形成一个项目应用。

组件致力于低耦合和高内聚的设计模式,一个完全解耦的组件,可以完全不引用 / 依赖另一个同层级的组件,要做到这一点意味着要有独立的【应用核心代码】

🌰 例如:任务组件 ComA 依赖于账户组件 ComB 组件,那么可以通过【任务应用层】来组合【账户领域层】的形式,共享账户的领域实体进而触发不同组件在业务逻辑上的副作用,消除组件直接的依赖耦合

🌟结果: 一个项目有着多个低耦合高内聚的抽象组件,可独立触发核心应用逻辑,并消费所需的业务模型数据


4.2 设计思考

上述一顿库库输出后,大概率还是一脸懵,直接用可不行,得要梳理下适合开发实践的设计思路来才行。

4.2.1 抽象设计

先具体分析上述开发示例,回到我们最初的几个疑惑,尝试解答:


Q:React 定位是一个用于渲染用户界面 (UI) 的 JavaScript 库,那么数据状态和业务逻辑与视图和交互无关怎么更好地设计结合?

A:React 是一个 UI 组件库,作为 UI 组件应该只消费业务状态/数据UI 视图交互的逻辑。本质上不应该在组件内部关心业务的生产逻辑。



Q:敏捷业务迭代中,不断变化的部分导致开发成本,能找出什么是敏捷在,什么是敏捷在不变

A:以视图和逻辑来抽象拆分

  • 视图
  • 视图组件 (变)
  • 视图交互
  • 逻辑
  • 业务逻辑
  • 接口协议一般只增 不变
  • 基础服务逻辑 不变

Q:在敏捷迭代中,单看逻辑和视觉的变更都不难,为何结合起来迭代如此花费成本?

A:独立的逻辑/视觉变更开发成本不大,但因为业务逻辑通常耦合在视图组件中,通常在迭代过程需花大量改造和兼容成本,修改组件与业务状态/数据的结构,甚至需重构整体项目,



Q:面向视图开发,还是面向数据开发?

A:一般在业务迭代过程中都面向视图开发,基本开发流程大概如下 🌰

  1. 绘制 UI 结构 + 样式 => 前端组件
  2. 组件中初始化业务主逻辑 => 声明接口 data 数据 state
  3. 根据视图动态数据 => 消费 data 数据
  4. 根据视图交互事件 => 声明并绑定交互逻辑(新的控制 state 状态)

面向视图开发,通常会导致视图组件与数据逻辑强耦合

🌰 例如上述示例的需求四


4.2.2 具体设计

基于上述的构架思路以及抽象开发过程中的关注点,在于如何将视图组件和数据逻辑解耦,这是一个老生常谈的问题,重点在于如何通过合理的架构模式将其优雅地分层。

  1. 定义清楚两者的关系,在视图组件中只做数据的消费者,而数据逻辑只作为视图组件的生产者

  1. 数据逻辑需关注点分离出独立的业务领域,并按照分层约定项目目录,做好职责划分。
pages
├─UI // 视图组件
dataArea  // 数据领域
├─newHome // newHome 页面挂钩的业务领域
|    ├─models // 数据模型 => 业务数据/状态模型 & 约定
|    |   ├─Account  // 🌰 账户业务领域
|    |   |  └index.tsx
|    ├─server // 数据服务 => 业务底层数据服务
|    |   └Account.ts  // 🌰 账户业务领域
|    ├─applications // 应用服务 => 上层业务逻辑
|    |      └Account.ts // 🌰 账户业务领域
├─common
|   ├─zustand // 统一的全局数据管理库
|   ├─mtop  // 数据请求库

  1. 视图组件尽可能无状态抽象可复用,视图组件内聚状态,通信交互则通过数据逻辑触发应用服务 applications,消费数据模型 models 即可。

架构设计如下图:

🔧 五、实践

有了设计思路以及思考具体设计后,针对上边 三、示例做一次实践感受下实践体感

一样是核心关于【账户信息】的业务需求

5.1 需求一:金额账户

🌈 需求一:展示账户信息

解决一:

  1. 数据逻辑:约定创建数据逻辑目录,声明业务领域【account】,并将业务层关注点分离职责
  2. model 业务模型
import { create } from 'zustand';
interface IAccountModel {
  account: number;
}
/**
 * 账户模型
 */
const accountModel = create<IAccountModel>(() => ({
  account: undefined,
}));
export default accountModel;
  1. server 业务服务
import accountModel from '../models/account';
/** 账户服务 */
const accountServer = {
  /**
   * @des 获取数据
   */
  getData: () => {
    setTimeout(() => {
      // 模拟接口请求
      accountModel.setState({ account: 12.34 });
    }, 1000);
  },
};
export default accountServer;
  1. application 业务应用
import accountServer from '../server/account';
/**
 * @des 账户应用
 */
const accountApplication = {
  /**
   * @des 初始化
   */
  init: () => {
    accountServer.getData();
  },
};
export default accountApplication;

  1. 视图组件:Account
  2. 触发业务应用逻辑 - 初始化
  3. 消费业务模型数据
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;

完成一:


5.2 需求二:互动效果

🌈 需求二:【权益氛围感】金币飞入动效

解决二:

  1. 金币飞入通用组件
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;
.container {
  position: absolute;
  top: 100rpx;
  left: 100rpx;
  background-color: rgba(255, 255, 255, 0.6);
  .coin {
    width: 106rpx;
    height: 106rpx;
    background-image: url("https://gw.alicdn.com/imgextra/i4/O1CN01hVWasj25i4dZdV9sS_!!6000000007559-2-tps-160-160.png");
    background-position: center;
    background-size: contain;
    background-repeat: no-repeat;
    position: absolute;
    top: 0;
    left: 0;
  }
}

  1. 金币组件由账户组件消费,直接在组件内控制展示 state 状态即可。
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 && (
        <CoinsFly
          style={{
            top: '322rpx',
            left: '316rpx',
            zIndex: 1,
          }}
          onEnd={() => {
            setShowCoinsFly(false);
          }}
        />
      )}
    </div>
  );
};
export default Account;

实现二:


5.3 需求三:权益承接

🌈 需求三:提现弹窗承接

解决三:

  1. 数据逻辑:相关 POP 弹窗,可以声明为一个业务领域【pop】
  2. model 业务模型
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;
  1. server 业务服务
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;
  1. application 业务应用
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;

  1. 抽象弹窗视图组件,而非【提现弹窗】的定制化视图组件
  2. 消费弹窗数据模型 popModel
  3. 触发弹窗业务应用 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>
          {/* 按钮 */}
          <div
            className={styles.btn}
            onClick={() => close()}
          >开心收下</div>
        </div>
      </div>
    </div >
  );
};
export default Dialog;

  1. 账户视图组件,通过弹窗的应用逻辑交互
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>
        <div
          className={styles.withdraw}
          onClick={() => {
            openWithdrawDialog({
              a: '3000',
              b: '123456789123456789',
              c: '支付宝打款',
              d: '提现成功,预计2小时到账',
              e: '0.3',
            });
          }}
        />
      </div>
      {showCoinsFly && (
        <CoinsFly
          style={{
            top: '322rpx',
            left: '316rpx',
            zIndex: 1,
          }}
          onEnd={() => {
            setShowCoinsFly(false);
          }}
        />
      )}
    </div>
  );
};
export default Account;

实现三:

总结:至此,我们已经有了【account】和【pop】的业务领域数据逻辑,并且做到了【账户】和【弹窗】视图组件的完全解耦,而不需要内置 hook 状态来耦合视图组件。


5.4 需求四:任务体系

🌈 需求四:最后的交互需求中,按照前边铺垫好的架构,我们怎么快速实现~

完成任务后,点击完成任务弹出提现成功弹窗,收下弹窗金币飞入再金额刷新。

解决四:

  1. 本次需要刷新【账户】信息,添加 reFreshData 业务逻辑
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;

  1. 沉浸式写一个任务组件 Task
  2. 通过 pop 业务应用 open 更新数据并打开弹窗
  3. 关闭时调用 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>
      {/* 按钮 */}
      <div
        className={styles.taskBtn}
        onClick={() => btnCallback()}
      >
        {TASK_INFO_MAP[state]?.btn || ''}
      </div>
    </div>
  );
};
export default Task;

实现四:

总结:能明显看到在业务敏捷迭代过程 & 复杂的需求中,只要抓住了业务的关注点,明确 不变 构造好一个干净的前端架构,就能释放绝大部分的开发人力


5.5 思考

整体的架构一定有设计者核心的逻辑和目的,而上层的设计只是过程中的关键路径/方法论

现阶段我粗浅地认为核心的逻辑和目的在于:【找出业务迭代的关键因子,以最短路径 & 最小影响达到目的】

综上实践过程有几点关键路径/方法论可以梳理总结下:

  1. 以业务需求定义前端页面为导向,抽象定义其业务领域模型。

领域驱动设计(Domain-Driven Design):将软件系统的核心逻辑和业务规则放在领域模型中,通过领域模型来驱动项目设计和开发,领域驱动设计强调对业务领域的深入理解和模型化。

  1. 关注点分离,以业务领域模型来驱动通用服务 / 应用逻辑 / UI 组件等分层处理。

分层架构(Layered Architecture):将系统划分为多个层次,每个层次具有不同的关注点和责任,分层架构提供了松耦合、可测试和可维护的系统结构。

  1. 自内向外的通信处理,数据流和业务逻辑清晰可见。

完整架构分层:

pages // 视图组件
├─components
|    ├─Account  // 账户视图组件
|    ├─CoinsFly // 互动视图组件
|    ├─Dialog   // 弹窗视图组件
|    ├─Task     // 任务视图组件
├─home
dataArea  // 数据领域
├─newHome
|    ├─models
|    |   └account.ts  // 🌰 账户业务模型
|    |   └common.ts // 🌰 基础能力模型(如关注、订阅、全局状态数据)
|    ├─server
|    |   └account.ts  // 🌰 账户业务服务
|    |   └common.ts // 🌰 基础能力服务
|    ├─applications
|    |   └account.ts  // 🌰 账户业务应用
|    |   └common.ts // 🌰 基础能力应用

🌈 六、协同

当实现了一个自认为合理的架构设计时,不出意外会在过程中逐渐尝到甜头。

但往往事情总是相对的,在项目新参与的同学协作过程中初次接触则需要理解和熟悉相关架构。

因此以协作的目的(读懂项目业务代码),来简单介绍一下你该怎么看。

  1. 看一下 models 里都有哪一些,理解这个项目定义了哪些业务模型。

🌰如:预告页项目 - (项目注释很重要~ 👀 )

  • liveReplayModel: 播回放模型
  • previewInfoModel: 主播预告信息模型 1.主播 Header 信息、2.预告条信息、3.预告商品信息
  • commonModel: 通用业务能力模型 1.全局配置、2.关注 / 分享 / 订阅

  1. 看主逻辑在哪调用,业务应用的初始化都做了什么~

一般来说会在项目入口处调用初始化的主逻辑: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;

  1. 看视图组件是怎么抽象的,也就能知道整个页面的结构布局

  1. 上面整体过一轮基本可以了解个大概,最后详情可以看每一个业务领域的 application 和 server 做了什么,以及在哪一些视图组件监听/触发/消费的即可。

看完之后怎么打代码,该怎么做?

可以看一下这一篇文章hhh,这就是写这个的一点 🤏 意义了。

🚩 七、规划

总得来说是实践过程夹杂着一些想法和小心思,但方法论和约束还是过于浅显。如何制定框架层面的架构 SOP 的规范(如 🥚 egg 围绕企业的开发规范范式),以及简单/准确/快速地找到并且划分业务领域模型的方法论

📖 Ref

from @成禹 无数深夜的 ❤️‍🔥 激情讨论 and 老板给予业务落地实践的信任。

架构理念

架构实践

相关实践学习
【文生图】一键部署Stable Diffusion基于函数计算
本实验教你如何在函数计算FC上从零开始部署Stable Diffusion来进行AI绘画创作,开启AIGC盲盒。函数计算提供一定的免费额度供用户使用。本实验答疑钉钉群:29290019867
建立 Serverless 思维
本课程包括: Serverless 应用引擎的概念, 为开发者带来的实际价值, 以及让您了解常见的 Serverless 架构模式
相关文章
|
4天前
|
JSON 前端开发 API
以项目登录接口为例-大前端之开发postman请求接口带token的请求测试-前端开发必学之一-如果要学会联调接口而不是纯写静态前端页面-这个是必学-本文以优雅草蜻蜓Q系统API为实践来演示我们如何带token请求接口-优雅草卓伊凡
以项目登录接口为例-大前端之开发postman请求接口带token的请求测试-前端开发必学之一-如果要学会联调接口而不是纯写静态前端页面-这个是必学-本文以优雅草蜻蜓Q系统API为实践来演示我们如何带token请求接口-优雅草卓伊凡
27 5
以项目登录接口为例-大前端之开发postman请求接口带token的请求测试-前端开发必学之一-如果要学会联调接口而不是纯写静态前端页面-这个是必学-本文以优雅草蜻蜓Q系统API为实践来演示我们如何带token请求接口-优雅草卓伊凡
|
4天前
|
存储 SQL 监控
转转平台IM系统架构设计与实践(二):详细设计与实现
以转转IM架构为起点,介绍IM相关组件以及组件间的关系;以IM登陆和发消息的数据流转为跑道,介绍IM静态数据结构、登陆和发消息时的动态数据变化;以IM常见问题为风景,介绍保证IM实时性、可靠性、一致性的一般方案;以高可用、高并发为终点,介绍保证IM系统稳定及性能的小技巧。
20 6
|
24天前
|
存储 缓存 关系型数据库
社交软件红包技术解密(六):微信红包系统的存储层架构演进实践
微信红包本质是小额资金在用户帐户流转,有发、抢、拆三大步骤。在这个过程中对事务有高要求,所以订单最终要基于传统的RDBMS,这方面是它的强项,最终订单的存储使用互联网行业最通用的MySQL数据库。支持事务、成熟稳定,我们的团队在MySQL上有长期技术积累。但是传统数据库的扩展性有局限,需要通过架构解决。
65 18
|
1月前
|
搜索推荐 NoSQL Java
微服务架构设计与实践:用Spring Cloud实现抖音的推荐系统
本文基于Spring Cloud实现了一个简化的抖音推荐系统,涵盖用户行为管理、视频资源管理、个性化推荐和实时数据处理四大核心功能。通过Eureka进行服务注册与发现,使用Feign实现服务间调用,并借助Redis缓存用户画像,Kafka传递用户行为数据。文章详细介绍了项目搭建、服务创建及配置过程,包括用户服务、视频服务、推荐服务和数据处理服务的开发步骤。最后,通过业务测试验证了系统的功能,并引入Resilience4j实现服务降级,确保系统在部分服务故障时仍能正常运行。此示例旨在帮助读者理解微服务架构的设计思路与实践方法。
101 17
|
1月前
|
存储 消息中间件 小程序
转转平台IM系统架构设计与实践(一):整体架构设计
本文描述了转转IM为整个平台提供的支撑能力,给出了系统的整体架构设计,分析了系统架构的特性。
75 10
|
2月前
|
弹性计算 Java 关系型数据库
Web应用上云经典架构实践教学
Web应用上云经典架构实践教学
Web应用上云经典架构实践教学
|
1月前
|
负载均衡 Serverless 持续交付
云端问道9期实践教学-省心省钱的云上Serverless高可用架构
详细介绍了云上Serverless高可用架构的一键部署流程
57 10
|
1月前
|
存储 人工智能 运维
面向AI的服务器计算软硬件架构实践和创新
阿里云在新一代通用计算服务器设计中,针对处理器核心数迅速增长(2024年超100核)、超多核心带来的业务和硬件挑战、网络IO与CPU性能增速不匹配、服务器物理机型复杂等问题,推出了磐久F系列通用计算服务器。该系列服务器采用单路设计减少爆炸半径,优化散热支持600瓦TDP,并实现CIPU节点比例灵活配比及部件模块化可插拔设计,提升运维效率和客户响应速度。此外,还介绍了面向AI的服务器架构挑战与软硬件结合创新,包括内存墙问题、板级工程能力挑战以及AI Infra 2.0服务器的开放架构特点。最后,探讨了大模型高效推理中的显存优化和量化压缩技术,旨在降低部署成本并提高系统效率。
|
2月前
|
运维 监控 安全
天财商龙:云上卓越架构治理实践
天财商龙成立于1998年,专注于为餐饮企业提供信息化解决方案,涵盖点餐、收银、供应链和会员系统等。自2013年起逐步实现业务上云,与阿里云合作至今已十年。通过采用阿里云的WA体系,公司在账号管理、安全保障、监控体系和成本管控等方面进行了全面优化,提升了业务稳定性与安全性,并实现了显著的成本节约。未来,公司将持续探索智能化和全球化发展,进一步提升餐饮行业的数字化水平。
|
2月前
|
运维 安全 架构师
架构师工具箱:Well-Architected云治理提效实践
本次分享基于阿里云Well-Architected Framework的最佳实践案例,涵盖企业从上云到优化的全过程。安畅作为国内领先的云管理服务提供商(Cloud MSP),拥有800多名员工,其中70%为技术工程师,为企业提供架构安全、数据智能等技术服务。内容包括Landing Zone与Well-Architected的关系、企业云治理现状及需求分析,重点探讨了安全合规、成本优化、资源稳定性和效率提升等方面的最佳实践,并通过具体客户案例展示了如何通过自动化工具和定制化解决方案帮助企业提升云上业务价值。

热门文章

最新文章

  • 1
    以项目登录接口为例-大前端之开发postman请求接口带token的请求测试-前端开发必学之一-如果要学会联调接口而不是纯写静态前端页面-这个是必学-本文以优雅草蜻蜓Q系统API为实践来演示我们如何带token请求接口-优雅草卓伊凡
    27
  • 2
    大前端之前端开发接口测试工具postman的使用方法-简单get接口请求测试的使用方法-简单教学一看就会-以实际例子来说明-优雅草卓伊凡
    47
  • 3
    【2025优雅草开源计划进行中01】-针对web前端开发初学者使用-优雅草科技官网-纯静态页面html+css+JavaScript可直接下载使用-开源-首页为优雅草吴银满工程师原创-优雅草卓伊凡发布
    26
  • 4
    巧用通义灵码,提升前端研发效率
    90
  • 5
    【11】flutter进行了聊天页面的开发-增加了即时通讯聊天的整体页面和组件-切换-朋友-陌生人-vip开通详细页面-即时通讯sdk准备-直播sdk准备-即时通讯有无UI集成的区别介绍-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草Alex
    138
  • 6
    详解智能编码在前端研发的创新应用
    94
  • 7
    智能编码在前端研发的创新应用
    81
  • 8
    【09】flutter首页进行了完善-采用android studio 进行真机调试开发-增加了直播间列表和短视频人物列表-增加了用户中心-卓伊凡换人优雅草Alex-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草Alex
    37
  • 9
    【08】flutter完成屏幕适配-重建Android,增加GetX路由,屏幕适配,基础导航栏-多版本SDK以及gradle造成的关于fvm的使用(flutter version manage)-卓伊凡换人优雅草Alex-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草Alex
    117
  • 10
    【07】flutter完成主页-完成底部菜单栏并且做自定义组件-完整短视频仿抖音上下滑动页面-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草央千澈
    74