Vue Ajax与状态管理完全指南:从数据请求到全局状态控制

一、Ajax请求:与后端沟通的桥梁

为什么需要Ajax?

在传统网页中,每次与服务器交互都需要刷新整个页面。Ajax(Asynchronous JavaScript and XML)让我们能够:

  • 异步请求:不刷新页面获取/发送数据
  • 更好体验:局部更新,页面无刷新
  • 提高性能:减少不必要的数据传输

多种请求方式对比

方式 说明 特点
原生XHR 原生API,最早的标准 代码冗长,使用复杂
jQuery 封装XHR,简化操作 依赖jQuery,体积大
axios 基于Promise,推荐 功能全面,体积小,支持拦截器
fetch 现代浏览器原生API 语法简洁,但兼容性需处理

axios:Vue项目的最佳选择

bash 复制代码
# 第一步:安装axios
npm install axios

基础使用

bash 复制代码
import axios from 'axios'

// GET请求
axios.get('https://api.example.com/users')
  .then(response => {
    console.log(response.data)
  })
  .catch(error => {
    console.error('请求失败:', error)
  })

// POST请求
axios.post('https://api.example.com/users', {
  name: '张三',
  age: 25
})
.then(response => {
    console.log('创建成功:', response.data)
  })

// 并发请求
axios.all([
  axios.get('/api/users'),
  axios.get('/api/posts')
])
.then(axios.spread((users, posts) => {
    console.log('用户:', users.data)
    console.log('文章:', posts.data)
  }))

axios实例配置

bash 复制代码
// 创建axios实例
const api = axios.create({
  baseURL: 'https://api.example.com',
  timeout: 5000, // 超时时间
  headers: {
    'Content-Type': 'application/json'
  }
})

// 请求拦截器
api.interceptors.request.use(
  config => {
    // 在发送请求前做些什么(如添加token)
    const token = localStorage.getItem('token')
    if (token) {
      config.headers.Authorization = `Bearer ${token}`
    }
    return config
  },
  error => {
    return Promise.reject(error)
  }
)

// 响应拦截器
api.interceptors.response.use(
  response => {
    // 对响应数据做点什么
    return response.data
  },
  error => {
    // 对响应错误做点什么
    if (error.response?.status === 401) {
      // token过期,跳转到登录页
      router.push('/login')
    }
    return Promise.reject(error)
  }
)

// 使用实例
api.get('/users')
api.post('/login', { username, password })

二、解决跨域问题:代理服务器配置

什么是跨域?

浏览器有 "同源策略 ":只有协议(http/https)主机名(localhost)端口号(8080) 全一致,才允许请求接口,否则就是 "跨域"(比如前端 8080 请求后端 5000 的接口)。

跨域解决方案(此处只需关注代理)

  1. CORS:后端配置(最常用,前端无需操作);
  2. JSONP:只支持 GET 请求,几乎不用;
  3. 代理服务器:Vue 脚手架自带,前端简单配置即可(重点)

配置代理的两种方式

第一步:创建 / 修改 vue.config.js(项目根目录)

bash 复制代码
module.exports = {
  pages: {
    index: {
      entry: 'src/main.js', // 项目入口文件
    },
  },
  lintOnSave: false, // 关闭代码格式检查(避免新手报错)
  // 代理服务器核心配置
  devServer: {
    // 方式一:单代理(只能代理一个后端地址)
    // proxy: 'http://localhost:5000' 

    // 方式二:多代理(推荐,支持多个后端地址)
    proxy: {
      '/jojo': { // 前缀:所有以/jojo开头的请求走这个代理
        target: 'http://localhost:5000', // 后端服务器地址1
        pathRewrite: {'^/jojo': ''}, // 去掉前缀(后端接口无/jojo)
        changeOrigin: true // 伪装请求来源(让后端以为是5000自己的请求)
      },
      '/atguigu': { // 前缀:所有以/atguigu开头的请求走这个代理
        target: 'http://localhost:5001', // 后端服务器地址2
        pathRewrite: {'^/atguigu': ''},
        changeOrigin: true
      }
    }
  }
}

第二步:组件中发送请求(src/App.vue)

bash 复制代码
<template>
  <div id="root">
    <button @click="getStudents">获取学生信息(5000端口)</button><br/>
    <button @click="getCars">获取汽车信息(5001端口)</button>
  </div>
