为了保证 Farris UI Vue 的源代码风格一致,确保组件研发质量,方便社区开发者阅读代码参与贡献,需要所有参与 Farris UI Vue 组件开发的贡献,都要遵循此组件开发规范。
Farris UI Vue 的组件,都包含在ui-vue
包的components
目录下,每一个子组件为独立目录,目录名为全小写的组件名,当组件名包含多个英文单词时,使用-
分隔。
目录规范分成以下部分:
以下是单个组件目录的结构
input-group
├── test // 单元测试
| └── input-group.spec.tsx
├── src // 组件源码
| ├── components // 子组件
| | └── input-group-sub.component.tsx
| ├── composition // 组件的可复用逻辑
| | ├── types.ts // 组合式Api返利值接口类型
| | ├── use-append-button.ts // 实现组件特性1的组合式Api
| | └── use-clear.ts // 实现组件特性2的组合式Api
| ├── input-group.component.tsx // 组件代码
| └── input-group.props.ts // 定义组件Api
└── index.ts // 组件入口文件
index.ts
import type { App } from 'vue';
import InputGroup from './src/input-group.component';
export * from './src/input-group.props';
export { InputGroup };
export default {
install(app: App): void {
app.component(InputGroup.name, InputGroup);
}
};
input-group.props.ts
import { PropType, ExtractPropTypes } from 'vue';
export const inputGroupProps = {
/** 是否自动完成 */
autocomplete: { Type: String, default: 'off' },
/** 自定义CLASS */
customClass: { Type: String, default: '' },
/** 禁用 */
disable: { Type: Boolean, default: false },
/** 允许编辑 */
editable: { Type: Boolean, default: true },
/** 启用清除按钮 */
enableClear: { Type: Boolean, default: true },
/** 启用提示文本 */
enableTitle: { Type: Boolean, default: true },
/** 启用密码 */
enableViewPassword: { Type: Boolean, default: true },
/** 扩展信息 */
extendInfo: { Type: String, default: '' },
/** 始终显示占位符文本 */
forcePlaceholder: { Type: Boolean, default: false },
/** 扩展按钮 */
groupText: { Type: String, default: '' },
/** 扩展按钮模版 */
groupTextTemplate: { Type: templateRef<any>, default: ref<HTMLElement | null>(null) },
/** 密码模式 */
isPassword: { Type: Boolean, default: false },
/** 最大长度 */
maxLength: { Type: Number || undefined, default: undefined },
/** 最小长度 */
minLength: { Type: Number || undefined, default: undefined },
/** 组件值 */
modelValue: { Type: String || Boolean, default: '' },
/** 隐藏边线 */
noborder: { Type: Boolean, default: false },
/** 启用提示信息 */
placeholder: { Type: String, default: '' },
/** 只读 */
readonly: { Type: Boolean, default: false },
/** 当组件禁用或只读时显示后边的按钮 */
showButtonWhenDisabled: { Type: Boolean, default: false },
/** tab索引 */
tabIndex: { Type: Number || undefined, default: undefined },
/** 文本在输入框中的对齐方式 */
textAlign: { Type: String, default: 'left' },
/** 扩展信息;在输入框前面 显示 ① 图标鼠标滑过后显示 */
useExtendInfo: { Type: Boolean, default: false },
/** 输入值 */
value: { Type: String, default: '' },
} as Record<string, any>;
export type InputGroupProps = ExtractPropTypes<typeof inputGroupProps>;
组件名
Props命名组件的属性对象,其中组件名
采用camel命名。default
默认值为对象时,可以折行。Type
在前,default
在后,需要为每一个props属性声明默认值。Record<string, any>
。组件名
Props输出属性对象类型,其中组件名
采用Pascal命名。input-group.component.tsx
import { defineComponent, ref, SetupContext } from 'vue';
import { InputGroupProps, inputGroupProps } from './input-group.props';
import { useAppendedButton } from './composition/use-appended-button';
import { usePassword } from './composition/use-password';
import { TextBoxProps, useClear, useTextBox } from '../../common';
import getEditorRender from './components/text-edit.component';
import getAppendedButtonRender from './components/appended-button.component';
import './input-group.scss';
export default defineComponent({
name: 'FInputGroup',
props: inputGroupProps,
emits: [
'clear',
'change',
'blur',
'click',
'clickHandle',
'focus',
'input',
'keydown',
'keyup',
'iconMouseEnter',
'iconMouseLeave',
'update:modelValue'
] as (string[] & ThisType<void>) | undefined,
setup(props: InputGroupProps, context: SetupContext) {
const modelValue = ref(props.modelValue);
const displayText = ref(props.modelValue);
const useTextBoxComposition = useTextBox(props as TextBoxProps, context, modelValue, displayText);
const { inputGroupClass, inputType } = useTextBoxComposition;
const useAppendedButtonComposition = useAppendedButton(props, context);
const { shouldShowAppendedButton } = useAppendedButtonComposition;
const useClearComposition = useClear(props as TextBoxProps, context, useTextBoxComposition);
const { onMouseEnter, onMouseLeave } = useClearComposition;
const usePasswordComposition = usePassword(props, context, inputType, useAppendedButtonComposition);
const renderEditor = getEditorRender(props, context, usePasswordComposition, useTextBoxComposition);
const renderAppendedButton = getAppendedButtonRender(
props,
context,
useAppendedButtonComposition,
useClearComposition,
usePasswordComposition
);
return () => {
return (
<div id="inputGroup" class={inputGroupClass.value} onMouseenter={onMouseEnter} onMouseleave={onMouseLeave}>
{renderEditor()}
{shouldShowAppendedButton.value && renderAppendedButton()}
</div>
);
};
}
});
defineComponent
函数定义组件时,以name为F组件名
命名组件,其中组件名采用Pascal命名。setup
函数形参props
和context
的类型。setup
函数内采用ref
函数承接,props对象属性,不在render
函数内直接使用props.someProps
访问属性对象。use{Feature}Composition
为名称声明composition api对象,以便于传递复用,以解构形式提取composition api对象属性。render
方法内实现组件html细节,将组件html片段拆解到子组件render方法,以render子组件
方式命名子组件render方法,以便于可以语义化阅读render方法。render
方法中写if
语句,采用shouldShow子组件
声明Ref对象或者ComputeRef对象承接条件表达式,使用逻辑表达式实现按条件渲染。use-clear.ts
import { computed, SetupContext } from 'vue';
import { InputGroupProps } from '../input-group.props';
import { UseClear } from './types';
export function useClear(
props: InputGroupProps,
context: SetupContext,
useTextBoxComposition: UseTextBox
): UseClear {
const hasShownClearButton = ref(false);
const shouldShowClearButton = computed(() => props.enableClear && !props.readonly && !props.disabled);
const { changeTextBoxValue, isEmpty } = useTextBoxComposition;
const clearButtonClass = computed(() => ({
'input-group-text': true,
'input-group-clear': true
}));
const clearButtonStyle = computed(() => {
const styleObject = {
width: '24px',
display: hasShownClearButton.value ? 'flex' : 'none'
} as Record<string, any>;
return styleObject;
});
/** 清空输入框中的值 */
function onClearValue($event: MouseEvent) {
$event.stopPropagation();
if (shouldShowClearButton.value) {
changeTextBoxValue('');
hasShownClearButton.value = false;
context.emit('clear');
}
}
function onMouseEnter(event: MouseEvent) {
if (shouldShowClearButton.value && !isEmpty.value) {
hasShownClearButton.value = true;
}
}
function onMouseLeave(event: MouseEvent) {
if (shouldShowClearButton.value) {
hasShownClearButton.value = false;
}
}
return { clearButtonClass, clearButtonStyle, hasShownClearButton, onClearValue, onMouseEnter, onMouseLeave, shouldShowClearButton };
}
Ref
或者ComputeRef
类型对象承接条件表达式,不直接使用props对象。子组件
Class,子组件
Style命名ComputeRef
类型对象声明组件样式。 const buttonEditClass = computed(() => {
const classObject = {
'f-button-edit': true,
'f-cmp-inputgroup': true,
'f-button-edit-nowrap': !props.wrapText
} as Record<string, boolean>;
if (customClass.value) {
customClass.value.split(' ').reduce<Record<string, boolean>>((result: Record<string, boolean>, className: string) => {
result[className] = true;
return result;
}, classObject);
}
return classObject;
});
input-group.spec.tsx
import { mount } from '@vue/test-utils'
import { InputGroup } from '..'
describe('f-input-group', () => {
it('variant', () => {
const wrapper = mount({
setup() {
return () => {
return <InputGroup editable={false}></InputGroup>
}
}
})
expect(wrapper.find('.f-cmp-inputgroup').exists()).toBeTruthy()
expect(wrapper.find('div').find('div').find('input').find('[readlony]').exists).toBeTruthy()
})
})
F
前缀,组件选择器前使用f-
前缀。函数中混杂多层if
、for
循环编写的不同抽象层级,容易让人迷惑,不易读懂代码,只要做几次简单抽象,编写几个在同一抽象层级的函数,就可以一眼看懂代码。
function generateDictTree(visibleTreeNodes: VisualTreeNode[]) {
const childrenMap = generateChildrenMap(visibleTreeNodes);
const dictTree = generateDictTreeItems(visibleTreeNodes, childrenMap);
traversingAndRecordAncestor(dictTree);
traversingAndRecordDescendant(dictTree);
reorderDescendant(dictTree);
calculateDescendantsLineLength(dictTree);
return dictTree;
}
function traversingAndRecordAncestor(treeItems: DictTreeItem[]) {
treeItems.forEach((treeItem: DictTreeItem) => {
const currentId = treeItem.id;
treeItem.descendants.map((treeNodeId: number) => treeItems[treeNodeId])
.forEach((descendant: DictTreeItem) => {
descendant.ancestors = [...treeItem.ancestors, currentId];
});
});
}
名副其实说起来简单。我们想要强调,这事很严肃。选个好名字要花时间,但省下来的时间比花掉的多。注意命名,而且一旦发现有更好的名称,就换掉旧的。这么做,读你代码的人(包括你自己)都会更开心。
变量、函数或类的名称应该已经答复了所有的大问题。它该告诉你,它为什么会存在,它做什么事,应该怎么用。如果名称需要注释来补充,那就不算是名副其实。
例如:排序方法中的
a
,b
,可以替换为表达先后关系的preNodeId
,postNodeId
别用 accountList 来指称一组账号,除非它真的是 List 类型。List 一词对程序员有特殊意义。如果包纳账号的容器并非真是个 List,就会引起错误的判断。所以,用 accountGroup 或 bunchOfAccounts,甚至直接用 accounts 都会好一些。
如果程序员只是为满足编译器或解释器的需要而写代码,就会制造麻烦。例如,因为同一作用范围内两样不同的东西不能重名,你可能会随手改掉其中一个的名称。有时干脆以错误的拼写充数,结果就是出现在更正拼写错误后导致编译器出错的情况。 光是添加数字系列或是废话远远不够,即便这足以让编译器满意。如果名称必须相异,那其意思也应该不同才对。 以数字系列命名(a1、a2,……aN)是依义命名的对立面。这样的名称纯属误导——完全没有提供正确信息;没有提供导向作者意图的线索。
人类长于记忆和使用单词。大脑的相当一部分就是用来容纳和处理单词的。单词能读得出来。人类进化到大脑中有那么大的一块地方用来处理言语,若不善加利用,实在是种耻辱。 如果名称读不出来,讨论的时候就会像个傻鸟。“哎,这儿,鼻涕阿三喜摁踢(bee cee arr three cee enn tee)上头,有个皮挨死极翘(pee ess zee kyew)整数,看见没?”这不是小事,因为编程本就是一种社会活动。 有家公司,程序里面写了个 genymdhms(生成日期,年、月、日、时、分、秒),他们一般读作“gen why emm dee aich emm ess”。我有个见字照读的恶习,于是开口就念“gen-yah-mudda-hims”。后来好些设计师和分析师都有样学样,听起来傻乎乎的。我们知道典故,所以会觉得很搞笑。搞笑归搞笑,实际是在强忍糟糕的命名。在给新开发者解释变量的意义时,他们总是读出傻乎乎的自造词,而非恰当的英语词。比较
单字母名称和数字常量有个问题,就是很难在一大篇文字中找出来。 找 MAX_CLASSES_PER_STUDENT 很容易,但想找数字 7 就麻烦了,它可能是某些文件名或其他常量定义的一部分,出现在因不同意图而采用的各种表达式中。如果该常量是个长数字,又被人错改过,就会逃过搜索,从而造成错误。 同样,e 也不是个便于搜索的好变量名。它是英文中最常用的字母,在每个程序、每段代码中都有可能出现。由此而见,长名称胜于短名称,搜得到的名称胜于用自造编码代写就的名称。
函数的第一规则是要短小。第二条规则是还要更短小。我无法证明这个断言。我给不出任何证实了小函数更好的研究结果。我能说的是,近 40 年来,我写过各种不同大小的函数。我写过令人憎恶的长达 3000 行的厌物,也写过许多 100 行到 300 行的函数,我还写过 20 行到 30 行的。经过漫长的试错,经验告诉我,函数就该小。
函数应该做一件事。做好这件事。只做这一件事。
别害怕长名称。长而具有描述性的名称,要比短而令人费解的名称好。长而具有描述性的名称,要比描述性的长注释好。使用某种命名约定,让函数名称中的多个单词容易阅读,然后使用这些单词给函数取个能说清其功用的名称。
选择描述性的名称能理清你关于模块的设计思路,并帮你改进之。追索好名称,往往导致对代码的改善重构。
命名方式要保持一致。使用与模块名一脉相承的短语、名词和动词给函数命名。例如,includeSetupAndTeardownPages
、includeSetupPages
、includeSuiteSetupPage
和 includeSetupPage
等。这些名称使用了类似的措辞,依序讲出一个故事。
待上线 Doc 站点后补充
本项目的 Farris UI Vue 组件开发规范遵循CC-By 3.0 协议。
此处可能存在不合适展示的内容,页面不予展示。您可通过相关编辑功能自查并修改。
如您确认内容无涉及 不当用语 / 纯广告导流 / 暴力 / 低俗色情 / 侵权 / 盗版 / 虚假 / 无价值内容或违法国家有关法律法规的内容,可点击提交进行申诉,我们将尽快为您处理。