前端業務程式碼配置化處理條件判斷邏輯

Mcgrady*發表於2020-06-24

業務程式碼配置化

寫業務中判斷邏輯無處不在,隨著功能迭代將越來越難以維護,我們該如何應對呢?

什麼是業務程式碼配置化?

根據業務場景使用配置化的 Object|Array|Map 處理條件判斷邏輯,通常需要配置檔案 CONFIG.js,若邏輯複雜需新增 getConfig 的處理函式 - tool.js

  • 本質上 if/else 邏輯是一種狀態匹配

  • 表驅動法,使用表資料,儲存對應的狀態處理

  • 可讀性好,減少了繁雜巢狀的 if-else,讀取配置,邏輯更清晰

  • 可維護性高,邏輯分支的增刪只是 CONFIG 的增刪

不同業務場景的程式碼配置化

簡單的狀態對映

  • 按需使用 Object|Map 配置
單一條件
  • Object 形式:
// CONFIG.JS
  export const STATUS = {
    STUDENT: 0,
    TEACHER: 1,
    MA_NONG: 2,
  };
  export const WORK_MAP = {
    STATUS.STUDENT: '學生',
    STATUS.TEACHER: '老師',
    STATUS.MA_NONG: '碼農',
  };

// index.js
  this.setData({
    work: WORK_MAP[status],
  });

  axios.post(url, { status: STATUS.MA_NONG });
  • Map 形式:
// CONFIG.JS
  export const WORK_MAP = new Map([[0, '學生'], [1, '老師'], [2, '碼農']]);
// index.js
  this.setData({
    work: WORK_MAP.get(status),
  });


多重條件
  const config = new Map([
    [
      (condition0, condition1, condition2) =>
        condition0 && condition1 && condition2,
      () => {
        console.log('map0');
      },
    ],
    [
      (condition0, condition1, condition2) =>
        condition0 || condition1 || condition2,
      () => {
        console.log('map1');
      },
    ],
  ]);
  config.forEach((action, _if) => _if(0, 1, 0) && action());

每個狀態有多種屬性或行為

  • 多個屬性或行為
  • 使用 Array 配置
// CONFIG.JS
  export const CONFIG = [
    {
      status: STATUS.STUDENT,
      name: '學生',
      action: '談戀愛',
    },
    {
      status: STATUS.TEACHER,
      name: '老師',
      action: '教書',
    },
    {
      status: STATUS.MA_NONG,
      name: '碼農',
      action: '寫bug',
    },
  ];

// index.js
  <!-- 根據狀態不同的行為 -->
  function action(status) {
    const { name, work } = CONFIG.find(i => i.status === status);
    console.log(`${name}在${action}`);
  }

每個狀態有多種屬性且引數定製化

  • 屬性高度定製化,不同狀態需要適配介面不同的欄位
  • 使用 ArrayMap 配置
  • 通過配置函式並傳參注入介面資料可滿足定製化需求
// CONFIG.JS
  export const CONFIG = [
    {
      status: STATUS.STUDENT,
      name: '學生',
      action: () => {
        console.log('學生最喜歡談戀愛了');
      },
    },
    {
      status: STATUS.TEACHER,
      name: '老師',
      action: (info) => {
        alert(`老師${info.age}歲,每天${info.action}`);
      },
    },
    {
      status: STATUS.MA_NONG,
      name: '碼農',
      action: (info) => {
        toast(`碼農工作${info.workTime}年了,頭髮僅剩${info.hair}根了`);
      },
    },
  ];

// index.js
  <!-- 根據介面狀態action -->
  function action(res) {
    const { action, info } = CONFIG.find(i => i.status === res.status);
    action && action(info); // 傳參定製化
  }

例項

大首頁瀑布流 item 樣式

  • 根據 list 介面下發的 item 的型別(type)&樣式(layout)欄位取 item 中的封面、標題、標籤、頭像...,欄位各不相同
  • 十幾種 item 型別,有的還有不同的 layout,item 資料下發方式不同
  • 公共元件,需要適配其他模組的介面資料作展示

