75 Star 187 Fork 98

UBML / farris-vue

加入 Gitee
与超过 1200万 开发者一起发现、参与优秀开源项目,私有仓库也完全免费 :)
免费加入
克隆/下载
vue_component_style_guide.md 18.17 KB
一键复制 编辑 原始数据 按行查看 历史
Sagi 提交于 2024-05-20 00:28 . docs: update component style guide

Farris UI Vue 组件开发规范

为了保证 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属性声明默认值。
  • 在组件的props对象中,请按英文字母升序排序。
  • 使用多行注释语法注释props对象中的属性。
  • 为兼容元组件标准,在声明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函数形参propscontext的类型。
  • setup函数内采用ref函数承接,props对象属性,不在render函数内直接使用props.someProps访问属性对象。
  • 以特性划分composition api。
  • 为方便传递复用compostion api对象,可以以use{Feature}Composition为名称声明composition api对象,以便于传递复用,以解构形式提取composition api对象属性。
  • 不在render方法内实现组件html细节,将组件html片段拆解到子组件render方法,以render子组件方式命名子组件render方法,以便于可以语义化阅读render方法。
  • 不在render方法中写if语句,采用shouldShow子组件声明Ref对象或者ComputeRef对象承接条件表达式,使用逻辑表达式实现按条件渲染。

组合式 Api 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类型对象声明组件样式。
  • 采用对象形式声明class,对于有自定义class的子组件,采用以下形式承接扩展样式。
    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-前缀。
  • 直接用 v-model 传递双向绑定的参数。
  • 使用属性透传传递原生属性,不允许在单独定义 API 声明原生属性。
  • 正确定义和使用 TypeScript 类型,代码中无 TypeScript 类型报错。
  • 变量采用语义化命名,原则上不需要通过注释说明变量或函数功能,详细命名规则参考Farris UI TypeScript 编码指南
  • 需要将组件的 props 定义在独立的文件 some-component.props.ts 文件中,并在此文件中同时导出 props 和 PropsType。
  • 应该在 setup 函数返回的 render 函数中编写组件的 Html 模板。
  • 必须在组件的 index.ts 文件中导出组件参数的类型,以便于在引用组件时,方便 TypeScript 进行类型提示。
  • defineComponent 函数接收的参数顺序为 name、props、emits、inheritAttrs、setup。
  • 不要在组件内显式声明 components 和 directives。
  • 需要按照字典顺序排列组件的变量。

「Clean Code」原则

遵循「One Level Of Abstraction Per Function」原则

函数中混杂多层iffor循环编写的不同抽象层级,容易让人迷惑,不易读懂代码,只要做几次简单抽象,编写几个在同一抽象层级的函数,就可以一眼看懂代码。

  function generateDictTree(visibleTreeNodes: VisualTreeNode[]) {
      const childrenMap = generateChildrenMap(visibleTreeNodes);
      const dictTree = generateDictTreeItems(visibleTreeNodes, childrenMap);
      traversingAndRecordAncestor(dictTree);
      traversingAndRecordDescendant(dictTree);
      reorderDescendant(dictTree);
      calculateDescendantsLineLength(dictTree);
      return dictTree;
  }

遵循「Explain Yourself In Code」原则,使用数组的forEach、map方法更好的表达代码

  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];
              });
      });
  }

Use Intention-Revealing Names 名副其实

名副其实说起来简单。我们想要强调,这事很严肃。选个好名字要花时间,但省下来的时间比花掉的多。注意命名,而且一旦发现有更好的名称,就换掉旧的。这么做,读你代码的人(包括你自己)都会更开心。

变量、函数或类的名称应该已经答复了所有的大问题。它该告诉你,它为什么会存在,它做什么事,应该怎么用。如果名称需要注释来补充,那就不算是名副其实。

例如:排序方法中的a,b,可以替换为表达先后关系的preNodeId, postNodeId

Avoid Disinformation 避免误导

别用 accountList 来指称一组账号,除非它真的是 List 类型。List 一词对程序员有特殊意义。如果包纳账号的容器并非真是个 List,就会引起错误的判断。所以,用 accountGroup 或 bunchOfAccounts,甚至直接用 accounts 都会好一些。