</template>

<script>
import axios from 'axios'
export default {
  name: 'App',
  methods: {
    getStudents() {
      // 前端8080 → 代理前缀/jojo → 实际请求5000/students
      axios.get('http://localhost:8080/jojo/students')
        .then(res => console.log('请求成功:', res.data))
        .catch(err => console.log('请求失败:', err.message))
    },
    getCars() {
      // 前端8080 → 代理前缀/atguigu → 实际请求5001/cars
      axios.get('http://localhost:8080/atguigu/cars')
        .then(res => console.log('请求成功:', res.data))
        .catch(err => console.log('请求失败:', err.message))
    }
  }
}
</script>

两种代理方式对比

方式 配置写法 优点 缺点
方式一 proxy: 'http://localhost:5000' 配置极简 只能代理 1 个地址,无法灵活控制
方式二 proxy: { '/前缀': { target: '地址' } } 支持多代理,灵活可控 配置稍繁琐,请求需加前缀

补充:vue-resource(了解即可)

vue-resource是 Vue 早期的 Ajax 插件,现在官方推荐 axios,只需简单了解:

bash 复制代码
# 安装
npm i vue-resource

运行

bash 复制代码
// src/main.js 全局注册
import Vue from 'vue'
import vueResource from 'vue-resource'
Vue.use(vueResource)

// 组件中使用(替代axios)
this.$http.get('接口地址').then(res => console.log(res.data))

三、插槽:父向子传 HTML 结构

为什么需要插槽?

想象一下:你买了一个手机壳,但不同人的手机型号不同。插槽就像手机壳上"放手机的地方",让父组件可以插入自定义内容。

3.1 插槽的核心作用

插槽是父组件给子组件传递 HTML 结构 的专属方式(也是组件通信),解决 "子组件部分内容需要自定义" 的问题(比如卡片组件的标题、内容区想自定义)。

3.2 三种插槽类型

1. 默认插槽(基础版)

bash 复制代码
<!-- 父组件(App.vue) -->
<template>
  <div>
    <!-- 父组件给子组件传HTML结构 -->
    <Category>
      <div>我是父组件传给子组件的HTML内容</div>
    </Category>
  </div>
</template>

<script>
import Category from './components/Category.vue'
export default { components: { Category } }
</script>

<!-- 子组件(Category.vue) -->
<template>
  <div class="category">
    <!-- slot是插槽:父传了内容就显示传的,没传就显示默认内容 -->
    <slot>插槽默认内容(父组件没传时显示)</slot>
  </div>
</template>

2. 具名插槽(多区域自定义)

当子组件有多个自定义区域(比如卡片的"头部""底部"),用name区分插槽

bash 复制代码
<!-- 父组件(App.vue) -->
<<template>
  <div>
    <Category>
      <!-- 方式1:slot属性(旧写法) -->
      <template slot="center">
        <div>我是中间区域的内容</div>
      </template>
      <!-- 方式2:v-slot:name(新写法,推荐) -->
      <template v-slot:footer>
        <div>我是底部区域的内容</div>
      </template>
    </Category>
  </div>
</template>

<!-- 子组件(Category.vue) -->
<template>
  <div class="category">
    <!-- 命名插槽:center -->
    <slot name="center">默认中间内容</slot>
    <!-- 命名插槽:footer -->
    <slot name="footer">默认底部内容</slot>
  </div>
</template>

3. 作用域插槽(子传数据,父自定义结构)

核心场景:数据在子组件里,但父组件想自定义数据的展示形式(比如子组件有游戏列表,父组件想分别用ul、h4展示)

bash 复制代码
<!-- 子组件(Category.vue) -->
<template>
  <div class="category">
    <!-- 把子组件的games数据传给父组件(:games="games") -->
    <slot :games="games"></slot>
  </div>
</template>

<script>
export default {
  name: 'Category',
  data() {
    return {
      games: ['红色警戒', '穿越火线', '劲舞团', '超级玛丽'] // 子组件的私有数据
    }
  }
}
</script>