CONFIG.JS

import { Layout, NewsType, Redirect } from 'src/constant';
import { formatImage, formatUser } from './tool';

/**
 * 配置項
 * @param {String} title 標題
 * @param {String} cover 封面
 * @param {String} tag 標籤
 * @param {Object} user 使用者資訊
 * @param {Boolean} showFooter 是否顯示footer
 * @param {Boolean} isAd 是否廣告
 * @param {Function} itemWrap 相容介面資料函式,資料可能以ref_xxx下發,比如帖子:ref_post
 * ......
 */

<!-- 預設配置項 -->
export const DEFAULT = {
  title: ({ title = '' }) => title,
  cover: ({ image_list = [], article_images = [] }) =>
    formatImage(image_list[0]) || formatImage(article_images[0]),
  showFooter: true,
  user: ({ user, user_account, article_source_tx = '' }) =>
    user
      ? formatUser(user)
      : user_account
      ? formatUser(user_account)
      : {
          icon: '',
          nickname: article_source_tx,
        },
};

export const CONFIG = [
  {
    type: NewsType.NEWS,
    ...DEFAULT,
    tag: '資訊',
    tagClass: 'news',
  },
  {
    type: NewsType.VIDEO,
    ...DEFAULT,
    tag: '',
    tagClass: 'video',
    cover: ({ image_gif_list = [], image_list = [] }) => formatImage(image_gif_list[0] || image_list[0]),
    video: ({ video_url = '', tencent_vid = '', image_gif_list = [] }) => ({
      hasVideo: true,
      src: tencent_vid || video_url,
      video_url,
      tencent_vid,
      gifCover: formatImage(image_gif_list[0]),
    }),
  },
  {
    type: Redirect.EVAL_DETAIL,
    layouts: [
      {
        layout: Layout.EVALUATION,
        ...DEFAULT,
        tag: '口碑',
        tagClass: 'praise',
      },
      {
        layout: Layout.PRAISE_COMPONENT,
        ...DEFAULT,
        tag: '口碑',
        tagClass: 'praise',
        itemWrap: ({ ref_car_score = {} }) => ref_car_score,
        title: ({ chosen_topic = '' }) => chosen_topic,
        commentCount: ({ comment_count = null }) => comment_count,
        cover: ({ images = [], recommend_images = [] }) =>
          formatImage(images[0]) ||
          formatImage(getCoverFromRecommendImages(recommend_images)),
      },
      {
        layout: Layout.NORMAL,
        ...DEFAULT,
      },
    ],
  },
];

tool.js

import { CONFIG, DEFAULT, AD_CONFIG } from './CONFIG';
// 獲取瀑布流item資料
export const getPanelData = (item) => {
  const getConfigByTypeAndLayout = () => {
    let config = CONFIG.find((i) => i.type == item.type);
    if (item.isAd) {
      config = AD_CONFIG;
    }
    if (config && config.layouts) {
      config = config.layouts.find(
        (i) => i.layout === item.layout_type || i.layout === item.display_style
      );
    }
    if (!config) {
      config = DEFAULT;
      console.log('no-config', item.type, item.layout_type, item);
    }
    return config;
  };
  const getPanelDataByConfig = (c) => {
    const panel = {};
    let _item = item;
    if (c.itemWrap) {
      _item = c.itemWrap(item);
    }
    Object.keys(c).forEach((key) => {
      if (typeof c[key] === 'function') {
        panel[key] = c[key](_item);
      } else {
        panel[key] = c[key];
      }
    });
    return panel;
  };
  // 根據item的型別、樣式獲取配置
  const config = getConfigByTypeAndLayout(item);
  // 根據配置獲取瀑布流item資訊
  return getPanelDataByConfig(config);
};

相關文章