企业🤖AI Agent构建引擎,智能编排和调试,一键部署,支持私有化部署方案 广告
# Vue > 原文:[https://docs.gitlab.com/ee/development/fe_guide/vue.html](https://docs.gitlab.com/ee/development/fe_guide/vue.html) * [Examples](#examples) * [Vue architecture](#vue-architecture) * [Components and Store](#components-and-store) * [An `index.js` file](#an-indexjs-file) * [Bootstrapping Gotchas](#bootstrapping-gotchas) * [Providing data from HAML to JavaScript](#providing-data-from-haml-to-javascript) * [Accessing the `gl` object](#accessing-the-gl-object) * [Accessing feature flags](#accessing-feature-flags) * [A folder for Components](#a-folder-for-components) * [A folder for the Store](#a-folder-for-the-store) * [Vuex](#vuex) * [Mixing Vue and jQuery](#mixing-vue-and-jquery) * [Style guide](#style-guide) * [Testing Vue Components](#testing-vue-components) * [Test the component’s output](#test-the-components-output) * [Events](#events) * [Vue.js Expert Role](#vuejs-expert-role) * [Vue 2 -> Vue 3 Migration](#vue-2---vue-3-migration) * [Appendix - Vue component subject under test](#appendix---vue-component-subject-under-test) # Vue[](#vue "Permalink") 要开始使用 Vue,请通读[其文档](https://vuejs.org/v2/guide/) . ## Examples[](#examples "Permalink") 在以下示例中可以找到以下各节中描述的内容: * [Web IDE](https://gitlab.com/gitlab-org/gitlab-foss/tree/master/app/assets/javascripts/ide/stores) * [Security products](https://gitlab.com/gitlab-org/gitlab/tree/master/ee/app/assets/javascripts/vue_shared/security_reports) * [Registry](https://gitlab.com/gitlab-org/gitlab-foss/tree/master/app/assets/javascripts/registry/stores) ## Vue architecture[](#vue-architecture "Permalink") 用 Vue.js 构建的所有新功能都必须遵循[Flux 架构](https://facebook.github.io/flux/) . 我们试图实现的主要目标是只有一个数据流和一个数据条目. 为了实现此目标,我们使用[vuex](#vuex) . 您还可以在 Vue 文档中了解有关[状态管理](https://vuejs.org/v2/guide/state-management.html#Simple-State-Management-from-Scratch)和[数据流的一种方式的](https://vuejs.org/v2/guide/components.html#One-Way-Data-Flow)此体系结构. ### Components and Store[](#components-and-store "Permalink") 在使用 Vue.js 实现的某些功能(例如[问题](https://gitlab.com/gitlab-org/gitlab-foss/tree/master/app/assets/javascripts/boards)公告[板](https://gitlab.com/gitlab-org/gitlab-foss/tree/master/app/assets/javascripts/boards)或[环境表)中,](https://gitlab.com/gitlab-org/gitlab-foss/tree/master/app/assets/javascripts/environments)您可以找到明确的关注点分离: ``` new_feature ├── components │ └── component.vue │ └── ... ├── store │ └── new_feature_store.js ├── index.js ``` *为了保持一致性,我们建议您采用相同的结构.* Let’s look into each of them: ### An `index.js` file[](#an-indexjs-file "Permalink") 这是新功能的索引文件. 这是新功能的根 Vue 实例所在的位置. 应在此文件中导入和初始化商店和服务,并作为主要组件的道具提供. 请务必阅读[特定](./performance.html#page-specific-javascript)于[页面的 JavaScript](./performance.html#page-specific-javascript) . ### Bootstrapping Gotchas[](#bootstrapping-gotchas "Permalink") #### Providing data from HAML to JavaScript[](#providing-data-from-haml-to-javascript "Permalink") 挂载 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 元素的需要,因为它将进行测试更轻松. 请参见以下示例: ``` // haml .js-vue-app{ data: { endpoint: 'foo' }} // index.js document.addEventListener('DOMContentLoaded', () => new Vue({ el: '.js-vue-app', data() { const dataset = this.$options.el.dataset; return { endpoint: dataset.endpoint, }; }, render(createElement) { return createElement('my-component', { props: { endpoint: this.endpoint, }, }); }, })); ``` #### Accessing the `gl` object[](#accessing-the-gl-object "Permalink") 当我们需要查询`gl`对象以获取在应用程序生命周期内不会更改的数据时,我们应该在查询 DOM 的同一位置进行处理. 通过遵循这种做法,我们可以避免模拟`gl`对象的需求,这将使测试更加容易. 应该在初始化我们的 Vue 实例时完成,并且数据应作为主要组件的`props`提供: ``` document.addEventListener('DOMContentLoaded', () => new Vue({ el: '.js-vue-app', render(createElement) { return createElement('my-component', { props: { username: gon.current_username, }, }); }, })); ``` #### Accessing feature flags[](#accessing-feature-flags "Permalink") 使用 Vue 的[提供/注入](https://vuejs.org/v2/api/#provide-inject)机制使功能标志可用于 Vue 应用程序中的任何后代组件. `glFeatures`对象已经在`commons/vue.js` ,因此仅需要 mixin 即可使用这些标志: ``` // An arbitrary descendant component import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; export default { // ... mixins: [glFeatureFlagsMixin()], // ... created() { if (this.glFeatures.myFlag) { // ... } }, } ``` 这种方法有一些好处: * 任意深度嵌套的组件都可以选择加入并访问该标志,而中间组件则不知道(例如,通过 prop 将标志向下传递). * 良好的可测试性,因为可以从`vue-test-utils`将该标志提供给`mount` / `shallowMount` ,只是作为一个道具. ``` import { shallowMount } from '@vue/test-utils'; shallowMount(component, { provide: { glFeatures: { myFlag: true }, }, }); ``` * 除了在应用程序的[入口点](#accessing-the-gl-object)之外,无需访问全局变量. ### A folder for Components[](#a-folder-for-components "Permalink") 此文件夹包含此新功能特定的所有组件. 如果您需要使用或创建可能在其他地方使用的组件,请参考`vue_shared/components` . 了解何时创建组件的一个很好的经验法则是考虑它是否可以在其他地方重用. 例如,表在整个 GitLab 中被大量使用,表非常适合组件. 另一方面,仅在一个表中使用的表单元格不能很好地利用此模式. 您可以在 Vue.js 网站[Component System 中](https://vuejs.org/v2/guide/#Composing-with-Components)阅读有关组件的更多信息. ### A folder for the Store[](#a-folder-for-the-store "Permalink") #### Vuex[](#vuex "Permalink") 检查此[页面](vuex.html)以获取更多详细信息. ### Mixing Vue and jQuery[](#mixing-vue-and-jquery "Permalink") * 不建议将 Vue 和 jQuery 混合使用. * 如果您需要在 Vue 中使用特定的 jQuery 插件,请[围绕它创建一个包装器](https://vuejs.org/v2/examples/select2.html) . * Vue 使用 jQuery 事件侦听器侦听现有的 jQuery 事件是可以接受的. * 不建议为 Vue 添加新的 jQuery 事件以与 jQuery 交互. ## Style guide[](#style-guide "Permalink") 在编写 Vue 组件和模板时,请参考我们的[样式指南](style/vue.html)的 Vue 部分以获取最佳实践. ## Testing Vue Components[](#testing-vue-components "Permalink") 每个 Vue 组件都有一个唯一的输出. 此输出始终存在于 render 函数中. 尽管我们可以分别测试 Vue 组件的每种方法,但我们的目标必须是测试 render / template 函数的输出,该输出始终代表状态. 这是[此 Vue 组件](#appendix---vue-component-subject-under-test)结构良好的单元测试的示例: ``` import { shallowMount } from '@vue/test-utils'; import { GlLoadingIcon } from '@gitlab/ui'; import MockAdapter from 'axios-mock-adapter'; 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(() => { // IMPORTANT: Use axios-mock-adapter for stubbing axios API requests mock = new MockAdapter(axios); mock.onGet(TEST_TODO_PATH).reply(200, TEST_TODOS); mock.onPost(TEST_TODO_PATH).reply(200); }); afterEach(() => { // IMPORTANT: Clean up the component instance and axios mock adapter wrapper.destroy(); wrapper = null; mock.restore(); }); // NOTE: It is very helpful to separate setting up the component from // its collaborators (i.e. Vuex, axios, etc.) const createWrapper = (props = {}) => { wrapper = shallowMount(App, { propsData: { path: TEST_TODO_PATH, ...props, }, }); }; // NOTE: Helper methods greatly help test maintainability and readability. const findLoader = () => wrapper.find(GlLoadingIcon); const findAddButton = () => wrapper.find('[data-testid="add-button"]'); const findTextInput = () => wrapper.find('[data-testid="text-input"]'); const findTodoData = () => wrapper.findAll('[data-testid="todo-item"]').wrappers.map(wrapper => ({ text: wrapper.text() })); describe('when mounted and loading', () => { beforeEach(() => { // Create request which will never resolve mock.onGet(TEST_TODO_PATH).reply(() => new Promise(() => {})); createWrapper(); }); it('should render the loading state', () => { expect(findLoader().exists()).toBe(true); }); }); describe('when todos are loaded', () => { beforeEach(() => { createWrapper(); // IMPORTANT: This component fetches data asynchronously on mount, so let's wait for the Vue template to update return wrapper.vm.$nextTick(); }); it('should not show loading', () => { expect(findLoader().exists()).toBe(false); }); it('should render todos', () => { expect(findTodoData()).toEqual(TEST_TODOS); }); it('when todo is added, should post new todo', () => { findTextInput().vm.$emit('update', TEST_NEW_TODO) findAddButton().vm.$emit('click'); return wrapper.vm.$nextTick() .then(() => { expect(mock.history.post.map(x => JSON.parse(x.data))).toEqual([{ text: TEST_NEW_TODO }]); }); }); }); }); ``` ### Test the component’s output[](#test-the-components-output "Permalink") Vue 组件的主要返回值是渲染的输出. 为了测试组件,我们需要测试渲染的输出. [Vue](https://vuejs.org/v2/guide/unit-testing.html)指南的单元测试向我们确切地表明: ### Events[](#events "Permalink") 我们应该测试响应组件中的操作而发出的事件,这对于验证是否使用正确的参数触发了正确的事件很有用. 对于任何 DOM 事件,我们都应使用[`trigger`](https://vue-test-utils.vuejs.org/api/wrapper/#trigger)来触发事件. ``` // Assuming SomeButton renders: <button>Some button</button> wrapper = mount(SomeButton); ... it('should fire the click event', () => { const btn = wrapper.find('button') btn.trigger('click'); ... }) ``` 当我们需要触发 Vue 事件时,我们应该使用[`emit`](https://vuejs.org/v2/guide/components-custom-events.html)事件来触发事件. ``` wrapper = shallowMount(DropdownItem); ... it('should fire the itemClicked event', () => { DropdownItem.vm.$emit('itemClicked'); ... }) ``` 我们应该通过对[`emitted()`](https://vue-test-utils.vuejs.org/api/wrapper/#emitted)方法的结果进行断言来验证事件已被触发 ## Vue.js Expert Role[](#vuejs-expert-role "Permalink") 仅当您自己的合并请求并且您的评论显示时,您才应该申请成为 Vue.js 专家: * 对 Vue 和 Vuex 反应性的深入了解 * Vue 和 Vuex 代码是根据官方规范和我们的准则构建的 * 全面了解测试 Vue 和 Vuex 应用程序 * Vuex 代码遵循[记录的模式](vuex.html#naming-pattern-request-and-receive-namespaces) * 有关现有 Vue 和 Vuex 应用程序以及现有可重用组件的知识 ## Vue 2 -> Vue 3 Migration[](#vue-2---vue-3-migration "Permalink") > 暂时添加此部分是为了支持将代码库从 Vue 2.x 迁移到 Vue 3.x 的工作. 当前,我们建议尽量减少向代码库中添加某些功能,以防止增加最终迁移的技​​术负担: * filters; * 活动巴士; * 功能模板化 * `slot` attributes 您可以找到有关[迁移到 Vue 3 的](vue3_migration.html)更多详细信息 ## Appendix - Vue component subject under test[](#appendix---vue-component-subject-under-test "Permalink") 这是示例组件的模板,已在" [测试 Vue 组件"](#testing-vue-components)部分中进行了[测试](#testing-vue-components) : ``` <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" >{{ toddo.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="success" data-testid="add-button" @click="addTodo" >Add</gl-button> </footer> </template> </div> </template> ```