

文章目录
- [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 常见测试场景对比
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 流程
- 编写失败的测试: 先编写测试,验证未实现的功能
 - 编写最少的代码使测试通过: 实现功能使测试通过
 - 重构代码: 优化实现,保持测试通过
 
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 异步测试问题
问题 : 测试未等待组件更新就进行断言
解决方案 : 使用 await 和 nextTick
            
            
              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()
      }
    },
    // 其他模块可以省略
  }
})
        结语
感谢您的阅读!期待您的一键三连!欢迎指正!
