编码规范

HTML规范

HTML5 文档类型

  • 在每个 HTML 页面的开头使用这个简单的文档类型,在每个浏览器中强制执行标准模式和更一致的呈现。

  • 从 HTML5 规范:

    鼓励作者在根 html 元素上指定 lang 属性,给出文档的语言。这有助于语音合成工具确定使用什么发音,帮助翻译工具确定使用什么规则,等等。

<!doctype html>
<html lang="zh-CN">
  <head>
  </head>
</html>

语法部分

  • 不要大写标签,包括文档类型。
  • 使用带有两个空格的软制表符,它们是保证代码在任何环境中呈现相同的唯一方法。
  • 嵌套元素应该缩进一次(两个空格)。
  • 始终在属性上使用双引号,而不是单引号。
  • 在自闭合元素中包含斜杠(兼容jsx)。

字符编码

通过声明明确的字符编码,快速轻松地确保正确呈现您的内容。这样做时,可以避免在 HTML 中使用字符实体,只要它们的编码与文档的编码匹配(通常是 UTF-8)。

CSS 和 JavaScript 引入

  • 根据 HTML5 规范,通常不需要type在包含 CSS 和 JavaScript 文件时指定 text/csstext/javascript是它们各自的默认值。
  • 为避免页面阻塞渲染,CSS放在head底部引入,js放在body底部引入。

布尔属性

布尔属性是不需要声明值的属性。声明其属性名即为true,不写即为false

<select>
  <option value="1" selected>1</option>
</select>

<video control autoplay></video>

减少不必要的嵌套

尽可能在编写 HTML 时避免多余的父元素。举个例子:

<!-- Bad -->
<span class="avatar">
  <img src="...">
</span>

<!-- Good -->
<img class="avatar" src="...">

CSS规范

语法部分

  • 使用带有两个空格的软制表符——它们是保证代码在任何环境中呈现相同的唯一方法。
  • 对选择器进行分组时,将各个选择器保持在一行中。
  • 为了便于阅读,在声明块的左大括号之前包含一个空格。
  • 将声明块的左打括号放在选择器本行,右大括号放在新行上。
  • 每个声明都应该出现在自己的行中,以便更准确地报告错误。
  • 以分号结束所有声明。最后一个声明是可选的,但为了避免出错的可能性,统一分号结束。
  • 逗号分隔的属性值应在每个逗号后包含一个空格(例如,box-shadow)。
  • 不要 rgb(), rgba(), hsl(), hsla(), 或rect()值中包含逗号后的空格。这有助于将多个颜色值(逗号,无空格)与多个属性值(逗号和空格)区分开来。
  • 不要用前导零作为属性值或颜色参数的前缀(例如,.5代替0.5-.5px代替-0.5px)。
  • 小写所有十六进制值,例如#fff. 查看时,小写字母更容易辨别。
  • 在可用的情况下使用简写十六进制值,例如,#fff而不是#ffffff
  • 避免为零值指定单位,例如,margin: 0;而不是margin: 0px;

书写顺序

  1. 定位属性
  2. 盒子模型
  3. 视觉展示
  4. 文字排版
  5. 其它项

定位是第一位的,因为它可以从正常的文档流中移除一个元素并覆盖与盒子模型相关的样式。盒子模型无论是 flex、float、grid 还是 table都遵循它规定了组件的尺寸、放置和对齐方式。其他一切都发生组件内部或不影响前两个部分,因此它们排在最后。

.example {
  /* 定位属性 */
  position: absolute;
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
  z-index: 100;

  /* 盒子模型 */
  display: flex;
  align-items: center;
  width: 100px;
  height: 100px;
  
  /* 视觉展示 */
  background-color: #f5f5f5;
  border: 1px solid #e5e5e5;
  border-radius: 3px;

  /* 文字排版 */
  font: normal 13px "Helvetica Neue", sans-serif;
  line-height: 1.5;
  color: #333;
  text-align: center;

  /* 其他项 */
  opacity: 1;
  cursor: pointer;
}

媒体查询放置

尽可能将媒体查询放置在其相关规则集附近。不要将它们全部捆绑在单独的样式表中或文档末尾。

.element { ... }
.element-avatar { ... }
.element-selected { ... }

@media (min-width: 480px) {
  .element { ...}
  .element-avatar { ... }
  .element-selected { ... }
}

减少嵌套层级

避免不必要的嵌套。仅仅因为可以嵌套,并不意味着就总是应该。仅当必须将样式范围限定为父级并且要嵌套多个元素时才考虑嵌套。

React规范

导入顺序

导入顺序为 node_modules -> $src/开头文件 -> 相对路径文件 -> 当前组件样式文件

// 导入 node_modules 依赖
import React from 'react';
// 导入公共组件
import Button from '$src/components/Button';
// 导入相对路径组件
import Foo from './Foo';
// 导入对应同名的 .less 文件,命名为 styles
import styles from './style.module.scss';

一个组件对应一个样式文件

我们以组件的颗粒度大小为抽象单元,样式文件则应与组件本身保持一致。不推荐交叉引入样式文件的做法,这样会导致重构混乱,无法明确当前这个样式被多少个组件使用。

- Foo.jsx
- Foo.module.scss
- Bar.jsx
- Bar.module.scss

组件内部编写代码的顺序

组件内部的顺序为 state -> custom Hooks -> effects -> 内部 function -> 其他逻辑 -> JSX