<!-- 父组件(App.vue) -->
<template>
  <div>
    <!-- 方式1:scope接收子组件传的参数 -->
    <Category>
      <template scope="scopeData">
        <!-- 父自定义:用ul展示数据 -->
        <ul>
          <li v-for="g in scopeData.games" :key="g">{{ g }}</li>
        </ul>
      </template>
    </Category>

    <!-- 方式2:slot-scope(旧写法,兼容用) -->
    <Category>
      <template slot-scope="scopeData">
        <!-- 父自定义:用h4展示数据 -->
        <h4 v-for="g in scopeData.games" :key="g">{{ g }}</h4>
      </template>
    </Category>
  </div>
</template>

3.3 插槽的实际应用场景

bash 复制代码
<!-- 场景1:可配置的按钮组件 -->
<!-- Button.vue -->
<template>
  <button :class="['btn', type]">
    <slot>按钮</slot>  <!-- 默认文本 -->
  </button>
</template>

<!-- 使用 -->
<Button type="primary">提交</Button>
<Button type="danger">
  <i class="icon-delete"></i> 删除
</Button>

<!-- 场景2:可复用的模态框 -->
<!-- Modal.vue -->
<template>
  <div v-if="visible" class="modal">
    <div class="modal-content">
      <div class="modal-header">
        <slot name="header">
          <h3>提示</h3>
        </slot>
        <button @click="$emit('close')">×</button>
      </div>
      <div class="modal-body">
        <slot></slot>
      </div>
      <div class="modal-footer">
        <slot name="footer">
          <button @click="$emit('confirm')">确认</button>
          <button @click="$emit('close')">取消</button>
        </slot>
      </div>
    </div>
  </div>
</template>

3.4 插槽核心总结

类型 核心特点 使用场景
默认插槽 无 name,单个自定义区域 子组件只有 1 处需要自定义
具名插槽 有 name,多个自定义区域 子组件有多处需要自定义
作用域插槽 子传数据给父,父自定义数据展示结构 数据在子、结构在父

四、Vuex:集中式状态管理(多组件共享数据)

为什么需要Vuex?

  • 随着应用变大,组件间共享状态变得复杂:
  • props层层传递:深度嵌套组件通信困难
  • 事件总线混乱:难以追踪状态变化
  • 状态不一致:不同组件可能修改同一状态

4.1 什么是 Vuex?

Vuex 是 Vue 的 "全局数据仓库 ",专门管理多个组件共享的数据(比如用户登录态、购物车、全局计数器),所有组件都能读写这个仓库的数据,不用再挨个组件传值。

何时使用Vuex?

  • 需要共享的状态:用户信息、购物车、主题设置
  • 需要跨组件通信:多个组件需要同步状态
  • 需要持久化:登录状态、用户偏好
  • ❌ 局部状态:单个组件内部的状态(使用组件data)
  • ❌ 简单父子通信(使用props和事件)

Vuex 核心流程(餐饮比喻)

  • State:数据仓库(后厨的食材);
  • Mutations:修改数据的唯一入口(大厨,只能同步操作);
  • Actions:处理业务逻辑(服务员,可异步操作,最终找大厨);
  • Dispatch:组件通知服务员(this.$store.dispatch());
  • Commit:服务员通知大厨(context.commit())。

4.2 搭建 Vuex 环境(Vue2 用 Vuex3)

bash 复制代码
# 安装Vuex3(Vue2专用,Vue3用Vuex4)
npm i vuex@3

第一步:创建 src/store/index.js(核心文件)

bash 复制代码
// 1. 引入Vue和Vuex
import Vue from 'vue'
import Vuex from 'vuex'

// 2. 安装Vuex插件
Vue.use(Vuex)

// 3. 准备核心模块
const actions = {} // 处理业务逻辑
const mutations = {} // 修改State
const state = {} // 存储数据

// 4. 创建并暴露Store
export default new Vuex.Store({
  actions,
  mutations,
  state
})

第二步:在 main.js 中注册 Store

bash 复制代码
import Vue from 'vue'
import App from './App.vue'
import store from './store' // 引入store

Vue.config.productionTip = false

new Vue({
  el: '#app',
  render: h => h(App),
  store // 全局注册:所有组件都能通过this.$store访问
})

4.3 Vuex 基本使用(求和案例)

第一步:完善 store/index.js

bash 复制代码
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)

