Vue

原文:https://docs.gitlab.com/ee/development/fe_guide/vue.html

Vue

要开始使用 Vue,请通读其文档 .

Examples

在以下示例中可以找到以下各节中描述的内容:

Vue architecture

用 Vue.js 构建的所有新功能都必须遵循Flux 架构 . 我们试图实现的主要目标是只有一个数据流和一个数据条目. 为了实现此目标,我们使用vuex .

您还可以在 Vue 文档中了解有关状态管理数据流的一种方式的此体系结构.

Components and Store

在使用 Vue.js 实现的某些功能(例如问题公告环境表)中,您可以找到明确的关注点分离:

  1. new_feature
  2. ├── components
  3. └── component.vue
  4. └── ...
  5. ├── store
  6. └── new_feature_store.js
  7. ├── index.js

为了保持一致性,我们建议您采用相同的结构.

Let’s look into each of them:

An index.js file

这是新功能的索引文件. 这是新功能的根 Vue 实例所在的位置.

应在此文件中导入和初始化商店和服务,并作为主要组件的道具提供.

请务必阅读特定页面的 JavaScript .

Bootstrapping Gotchas

Providing data from HAML to JavaScript

挂载 Vue 应用程序时,可能需要从 Rails 向 JavaScript 提供数据. 为此,您可以使用 HTML 元素中的data属性,并在安装应用程序时查询它们.

Note: You should only do this while initializing the application, because the mounted element will be replaced with Vue-generated DOM.

通过render函数中的props将数据从 DOM 提供给 Vue 实例而不是查询主 Vue 组件内部的 DOM 的好处是避免了在单元测试中创建固定装置或 HTML 元素的需要,因为它将进行测试更轻松. 请参见以下示例:

  1. // haml
  2. .js-vue-app{ data: { endpoint: 'foo' }}
  3. // index.js
  4. document.addEventListener('DOMContentLoaded', () => new Vue({
  5. el: '.js-vue-app',
  6. data() {
  7. const dataset = this.$options.el.dataset;
  8. return {
  9. endpoint: dataset.endpoint,
  10. };
  11. },
  12. render(createElement) {
  13. return createElement('my-component', {
  14. props: {
  15. endpoint: this.endpoint,
  16. },
  17. });
  18. },
  19. }));

Accessing the gl object

当我们需要查询gl对象以获取在应用程序生命周期内不会更改的数据时,我们应该在查询 DOM 的同一位置进行处理. 通过遵循这种做法,我们可以避免模拟gl对象的需求,这将使测试更加容易. 应该在初始化我们的 Vue 实例时完成,并且数据应作为主要组件的props提供:

  1. document.addEventListener('DOMContentLoaded', () => new Vue({
  2. el: '.js-vue-app',
  3. render(createElement) {
  4. return createElement('my-component', {
  5. props: {
  6. username: gon.current_username,
  7. },
  8. });
  9. },
  10. }));

Accessing feature flags

使用 Vue 的提供/注入机制使功能标志可用于 Vue 应用程序中的任何后代组件. glFeatures对象已经在commons/vue.js ,因此仅需要 mixin 即可使用这些标志:

  1. // An arbitrary descendant component
  2. import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
  3. export default {
  4. // ...
  5. mixins: [glFeatureFlagsMixin()],
  6. // ...
  7. created() {
  8. if (this.glFeatures.myFlag) {
  9. // ...
  10. }
  11. },
  12. }

这种方法有一些好处:

  • 任意深度嵌套的组件都可以选择加入并访问该标志,而中间组件则不知道(例如,通过 prop 将标志向下传递).
  • 良好的可测试性,因为可以从vue-test-utils将该标志提供给mount / shallowMount ,只是作为一个道具.

    1. import { shallowMount } from '@vue/test-utils';
    2. shallowMount(component, {
    3. provide: {
    4. glFeatures: { myFlag: true },
    5. },
    6. });
  • 除了在应用程序的入口点之外,无需访问全局变量.

A folder for Components

此文件夹包含此新功能特定的所有组件. 如果您需要使用或创建可能在其他地方使用的组件,请参考vue_shared/components .

了解何时创建组件的一个很好的经验法则是考虑它是否可以在其他地方重用.

例如,表在整个 GitLab 中被大量使用,表非常适合组件. 另一方面,仅在一个表中使用的表单元格不能很好地利用此模式.

您可以在 Vue.js 网站Component System 中阅读有关组件的更多信息.

A folder for the Store

Vuex

检查此页面以获取更多详细信息.

Mixing Vue and jQuery

  • 不建议将 Vue 和 jQuery 混合使用.
  • 如果您需要在 Vue 中使用特定的 jQuery 插件,请围绕它创建一个包装器 .
  • Vue 使用 jQuery 事件侦听器侦听现有的 jQuery 事件是可以接受的.
  • 不建议为 Vue 添加新的 jQuery 事件以与 jQuery 交互.

Style guide

在编写 Vue 组件和模板时,请参考我们的样式指南的 Vue 部分以获取最佳实践.

Testing Vue Components

每个 Vue 组件都有一个唯一的输出. 此输出始终存在于 render 函数中.

尽管我们可以分别测试 Vue 组件的每种方法,但我们的目标必须是测试 render / template 函数的输出,该输出始终代表状态.

