Vue
要开始使用 Vue,请阅读 Vue 官方文档。
示例
以下章节描述的内容可在这些示例中找到:
何时添加 Vue 应用
有时,HAML 页面就足够满足需求。此说法主要适用于静态页面或逻辑非常简单的页面。我们如何判断是否值得在页面中添加 Vue 应用?答案是:“当我们需要维护应用状态并使其与渲染的页面保持同步时”。
为了更好地解释这一点,让我们想象一个页面上有一个开关,切换它会发送 API 请求。这种情况不涉及需要维护的状态,我们只需发送请求并切换开关即可。但如果再添加一个开关,它必须始终与第一个开关状态相反,这时就需要一个 状态:一个开关需要"感知"另一个开关的状态。如果用原生 JavaScript 实现,这种逻辑通常需要监听 DOM 事件并修改 DOM 来响应。这类情况使用 Vue.js 会更容易处理,因此我们应该在此处创建 Vue 应用。
如何向页面添加 Vue 应用
- 在
app/assets/javascripts中为你的 Vue 应用创建新文件夹。 - 添加 页面特定 JavaScript 来加载你的应用。
- 你可以使用 `initSimpleApp 辅助函数 来简化 从 HAML 向 JS 传递数据。
哪些信号表明你可能需要 Vue 应用?
- 当你需要基于多个因素定义复杂条件并在用户交互时更新它们;
- 当你需要维护任何形式的应用状态并在标签/元素间共享;
- 当你预期未来会添加复杂逻辑 - 从基础 Vue 应用开始,比后续将 JS/HAML 重写为 Vue 更容易。
避免页面中存在多个 Vue 应用
过去,我们通过向渲染的 HAML 页面不同部分添加多个小型 Vue 应用来逐步增强页面交互性。然而,这种方法导致了多个问题:
- 大多数情况下,这些应用不共享状态,独立执行 API 请求,导致请求数量增加;
- 我们必须通过多个端点从 Rails 向 Vue 提供数据;
- 我们无法在页面加载后动态渲染 Vue 应用,导致页面结构变得僵化;
- 我们无法充分利用客户端路由替代 Rails 路由;
- 多个应用会导致不可预测的用户体验、增加页面复杂度、使调试过程更困难;
- 应用间的通信方式会影响 Web Vitals 指标。
由于这些原因,在已有 Vue 应用的页面(这不包括新旧导航)中添加新 Vue 应用时,我们需要谨慎。在添加新应用前,请确保绝对不可能通过扩展现有应用来实现所需功能。如有疑问,请随时在 #frontend 或 #frontend-maintainers Slack 频道寻求架构建议。
如果仍需添加新应用,请确保它与现有应用共享本地状态。 学习:如何选择状态管理器?
Vue 架构
我们通过 Vue 架构试图实现的主要目标是:只有一个数据流,且只有一个数据入口。 为实现此目标,我们使用 Pinia 或 Apollo Client
你还可以在 Vue 文档中阅读关于 状态管理 和单向数据流 的内容。
组件与 Store
在 Vue.js 实现的某些功能中,如 问题看板 或 环境表格 你可以看到清晰的职责分离:
new_feature
├── components
│ └── component.vue
│ └── ...
├── store
│ └── new_feature_store.js
├── index.js为保持一致性,建议你遵循相同结构。
让我们逐一查看它们:
index.js 文件
这是新功能的入口文件。新功能的根 Vue 实例应在此处。
Store 和 Service 应在此文件中导入并初始化,作为 prop 提供给主组件。
请务必阅读 页面特定 JavaScript。
启动注意事项
从 HAML 向 JavaScript 提供数据
在挂载 Vue 应用时,你可能需要从 Rails 向 JavaScript 提供数据。为此,可以在 HTML 元素中使用 data 属性,并在挂载应用时查询它们。这仅在初始化应用时进行,因为挂载的元素会被 Vue 生成的 DOM 替换。
data 属性仅能接受字符串值,因此你需要将其他变量类型转换为字符串。
通过 props 或在 render 函数中使用 provide 从 DOM 向 Vue 实例提供数据(而不是在主 Vue 组件中查询 DOM)的优势在于:在单元测试中无需创建 fixture 或 HTML 元素。
initSimpleApp 辅助函数
initSimpleApp 是一个简化 Vue.js 中挂载组件过程的辅助函数。它接受两个参数:表示 HTML 中挂载点的选择器字符串,以及一个 Vue 组件。
使用 initSimpleApp:
- 在页面中包含一个具有 ID 或唯一类的 HTML 元素。
- 添加包含 JSON 对象的
data-view-model属性。 - 导入所需的 Vue 组件,并将其与有效的 CSS 选择器字符串一起传递给
initSimpleApp。该字符串选择 HTML 元素并在此处挂载组件。
initSimpleApp 会自动检索 data-view-model 属性的内容作为 JSON 对象,并将其作为 props 传递给挂载的 Vue 组件。这可用于预填充组件数据。
示例:
//my_component.vue
<template>
<div>
<p>Prop1: {{ prop1 }}</p>
<p>Prop2: {{ prop2 }}</p>
</div>
</template>
<script>
export default {
name: 'MyComponent',
props: {
prop1: {
type: String,
required: true
},
prop2: {
type: Number,
required: true
}
}
}
</script><div id="js-my-element" data-view-model='{"prop1": "my object", "prop2": 42 }'></div>//index.js
import MyComponent from './my_component.vue'
import { initSimpleApp } from '~/helpers/init_simple_app_helper'
initSimpleApp('#js-my-element', MyComponent, { name: 'MyAppRoot' })使用 provide/inject 而非 props 传递值
要使用 initSimpleApp 通过 provide/inject 传递值:
- 在页面中包含一个具有 ID 或唯一类的 HTML 元素。
- 添加包含 JSON 对象的
data-provide属性。 - 导入所需的 Vue 组件,并将其与有效的 CSS 选择器字符串一起传递给
initSimpleApp。该字符串选择 HTML 元素并在此处挂载组件。
initSimpleApp 会自动检索 data-provide 属性的内容作为 JSON 对象,并将其作为 inject 传递给挂载的 Vue 组件。这可用于预填充组件数据。
示例:
//my_component.vue
<template>
<div>
<p>Inject1: {{ inject1 }}</p>
<p>Inject2: {{ inject2 }}</p>
</div>
</template>
<script>
export default {
name: 'MyComponent',
inject: {
inject1: {
default: '',
},
inject2: {
default: 0
}
},
}
</script><div id="js-my-element" data-provide='{"inject1": "my object", "inject2": 42 }'></div>//index.js
import MyComponent from './my_component.vue'
import { initSimpleApp } from '~/helpers/init_simple_app_helper'
initSimpleApp('#js-my-element', MyComponent, { name: 'MyAppRoot' })provide 和 inject
Vue 通过 provide 和 inject 支持依赖注入。在组件中,inject 配置可访问 provide 传递下来的值。以下 Vue 应用初始化示例展示了 provide 配置如何将值从 HAML 传递给组件:
#js-vue-app{ data: { endpoint: 'foo' }}
// index.js
const el = document.getElementById('js-vue-app');
if (!el) return false;
const { endpoint } = el.dataset;
return new Vue({
el,
name: 'MyComponentRoot',
render(createElement) {
return createElement('my-component', {
provide: {
endpoint
},
});
},
});组件或其任何子组件可通过 inject 访问该属性:
<script>
export default {
name: 'MyComponent',
inject: ['endpoint'],
...
...
};
</script>
<template>
...
...
</template>当满足以下条件时,使用依赖注入从 HAML 提供值是理想选择:
- 注入的值无需对其数据类型或内容进行显式验证。
- 该值无需是响应式的。
- 在层次结构中存在多个组件需要访问该值,此时 prop 传递(prop-drilling)变得不便。prop 传递是指相同的 prop 通过层次结构中的所有组件传递,直到真正使用它的组件。
如果同时满足以下两个条件,依赖注入可能会破坏子组件(直接子组件或多层嵌套子组件):
inject配置中声明的值未定义默认值。- 父组件未使用
provide配置提供该值。
在需要默认值的情况下,默认值 可能很有用。
props
如果来自 HAML 的值不符合依赖注入的条件,请使用 props。请参考以下示例。
// haml
#js-vue-app{ data: { endpoint: 'foo' }}
// index.js
const el = document.getElementById('js-vue-app');
if (!el) return false;
const { endpoint } = el.dataset;
return new Vue({
el,
name: 'MyComponentRoot',
render(createElement) {
return createElement('my-component', {
props: {
endpoint
},
});
},
});为挂载 Vue 应用而添加 id 属性时,请确保该 id 在代码库中是唯一的。
有关我们明确声明传入 Vue 应用数据的更多信息,请参阅我们的 Vue 风格指南。
向 Vue 应用提供 Rails 表单字段
使用 Rails 编写表单时,表单输入的 name、id 和 value 属性会生成以匹配后端。在将 Rails 表单转换为 Vue 时,或在 集成组件(如日期选择器或项目选择器)到表单中时,能够访问这些生成的属性会很有帮助。parseRailsFormFields 工具函数可用于解析生成的表单输入属性,以便将其传递给 Vue 应用。这使我们能够集成 Vue 组件,而无需更改表单提交方式。
-# form.html.haml
= form_for user do |form|
.js-user-form
= form.text_field :name, class: 'form-control gl-form-input', data: { js_name: 'name' }
= form.text_field :email, class: 'form-control gl-form-input', data: { js_name: 'email' }js_name 数据属性用作结果 JavaScript 对象中的键。例如 = form.text_field :email, data: { js_name: 'fooBarBaz' } 将被转换为 { fooBarBaz: { name: 'user[email]', id: 'user_email', value: '' } }
// index.js
import Vue from 'vue';
import { parseRailsFormFields } from '~/lib/utils/forms';
import UserForm from './components/user_form.vue';
export const initUserForm = () => {
const el = document.querySelector('.js-user-form');
if (!el) {
return null;
}
const fields = parseRailsFormFields(el);
return new Vue({
el,
name: 'UserFormRoot',
render(h) {
return h(UserForm, {
props: {
fields,
},
});
},
});
};<script>
// user_form.vue
import { GlButton, GlFormGroup, GlFormInput } from '@gitlab/ui';
export default {
name: 'UserForm',
components: { GlButton, GlFormGroup, GlFormInput },
props: {
fields: {
type: Object,
required: true,
},
},
};
</script>
<template>
<div>
<gl-form-group :label-for="fields.name.id" :label="__('Name')">
<gl-form-input v-bind="fields.name" width="lg" />
</gl-form-group>
<gl-form-group :label-for="fields.email.id" :label="__('Email')">
<gl-form-input v-bind="fields.email" type="email" width="lg" />
</gl-form-group>
<gl-button type="submit" category="primary" variant="confirm">{{ __('Update') }}</gl-button>
</div>
</template>访问 gl 对象
我们在查询 DOM 的同一位置查询 gl 对象中在应用生命周期内不会变化的数据。通过遵循此实践,我们可以避免模拟 gl 对象,从而使测试更容易。这应在初始化 Vue 实例时完成,数据应作为 props 提供给主组件:
return new Vue({
el: '.js-vue-app',
name: 'MyComponentRoot',
render(createElement) {
return createElement('my-component', {
props: {
avatarUrl: gl.avatarUrl,
},
});
},
});访问权限(Abilities)
将权限推送到 前端 后,使用 Vue 中的 provide 和 inject 机制,使权限对 Vue 应用中任何后代组件可用。glAbilties 对象已在 commons/vue.js 中提供,因此只需使用 mixin 即可使用这些标志:
// 任意后代组件
import glAbilitiesMixin from '~/vue_shared/mixins/gl_abilities_mixin';
export default {
// ...
mixins: [glAbilitiesMixin()],
// ...
created() {
if (this.glAbilities.someAbility) {
// ...
}
},
}访问功能标志(Feature Flags)
将功能标志推送到 前端 后,使用 Vue 中的 provide 和 inject 机制,使功能标志对 Vue 应用中任何后代组件可用。glFeatures 对象已在 commons/vue.js 中提供,因此只需使用 mixin 即可使用这些标志:
// 任意后代组件
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
export default {
// ...
mixins: [glFeatureFlagsMixin()],
// ...
created() {
if (this.glFeatures.myFlag) {
// ...
}
},
}这种方法有几个好处:
-
任意深度嵌套的组件可以选择并访问该标志,无需中间组件知晓(通过 props 传递标志)。
-
良好的可测试性,因为标志可以作为 prop 从
vue-test-utils的mount/shallowMount提供。import { shallowMount } from '@vue/test-utils'; shallowMount(component, { provide: { glFeatures: { myFlag: true }, }, }); -
除了在应用的 入口点 外,无需访问全局变量。
页面重定向与显示警告
如果需要重定向到另一个页面并显示警告,可以使用 visitUrlWithAlerts 工具函数。当你重定向到新创建的资源并显示成功警告时,这会很有用。
默认情况下,页面重新加载时会清除警告。如果需要让警告在页面上持久化,可以将 persistOnPages 键设置为 Rails 控制器操作的数组。要在控制台中查找 Rails 控制器操作,请运行 document.body.dataset.page。
示例:
visitUrlWithAlerts('/dashboard/groups', [
{
id: 'resource-building-in-background',
message: 'Resource is being built in the background.',
variant: 'info',
persistOnPages: ['dashboard:groups:index'],
},
])如果需要手动移除持久化警告,可以使用 removeGlobalAlertById 工具函数。
如果需要以编程方式关闭警告,可以使用 dismissGlobalAlertById 工具函数。
组件文件夹
此文件夹保存所有与此新功能特定的组件。
要使用或创建可能在其他地方使用的组件,请参考 vue_shared/components。
判断何时应创建组件的一个好准则是:思考它是否可能在其他地方重用。 例如,表格在 GitLab 中许多地方使用,表格是组件的良好选择。另一方面,仅在一个表格中使用的表格单元格则不适合使用此模式。
你可以在 Vue.js 网站上阅读更多关于组件的内容,组件系统。
Pinia
Vuex
Vue Router
向页面添加 Vue Router:
-
在 Rails 路由文件中使用通配符
*vueroute添加捕获所有路由:# ee/config/routes/project.rb 中的示例 resources :iteration_cadences, path: 'cadences(/*vueroute)', action: :index上述示例将
iteration_cadences控制器的index页面提供给与path开头匹配的任何路由,例如groupname/projectname/-/cadences/123/456/。 -
将基础路由(
*vueroute之前的所有内容)传递给前端,用作初始化 Vue Router 的base参数:.js-my-app{ data: { base_path: project_iteration_cadences_path(project) } } -
初始化路由器:
Vue.use(VueRouter); export function createRouter(basePath) { return new VueRouter({ routes: createRoutes(), mode: 'history', base: basePath, }); } -
为未识别的路由添加
path: '*'的后备方案。可以:-
在路由数组末尾添加重定向:
const routes = [ { path: '/', name: 'list-page', component: ListPage, }, { path: '*', redirect: '/', }, ]; -
在路由数组末尾添加后备组件:
const routes = [ { path: '/', name: 'list-page', component: ListPage, }, { path: '*', component: NotFound, }, ];
-
-
可选。为允许在子路由中使用路径助手,添加
controller和action参数以使用父控制器。resources :iteration_cadences, path: 'cadences(/*vueroute)', action: :index do resources :iterations, only: [:index, :new, :edit, :show], constraints: { id: /\d+/ }, controller: :iteration_cadences, action: :index end这意味着像
/cadences/123/iterations/456/edit这样的路由可以在后端验证,例如检查组或项目成员资格。 这也意味着我们可以使用_path助手,这意味着我们可以在功能规范中加载页面,而无需手动构建路径的*vueroute部分。
Vue 与 jQuery 混合使用
- 不推荐混合使用 Vue 和 jQuery。
- 要在 Vue 中使用特定的 jQuery 插件,为其创建包装器。
- Vue 可以使用 jQuery 事件监听器监听现有的 jQuery 事件。
- 不建议为 Vue 与 jQuery 交互而添加新的 jQuery 事件。
Vue 与 JavaScript 类混合使用(在 data 函数中)
在 Vue 文档 中,Data 函数/对象定义如下:
Vue 实例的数据对象。Vue 递归地将其属性转换为 getter/setter 以使其"响应式"。该对象必须是普通对象:浏览器 API 对象和原型属性将被忽略。一个准则是数据应该只是数据 - 不建议观察具有自身状态行为的对象。
基于 Vue 的指导原则:
- 不要 在你的 data 函数 中使用或创建 JavaScript 类。
- 不要 添加新的 JavaScript 类实现。
- 应该 使用具有内聚解耦的组件或 状态管理器 来封装复杂的状态管理。
- 应该 维护使用这些方法的现有实现。
- 应该 在组件发生重大更改时将其迁移到纯对象模型。
- 应该 将业务逻辑移到单独的文件中,以便与组件分开测试。
为什么
在大型代码库中,使用 JavaScript 类会带来可维护性问题的其他原因:
- 类创建后,可能会以违反 Vue 响应式和最佳实践的方式进行扩展。
- 类增加了一层抽象,使组件 API 及其内部工作原理不够清晰。
- 使测试变得更困难。因为类由组件 data 函数实例化,所以更难"管理"组件和类。
- 在函数式代码库中添加面向对象原则(OOP)增加了编写代码的方式,降低了一致性和清晰度。
风格指南
在编写和测试 Vue 组件和模板时,请参考我们 风格指南 中的 Vue 部分。
组合式 API (Composition API)
在 Vue 2.7 中,可以在 Vue 组件中作为独立组合式函数使用 组合式 API。
优先使用 <script> 而非 <script setup>
组合式 API 允许将逻辑放在组件的 <script> 部分或专用的 <script setup> 部分。我们应该使用 <script> 并通过 setup() 属性将组合式 API 添加到组件中:
<script>
import { computed } from 'vue';
export default {
name: 'MyComponent',
setup(props) {
const doubleCount = computed(() => props.count*2)
}
}
</script>v-bind 的限制
除非绝对必要,否则避免使用 v-bind="$attrs"。在开发原生控件包装器时可能需要它(这是 gitlab-ui 组件的良好候选)。在任何其他情况下,始终优先使用 props 和显式数据流。
使用 v-bind="$attrs" 会导致:
- 组件契约的丢失。
props专门设计用于解决此问题。 - 树中每个组件的高维护成本。
v-bind="$attrs"特别难以调试,因为你必须扫描整个组件层次结构来理解数据流。 - 迁移到 Vue 3 时出现问题。Vue 3 中的
$attrs包含事件监听器,这可能在 Vue 3 迁移完成后导致意外副作用。
力求每个组件一种 API 风格
向 Vue 组件添加 setup() 属性时,考虑将其完全重构为组合式 API。这不总是可行的,特别是对于大型组件,但为了可读性和可维护性,应力求每个组件使用一种 API 风格。
组合式函数 (Composables)
使用组合式 API,我们有了新的方式来抽象逻辑,包括将响应式状态抽象为 组合式函数。组合式函数是接受参数并返回可在 Vue 组件中使用的响应式属性和方法的函数。
// useCount.js
import { ref } from 'vue';
export function useCount(initialValue) {
const count = ref(initialValue)
function incrementCount() {
count.value += 1
}
function decrementCount() {
count.value -= 1
}
return { count, incrementCount, decrementCount }
}// MyComponent.vue
import { useCount } from 'useCount'
export default {
name: 'MyComponent',
setup() {
const { count, incrementCount, decrementCount } = useCount(5)
return { count, incrementCount, decrementCount }
}
}函数和文件名前缀使用 use
Vue 中组合式函数的常见命名约定是前缀为 use,然后简要指代组合式功能(例如 useBreakpoints、useGeolocation 等)。包含组合式函数的 .js 文件也应遵循相同规则 - 即使文件包含多个组合式函数,文件名也应以 use_ 开头。
避免生命周期陷阱
构建组合式函数时,应力求保持其尽可能简单。生命周期钩子增加了组合式函数的复杂性,并可能导致意外副作用。为避免这种情况,我们应遵循以下原则:
- 尽可能减少生命周期钩子的使用,优先接受/返回回调。
- 如果你的组合式函数需要生命周期钩子,请确保它也执行清理。如果在
onMounted上添加了监听器,则应在同一组合式函数中的onUnmounted上移除它。 - 始立立即设置生命周期钩子:
// bad
const useAsyncLogic = () => {
const action = async () => {
await doSomething();
onMounted(doSomethingElse);
};
return { action };
};
// OK
const useAsyncLogic = () => {
const done = ref(false);
onMounted(() => {
watch(
done,
() => done.value && doSomethingElse(),
{ immediate: true },
);
});
const action = async () => {
await doSomething();
done.value = true;
};
return { action };
};避免逃生舱口 (Escape Hatches)
编写一个组合式函数作为黑盒,使用 Vue 提供的一些逃生舱口可能很诱人。但在大多数情况下,这会使它们过于复杂且难以维护。一个逃生舱口是 getCurrentInstance 方法。此方法返回当前渲染组件的实例。不应使用该方法,而应优先通过参数将数据或方法传递给组合式函数。
const useSomeLogic = () => {
doSomeLogic();
getCurrentInstance().emit('done'); // bad
};const done = () => emit('done');
const useSomeLogic = (done) => {
doSomeLogic();
done(); // good, 组合式函数不试图过于智能
}测试组合式函数
测试 Vue 组件
有关测试 Vue 组件的指南和最佳实践,请参阅 Vue 测试风格指南。
每个 Vue 组件都有独特的输出。此输出始终存在于渲染函数中。
尽管 Vue 组件的每个方法都可以单独测试,但我们的目标是测试渲染函数的输出,它始终代表所有时刻的状态。
请访问 Vue 测试指南 获取帮助。
以下是 此 Vue 组件 结构良好的单元测试示例:
import { GlLoadingIcon } from '@gitlab/ui';
import MockAdapter from 'axios-mock-adapter';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import axios from '~/lib/utils/axios_utils';
import App from '~/todos/app.vue';
const TEST_TODOS = [{ text: 'Lorem ipsum test text' }, { text: 'Lorem ipsum 2' }];
const TEST_NEW_TODO = 'New todo title';
const TEST_TODO_PATH = '/todos';
describe('~/todos/app.vue', () => {
let wrapper;
let mock;
beforeEach(() => {
// 重要:使用 axios-mock-adapter 来存根 axios API 请求
mock = new MockAdapter(axios);
mock.onGet(TEST_TODO_PATH).reply(200, TEST_TODOS);
mock.onPost(TEST_TODO_PATH).reply(200);
});
afterEach(() => {
// 重要:清理 axios mock 适配器
mock.restore();
});
// 将组件设置与其协作者(例如 Vuex 和 axios)分开非常有帮助。
const createWrapper = (props = {}) => {
wrapper = shallowMountExtended(App, {
propsData: {
path: TEST_TODO_PATH,
...props,
},
});
};
// 辅助方法极大地有助于测试的可维护性和可读性。
const findLoader = () => wrapper.findComponent(GlLoadingIcon);
const findAddButton = () => wrapper.findByTestId('add-button');
const findTextInput = () => wrapper.findByTestId('text-input');
const findTodoData = () =>
wrapper
.findAllByTestId('todo-item')
.wrappers.map((item) => ({ text: item.text() }));
describe('当挂载并加载时', () => {
beforeEach(() => {
// 创建永不解析的请求
mock.onGet(TEST_TODO_PATH).reply(() => new Promise(() => {}));
createWrapper();
});
it('应显示加载状态', () => {
expect(findLoader().exists()).toBe(true);
});
});
describe('当 todos 已加载时', () => {
beforeEach(() => {
createWrapper();
// 重要:此组件在挂载时异步获取数据,因此等待 Vue 模板更新
return wrapper.vm.$nextTick();
});
it('不应显示加载', () => {
expect(findLoader().exists()).toBe(false);
});
it('应渲染 todos', () => {
expect(findTodoData()).toEqual(TEST_TODOS);
});
it('当添加 todo 时,应发布新 todo', async () => {
findTextInput().vm.$emit('update', TEST_NEW_TODO);
findAddButton().vm.$emit('click');
await wrapper.vm.$nextTick();
expect(mock.history.post.map((x) => JSON.parse(x.data))).toEqual([{ text: TEST_NEW_TODO }]);
});
});
});子组件
-
测试任何定义子组件是否渲染/如何渲染的指令(例如
v-if和v-for)。 -
测试我们传递给子组件的任何 props(特别是如果 prop 在被测组件中通过
computed属性计算)。请记住使用.props()而非.vm.someProp。 -
测试我们是否正确响应来自子组件发出的事件:
const checkbox = wrapper.findByTestId('checkboxTestId'); expect(checkbox.attributes('disabled')).not.toBeDefined(); findChildComponent().vm.$emit('primary'); await nextTick(); expect(checkbox.attributes('disabled')).toBeDefined(); -
不要 测试子组件的内部实现:
// bad expect(findChildComponent().find('.error-alert').exists()).toBe(false); // good expect(findChildComponent().props('withAlertContainer')).toBe(false);
事件
我们应该测试组件中响应操作发出的事件。此测试验证是否以正确的参数触发了正确的事件。
对于任何原生 DOM 事件,我们应使用 trigger 来触发事件。
// 假设 SomeButton 渲染: <button>Some button</button>
wrapper = mount(SomeButton);
...
it('应触发点击事件', () => {
const btn = wrapper.find('button')
btn.trigger('click');
...
})当触发 Vue 事件时,使用 emit。
wrapper = shallowMount(DropdownItem);
...
it('应触发 itemClicked 事件', () => {
DropdownItem.vm.$emit('itemClicked');
...
})我们应通过断言 emitted() 方法的结果来验证事件是否已触发。
优先使用 vm.$emit 而非 trigger 从子组件发出事件是一种良好实践。
在组件上使用 trigger 意味着我们将其视为白盒:假设子组件的根元素具有原生 click 事件。此外,在 Vue3 模式下,在子组件上使用 trigger 时,某些测试会失败。
const findButton = () => wrapper.findComponent(GlButton);
// bad
findButton().trigger('click');
// good
findButton().vm.$emit('click');Vue.js 专家角色
只有当你的合并请求(MR)和代码审查显示出以下特征时,才应申请成为 Vue.js 专家:
- 深入理解 Vue 响应式原理
- Vue 和 Pinia 代码结构符合官方和我们的指南
- 全面理解测试 Vue 组件和 Pinia Store
- 了解现有的 Vue 和 Pinia 应用以及可重用组件
Vue 2 -> Vue 3 迁移
我们建议尽量减少向代码库添加某些功能,以避免增加技术债务,为最终迁移做准备:
- 过滤器 (filters);
- 事件总线 (event buses);
- 函数式模板 (functional templated)
slot属性
你可以在 迁移到 Vue 3 中找到更多详细信息
附录 - Vue 组件测试对象
这是在 测试 Vue 组件 部分中测试的示例组件模板:
<template>
<div class="content">
<gl-loading-icon v-if="isLoading" />
<template v-else>
<div
v-for="todo in todos"
:key="todo.id"
:class="{ 'gl-strike': todo.isDone }"
data-testid="todo-item"
>{{ todo.text }}</div>
<footer class="gl-border-t-1 gl-mt-3 gl-pt-3">
<gl-form-input
type="text"
v-model="todoText"
data-testid="text-input"
>
<gl-button
variant="confirm"
data-testid="add-button"
@click="addTodo"
>Add</gl-button>
</footer>
</template>
</div>
</template>