Make Meaningful Distinctions 做有意义的区分

如果程序员只是为满足编译器或解释器的需要而写代码,就会制造麻烦。例如,因为同一作用范围内两样不同的东西不能重名,你可能会随手改掉其中一个的名称。有时干脆以错误的拼写充数,结果就是出现在更正拼写错误后导致编译器出错的情况。 光是添加数字系列或是废话远远不够,即便这足以让编译器满意。如果名称必须相异,那其意思也应该不同才对。 以数字系列命名(a1、a2,……aN)是依义命名的对立面。这样的名称纯属误导——完全没有提供正确信息;没有提供导向作者意图的线索。

Use Pronounceable Names 使用读得出来的名称

人类长于记忆和使用单词。大脑的相当一部分就是用来容纳和处理单词的。单词能读得出来。人类进化到大脑中有那么大的一块地方用来处理言语,若不善加利用,实在是种耻辱。 如果名称读不出来,讨论的时候就会像个傻鸟。“哎,这儿,鼻涕阿三喜摁踢(bee cee arr three cee enn tee)上头,有个皮挨死极翘(pee ess zee kyew)整数,看见没?”这不是小事,因为编程本就是一种社会活动。 有家公司,程序里面写了个 genymdhms(生成日期,年、月、日、时、分、秒),他们一般读作“gen why emm dee aich emm ess”。我有个见字照读的恶习,于是开口就念“gen-yah-mudda-hims”。后来好些设计师和分析师都有样学样,听起来傻乎乎的。我们知道典故,所以会觉得很搞笑。搞笑归搞笑,实际是在强忍糟糕的命名。在给新开发者解释变量的意义时,他们总是读出傻乎乎的自造词,而非恰当的英语词。比较

Use Searchable Names 使用可搜索的名称

单字母名称和数字常量有个问题,就是很难在一大篇文字中找出来。 找 MAX_CLASSES_PER_STUDENT 很容易,但想找数字 7 就麻烦了,它可能是某些文件名或其他常量定义的一部分,出现在因不同意图而采用的各种表达式中。如果该常量是个长数字,又被人错改过,就会逃过搜索,从而造成错误。 同样,e 也不是个便于搜索的好变量名。它是英文中最常用的字母,在每个程序、每段代码中都有可能出现。由此而见,长名称胜于短名称,搜得到的名称胜于用自造编码代写就的名称。

方法

Small! 短小

函数的第一规则是要短小。第二条规则是还要更短小。我无法证明这个断言。我给不出任何证实了小函数更好的研究结果。我能说的是,近 40 年来,我写过各种不同大小的函数。我写过令人憎恶的长达 3000 行的厌物,也写过许多 100 行到 300 行的函数,我还写过 20 行到 30 行的。经过漫长的试错,经验告诉我,函数就该小。

Do One Thing 只做一件事

函数应该做一件事。做好这件事。只做这一件事。

Use Descriptive Names 使用描述性的名称

别害怕长名称。长而具有描述性的名称,要比短而令人费解的名称好。长而具有描述性的名称,要比描述性的长注释好。使用某种命名约定,让函数名称中的多个单词容易阅读,然后使用这些单词给函数取个能说清其功用的名称。 选择描述性的名称能理清你关于模块的设计思路,并帮你改进之。追索好名称,往往导致对代码的改善重构。 命名方式要保持一致。使用与模块名一脉相承的短语、名词和动词给函数命名。例如,includeSetupAndTeardownPagesincludeSetupPagesincludeSuiteSetupPageincludeSetupPage 等。这些名称使用了类似的措辞,依序讲出一个故事。

组件文档规范

待上线 Doc 站点后补充

关于

本项目的 Farris UI Vue 组件开发规范遵循CC-By 3.0 协议

Creative Commons License

TypeScript
1
https://gitee.com/ubml/farris-vue.git
git@gitee.com:ubml/farris-vue.git
ubml
farris-vue
farris-vue
main

搜索帮助