【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()
      }
    },
    // 其他模块可以省略
  }
})

结语

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

相关推荐
树叶会结冰7 分钟前
HTML语义化:当网页会说话
前端·html
冰万森13 分钟前
解决 React 项目初始化(npx create-react-app)速度慢的 7 个实用方案
前端·react.js·前端框架
牧羊人_myr26 分钟前
Ajax 技术详解
前端
浩男孩35 分钟前
🍀封装个 Button 组件,使用 vitest 来测试一下
前端
蓝银草同学39 分钟前
阿里 Iconfont 项目丢失?手把手教你将已引用的 SVG 图标下载到本地
前端·icon
布列瑟农的星空1 小时前
重学React —— React事件机制 vs 浏览器事件机制
前端
程序定小飞1 小时前
基于springboot的在线商城系统设计与开发
java·数据库·vue.js·spring boot·后端
一小池勺1 小时前
CommonJS
前端·面试
孙牛牛1 小时前
实战分享:一招解决嵌套依赖版本失控问题,以 undici 为例
前端