/**
 * 组件注释(简明概要)
 */
const Foo: React.FC<FooProps> = ({ bar }) => {
  // 1. state

  // 2. custom Hooks

  // 3. effects

  // 4. 内部 function

  // 5. 其他逻辑...

  return (
    <div className={styles.wrapper}>
      {bar}
      <Child />
    </div>
  );
};

事件函数命名区分

内部方法按照 handle{Type}{Event} 命名,例如 handleNameChange。暴露外部的方法按照 on{Type}{Event},例如 onNameChange。这样做的好处可以直接通过函数名区分是否为外部参数。

例如 antd/Button 组件片段:

const handleClick = (e: React.MouseEvent<HTMLButtonElement | HTMLAnchorElement, MouseEvent>) => {
  const { onClick, disabled } = props;
  if (innerLoading || disabled) {
    e.preventDefault();
    return;
  }
  (onClick as React.MouseEventHandler<HTMLButtonElement | HTMLAnchorElement>)?.(e);
};

不要直接使用 export default 导出未命名的组件

这种方式导出的组件在 React Inspector 查看时会显示为 Unknown

// 错误做法
export default () => {};

// 正确做法
export default function Foo() {}

// 推荐做法
function Foo() {}

export default Foo;

受控/非受控状态

React 表单管理中有两个经常使用的术语: 受控输入和非受控输入。简单来说,受控的意思是当前组件的状态成为该表单的唯一数据源。表明这个表单的值在当前组件的控制中,并只能通过 setState 来更新。

受控/非受控的概念在组件设计上极为常见。受控组件通常以 valueonChange 成对出现。传入到子组件中,子组件无法直接修改这个 value,只能通过 onChange 回调告诉父组件更新。 非受控组件则可以传入 defaultValue 属性来提供初始值。

若该状态作为组件的核心逻辑时,那么它应该支持受控,或兼容非受控模式。若该状态为次要逻辑,可以根据实际情况选择性支持受控模式。

例如 Select 组件处理受控与非受控逻辑:

function Select(props) {
  // value 和 onChange 为核心逻辑,支持受控。兼容传入 defaultValue 成为非受控
  // defaultOpen 为次要逻辑,可以非受控
  const { value: controlledValue, onChange: onControlledChange, defaultValue, defaultOpen } = props;
  // 非受控模式使用内部 state
  const [innerValue, onInnerValueChange] = React.useState(defaultValue);
  // 次要逻辑,选择框展开状态
  const [visible, setVisible] = React.useState(defaultOpen);

  // 通过检测参数上是否包含 value 的属性判断是否为受控,尽管 value 为 undefined
  const shouldControlled = Reflect.has(props, 'value');

  // 支持受控和非受控处理
  const value = shouldControlled ? controlledValue : innerValue;
  const onChange = shouldControlled ? onControlledChange : onInnerValueChange;

  // ...
}

通用性原则

通用性设计其实是一定意义上放弃对 DOM 的掌控,而将 DOM 结构的决定权转移给开发者,比如预留自定义渲染。

举个例子, fusion 中的 Table通过 cell 函数将每个单元格渲染的决定权交给使用者,这样极大提高了组件的可扩展性:

const columns = [
  {
    title: '名称',
    dataIndex: 'name',
    width: 200,
    cell(value) {
      return <em>{value}</em>;
    },
  },
];

<Table columns={columns} />;

统一 API

当各个组件数量变多之后,组件与组件直接可能存在某种契合的关系,我们可以统一某种行为 API 的一致性,这样可以降低使用者对各个组件 API 名称的心智负担。否则组件传参就会如同一根一根数面条一样痛苦。

举个例子,经典的 valueonChange 的 API 可以在各个不同的表单域上出现。可以通过包装的方式导出更多高阶组件,这些高阶组件又可以被表单管理组件所容纳。

我们可以约定在各个组件上比如 visibleonVisibleChangeborderedsizeonSubmit 这样的 API,使其在各个组件中保持一致性。

tsx规范

使用 组件名 + Props 形式命名 Props 类型并导出。

类型与参数书写的顺序保持一致。变量的注释禁止放末尾,原因是会导致编辑器识别错位,无法正确提示

/**
 * 类型定义(命名:组件名 + Props)
 */
export interface FooProps {
  /**
   * 多行注释(建议)
   */
  email: string;
  // 单行注释(不推荐)
  mobile: string;
  username: string; // 末尾注释(禁止)
}

使用 React.FC 定义

const Foo: React.FC<FooProps> = ({ email, mobile, usename }) => {};

泛型,代码提示更智能

以下例子,可以用过泛型让 valueonChange 回调中的类型保持一致,并做到编辑器智能类型提示。

export interface FooProps<T> {
  value: T;
  onChange: (value: T) => void;
}

export function Foo<T extends React.Key>(props: FooProps<T>) {}

禁止直接使用 any 类型

无论隐式和显式的方式,都不推荐使用 any 类型。定义了 any 的参数会让使用该组件的人产生极度困惑,无法明确地知道其中的类型。我们可以通过泛型的方式去声明。

// 隐式 any (禁止)
let foo;
function bar(param) {}

// 显式 any (禁止)
let hello: any;
function world(param: any) {}

// 使用泛型继承,缩小类型范围 (推荐)
function Foo<P extends Record<string, unknow>>(param: P) {}
Loading...