Help us learn about your current experience with the documentation. Take the survey.

Vue.js 风格指南

代码检查

我们默认使用 eslint-vue-plugin,并配置 plugin:vue/recommended。 查看 rules 获取更多文档。

基本规则

  1. Vue 模板使用 .vue 扩展名。不要在 HAML 中使用 %template

  2. 明确定义传递给 Vue 应用的数据

    // bad
    return new Vue({
      el: '#element',
      name: 'ComponentNameRoot',
      components: {
        componentName
      },
      provide: {
        ...someDataset
      },
      props: {
        ...anotherDataset
      },
      render: createElement => createElement('component-name'),
    }));
    
    // good
    const { foobar, barfoo } = someDataset;
    const { foo, bar } = anotherDataset;
    
    return new Vue({
      el: '#element',
      name: 'ComponentNameRoot',
      components: {
        componentName
      },
      provide: {
        foobar,
        barfoo
      },
      props: {
        foo,
        bar
      },
      render: createElement => createElement('component-name'),
    }));

    我们不鼓励在这种特定情况下使用展开运算符,以保持代码库的明确性、可发现性和可搜索性。 这适用于任何能从上述模式中受益的地方,例如在初始化 Vuex 状态时。 上述模式还使我们在实例化过程中能够轻松解析非标量值。

    return new Vue({
      el: '#element',
      name: 'ComponentNameRoot',
      components: {
        componentName
      },
      props: {
        foo,
        bar: parseBoolean(bar)
      },
      render: createElement => createElement('component-name'),
    }));

模板中的组件使用

  1. 在模板中使用组件时,优先使用 kebab-case 命名风格

    // bad
    <MyComponent />
    
    // good
    <my-component />

<style> 标签

我们不使用 <style> 标签,原因如下:

  1. 无法使用 SCSS 变量和混入(mixins),也无法使用 Tailwind CSS@apply 指令。
  2. 这些样式会在运行时插入。
  3. 我们已经有其他几种方式来定义 CSS。

应该使用 Tailwind CSS 工具类页面特定 CSS,而不是使用 <style> 标签。

Vue 测试

随着时间的推移,在我们有效测试 Vue 组件的过程中,出现了一些编程模式和风格偏好。以下指南描述了其中的一些。 这些不是严格的指导原则,而是一些建议和良好实践,旨在深入了解我们在 GitLab 中如何编写 Vue 测试。

挂载组件

通常,在测试 Vue 组件时,每个测试块都应该"重新挂载"该组件。

实现方法如下:

  1. 在顶层 describe 块中创建一个可变的 wrapper 变量。
  2. 使用 mountshallowMount 挂载组件。
  3. 将生成的 Wrapper 实例重新赋值给我们的 wrapper 变量。

创建全局可变的 wrapper 提供了多种优势,包括:

  • 定义查找组件/DOM 元素的通用函数:

    import MyComponent from '~/path/to/my_component.vue';
    describe('MyComponent', () => {
      let wrapper;
    
      // this can now be reused across tests
      const findMyComponent = wrapper.findComponent(MyComponent);
      // ...
    })
  • 使用 beforeEach 块挂载组件(有关更多信息,请参阅 the createComponent factory)。

  • 使用 shared_test_setup.js 中设置的 enableAutoDestroy 在测试运行后自动销毁组件。

异步子组件

shallowMount 不会为异步子组件创建组件存根(stubs)。为了正确存根异步子组件,请使用 stubs 选项。确保异步子组件定义了 name 选项,否则 wrapperfindComponent 方法可能无法正常工作。

createComponent 工厂函数

为了避免重复挂载逻辑,定义一个可在每个测试块中重用的 createComponent 工厂函数很有用。这是一个闭包,应该将我们的 wrapper 变量重新赋值为 mountshallowMount 的结果:

import MyComponent from '~/path/to/my_component.vue';
import { shallowMount } from '@vue/test-utils';

describe('MyComponent', () => {
  // Initiate the "global" wrapper variable. This will be used throughout our test:
  let wrapper;

  // Define our `createComponent` factory:
  function createComponent() {
    // Mount component and reassign `wrapper`:
    wrapper = shallowMount(MyComponent);
  }

  it('mounts', () => {
    createComponent();

    expect(wrapper.exists()).toBe(true);
  });

  it('`isLoading` prop defaults to `false`', () => {
    createComponent();

    expect(wrapper.props('isLoading')).toBe(false);
  });
})

同样,我们可以在 beforeEach 块中调用 createComponent 来进一步去重测试:

import MyComponent from '~/path/to/my_component.vue';
import { shallowMount } from '@vue/test-utils';

