【Vue】单元测试(Jest/Vue Test Utils)

个人主页:Guiat
归属专栏:Vue

文章目录

  • [1. Vue 单元测试简介](#1. Vue 单元测试简介)
    • [1.1 为什么需要单元测试](#1.1 为什么需要单元测试)
    • [1.2 测试工具介绍](#1.2 测试工具介绍)
  • [2. 环境搭建](#2. 环境搭建)
    • [2.1 安装依赖](#2.1 安装依赖)
    • [2.2 配置 Jest](#2.2 配置 Jest)
  • [3. 编写第一个测试](#3. 编写第一个测试)
    • [3.1 组件示例](#3.1 组件示例)
    • [3.2 编写测试用例](#3.2 编写测试用例)
    • [3.3 运行测试](#3.3 运行测试)
  • [4. Vue Test Utils 核心 API](#4. Vue Test Utils 核心 API)
    • [4.1 挂载组件](#4.1 挂载组件)
    • [4.2 常用断言和操作](#4.2 常用断言和操作)
  • [5. 测试组件交互](#5. 测试组件交互)
    • [5.1 测试用户输入](#5.1 测试用户输入)
    • [5.2 测试 Props 和自定义事件](#5.2 测试 Props 和自定义事件)
  • [6. 模拟依赖](#6. 模拟依赖)
    • [6.1 模拟 API 请求](#6.1 模拟 API 请求)
    • [6.2 模拟 Vuex](#6.2 模拟 Vuex)
  • [7. 测试 Vue Router](#7. 测试 Vue Router)
    • [7.1 模拟 Vue Router](#7.1 模拟 Vue Router)
    • [7.2 测试路由组件](#7.2 测试路由组件)
  • [8. 快照测试](#8. 快照测试)
    • [8.1 基本快照测试](#8.1 基本快照测试)
    • [8.2 更新快照](#8.2 更新快照)
  • [9. 测试覆盖率](#9. 测试覆盖率)
    • [9.1 理解覆盖率指标](#9.1 理解覆盖率指标)
    • [9.2 覆盖率报告](#9.2 覆盖率报告)
  • [10. 测试最佳实践](#10. 测试最佳实践)
    • [10.1 组织测试](#10.1 组织测试)
    • [10.2 测试原则](#10.2 测试原则)
    • [10.3 常见测试场景对比](#10.3 常见测试场景对比)
  • [11. 持续集成中的测试](#11. 持续集成中的测试)
    • [11.1 配置 CI 流程](#11.1 配置 CI 流程)
    • [11.2 测试报告整合](#11.2 测试报告整合)
  • [12. 测试驱动开发 (TDD) 与 Vue](#12. 测试驱动开发 (TDD) 与 Vue)
    • [12.1 TDD 流程](#12.1 TDD 流程)
    • [12.2 TDD 示例](#12.2 TDD 示例)
  • [13. 常见问题与解决方案](#13. 常见问题与解决方案)
    • [13.1 异步测试问题](#13.1 异步测试问题)
    • [13.2 复杂 DOM 结构查找问题](#13.2 复杂 DOM 结构查找问题)
    • [13.3 模拟复杂的 Vuex Store](#13.3 模拟复杂的 Vuex Store)

正文

1. Vue 单元测试简介

单元测试是确保代码质量和可维护性的重要手段,在 Vue 应用开发中,Jest 和 Vue Test Utils 是最常用的测试工具组合。

1.1 为什么需要单元测试

  • 提早发现 bug,减少线上问题
  • 重构代码时提供安全保障
  • 作为代码的活文档,帮助理解组件功能
  • 促进更好的代码设计和模块化

1.2 测试工具介绍

  • Jest: Facebook 开发的 JavaScript 测试框架,提供断言库、测试运行器和覆盖率报告
  • Vue Test Utils: Vue.js 官方的单元测试实用工具库,提供挂载组件和与之交互的方法

2. 环境搭建

2.1 安装依赖

bash 复制代码
# 使用 Vue CLI 创建项目时选择单元测试
vue create my-project

# 或在现有项目中安装
npm install --save-dev jest @vue/test-utils vue-jest babel-jest

2.2 配置 Jest

package.json 中添加 Jest 配置:

json 复制代码
{
  "jest": {
    "moduleFileExtensions": [
      "js",
      "vue"
    ],
    "transform": {
      "^.+\\.vue$": "vue-jest",
      "^.+\\.js$": "babel-jest"
    },
    "moduleNameMapper": {
      "^@/(.*)$": "<rootDir>/src/$1"
    },
    "testMatch": [
      "**/tests/unit/**/*.spec.[jt]s?(x)"
    ],
    "collectCoverage": true,
    "collectCoverageFrom": [
      "src/**/*.{js,vue}",
      "!src/main.js",
      "!src/router/index.js",
      "!**/node_modules/**"
    ]
  }
}

3. 编写第一个测试

3.1 组件示例

假设有一个简单的计数器组件 Counter.vue

vue 复制代码
<template>
  <div>
    <span class="count">{{ count }}</span>
    <button @click="increment">增加</button>
    <button @click="decrement">减少</button>
  </div>
</template>

<script>
export default {
  data() {
    return {
      count: 0
    }
  },
  methods: {
    increment() {
      this.count += 1
    },
    decrement() {
      this.count -= 1
    }
  }
}
</script>

3.2 编写测试用例

创建 tests/unit/Counter.spec.js 文件:

javascript 复制代码
import { mount } from '@vue/test-utils'
import Counter from '@/components/Counter.vue'

describe('Counter.vue', () => {
  it('初始计数为0', () => {
    const wrapper = mount(Counter)
    expect(wrapper.find('.count').text()).toBe('0')
  })

  it('点击增加按钮后计数加1', async () => {
    const wrapper = mount(Counter)
    await wrapper.findAll('button').at(0).trigger('click')
    expect(wrapper.find('.count').text()).toBe('1')
  })

  it('点击减少按钮后计数减1', async () => {
    const wrapper = mount(Counter)
    await wrapper.findAll('button').at(1).trigger('click')
    expect(wrapper.find('.count').text()).toBe('-1')
  })
})

3.3 运行测试

bash 复制代码
npm run test:unit

4. Vue Test Utils 核心 API

4.1 挂载组件

javascript 复制代码
// 完全挂载组件及其子组件
const wrapper = mount(Component, {
  propsData: { /* 组件 props */ },
  data() { /* 覆盖组件数据 */ },
  mocks: { /* 模拟全局对象 */ },
  stubs: { /* 替换子组件 */ }
})

// 只挂载当前组件,不渲染子组件
const wrapper = shallowMount(Component, options)

4.2 常用断言和操作

javascript 复制代码
// 查找元素
wrapper.find('div') // CSS 选择器
wrapper.find('.class-name')
wrapper.find('[data-test="id"]')
wrapper.findComponent(ChildComponent)

// 检查内容和属性
expect(wrapper.text()).toContain('Hello')
expect(wrapper.html()).toContain('<div>')
expect(wrapper.attributes('id')).toBe('my-id')
expect(wrapper.classes()).toContain('active')

// 触发事件
await wrapper.find('button').trigger('click')
await wrapper.find('input').setValue('new value')

// 访问组件实例
console.log(wrapper.vm.count) // 访问数据
wrapper.vm.increment() // 调用方法

// 更新组件
await wrapper.setProps({ color: 'red' })
await wrapper.setData({ count: 5 })

5. 测试组件交互

5.1 测试用户输入

假设有一个表单组件 Form.vue

vue 复制代码
<template>
  <form @submit.prevent="submitForm">
    <input v-model="username" data-test="username" />
    <input type="password" v-model="password" data-test="password" />
    <button type="submit" data-test="submit">登录</button>
    <p v-if="error" data-test="error">{{ error }}</p>
  </form>
</template>

<script>
export default {
  data() {
    return {
      username: '',
      password: '',
      error: ''
    }
  },
  methods: {
    submitForm() {
      if (!this.username || !this.password) {
        this.error = '用户名和密码不能为空'
        return
      }
      this.$emit('form-submitted', {
        username: this.username,
        password: this.password
      })
      this.error = ''
    }
  }
}
</script>

测试代码 Form.spec.js

javascript 复制代码
import { mount } from '@vue/test-utils'
import Form from '@/components/Form.vue'

describe('Form.vue', () => {
  it('提交空表单时显示错误信息', async () => {
    const wrapper = mount(Form)
    await wrapper.find('[data-test="submit"]').trigger('click')
    expect(wrapper.find('[data-test="error"]').text()).toBe('用户名和密码不能为空')
  })

  it('表单正确提交时触发事件', async () => {
    const wrapper = mount(Form)
    
    await wrapper.find('[data-test="username"]').setValue('user1')
    await wrapper.find('[data-test="password"]').setValue('pass123')
    await wrapper.find('form').trigger('submit')
    
    expect(wrapper.emitted('form-submitted')).toBeTruthy()
    expect(wrapper.emitted('form-submitted')[0][0]).toEqual({
      username: 'user1',
      password: 'pass123'
    })
  })
})

5.2 测试 Props 和自定义事件

假设有一个展示商品的组件 ProductItem.vue

vue 复制代码
<template>
  <div class="product-item">
    <h3>{{ product.name }}</h3>
    <p>{{ product.price }}元</p>
    <button @click="addToCart">加入购物车</button>
  </div>
</template>

<script>
export default {
  props: {
    product: {
      type: Object,
      required: true
    }
  },
  methods: {
    addToCart() {
      this.$emit('add-to-cart', this.product.id)
    }
  }
}
</script>

测试代码 ProductItem.spec.js

javascript 复制代码
import { mount } from '@vue/test-utils'
import ProductItem from '@/components/ProductItem.vue'

describe('ProductItem.vue', () => {
  const product = {
    id: 1,
    name: '测试商品',
    price: 99
  }
  
  it('正确渲染商品信息', () => {
    const wrapper = mount(ProductItem, {
      propsData: { product }
    })
    
    expect(wrapper.find('h3').text()).toBe('测试商品')
    expect(wrapper.find('p').text()).toBe('99元')
  })
  
  it('点击按钮触发加入购物车事件', async () => {
    const wrapper = mount(ProductItem, {
      propsData: { product }
    })
    
    await wrapper.find('button').trigger('click')
    
    expect(wrapper.emitted('add-to-cart')).toBeTruthy()
    expect(wrapper.emitted('add-to-cart')[0]).toEqual([1])
  })
})

6. 模拟依赖

6.1 模拟 API 请求

假设有一个使用 axios 获取用户数据的组件 UserList.vue

vue 复制代码
<template>
  <div>
    <div v-if="loading">加载中...</div>
    <ul v-else>
      <li v-for="user in users" :key="user.id" data-test="user">
        {{ user.name }}
      </li>
    </ul>
    <div v-if="error" data-test="error">{{ error }}</div>
  </div>
</template>

<script>
import axios from 'axios'

export default {
  data() {
    return {
      users: [],
      loading: true,
      error: null
    }
  },
  created() {
    this.fetchUsers()
  },
  methods: {
    async fetchUsers() {
      try {
        this.loading = true
        const response = await axios.get('/api/users')
        this.users = response.data
      } catch (error) {
        this.error = '获取用户列表失败'
      } finally {
        this.loading = false
      }
    }
  }
}
</script>

测试代码 UserList.spec.js

javascript 复制代码
import { mount, flushPromises } from '@vue/test-utils'
import UserList from '@/components/UserList.vue'
import axios from 'axios'

// 模拟 axios
jest.mock('axios')

describe('UserList.vue', () => {
  it('成功获取用户列表', async () => {
    // 设置 axios.get 的模拟返回值
    axios.get.mockResolvedValue({
      data: [
        { id: 1, name: '用户1' },
        { id: 2, name: '用户2' }
      ]
    })
    
    const wrapper = mount(UserList)
    
    // 等待异步操作完成
    await flushPromises()
    
    // 断言加载状态消失
    expect(wrapper.find('div').text()).not.toBe('加载中...')
    
    // 断言用户列表已渲染
    const users = wrapper.findAll('[data-test="user"]')
    expect(users).toHaveLength(2)
    expect(users.at(0).text()).toBe('用户1')
    expect(users.at(1).text()).toBe('用户2')
  })
  
  it('获取用户列表失败', async () => {
    // 设置 axios.get 模拟抛出错误
    axios.get.mockRejectedValue(new Error('API 错误'))
    
    const wrapper = mount(UserList)
    
    // 等待异步操作完成
    await flushPromises()
    
    // 断言显示错误信息
    expect(wrapper.find('[data-test="error"]').text()).toBe('获取用户列表失败')
  })
})

6.2 模拟 Vuex

假设有一个使用 Vuex 的计数器组件 VuexCounter.vue

vue 复制代码
<template>
  <div>
    <span data-test="count">{{ count }}</span>
    <button @click="increment">增加</button>
    <button @click="decrement">减少</button>
  </div>
</template>

<script>
import { mapState, mapActions } from 'vuex'

export default {
  computed: {
    ...mapState(['count'])
  },
  methods: {
    ...mapActions(['increment', 'decrement'])
  }
}
</script>

测试代码 VuexCounter.spec.js

javascript 复制代码
import { mount, createLocalVue } from '@vue/test-utils'
import Vuex from 'vuex'
import VuexCounter from '@/components/VuexCounter.vue'

// 创建扩展的 Vue 实例
const localVue = createLocalVue()
localVue.use(Vuex)

describe('VuexCounter.vue', () => {
  let store
  let actions
  let state
  
  beforeEach(() => {
    // 设置模拟的 state 和 actions
    state = {
      count: 5
    }
    
    actions = {
      increment: jest.fn(),
      decrement: jest.fn()
    }
    
    // 创建模拟的 store
    store = new Vuex.Store({
      state,
      actions
    })
  })
  
  it('从 store 渲染计数', () => {
    const wrapper = mount(VuexCounter, {
      store,
      localVue
    })
    
    expect(wrapper.find('[data-test="count"]').text()).toBe('5')
  })
  
  it('调度 increment action', async () => {
    const wrapper = mount(VuexCounter, {
      store,
      localVue
    })
    
    await wrapper.findAll('button').at(0).trigger('click')
    expect(actions.increment).toHaveBeenCalled()
  })
  
  it('调度 decrement action', async () => {
    const wrapper = mount(VuexCounter, {
      store,
      localVue
    })
    
    await wrapper.findAll('button').at(1).trigger('click')
    expect(actions.decrement).toHaveBeenCalled()
  })
})

7. 测试 Vue Router

7.1 模拟 Vue Router

使用 Vue Router 的导航组件 Navigation.vue

vue 复制代码
<template>
  <nav>
    <router-link to="/" data-test="home">首页</router-link>
    <router-link to="/about" data-test="about">关于</router-link>
    <button @click="goToContact" data-test="contact">联系我们</button>
  </nav>
</template>

<script>
export default {
  methods: {
    goToContact() {
      this.$router.push('/contact')
    }
  }
}
</script>

测试代码 Navigation.spec.js

javascript 复制代码
import { mount, createLocalVue } from '@vue/test-utils'
import VueRouter from 'vue-router'
import Navigation from '@/components/Navigation.vue'

const localVue = createLocalVue()
localVue.use(VueRouter)

describe('Navigation.vue', () => {
  it('点击按钮进行路由导航', async () => {
    const router = new VueRouter()
    // 监视 router.push 方法
    router.push = jest.fn()
    
    const wrapper = mount(Navigation, {
      localVue,
      router
    })
    
    await wrapper.find('[data-test="contact"]').trigger('click')
    expect(router.push).toHaveBeenCalledWith('/contact')
  })
})

7.2 测试路由组件

假设有一个根据路由参数显示内容的组件 UserDetails.vue

vue 复制代码
<template>
  <div>
    <h1 data-test="user-name">{{ userName }}</h1>
  </div>
</template>

<script>
export default {
  data() {
    return {
      userName: ''
    }
  },
  created() {
    // 根据路由参数获取用户名
    this.userName = `用户 ${this.$route.params.id}`
  }
}
</script>

测试代码 UserDetails.spec.js

javascript 复制代码
import { mount, createLocalVue } from '@vue/test-utils'
import VueRouter from 'vue-router'
import UserDetails from '@/components/UserDetails.vue'

const localVue = createLocalVue()
localVue.use(VueRouter)

describe('UserDetails.vue', () => {
  it('根据路由参数显示用户名', () => {
    // 创建带有初始路由和参数的路由实例
    const router = new VueRouter()
    
    // 创建带有模拟路由的组件
    const wrapper = mount(UserDetails, {
      localVue,
      mocks: {
        $route: {
          params: {
            id: '42'
          }
        }
      }
    })
    
    expect(wrapper.find('[data-test="user-name"]').text()).toBe('用户 42')
  })
})

8. 快照测试

快照测试可以确保组件 UI 不会意外改变。

8.1 基本快照测试

javascript 复制代码
import { mount } from '@vue/test-utils'
import MessageDisplay from '@/components/MessageDisplay.vue'

describe('MessageDisplay.vue', () => {
  it('渲染的 UI 与上次快照匹配', () => {
    const wrapper = mount(MessageDisplay, {
      propsData: {
        message: '欢迎使用 Vue!'
      }
    })
    
    expect(wrapper.html()).toMatchSnapshot()
  })
})

8.2 更新快照

当组件合法变更后,需要更新快照:

bash 复制代码
# 更新所有快照
jest --updateSnapshot

# 更新特定测试的快照
jest --updateSnapshot -t 'MessageDisplay'

9. 测试覆盖率

9.1 理解覆盖率指标

Jest 提供四种覆盖率指标:

  • 语句覆盖率(Statements): 程序中执行到的语句比例
  • 分支覆盖率(Branches): 程序中执行到的分支比例(if/else)
  • 函数覆盖率(Functions): 被调用过的函数比例
  • 行覆盖率(Lines): 程序中执行到的行数比例

9.2 覆盖率报告

执行带覆盖率报告的测试:

bash 复制代码
jest --coverage

典型的覆盖率报告输出:

复制代码
-----------------------|---------|----------|---------|---------|-------------------
File                   | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s 
-----------------------|---------|----------|---------|---------|-------------------
All files              |   85.71 |    83.33 |   85.71 |   85.71 |                   
 components/Counter.vue|  100.00 |   100.00 |  100.00 |  100.00 |                   
 components/Form.vue   |   71.42 |    66.67 |   71.42 |   71.42 | 15-18             
-----------------------|---------|----------|---------|---------|-------------------

10. 测试最佳实践

10.1 组织测试

  • 按照组件结构组织测试文件
  • 为每个组件创建单独的测试文件
  • 使用清晰的测试描述和分组
javascript 复制代码
describe('组件名', () => {
  describe('功能1', () => {
    it('子功能 A', () => { /* ... */ })
    it('子功能 B', () => { /* ... */ })
  })
  
  describe('功能2', () => {
    it('子功能 C', () => { /* ... */ })
    it('子功能 D', () => { /* ... */ })
  })
})

10.2 测试原则

  • 测试行为而非实现: 关注组件的输出而非内部工作方式
  • 使用数据属性标记测试元素 : 使用 data-test 属性标记用于测试的元素
  • 一个测试只测一个行为: 每个测试只断言一个行为
  • 避免过度模拟: 尽量减少模拟的数量
  • 编写可维护的测试: 测试代码应该和产品代码一样重视质量

10.3 常见测试场景对比

graph TD A[测试场景] --> B[组件渲染] A --> C[用户交互] A --> D[API 调用] A --> E[Vuex 整合] A --> F[路由功能] B --> B1[使用 mount() 测试完整渲染] B --> B2[使用 shallowMount() 测试隔离组件] B --> B3[使用 toMatchSnapshot() 测试 UI 稳定性] C --> C1[使用 trigger() 测试点击事件] C --> C2[使用 setValue() 测试表单输入] C --> C3[使用 emitted() 测试自定义事件] D --> D1[使用 jest.mock() 模拟 axios] D --> D2[测试加载状态] D --> D3[测试成功/失败处理] E --> E1[模拟 Vuex store] E --> E2[测试 getter 计算属性] E --> E3[验证 actions 被正确调度] F --> F1[使用 mocks 模拟 $route] F --> F2[验证 router.push 调用] F --> F3[测试基于路由的组件行为]

11. 持续集成中的测试

11.1 配置 CI 流程

在 GitHub Actions 中设置 Vue 测试的 .github/workflows/test.yml 配置:

yaml 复制代码
name: Unit Tests

on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]

jobs:
  test:
    runs-on: ubuntu-latest

    steps:
    - uses: actions/checkout@v2
    - name: Use Node.js
      uses: actions/setup-node@v2
      with:
        node-version: '14'
    - name: Install dependencies
      run: npm ci
    - name: Run tests
      run: npm run test:unit
    - name: Upload coverage
      uses: codecov/codecov-action@v1
      with:
        token: ${{ secrets.CODECOV_TOKEN }}

11.2 测试报告整合

将 Jest 测试报告整合到 CI 系统,使用 JUnit 格式:

json 复制代码
// package.json
{
  "jest": {
    "reporters": [
      "default",
      ["jest-junit", {
        "outputDirectory": "./test-results/jest",
        "outputName": "results.xml"
      }]
    ]
  }
}

12. 测试驱动开发 (TDD) 与 Vue

12.1 TDD 流程

  1. 编写失败的测试: 先编写测试,验证未实现的功能
  2. 编写最少的代码使测试通过: 实现功能使测试通过
  3. 重构代码: 优化实现,保持测试通过

12.2 TDD 示例

假设我们要开发一个待办事项组件,先编写测试:

javascript 复制代码
// TodoList.spec.js
import { mount } from '@vue/test-utils'
import TodoList from '@/components/TodoList.vue'

describe('TodoList.vue', () => {
  it('显示待办事项列表', () => {
    const wrapper = mount(TodoList, {
      propsData: {
        todos: [
          { id: 1, text: '学习 Vue', done: false },
          { id: 2, text: '学习单元测试', done: true }
        ]
      }
    })
    
    const items = wrapper.findAll('[data-test="todo-item"]')
    expect(items).toHaveLength(2)
    expect(items.at(0).text()).toContain('学习 Vue')
    expect(items.at(1).text()).toContain('学习单元测试')
    expect(items.at(1).classes()).toContain('completed')
  })
  
  it('添加新的待办事项', async () => {
    const wrapper = mount(TodoList)
    
    await wrapper.find('[data-test="new-todo"]').setValue('新任务')
    await wrapper.find('form').trigger('submit')
    
    expect(wrapper.findAll('[data-test="todo-item"]')).toHaveLength(1)
    expect(wrapper.find('[data-test="todo-item"]').text()).toContain('新任务')
  })
})

然后实现组件:

vue 复制代码
<template>
  <div>
    <form @submit.prevent="addTodo">
      <input v-model="newTodo" data-test="new-todo" />
      <button type="submit">添加</button>
    </form>
    <ul>
      <li 
        v-for="todo in allTodos" 
        :key="todo.id" 
        :class="{ completed: todo.done }"
        data-test="todo-item"
      >
        {{ todo.text }}
      </li>
    </ul>
  </div>
</template>

<script>
export default {
  props: {
    todos: {
      type: Array,
      default: () => []
    }
  },
  data() {
    return {
      newTodo: '',
      localTodos: []
    }
  },
  computed: {
    allTodos() {
      return [...this.todos, ...this.localTodos]
    }
  },
  methods: {
    addTodo() {
      if (this.newTodo.trim()) {
        this.localTodos.push({
          id: Date.now(),
          text: this.newTodo,
          done: false
        })
        this.newTodo = ''
      }
    }
  }
}
</script>

<style scoped>
.completed {
  text-decoration: line-through;
}
</style>

13. 常见问题与解决方案

13.1 异步测试问题

问题 : 测试未等待组件更新就进行断言
解决方案 : 使用 awaitnextTick

javascript 复制代码
// 错误示例
wrapper.find('button').trigger('click')
expect(wrapper.text()).toContain('已更新')  // 可能失败

// 正确示例
await wrapper.find('button').trigger('click')
expect(wrapper.text()).toContain('已更新')  // 已等待更新

13.2 复杂 DOM 结构查找问题

问题 : 难以准确定位要测试的元素
解决方案 : 使用 data-test 属性标记测试元素

vue 复制代码
<template>
  <div>
    <h1 data-test="title">标题</h1>
    <p data-test="content">内容</p>
  </div>
</template>
javascript 复制代码
// 使用 data-test 属性查找元素
wrapper.find('[data-test="title"]')

13.3 模拟复杂的 Vuex Store

问题 : 大型应用中 Store 结构复杂
解决方案: 只模拟测试需要的部分

javascript 复制代码
const store = new Vuex.Store({
  modules: {
    user: {
      namespaced: true,
      state: { name: 'Test User' },
      getters: {
        fullName: () => 'Test User Full'
      },
      actions: {
        login: jest.fn()
      }
    },
    // 其他模块可以省略
  }
})

结语

感谢您的阅读!期待您的一键三连!欢迎指正!

相关推荐
Aphasia31114 分钟前
react必备JavaScript知识点(二)——类
前端·javascript
玖玖passion16 分钟前
数组转树:数据结构中的经典问题
前端
呼Lu噜23 分钟前
WPF-遵循MVVM框架创建图表的显示【保姆级】
前端·后端·wpf
珠峰下的沙砾26 分钟前
Vue3 里 CSS 深度作用选择器 :global
前端·javascript·css
航Hang*28 分钟前
WEBSTORM前端 —— 第2章:CSS —— 第3节:背景属性与显示模式
前端·css·css3·html5·webstorm
wuhen_n29 分钟前
CSS元素动画篇:基于当前位置的变换动画(一)
前端·css·html·css3·html5
拉不动的猪1 小时前
# 移动端与PC端全屏的处理
前端·javascript·面试
麦麦大数据1 小时前
vue+neo4j+flask 音乐知识图谱推荐系统
vue.js·mysql·flask·知识图谱·neo4j·推荐算法·音乐推荐
局外人LZ1 小时前
WXT+Vue3+sass+antd+vite搭建项目开发chrome插件
前端·chrome·vue·sass
excel1 小时前
招幕技术人员
前端·javascript·后端