// 1. Actions:处理业务逻辑(异步/条件判断)
const actions = {
  // 奇数时才加
  addOdd(context, value) {
    if (context.state.sum % 2) {
      context.commit('ADD', value) // 通知Mutations
    }
  },
  // 延迟500ms加(异步)
  addWait(context, value) {
    setTimeout(() => {
      context.commit('ADD', value)
    }, 500)
  }
}

// 2. Mutations:修改State数据(只能同步)
const mutations = {
  ADD(state, value) {
    state.sum += value
  },
  SUBTRACT(state, value) {
    state.sum -= value
  }
}

// 3. State:存储共享数据,应用的所有状态集中在这里
const state = {
  sum: 0 // 初始求和为0
}

// 4. 暴露Store
export default new Vuex.Store({
  actions,
  mutations,
  state
})

第二步:组件中使用 Vuex(Count.vue)

bash 复制代码
<template>
  <div>
    <h1>当前求和:{{ $store.state.sum }}</h1>
    <select v-model.number="n">
      <option value="1">1</option>
      <option value="2">2</option>
      <option value="3">3</option>
    </select>
    <button @click="increment">+</button>
    <button @click="decrement">-</button>
    <button @click="incrementOdd">奇数再加</button>
    <button @click="incrementWait">延迟加</button>
  </div>
</template>

<script>
export default {
  name: 'Count',
  data() {
    return { n: 1 } // 用户选择的数字
  },
  methods: {
    // 无业务逻辑:直接操作Mutations
    increment() {
      this.$store.commit('ADD', this.n)
    },
    decrement() {
      this.$store.commit('SUBTRACT', this.n)
    },
    // 有业务逻辑/异步:先操作Actions
    incrementOdd() {
      this.$store.dispatch('addOdd', this.n)
    },
    incrementWait() {
      this.$store.dispatch('addWait', this.n)
    }
  }
}
</script>

4.4 getters 配置项:数据加工

State 数据需要 "加工后使用 "(比如求和结果 ×10),用 getters(类似组件的计算属性)。
第一步:store/index.js 中添加 getters

bash 复制代码
// ...其他代码不变
const getters = {
  bigSum(state) {
    return state.sum * 10 // 求和结果×10
  }
}

// 暴露Store时加入getters
export default new Vuex.Store({
  actions,
  mutations,
  state,
  getters // 新增
})

第二步:组件中使用 getters

bash 复制代码
<!-- 直接通过$store.getters.xxx访问 -->
<h1>求和×10:{{ $store.getters.bigSum }}</h1>

4.5 四个 map 方法:简化代码

手动写$store.state.sum/$store.commit()太繁琐,Vuex 提供 4 个 map 方法,map函数让我们可以像使用本地数据一样使用Vuex,帮我们快速生成代码。

核心用法(Count.vue)

bash 复制代码
<template>
  <div>
    <h1>当前求和:{{ sum }}</h1>
    <h1>求和×10:{{ bigSum }}</h1>
    <button @click="increment(n)">+</button>
    <button @click="decrement(n)">-</button>
    <button @click="addOdd(n)">奇数再加</button>
    <button @click="addWait(n)">延迟加</button>
  </div>
</template>

<script>
// 引入4个map方法
import { mapState, mapGetters, mapMutations, mapActions } from 'vuex'

export default {
  name: 'Count',
  data() {
    return { n: 1 }
  },
  // 1. mapState + mapGetters:生成计算属性
  computed: {
    // mapState:映射State数据(数组写法,名称一致)
    ...mapState(['sum']),
    // mapGetters:映射getters数据
    ...mapGetters(['bigSum']),
    
    // 名称不一致用对象写法:...mapState({ 别名: '原名称' })
    // ...mapState({ heji: 'sum' })
  },
  // 2. mapMutations + mapActions:生成方法
  methods: {
    // mapMutations:映射Mutations方法
    ...mapMutations(['ADD', 'SUBTRACT']),
    // mapActions:映射Actions方法
    ...mapActions(['addOdd', 'addWait']),
    
    // 封装方法,传递参数n
    increment(n) { this.ADD(n) },
    decrement(n) { this.SUBTRACT(n) }
  }
}
</script>

4.6 模块化 + 命名空间(进阶:分类管理数据)

当项目数据多(比如 "求和数据""用户数据"),用模块化分类管理,代码更清晰。

第一步:修改 store/index.js

bash 复制代码
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)

