优先级 A 的规则:必要的
组件名为多个单词
组件名应该始终是多个单词的,根组件 App
以及 <transition>
、<component>
之类的 Vue 内置组件除外。
这样做可以避免跟现有的以及未来的 HTML 元素相冲突,因为所有的 HTML 元素名称都是单个单词的。
Prop 定义应该尽量详细
在你提交的代码中,prop 的定义应该尽量详细,至少需要指定其类型。
细致的 prop 定义有两个好处:
- 它们写明了组件的 API,所以很容易看懂组件的用法;
- 在开发环境下,如果向一个组件提供格式不正确的 prop,Vue 将会告警,以帮助你捕获潜在的错误来源。
禁止将 Prop 的类型定义为 Props
在定义类型是避免定义为 Props
名字的类型,添加上组件明前缀类似 ContextMenuProps
,才是正确的选择,否则会导致编译时出现异常 error TS4082: Default export of the module has or is using private name 'Props'.
反例
interface Props {
menuItems: ContextMenuItem[];
}
const props = defineProps<Props>();
正例
- 使用外部导入形式定义类型
ContextMenuProps
后导入到组件中。
import type { ContextMenuProps } from './types.ts';
const props = defineProps<ContextMenuProps>();
- 在组件中定义
ContextMenuProps
类型直接使用。
interface ContextMenuProps {
menuItems: ContextMenuItem[];
}
const props = defineProps<ContextMenuProps>();
- 不定义类型直接在
defineProps
定义泛型。
const props = defineProps<{
menuItems: ContextMenuItem[];
}>();
为 v-for 设置键值
在组件上总是必须用 key 配合 v-for,以便维护内部组件及其子树的状态。并且,key 最好始终添加一个唯一的键值。甚至在元素上维护可预测的行为,比如动画中的对象固化 (object constancy),也是一种好的做法。
详解:
假设你有一个待办事项列表:
const tools = ref([ { id: 1, text: '学习使用 v-for', }, { id: 2, text: '学习使用 key', }, ]);
然后你把它们按照字母顺序排序。在更新 DOM 的时候,Vue 将会优化渲染把可能的 DOM 变更降到最低。即可能删掉第一个待办事项元素,然后把它重新加回到列表的最末尾。
- 这里的问题在于,不要删除仍然会留在 DOM 中的元素。比如你想使用
<TransitionGroup>
给列表加过渡动画,或想在被渲染元素是<input>
时保持聚焦。在这些情况下,为每一个项目添加一个唯一的键值 (比如 :key="todo.id") 将会让 Vue 知道如何使行为更容易预测。
- 根据我们的经验,最好始终添加一个唯一的键值,以便你和你的团队永远不必担心这些极端情况。也在少数对性能有严格要求的情况下,为了避免对象固化,你可以刻意做一些非常规的处理。
避免 v-if 和 v-for 用在一起
永远不要把 v-if 和 v-for 同时用在同一个元素上。因为 v-for 比 v-if 具有更高的优先级。
一般我们在两种常见的情况下会倾向于这样做:
- 为了过滤一个列表中的项目 (比如
v-for="user in users" v-if="user.isActive"
)。在这种情形下,请将users
替换为一个计算属性 (比如activeUsers
),让其返回过滤后的列表。 - 为了避免渲染本应该被隐藏的列表 (比如
v-for="user in users" v-if="shouldShowUsers"
)。这种情形下,请将v-if
移动至容器元素上 (比如ul
、ol
)。
详解:
当 Vue 处理指令时,v-for 比 v-if 具有更高的优先级,所以这个模板:
<ul> <li v-for="user in users" v-if="user.isActive" :key="user.id"> {{ user.name }} </li> </ul>
将会经过如下运算:
users.map(function (user) { if (user.isActive) { return user.name; } });
因此哪怕我们只渲染出一小部分用户的元素,也得在每次重渲染的时候遍历整个列表,不论活跃用户是否发生了变化。
通过将其更换为在如下的一个计算属性上遍历:
const activeUsers = computed(() => { return users.filter(function (user) { return user.isActive; }); });
<ul> <li v-for="user in activeUsers" :key="user.id" > {{ user.name }} </li> </ul>
我们将会获得如下好处:
- 过滤后的列表只会在 users 数组发生相关变化时才被重新运算,过滤更高效。
- 使用
v-for="user in activeUsers"
之后,我们在渲染的时候只遍历活跃用户,渲染更高效。- 解耦渲染层的逻辑,可维护性 (对逻辑的更改和扩展) 更强。
为了获得同样的好处,我们也可以把:
<ul> <li v-for="user in users" v-if="shouldShowUsers" :key="user.id"> {{ user.name }} </li> </ul>
更新为:
<ul v-if="shouldShowUsers"> <li v-for="user in users" :key="user.id"> {{ user.name }} </li> </ul>
通过将
v-if
移动到容器元素,我们不会再对列表中的每个用户检查shouldShowUsers
。取而代之的是,我们只检查它一次,且不会在shouldShowUsers
为否的时候运算v-for
。
为组件样式设置作用域
对于应用来说,顶级 App 组件和布局组件中的样式可以是全局的,但是其它所有组件都应该是有作用域的。
这条规则只和单文件组件有关。你不一定要使用 scoped attribute 。设置作用域也可以通过 CSS Modules,那是一个基于 class 的类似 BEM 的策略,当然你也可以使用其它的库或约定。
不管怎样,对于组件库,我们应该更倾向于选用基于 class 的策略而不是 scoped
attribute。
这让覆写内部样式更容易:使用了常人可理解的 class 名称且没有太高的选择器优先级,而且不太会导致冲突。
详解:
如果你和其他开发者一起开发一个大型工程,或有时引入三方 HTML/CSS (比如来自 Auth0),设置一致的作用域会确保你的样式只会运用在它们想要作用的组件上。
不止要使用 `scoped` attribute,使用唯一的 class 名可以帮你确保那些三方库的 CSS 不会运用在你自己的 HTML 上。比如许多工程都使用了 `button`、`btn` 或 `icon` class 名,所以即便你不使用类似 BEM 的策略,添加一个 app 专属或组件专属的前缀 (比如 `ButtonClose-icon`) 也可以提供很多保护。
反例:
<template> <button class="btn btn-close">X</button> </template> <style> .btn-close { background-color: red; } </style>
正例:
<template> <button class="button button-close">X</button> </template> <!-- 使用 `scoped` attribute --> <style scoped> .button { border: none; border-radius: 2px; } .button-close { background-color: red; } </style>
<template> <button :class="[$style.button, $style.button - close]">X</button> </template> <!-- 使用 CSS Modules --> <style module> .button { border: none; border-radius: 2px; } .button-close { background-color: red; } </style>
<template> <button class="c-button c-button-close">X</button> </template> <!-- 使用 BEM 约定 --> <style> .c-button { border: none; border-radius: 2px; } .c-button-close { background-color: red; } </style>