组件化原则
本文大多数以 vant-ui 库为例
标准化
组件命名规则统一
- 形成命名空间,避免相同相同标签的冲突
- 规范化管理,统一风格
// vant/packages/vant/src/swipe-cell/SwipeCell.tsx
const [name, bem] = createNamespace('swipe-cell')
...
export default defineComponent({
name,
...
})
// vant 统一创建命名空间函数
// vant/packages/vant/src/utils/create.ts
export function createNamespace(name: string) {
const prefixedName = `van-${name}`
return [
prefixedName,
createBEM(prefixedName),
createTranslate(prefixedName)
] as const;
}
复制代码
样式分离独立文件
- SwipeCell.tsx
- index.less
复制代码
变量命名的统一
以vant-ui的SwipeCell组件为例
- 声明时始终采用(camelCase)
const getWidthByRef = (ref: Ref<HTMLElement | undefined>) =>
ref.value ? useRect(ref).width : 0;
const leftWidth = computed(() =>
isDef(props.leftWidth) ? +props.leftWidth : getWidthByRef(leftRef)
);
const rightWidth = computed(() =>
isDef(props.rightWidth) ? +props.rightWidth : getWidthByRef(rightRef)
);
复制代码
- 在模板和JSX中应该始终使用( kebab-case)
<van-swipe-cell :before-close="beforeClose"> .... </van-swipe-cell>
复制代码
使用什么技术统一风格
仍然以vant-ui相关库为例
- pnpm来管理项目
- .editorconfig来统一编辑器代码风格
- husky做git提交校验
- .eslintrc、.prettierrc来保证代码的风格一致性
- 代码层面上面使用ts+less+tsx作为组件的编写规范
组合性
以action-sheet组件为例
将CSS模块进行拆分
/* vant/packages/vant/src/action-sheet/index.less */
@import './var.less';
@import '../style/mixins/hairline';
复制代码
将JS模块进行拆分
// vant/packages/vant/src/action-sheet/ActionSheet.tsx
// Utils
import {
pick,
extend,
truthProp,
makeArrayProp,
makeStringProp,
createNamespace,
HAPTICS_FEEDBACK,
} from '../utils';
复制代码
复用性
样式规则复用
使用bem风格来极大限度的复用CSS 传送门:CSS 架构之 BEM
// vant/packages/vant/src/utils/create.ts
export function createBEM(name: string) {
return (el?: Mods, mods?: Mods): Mods => {
if (el && typeof el !== 'string') {
mods = el;
el = '';
}
el = el ? `${name}__${el}` : name;
return `${el}${genBem(el, mods)}`;
};
}
复制代码
组件复用
秉承高内聚和低耦合的规范
,以vant中uploader组件为例
组件内部互不影响,UploaderPreviewItem.tsx不工作的时候,uploader.tsx照样是可以工作的,那么就称之为uploader.tsx和UploaderPreviewItem.tsx低耦合
<!-- vant/packages/vant/src/uploader -->
- uploader.tsx
- UploaderPreviewItem.tsx
复制代码
所以再封装组件的时候,vue为例,尽量不要使用refs来访问父子的组件,能用props用props,即尽量与上下文无关
单一支职责
- 一个组件负责完成一个职责/功能。
共同封闭原则
- 将那些会同时修改,或者因为同一个目的发生修改的代码,放到一个组件里。举个例子,现在有 A、B、C 三个组件,A 和 B 都依赖 C。但是经常 A 的改动就需要改动 C 做一些兼容。这个时候,就适合将 C 组件直接移到到 A 组件中。
提高可配置性
- 尽可能对外暴露出属性和方法,比如vant-ui中action-sheet中的这段代码
// vant/packages/vant/src/action-sheet/ActionSheet.tsx
// slot 和 props
const renderCancel = () => {
if (slots.cancel || props.cancelText) {
return [
<div class={bem('gap')} />,
<button type="button" class={bem('cancel')} onClick={onCancel}>
{slots.cancel ? slots.cancel() : props.cancelText}
</button>,
];
}
};
const renderActionContent = (action: ActionSheetAction, index: number) => {
if (action.loading) {
return <Loading class={bem('loading-icon')} />;
}
if (slots.action) {
return slots.action({ action, index });
}
return [
<span class={bem('name')}>{action.name}</span>,
action.subname && <div class={bem('subname')}>{action.subname}</div>,
];
};
复制代码
扁平化的数据结构 比如vant-ui中action-sheet中的这段代码。如果采用object的形式,全部塞进去那么就会有问题,得一个一个去取值,搞不好还会漏掉。
// vant/packages/vant/src/action-sheet/ActionSheet.tsx
const actionSheetProps = extend({}, popupSharedProps, {
title: String,
round: truthProp,
actions: makeArrayProp<ActionSheetAction>(),
closeIcon: makeStringProp('cross'),
closeable: truthProp,
cancelText: String,
description: String,
closeOnPopstate: truthProp,
closeOnClickAction: Boolean,
safeAreaInsetBottom: truthProp,
})
复制代码
可维护性
文档的必要性
以ActionSheet组件为例
// 每个组件目录下面都会有README.md
// 组件结构如下
- demo/
- test/
- ActionSheet
- README.md
- README.zh-CN.md
- index.less
- index.ts
- var.less
复制代码
代码的可读性
- 组件的命名---看其名知其意
- 变量的命名---看其名知其意 如action-sheet组件中方var.less,看着就很舒服。
@import '../style/var.less';
@action-sheet-max-height: 80%;
@action-sheet-header-height: 48px;
@action-sheet-header-font-size: var(--van-font-size-lg);
@action-sheet-description-color: var(--van-text-color-2);
@action-sheet-description-font-size: var(--van-font-size-md);
@action-sheet-description-line-height: var(--van-line-height-md);
@action-sheet-item-background: var(--van-background-color-light);
@action-sheet-item-font-size: var(--van-font-size-lg);
@action-sheet-item-line-height: var(--van-line-height-lg);
@action-sheet-item-text-color: var(--van-text-color);
@action-sheet-item-disabled-text-color: var(--van-text-color-3);
@action-sheet-subname-color: var(--van-text-color-2);
@action-sheet-subname-font-size: var(--van-font-size-sm);
@action-sheet-subname-line-height: var(--van-line-height-sm);
@action-sheet-close-icon-size: 22px;
@action-sheet-close-icon-color: var(--van-gray-5);
@action-sheet-close-icon-padding: 0 var(--van-padding-md);
@action-sheet-cancel-text-color: var(--van-gray-7);
@action-sheet-cancel-padding-top: var(--van-padding-xs);
@action-sheet-cancel-padding-color: var(--van-background-color);
@action-sheet-loading-icon-size: 22px;
复制代码