// 模块1:求和相关
const countAbout = {
  namespaced: true, // 开启命名空间
  state: { sum: 0, x: 1 },
  mutations: { ADD(state, value) { state.sum += value } },
  actions: { addOdd(context, value) { /* 业务逻辑 */ } },
  getters: { bigSum(state) { return state.sum * 10 } }
}

// 模块2:用户相关
const personAbout = {
  namespaced: true, // 开启命名空间
  state: { list: ['张三', '李四'] },
  mutations: { ADD_PERSON(state, value) { state.list.push(value) } },
  actions: { /* 业务逻辑 */ }
}

// 注册模块
export default new Vuex.Store({
  modules: {
    countAbout, // 求和模块
    personAbout // 用户模块
  }
})

第二步:组件中使用模块化数据

bash 复制代码
<script>
import { mapState, mapGetters, mapMutations, mapActions } from 'vuex'

export default {
  computed: {
    // 方式1:直接读取(模块名.数据名)
    sum() { return this.$store.state.countAbout.sum },
    // 方式2:mapState(指定模块名)
    ...mapState('countAbout', ['sum', 'x']),
    
    // getters读取(方式1:字符串拼接)
    bigSum() { return this.$store.getters['countAbout/bigSum'] },
    // 方式2:mapGetters
    ...mapGetters('countAbout', ['bigSum'])
  },
  methods: {
    // commit(方式1:模块名/方法名)
    add() { this.$store.commit('countAbout/ADD', 1) },
    // 方式2:mapMutations(指定模块名)
    ...mapMutations('countAbout', { increment: 'ADD' }),
    
    // dispatch(方式1:模块名/方法名)
    addOdd() { this.$store.dispatch('countAbout/addOdd', 1) },
    // 方式2:mapActions
    ...mapActions('countAbout', ['addOdd'])
  }
}
</script>

五、最佳实践与常见问题

1. 组织store结构

bash 复制代码
store/
├── index.js          # 主文件,组装模块
├── modules/          # 模块目录
│   ├── user.js       # 用户相关状态
│   ├── cart.js       # 购物车状态
│   ├── products.js   # 商品状态
│   └── order.js      # 订单状态
├── getters.js        # 全局getters
└── mutation-types.js # 常量定义(可选)

2. 错误处理模式

bash 复制代码
// store/modules/user.js
actions: {
  async login({ commit }, credentials) {
    try {
      commit('SET_LOADING', true)
      const response = await axios.post('/api/login', credentials)
      
      if (response.data.success) {
        commit('SET_USER', response.data.user)
        return { success: true, data: response.data }
      } else {
        commit('SET_ERROR', response.data.message)
        return { success: false, error: response.data.message }
      }
    } catch (error) {
      commit('SET_ERROR', error.message)
      return { success: false, error: error.message }
    } finally {
      commit('SET_LOADING', false)
    }
  }
}

3. 调试技巧

bash 复制代码
// 在组件中
this.$store.subscribe((mutation, state) => {
  console.log('Mutation:', mutation.type)
  console.log('Payload:', mutation.payload)
  console.log('State:', state)
})

// 浏览器中
// 1. 安装Vue DevTools
// 2. 查看Vuex面板
// 3. 使用时间旅行调试

六、实战:电商应用状态管理

完整示例:商品列表与购物车

bash 复制代码
<!-- ProductList.vue -->
<template>
  <div class="product-list">
    <div v-for="product in products" :key="product.id" class="product-card">
      <h3>{{ product.name }}</h3>
      <p>价格:¥{{ product.price }}</p>
      <p>库存:{{ product.stock }}</p>
      <button 
        @click="addToCart(product)"
        :disabled="!product.stock || isInCart(product.id)"
      >
        {{ isInCart(product.id) ? '已加入' : '加入购物车' }}
      </button>
    </div>
  </div>
</template>

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

export default {
  computed: {
    ...mapState('products', ['list', 'loading']),
    ...mapState('cart', ['items']),
    
    products() {
      return this.list.filter(p => p.stock > 0)
    }
  },
  
  methods: {
    ...mapActions('cart', ['addItem']),
    ...mapGetters('cart', ['isItemInCart']),
    
    isInCart(productId) {
      return this.items.some(item => item.id === productId)
    },
    
    addToCart(product) {
      this.addItem(product)
      this.$message.success(`已添加 ${product.name}`)
    }
  },
  
  created() {
    this.$store.dispatch('products/fetchProducts')
  }
}
</script>

