元件庫開發心得

Design System、UML、開發流程

大綱

1. 前言
2. Design System

3. UI 元件分類

4. 元件庫 UML

5. 一個元件的誕生

6. 小談測試

前言

  1. 前公司接很多後台 ERP 的案子
    每次都花很多時間從頭造輪子很浪費時間

  2. 兩種策略 — 「自幹」或「用現成的」

    採用自幹的理由:

    1. 強調設計、風格明確
    2. 資源充足(資金、規模、人力)

市面上有哪些選擇 

  1. Design System(純設計、品牌端)
    1. Polaris - Shopify
    2. Material Design
    3. Ant Design
  2. CSS Framework(純樣式)
    1. Bootstrap
    2. Tailwind → Utility CSS
      Integrate w/ component logic → Headless UI
  3. UI Framework(樣式+邏輯,開箱即用包)
    1. Material-UI
    2. Ant Design
    3. Chakra UI

大推

Design System

Why Design System?

  1. 為系統提供一致的介面體驗
  2. 避免重複造輪子
  3. 加速開發流程
  4. 專注在商業邏輯

從 Design System 出發理解
網頁設計跟頁面架構背後的概念
能讓我們更快地知道 Framework 都在幹嘛

What if we don't have Design System?

What is Design System?

由一組共享、整合的元素及原則來定義產品整體的設計

Design System in Project

Design System in Project

Color System (Palette)

Spacing

Typography

Icons

import { IconDefinition } from './typings';

export const ArrowRightIcon: IconDefinition = {
  name: 'arrow-right',
  definition: {
    svg: {
      viewBox: '0 0 24 24',
    },
    path: {
      fill: 'currentColor',
      fillRule: 'evenodd',
      stroke: 'none',
      strokeWidth: 1,
      d: 'M13.8 6.8l5.198 5.198-5.198 5.198-1.06-1.06 3.384-3.386H5v-1.5h11.131L12.74 7.86 13.8 6.8z',
    },
  },
};
export interface IconDefinition {
  name: string;
  definition: {
    svg?: {
      viewBox?: string;
    };
    path?: {
      d?: string;
      fill?: string;
      fillRule?: 'nonzero' | 'evenodd' | 'inherit';
      stroke?: string;
      strokeWidth?: string | number;
      transform?: string;
    };
  };
}
const Icon: React.FC<IconProps> = (props) => {
  const { className, color = "black", icon, spin = false, style, ...rest } = props;
  const { definition } = icon;

  return (
    <i
      {...rest}
      aria-hidden
      className={`
        inline-block flex-shrink-0 select-none w-em h-em
        ${spin ? "animate-spin" : ""}
        ${color ? Color[color] : ""}
        ${className ? className : ""}
      `}
      data-icon-name={icon.name}
      style={style}
    >
      <svg {...definition.svg} focusable={false}>
        <path {...definition.path} />
      </svg>
    </i>
  );
};

Atomic Design

Design System

UI Component Library

Composed Component
(通常是在專案各自實作)

Layout / Template / Scaffold

Overview

因系統而異,在前公司大多是參考 Ant Design

Material-UI

  1. Layout
  2. Inputs
  3. Navigation
  4. Surfaces
  5. Feedback
  6. Data Display
  7. Utils
  8. Lab

Ant Design

  1. General
  2. Layout
  3. Navigation
  4. Data Entry
  5. Data Display
  6. Feedback
  7. Other

Chakra UI

  1. Layout
  2. Forms
  3. Data Display
  4. Feedback
  5. Typography
  6. Overlay
  7. Disclosure
  8. Navigation
  9. Media and Icons
  10. Others
  11. Hooks

General

Layout

與網站的排版佈局有關係的元件

常見的還有 Box、Container、Flex 和 Wrap 等等。

Navigation

導覽相關的元件
提示使用者處於網站中的哪一個階段
與這些元件的互動通常會切換頁面。

常見的還有 Drawer、Links 等等。

Data Entry

Inputs, Forms or Data Display

Feedback

使用者互動過後網頁給使用者的反應
讓使用者感受到網頁還有在運作

Utility

  1. Portal 系列
  2. Overlay
  3. Transition

UML

What is UML (Unified Modeling Language)?

以圖像化的方式,將系統的功能與結構畫成「模型」與藍圖

一種開放的方法,用於說明、視覺化、構建和編寫一個正在開發的、物件導向的、軟體密集系統的製品的開放方法

Component Diagram

反映出元件之間的依賴關係

Interface

Component

共用功能的抽象化

  1. TextField
  2. FormControl
  3. Portal
  4. Notifier
  5. Transition

就是一個個的 UI Component

  1. Input
  2. Button
  3. Popper
  4. Notification
  5. Fade

