1. 前言
2. Design System
3. UI 元件分類
4. 元件庫 UML
5. 一個元件的誕生
6. 小談測試
前公司接很多後台 ERP 的案子
每次都花很多時間從頭造輪子很浪費時間
兩種策略 — 「自幹」或「用現成的」
採用自幹的理由:
大推
從 Design System 出發理解
網頁設計跟頁面架構背後的概念
能讓我們更快地知道 Framework 都在幹嘛
由一組共享、整合的元素及原則來定義產品整體的設計
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>
);
};
Design System
UI Component Library
Composed Component
(通常是在專案各自實作)
Layout / Template / Scaffold
因系統而異,在前公司大多是參考 Ant Design
與網站的排版佈局有關係的元件
常見的還有 Box、Container、Flex 和 Wrap 等等。
導覽相關的元件
提示使用者處於網站中的哪一個階段
與這些元件的互動通常會切換頁面。
常見的還有 Drawer、Links 等等。
使用者互動過後網頁給使用者的反應
讓使用者感受到網頁還有在運作
以圖像化的方式,將系統的功能與結構畫成「模型」與藍圖
一種開放的方法,用於說明、視覺化、構建和編寫一個正在開發的、物件導向的、軟體密集系統的製品的開放方法
反映出元件之間的依賴關係
共用功能的抽象化
就是一個個的 UI Component
規格 Spec
介面 Interface
實作 Implement
Design System、UML 到 Spec 都是前期規劃
元件庫要好用很大程度決定於這些前期準備夠不夠完善
// 概念:先繼承原生的 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;
}
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
測 最基本的情況 Component 該有的要有
測 各種 props 有沒有正常顯示?
style, element, attribute
測 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);
});
});
};
};
公司規模不大的情況下
若能以市面上的方案出發來架構跟設計的話應該是可以避掉很多問題
畢竟錢跟資源真的挺有差的
條件不到位的情況下,也只會開發出半吊子
又浪費很多時間跟資金
Q & A Time