describe('MyComponent', () => {
  // Initiate the "global" wrapper variable. This will be used throughout our test
  let wrapper;

  // define our `createComponent` factory
  function createComponent() {
    // mount component and reassign `wrapper`
    wrapper = shallowMount(MyComponent);
  }

  beforeEach(() => {
    createComponent();
  });

  it('mounts', () => {
    expect(wrapper.exists()).toBe(true);
  });

  it('`isLoading` prop defaults to `false`', () => {
    expect(wrapper.props('isLoading')).toBe(false);
  });
})

createComponent 最佳实践

  1. 考虑使用单个(或有限数量的)对象参数,而不是多个参数。 为常见数据(如 props)定义单个参数是可以的,但请记住我们的 JavaScript 风格指南 并保持在参数数量限制内:

    // bad
    function createComponent(props, stubs, mountFn, foo) { }
    
    // good
    function createComponent({ props, stubs, mountFn, foo } = {}) { }
    
    // good
    function createComponent(props = {}, { stubs, mountFn, foo } = {}) { }
  2. 如果在同一组测试中需要同时使用 mountshallowMount,可以为 createComponent 工厂函数定义一个 mountFn 参数,该参数接受用于挂载组件的挂载函数(mountshallowMount):

    import { shallowMount } from '@vue/test-utils';
    
    function createComponent({ mountFn = shallowMount } = {}) { }
  3. 使用 mountExtendedshallowMountExtended 助手函数来暴露 wrapper.findByTestId()

    import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
    import { SomeComponent } from 'components/some_component.vue';
    
    let wrapper;
    
    const createWrapper = () => { wrapper = shallowMountExtended(SomeComponent); };
    const someButton = () => wrapper.findByTestId('someButtonTestId');
  4. 避免使用 datamethods 或任何其他扩展组件内部状态的挂载选项。

    import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
    import { SomeComponent } from 'components/some_component.vue';
    
    let wrapper;
    
    // bad :( - This circumvents the actual user interaction and couples the test to component internals.
    const createWrapper = ({ data }) => {
      wrapper = shallowMountExtended(SomeComponent, {
        data
      });
    };
    
    // good :) - Helpers like `clickShowButton` interact with the actual I/O of the component.
    const createWrapper = () => {
      wrapper = shallowMountExtended(SomeComponent);
    };
    const clickShowButton = () => {
      wrapper.findByTestId('show').trigger('click');
    }

设置组件状态

  1. 尽可能避免使用 setProps 设置组件状态。相反,在挂载组件时设置组件的 propsData

    // bad
    wrapper = shallowMount(MyComponent);
    wrapper.setProps({
      myProp: 'my cool prop'
    });
    
    // good
    wrapper = shallowMount({ propsData: { myProp: 'my cool prop' } });

    例外情况是当你希望以某种方式测试组件的响应性时。例如,你可能想在特定观察者(watcher)执行后测试组件的输出。使用 setProps 测试这种行为是可以的。

  2. 避免使用 setData,因为它设置的是组件的内部状态,绕过了对组件实际 I/O 的测试。相反,应该触发组件子元素的事件或其他副作用来强制状态更改。

访问组件状态

  1. 访问 props 或属性时,优先使用 wrapper.props('myProp') 语法,而不是 wrapper.props().myPropwrapper.vm.myProp

    // good
    expect(wrapper.props().myProp).toBe(true);
    expect(wrapper.attributes().myAttr).toBe(true);
    
    // better
    expect(wrapper.props('myProp').toBe(true);
    expect(wrapper.attributes('myAttr')).toBe(true);
  2. 断言多个 props 时,使用 toEqual 检查 props() 对象的深度相等性:

    // good
    expect(wrapper.props('propA')).toBe('valueA');
    expect(wrapper.props('propB')).toBe('valueB');
    expect(wrapper.props('propC')).toBe('valueC');
    
    // better
    expect(wrapper.props()).toEqual({
      propA: 'valueA',
      propB: 'valueB',
      propC: 'valueC',
    });
  3. 如果你只对某些 props 感兴趣,可以使用 toMatchObject。优先使用 toMatchObject 而不是 expect.objectContaining

    // good
    expect(wrapper.props()).toEqual(expect.objectContaining({
      propA: 'valueA',
      propB: 'valueB',
    }));
    
    // better
    expect(wrapper.props()).toMatchObject({
      propA: 'valueA',
      propB: 'valueB',
    });

测试 props 验证

检查组件 props 时,使用 assertProps 助手函数。Props 验证失败将作为错误抛出:

import { assertProps } from 'helpers/assert_props'

// ...

expect(() => assertProps(SomeComponent, { invalidPropValue: '1', someOtherProp: 2 })).toThrow()