一個元件的誕生

WorkFlow

規格 Spec

介面 Interface

實作 Implement

Design System、UML 到 Spec 都是前期規劃
元件庫要好用很大程度決定於這些前期準備夠不夠完善

Button Interface ( Refer to MUI & Ant )

// 概念:先繼承原生的 Button Props 再來擴充需要的 Props
export interface ButtonProps extends Omit<React.ComponentPropsWithRef<'button'>, 'prefix'> {
  color?: ButtonColorType;
  error?: boolean;
  disabled?: boolean;
  loading?: boolean;
  prefix?: ReactNode;
  size?: Size;
  suffix?: ReactNode;
  variant?: ButtonVariantType;
}

Button Implement

const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button(props, ref) {
  const {
    children,
    color = 'primary',
    error = false,
    disabled = false,
    loading = false,
    onClick,
    prefix: prefixProp,
    size = 'medium',
    suffix: suffixProp,
    variant = 'text',
    style,
    ...rest
  } = props;

  let prefix: ReactNode = prefixProp;
  let suffix: ReactNode = suffixProp;

  // 處理情境
  if (loading) {
    const loadingIcon = <Icon icon={SpinnerIcon} spin />;

    if (suffix && !prefix) {
      suffix = loadingIcon;
    } else {
      prefix = loadingIcon;
    }
  }

  const asIconBtn = children == null && !!(prefix || suffix);


  // 處理樣式 (CSS)
  const buttonColorClass = useMemo(() => {
    if (disabled) return Color['disabled'];
    if (error) return Color['error'];
    if (color) return Color[color];
  }, [color, disabled, error]);

  const padding = useMemo(() => {
    if (asIconBtn) return 'px-0';

    if (size === 'small') return 'px-3';

    return 'px-4';
  }, [asIconBtn, size]);

  const iconClass = useMemo(() => size === 'small' ? "text-lg" : "text-2xl", [size]);

  const variantClass = useMemo(() => {
    if (variant === 'contained') return `text-white ${ButtonVariant[variant]}-${buttonColorClass}`;

    if (variant === 'outlined') return `border-2 text-${buttonColorClass} ${ButtonVariant[variant]}-${buttonColorClass}`;

    return `${ButtonVariant[variant]}-${buttonColorClass}`;
  }, [buttonColorClass, variant]);

  const disabledClass = useMemo(() => disabled ? "opacity-40 cursor-not-allowed" : "hover:opacity-60", [disabled]);
  const loadingClass = useMemo(() => loading ? "cursor-default pointer-events-none" : "", [loading]);

  // HTML Structure
  return (
    <button
      {...rest}
      ref={ref}
      aria-disabled={disabled}
      className={`relative box-border flex-center flex-shrink-0 gap-1 m-0 rounded cursor-pointer select-none whitespace-nowrap uppercase focus:outline-none ${padding} ${iconClass} ${variantClass} ${disabledClass} ${loadingClass}`}
      style={style}
      disabled={disabled}
      onClick={(event: MouseEvent<HTMLButtonElement>) => {
        if (!disabled && !loading && onClick) {
          onClick(event);
        }
      }}
    >
      {prefix}
      {children && <span className={`${size ? ButtonSize[size] : ""}`}>{children}</span>}
      {suffix}
    </button>
  );
});

export default Button;

測試

各種測試

單元測試 Unit Test

整合測試 Integration Test

端對端測試 E2E Test

Unit Testing

  1. 測 最基本的情況 Component 該有的要有

  2. 測 各種 props 有沒有正常顯示?

    • style, element, attribute

  3. 測 coverage

    • 每一行每一個 if-else branch 都跑

describe('<Switch />', () => {
  // 1.
  describeForwardRefToHTMLElement(
    HTMLSpanElement,
    (ref) => render(<Switch ref={ref} />),
  );

 // 2.
  describeHostElementClassNameAppendable(
    'foo',
    (className) => render(<Switch className={className} />),
  );
  
  describe('prop: checked', () => { 
   	 // 3.
	 [false, true].forEach((checked) => {
        const message = checked
          ? 'should be checked if checked=true'
          : 'should be unchecked if checked=false';

        it(message, () => {
          const { getHostHTMLElement } = render(<Switch checked={checked} />);
          const element = getHostHTMLElement();
          const [inputElement] = element.getElementsByTagName('input');

          testChecked(element, inputElement, checked);
        });
      });
   };
 };
 

個人想法

公司規模不大的情況下
若能以市面上的方案出發來架構跟設計的話應該是可以避掉很多問題
畢竟錢跟資源真的挺有差的
條件不到位的情況下,也只會開發出半吊子
又浪費很多時間跟資金

Thanks For Listening

Q & A Time

Made with Slides.com