这是此 Vue 组件结构良好的单元测试的示例:

  1. import { shallowMount } from '@vue/test-utils';
  2. import { GlLoadingIcon } from '@gitlab/ui';
  3. import MockAdapter from 'axios-mock-adapter';
  4. import axios from '~/lib/utils/axios_utils';
  5. import App from '~/todos/app.vue';
  6. const TEST_TODOS = [
  7. { text: 'Lorem ipsum test text' },
  8. { text: 'Lorem ipsum 2' },
  9. ];
  10. const TEST_NEW_TODO = 'New todo title';
  11. const TEST_TODO_PATH = '/todos';
  12. describe('~/todos/app.vue', () => {
  13. let wrapper;
  14. let mock;
  15. beforeEach(() => {
  16. // IMPORTANT: Use axios-mock-adapter for stubbing axios API requests
  17. mock = new MockAdapter(axios);
  18. mock.onGet(TEST_TODO_PATH).reply(200, TEST_TODOS);
  19. mock.onPost(TEST_TODO_PATH).reply(200);
  20. });
  21. afterEach(() => {
  22. // IMPORTANT: Clean up the component instance and axios mock adapter
  23. wrapper.destroy();
  24. wrapper = null;
  25. mock.restore();
  26. });
  27. // NOTE: It is very helpful to separate setting up the component from
  28. // its collaborators (i.e. Vuex, axios, etc.)
  29. const createWrapper = (props = {}) => {
  30. wrapper = shallowMount(App, {
  31. propsData: {
  32. path: TEST_TODO_PATH,
  33. ...props,
  34. },
  35. });
  36. };
  37. // NOTE: Helper methods greatly help test maintainability and readability.
  38. const findLoader = () => wrapper.find(GlLoadingIcon);
  39. const findAddButton = () => wrapper.find('[data-testid="add-button"]');
  40. const findTextInput = () => wrapper.find('[data-testid="text-input"]');
  41. const findTodoData = () => wrapper.findAll('[data-testid="todo-item"]').wrappers.map(wrapper => ({ text: wrapper.text() }));
  42. describe('when mounted and loading', () => {
  43. beforeEach(() => {
  44. // Create request which will never resolve
  45. mock.onGet(TEST_TODO_PATH).reply(() => new Promise(() => {}));
  46. createWrapper();
  47. });
  48. it('should render the loading state', () => {
  49. expect(findLoader().exists()).toBe(true);
  50. });
  51. });
  52. describe('when todos are loaded', () => {
  53. beforeEach(() => {
  54. createWrapper();
  55. // IMPORTANT: This component fetches data asynchronously on mount, so let's wait for the Vue template to update
  56. return wrapper.vm.$nextTick();
  57. });
  58. it('should not show loading', () => {
  59. expect(findLoader().exists()).toBe(false);
  60. });
  61. it('should render todos', () => {
  62. expect(findTodoData()).toEqual(TEST_TODOS);
  63. });
  64. it('when todo is added, should post new todo', () => {
  65. findTextInput().vm.$emit('update', TEST_NEW_TODO)
  66. findAddButton().vm.$emit('click');
  67. return wrapper.vm.$nextTick()
  68. .then(() => {
  69. expect(mock.history.post.map(x => JSON.parse(x.data))).toEqual([{ text: TEST_NEW_TODO }]);
  70. });
  71. });
  72. });
  73. });

Test the component’s output

Vue 组件的主要返回值是渲染的输出. 为了测试组件,我们需要测试渲染的输出. Vue指南的单元测试向我们确切地表明:

Events

我们应该测试响应组件中的操作而发出的事件,这对于验证是否使用正确的参数触发了正确的事件很有用.

对于任何 DOM 事件,我们都应使用trigger来触发事件.

  1. // Assuming SomeButton renders: <button>Some button</button>
  2. wrapper = mount(SomeButton);
  3. ...
  4. it('should fire the click event', () => {
  5. const btn = wrapper.find('button')
  6. btn.trigger('click');
  7. ...
  8. })

当我们需要触发 Vue 事件时,我们应该使用emit事件来触发事件.

  1. wrapper = shallowMount(DropdownItem);
  2. ...
  3. it('should fire the itemClicked event', () => {
  4. DropdownItem.vm.$emit('itemClicked');
  5. ...
  6. })

我们应该通过对emitted()方法的结果进行断言来验证事件已被触发

Vue.js Expert Role

仅当您自己的合并请求并且您的评论显示时,您才应该申请成为 Vue.js 专家:

  • 对 Vue 和 Vuex 反应性的深入了解
  • Vue 和 Vuex 代码是根据官方规范和我们的准则构建的
  • 全面了解测试 Vue 和 Vuex 应用程序
  • Vuex 代码遵循记录的模式
  • 有关现有 Vue 和 Vuex 应用程序以及现有可重用组件的知识

Vue 2 -> Vue 3 Migration

暂时添加此部分是为了支持将代码库从 Vue 2.x 迁移到 Vue 3.x 的工作.

当前,我们建议尽量减少向代码库中添加某些功能,以防止增加最终迁移的技​​术负担:

  • filters;
  • 活动巴士;
  • 功能模板化
  • slot attributes

您可以找到有关迁移到 Vue 3 的更多详细信息

Appendix - Vue component subject under test

这是示例组件的模板,已在” 测试 Vue 组件”部分中进行了测试

  1. <template>
  2. <div class="content">
  3. <gl-loading-icon v-if="isLoading" />
  4. <template v-else>
  5. <div
  6. v-for="todo in todos"
  7. :key="todo.id"
  8. :class="{ 'gl-strike': todo.isDone }"
  9. data-testid="todo-item"
  10. >{{ toddo.text }}</div>
  11. <footer class="gl-border-t-1 gl-mt-3 gl-pt-3">
  12. <gl-form-input
  13. type="text"
  14. v-model="todoText"
  15. data-testid="text-input"
  16. >
  17. <gl-button
  18. variant="success"
  19. data-testid="add-button"
  20. @click="addTodo"
  21. >Add</gl-button>
  22. </footer>
  23. </template>
  24. </div>
  25. </template>