购物车组件

bash 复制代码
<!-- ShoppingCart.vue -->
<template>
  <div class="shopping-cart">
    <h2>购物车 ({{ cartCount }})</h2>
    
    <div v-if="cartItems.length === 0" class="empty-cart">
      购物车是空的
    </div>
    
    <div v-else>
      <div v-for="item in cartItems" :key="item.id" class="cart-item">
        <div class="item-info">
          <h4>{{ item.name }}</h4>
          <p>单价:¥{{ item.price }}</p>
        </div>
        
        <div class="item-quantity">
          <button @click="decreaseQuantity(item.id)">-</button>
          <span>{{ item.quantity }}</span>
          <button 
            @click="increaseQuantity(item.id)"
            :disabled="item.quantity >= item.stock"
          >+</button>
        </div>
        
        <div class="item-total">
          ¥{{ item.price * item.quantity }}
        </div>
        
        <button @click="removeItem(item.id)" class="remove-btn">
          删除
        </button>
      </div>
      
      <div class="cart-summary">
        <p>总计:¥{{ cartTotal }}</p>
        <button @click="checkout" :disabled="!isLoggedIn">
          {{ isLoggedIn ? '去结算' : '请先登录' }}
        </button>
      </div>
    </div>
  </div>
</template>

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

export default {
  computed: {
    ...mapState('cart', ['items']),
    ...mapState('user', ['info']),
    ...mapGetters('cart', ['totalCount', 'totalPrice']),
    
    cartItems() {
      return this.items
    },
    cartCount() {
      return this.totalCount
    },
    cartTotal() {
      return this.totalPrice
    },
    isLoggedIn() {
      return !!this.info
    }
  },
  
  methods: {
    ...mapActions('cart', [
      'updateQuantity',
      'removeItem',
      'clearCart'
    ]),
    
    increaseQuantity(productId) {
      const item = this.items.find(i => i.id === productId)
      if (item && item.quantity < item.stock) {
        this.updateQuantity({ productId, quantity: item.quantity + 1 })
      }
    },
    
    decreaseQuantity(productId) {
      const item = this.items.find(i => i.id === productId)
      if (item && item.quantity > 1) {
        this.updateQuantity({ productId, quantity: item.quantity - 1 })
      }
    },
    
    checkout() {
      this.$store.dispatch('order/createOrder', this.items)
        .then(() => {
          this.clearCart()
          this.$router.push('/orders')
        })
    }
  }
}
</script>

七、总结

核心要点回顾

  • Ajax 代理 :Vue 脚手架配置代理分单代理(简单)和多代理(灵活),核心是pathRewrite去掉前缀、changeOrigin伪装请求来源;
  • 插槽:默认插槽传单个 HTML、具名插槽传多个 HTML、作用域插槽实现 "子传数据,父定结构";
  • Vuex
    • State 存数据、Mutations 改数据(同步)、Actions 处理业务(异步)、Getters 加工数据;
    • 四个 map 方法简化代码,模块化需开启namespaced,访问时加模块名。
相关推荐
多看书少吃饭2 小时前
文件预览的正确做法:从第三方依赖到企业级自建方案(Vue + Java 实战)
java·前端·vue.js
菜鸟很沉2 小时前
Vue3 + Element Plus 实现大文件分片上传组件(支持秒传、断点续传)
javascript·vue.js
Amumu121382 小时前
Vue核心(一)
前端·javascript·vue.js
一直都在5722 小时前
Spring3整合MyBatis实现增删改查操作
前端·vue.js·mybatis
钟佩颖2 小时前
Vue....
前端·javascript·vue.js
匠心网络科技3 小时前
前端框架-Vue双向绑定核心机制全解析
前端·javascript·vue.js·前端框架
刘一说14 小时前
Vue3 组合式 API(Composition API):逻辑复用的革命性实践
vue.js·vue
xixixin_1 天前
【vue】中字符串与数组转换:为何首选 Computed 而非 Methods?
前端·javascript·vue.js
i_am_a_div_日积月累_1 天前
el-drawer注册全局点击事件无效;el-dialog注册全局点击事件无效
javascript·vue.js·elementui