Vue 2脚手架从入门到实战核心知识点全解析(day6):从工程结构到高级通信(附代码讲解)

作为前端开发的核心框架之一,Vue 的脚手架工具(Vue CLI)极大提升了开发效率。本文将从脚手架工程结构入手,系统梳理 Vue 核心配置、组件通信、存储方案等关键知识点,助力开发者夯实基础、高效开发。

一、Vue 脚手架工程结构详解

脚手架初始化后的项目结构清晰规范,各目录文件各司其职,理解其含义是高效开发的前提:

XML 复制代码
├── node_modules  # 项目依赖包,npm install生成
├── public        # 静态资源根目录,不会被webpack处理
│   ├── favicon.ico  # 浏览器标签页图标
│   └── index.html   # 应用入口HTML,Vue实例挂载的载体
├── src           # 源代码核心目录,开发核心区域
│   ├── assets    # 静态资源(图片、样式等),会被webpack处理
│   ├── component # 公共组件目录,可复用的UI单元
│   ├── App.vue   # 根组件,汇总所有业务组件的入口
│   └── main.js   # 程序入口文件,初始化Vue实例、配置全局资源
├── .gitignore    # Git版本控制忽略规则,指定无需提交的文件
├── babel.config.js # Babel配置文件,实现ES6+语法向下兼容
├── package.json  # 项目配置核心文件,包含依赖、脚本命令等信息
├── README.md     # 项目说明文档,记录开发规范、启动方式等
└── package-lock.json # 锁定依赖包版本,确保团队开发环境一致

二、Vue 核心版本差异与配置

2.1 核心文件版本区别

Vue 的核心文件分为完整版和运行时版,实际开发中需根据场景选择:

  • vue.js(完整版) :包含核心功能 + 模板解析器,支持直接在组件中使用template配置项,开发调试更便捷。
  • vue.runtime.xxx.js(运行时版) :仅包含核心功能,无模板解析器,体积更小。因缺少解析器,不能使用template,需通过render函数结合createElement方法构建 DOM 结构,适合生产环境。

2.2 脚手架配置技巧

脚手架默认配置可通过命令查看,个性化需求则通过配置文件实现:

  1. 查看默认配置:执行vue inspect > output.js,将默认配置输出到 output.js 文件
  2. 个性化配置:创建vue.config.js文件自定义配置,官方文档参考Vue CLI 配置,核心配置方向包括代理、入口文件、静态资源等。

三、Vue 组件核心配置

3.1 ref 属性:DOM 与组件的引用桥梁

ref 用于给元素或组件注册引用信息,替代传统 DOM 操作中的 id,使用简单高效:

  • 作用场景:获取 DOM 元素、调用子组件方法或访问子组件数据
  • 使用方式:
    1. 标记目标:<h1 ref="title">标题</h1><User ref="userComp"></User>
    2. 访问目标:this.$refs.title(获取 DOM 元素)、this.$refs.userComp(获取组件实例)

3.2 props 配置:父组件向子组件传值

props 是父向子通信的核心方式,支持数据验证,确保数据规范性:

  • 基础用法:

    1. 父组件传值:<User name="张三" age="20"></User>

    2. 子组件接收:

      • 简单接收:props: ['name', 'age']
      • 类型限制:props: { name: String, age: Number }
      • 完整配置:包含类型、必要性、默认值
      javascript 复制代码
      props: {
        name: {
          type: String,    // 数据类型
          required: true,  // 是否必传
          default: '匿名'  // 默认值(required为false时生效)
        }
      }
  • 注意事项:props 是只读属性,若需修改,需复制到 data 中(data() { return { userName: this.name } })再操作。

3.3 mixin 混入:组件代码复用方案

当多个组件存在相同配置(如 data、methods)时,mixin 可实现代码抽离复用:

  • 定义混入对象:

    javascript 复制代码
    // mixin.js
    export const myMixin = {
      data() { return { count: 0 } },
      methods: { increment() { this.count++ } }
    }
  • 使用方式:

    • 局部混入(仅当前组件生效):mixins: [myMixin]
    • 全局混入(所有组件生效,谨慎使用):Vue.mixin(myMixin)

3.4 插件:Vue 功能增强利器

插件用于扩展 Vue 的核心能力,本质是包含install方法的对象:

  • 插件定义:

    javascript 复制代码
    export const myPlugin = {
      install(Vue, options) {
        // 全局过滤器
        Vue.filter('formatDate', (val) => new Date(val).toLocaleString())
        // 全局指令
        Vue.directive('focus', { inserted(el) { el.focus() } })
        // 实例方法
        Vue.prototype.$showMsg = (msg) => alert(msg)
      }
    }
  • 插件使用:在 main.js 中引入后调用Vue.use(myPlugin),即可全局使用插件功能。

3.5 scoped 样式:组件样式隔离

避免组件间样式冲突的核心方案,通过给样式添加作用域实现:

  • 用法:在 style 标签添加scoped属性,如<style scoped>
  • 原理:webpack 编译时会给组件 DOM 添加唯一属性,样式选择器自动关联该属性,确保仅作用于当前组件。

四、组件通信全方案

组件通信是 Vue 开发的核心场景,不同场景对应不同方案,需灵活选择:

4.1 自定义事件:子组件向父组件传值

最常用的子向父通信方式,回调函数定义在父组件,子组件触发执行:

  • 绑定方式:
    1. 模板绑定:<User @sendMsg="handleMsg"></User>
    2. 编程式绑定:通过 ref 获取组件后绑定this.$refs.user.$on('sendMsg', this.handleMsg)
  • 触发方式:子组件中通过this.$emit('sendMsg', 数据)触发事件并传值
  • 进阶技巧:添加once修饰符(<User @sendMsg.once="handleMsg">)实现事件仅触发一次;通过this.$off('sendMsg')解绑事件。

4.2 全局事件总线:任意组件间通信

基于 Vue 实例实现的全局通信方案,适用于跨层级、非关联组件通信:

  • 安装总线:在 main.js 的 Vue 实例创建前配置

    javascript 复制代码
    new Vue({
      beforeCreate() {
        Vue.prototype.$bus = this // 将Vue实例作为总线挂载到原型
      },
      render: h => h(App)
    }).$mount('#app')
  • 使用方式:

    1. 接收数据:在组件 mounted 中绑定事件this.$bus.$on('msg', (data) => {})
    2. 发送数据:在任意组件中触发this.$bus.$emit('msg', 数据)
    3. 销毁事件:在组件 beforeDestroy 中解绑this.$bus.$off('msg'),避免内存泄漏

步骤:

  • 安装全局事件总线:

    javascript 复制代码
     new Vue({
        ......
        beforeCreate() {
          Vue.prototype.$bus = this //安装全局事件总线,$bus就是当前应用的vm
        },
          ......
      }) 
  • 使用事件总线:

    1. 接收数据:A组件想接收数据,则在A组件中给$bus绑定自定义事件,事件的<span style="color:red">回调留在A组件自身。</span>

      javascript 复制代码
        methods(){
          demo(data){......}
        }
        ......
        mounted() {
          this.$bus.$on('xxxx',this.demo)
        }
    2. 提供数据:this.$bus.$emit('xxxx',数据)

  • 最好在beforeDestroy钩子中,用$off去解绑<span style="color:red">当前组件所用到的</span>事件。

4.3 消息订阅与发布(pubsub):跨组件通信补充

第三方库实现的通信方案,适用于复杂组件结构,与全局总线功能类似:

  • 使用步骤:
    1. 安装依赖:npm i pubsub-js
    2. 引入库:import pubsub from 'pubsub-js'
    3. 订阅消息:this.pubId = pubsub.subscribe('msg', (msgName, data) => {})
    4. 发布消息:pubsub.publish('msg', 数据)
    5. 取消订阅:在 beforeDestroy 中执行pubsub.unsubscribe(this.pubId)

4.4 插槽:父组件向子组件传结构

特殊的通信方式,允许父组件向子组件指定位置插入 HTML 结构,分为三类:

  1. 默认插槽 :无名称的基础插槽,父组件直接插入内容

    javascript 复制代码
    <!-- 父组件 -->
    <Card><div>卡片内容</div></Card>
    <!-- 子组件 -->
    <template><div><slot>默认内容</slot></div></template>
  2. 具名插槽 :给插槽命名,实现多位置精准插入

    javascript 复制代码
    <!-- 父组件 -->
    <Card>
      <template v-slot:header><h3>标题</h3></template>
      <template #footer><button>按钮</button></template>
    </Card>
    <!-- 子组件 -->
    <template>
      <div>
        <slot name="header"></slot>
        <slot name="footer"></slot>
      </div>
    </template>
  3. 作用域插槽 :子组件向父组件传数据,父组件决定渲染结构(数据在子,结构在父)

    javascript 复制代码
    <!-- 父组件 -->
    <GameList>
      <template scope="scopeData">
        <ul><li v-for="g in scopeData.games" :key="g">{{g}}</li></ul>
      </template>
    </GameList>
    <!-- 子组件 -->
    <template>
      <div>
        <slot :games="games"></slot> <!-- 向父组件传数据 -->
      </div>
    </template>
    <script>
    export default {
      data() { return { games: ['王者荣耀', '原神'] } }
    }
    </script>

五、数据存储与网络请求配置

5.1 webStorage 本地存储

浏览器端本地存储方案,适用于存储少量非敏感数据,分为两类:

特性 sessionStorage localStorage
存储周期 浏览器窗口关闭后消失 永久存储,需手动清除
作用域 同一窗口共享 同一域名下所有窗口共享
核心 API 相同,均为 setItem、getItem、removeItem、clear
存储大小 约 5MB 约 5MB
  • 实用技巧:存储对象时需先 JSON 序列化(JSON.stringify(obj)),读取时反序列化(JSON.parse(str))。

5.2 脚手架配置代理

解决前端跨域问题的核心方案,通过配置 vue.config.js 实现请求转发:

  1. 简单代理(单目标):适合仅对接一个后端服务的场景

    javascript 复制代码
    module.exports = {
      devServer: {
        proxy: "http://localhost:5000" // 后端服务地址
      }
    }

    原理:请求前端不存在的资源时,自动转发到后端服务。

  2. 多代理配置(多目标):适合对接多个后端服务的场景

    javascript 复制代码
    module.exports = {
      devServer: {
        proxy: {
          '/api1': { // 匹配以/api1开头的请求
            target: 'http://localhost:5000', // 目标服务1
            changeOrigin: true, // 隐藏前端真实端口
            pathRewrite: {'^/api1': ''} // 移除请求前缀
          },
          '/api2': { // 匹配以/api2开头的请求
            target: 'http://localhost:5001', // 目标服务2
            changeOrigin: true,
            pathRewrite: {'^/api2': ''}
          }
        }
      }
    }

六、实用进阶技巧

6.1 nextTick:DOM 更新后的回调

当数据修改后需操作更新后的 DOM 时,使用 nextTick 确保操作时机正确:

  • 用法:this.$nextTick(() => { /* 操作DOM的代码 */ })
  • 原理:VueDOM 更新是异步的,nextTick 会在 DOM 更新完成后触发回调。

6.2 过渡与动画:提升用户体验

Vue 封装的过渡动画能力,通过<transition>标签实现元素插入 / 移除动画:

  • 基础用法:

    javascript 复制代码
    <template>
      <transition name="fade">
        <div v-show="isShow">动画元素</div>
      </transition>
    </template>
    <style>
    .fade-enter-active, .fade-leave-active { transition: opacity 0.5s; }
    .fade-enter, .fade-leave-to { opacity: 0; }
    </style>
  • 多元素过渡:使用<transition-group>标签,需给子元素添加唯一 key。

七、TodoList 案例核心总结

案例代码:

1. 项目结构

复制代码
src/
├── components/
│   ├── TodoHeader.vue   // 输入框组件:添加待办事项
│   ├── TodoList.vue     // 列表容器组件:展示待办列表
│   ├── TodoItem.vue     // 列表项组件:单个待办项的展示与操作
│   └── TodoFooter.vue   // 底部组件:全选、统计、清除已完成
├── App.vue              // 根组件:整合所有组件,管理核心数据
└── main.js              // 入口文件:创建Vue实例

2. 核心代码实现

main.js(入口文件)
javascript 复制代码
import Vue from 'vue'
import App from './App.vue'

Vue.config.productionTip = false

new Vue({
  render: h => h(App),
}).$mount('#app')
App.vue(根组件,数据管理核心)
javascript 复制代码
<template>
  <div id="app">
    <div class="todo-container">
      <div class="todo-wrap">
        <!-- 头部输入组件 -->
        <TodoHeader @addTodo="addTodo" />
        <!-- 列表组件(传递待办数据和操作方法) -->
        <TodoList 
          :todos="todos" 
          @deleteTodo="deleteTodo" 
          @checkTodo="checkTodo"
        />
        <!-- 底部统计组件(传递数据和操作方法) -->
        <TodoFooter 
          :todos="todos" 
          @checkAllTodo="checkAllTodo" 
          @clearAllDone="clearAllDone"
        />
      </div>
    </div>
  </div>
</template>

<script>
import TodoHeader from './components/TodoHeader'
import TodoList from './components/TodoList'
import TodoFooter from './components/TodoFooter'

export default {
  name: 'App',
  components: { TodoHeader, TodoList, TodoFooter },
  data() {
    return {
      // 从本地存储读取数据(初始化)
      todos: JSON.parse(localStorage.getItem('todos')) || []
    }
  },
  methods: {
    // 添加待办事项
    addTodo(todoName) {
      if (!todoName.trim()) return  // 空内容不添加
      this.todos.unshift({
        id: Date.now(),  // 用时间戳作为唯一ID
        title: todoName,
        done: false      // 默认未完成
      })
    },
    // 删除待办事项
    deleteTodo(id) {
      this.todos = this.todos.filter(todo => todo.id !== id)
    },
    // 切换待办事项状态(完成/未完成)
    checkTodo(id) {
      this.todos.forEach(todo => {
        if (todo.id === id) todo.done = !todo.done
      })
    },
    // 全选/取消全选
    checkAllTodo(isCheckAll) {
      this.todos.forEach(todo => {
        todo.done = isCheckAll
      })
    },
    // 清除所有已完成事项
    clearAllDone() {
      this.todos = this.todos.filter(todo => !todo.done)
    }
  },
  // 监听todos变化,同步到本地存储(实现数据持久化)
  watch: {
    todos: {
      deep: true,  // 深度监听(对象内部属性变化)
      handler(value) {
        localStorage.setItem('todos', JSON.stringify(value))
      }
    }
  }
}
</script>

<style>
/* 基础样式 */
body {
  background: #fff;
}
.todo-container {
  width: 600px;
  margin: 0 auto;
  padding: 20px;
  box-shadow: 0 0 5px #ccc;
}
.todo-wrap {
  padding: 10px;
  border: 1px solid #ddd;
  border-radius: 5px;
}
</style>
TodoHeader.vue(添加待办组件)
javascript 复制代码
<template>
  <div class="todo-header">
    <input 
      type="text" 
      placeholder="请输入你的任务名称,按回车确认"
      @keyup.enter="handleKeyUp"
      v-model="todoName"
    >
  </div>
</template>

<script>
export default {
  name: 'TodoHeader',
  data() {
    return {
      todoName: ''  // 绑定输入框内容
    }
  },
  methods: {
    // 回车添加待办
    handleKeyUp() {
      // 通过自定义事件将输入内容传递给父组件
      this.$emit('addTodo', this.todoName)
      // 清空输入框
      this.todoName = ''
    }
  }
}
</script>

<style scoped>
.todo-header input {
  width: 100%;
  height: 40px;
  padding: 0 10px;
  font-size: 16px;
  border: 1px solid #ddd;
  border-radius: 4px;
  box-sizing: border-box;
}
.todo-header input:focus {
  outline: none;
  border-color: #66afe9;
  box-shadow: 0 0 8px rgba(102, 175, 233, 0.6);
}
</style>
TodoList.vue(列表容器组件)
javascript 复制代码
<template>
  <ul class="todo-list">
    <!-- 遍历待办数据,渲染列表项 -->
    <TodoItem 
      v-for="todo in todos" 
      :key="todo.id" 
      :todo="todo"  // 传递单个待办项数据
      @deleteTodo="deleteTodo"  // 传递删除事件
      @checkTodo="checkTodo"    // 传递状态切换事件
    />
  </ul>
</template>

<script>
import TodoItem from './TodoItem'

export default {
  name: 'TodoList',
  components: { TodoItem },
  props: {
    todos: {  // 接收父组件传递的待办列表
      type: Array,
      required: true
    }
  },
  methods: {
    // 转发删除事件给父组件
    deleteTodo(id) {
      this.$emit('deleteTodo', id)
    },
    // 转发状态切换事件给父组件
    checkTodo(id) {
      this.$emit('checkTodo', id)
    }
  }
}
</script>

<style scoped>
.todo-list {
  margin: 0;
  padding: 0;
  list-style: none;
}
</style>
TodoItem.vue(列表项组件)
javascript 复制代码
<template>
  <li>
    <label>
      <!-- 复选框绑定待办状态 -->
      <input 
        type="checkbox" 
        :checked="todo.done"
        @change="handleCheck"
      >
      <!-- 已完成项添加删除线样式 -->
      <span :class="{ done: todo.done }">{{ todo.title }}</span>
    </label>
    <!-- 删除按钮 -->
    <button class="btn-del" @click="handleDelete">×</button>
  </li>
</template>

<script>
export default {
  name: 'TodoItem',
  props: {
    todo: {  // 接收单个待办项数据
      type: Object,
      required: true
    }
  },
  methods: {
    // 切换待办状态
    handleCheck() {
      this.$emit('checkTodo', this.todo.id)
    },
    // 删除当前待办项
    handleDelete() {
      if (confirm('确定要删除吗?')) {
        this.$emit('deleteTodo', this.todo.id)
      }
    }
  }
}
</script>

<style scoped>
li {
  padding: 10px;
  border-bottom: 1px solid #eee;
  display: flex;
  align-items: center;
  justify-content: space-between;
}
li:hover {
  background-color: #f5f5f5;
}
li label {
  flex: 1;
  cursor: pointer;
}
li label input {
  margin-right: 8px;
}
/* 已完成样式 */
.done {
  color: #999;
  text-decoration: line-through;
}
/* 删除按钮样式 */
.btn-del {
  width: 30px;
  height: 30px;
  border: none;
  background: #f44336;
  color: white;
  border-radius: 50%;
  cursor: pointer;
  font-size: 16px;
  display: flex;
  align-items: center;
  justify-content: center;
}
.btn-del:hover {
  background: #d32f2f;
}
</style>
TodoFooter.vue(底部统计组件)
javascript 复制代码
<template>
  <div class="todo-footer">
    <label>
      <!-- 全选复选框 -->
      <input 
        type="checkbox" 
        :checked="isCheckAll"
        @change="handleCheckAll"
      >
    </label>
    <span>
      <!-- 统计未完成数量 -->
      剩余 <span class="todo-count">{{ remainingCount }}</span> 项未完成
    </span>
    <!-- 清除已完成按钮(有已完成项才显示) -->
    <button 
      class="btn-clear" 
      @click="handleClearAll"
      v-show="hasDone"
    >
      清除已完成
    </button>
  </div>
</template>

<script>
export default {
  name: 'TodoFooter',
  props: {
    todos: {
      type: Array,
      required: true
    }
  },
  computed: {
    // 未完成数量
    remainingCount() {
      return this.todos.filter(todo => !todo.done).length
    },
    // 是否有已完成项
    hasDone() {
      return this.todos.some(todo => todo.done)
    },
    // 是否全选(所有项都完成时为true)
    isCheckAll() {
      return this.todos.length > 0 && this.todos.every(todo => todo.done)
    }
  },
  methods: {
    // 全选/取消全选
    handleCheckAll(e) {
      this.$emit('checkAllTodo', e.target.checked)
    },
    // 清除所有已完成项
    handleClearAll() {
      this.$emit('clearAllDone')
    }
  }
}
</script>

<style scoped>
.todo-footer {
  padding: 10px;
  border-top: 1px solid #ddd;
  display: flex;
  align-items: center;
  justify-content: space-between;
}
.todo-count {
  color: #e74c3c;
  font-weight: bold;
}
.btn-clear {
  padding: 5px 10px;
  border: none;
  background: #3498db;
  color: white;
  border-radius: 4px;
  cursor: pointer;
}
.btn-clear:hover {
  background: #2980b9;
}
</style>

3. 功能说明

  1. 添加功能:在输入框输入内容,按回车添加到待办列表
  2. 状态切换:点击复选框可切换单个待办项的完成状态
  3. 全选功能:底部复选框可一键全选 / 取消全选所有待办项
  4. 删除功能:点击单个待办项后的 "×" 可删除该项
  5. 清除功能:点击 "清除已完成" 可删除所有已完成的待办项
  6. 统计功能:实时显示未完成的待办项数量
  7. 数据持久化:通过 localStorage 保存数据,页面刷新后数据不丢失

4. 核心技术点

  • 组件化拆分与通信(props 传递数据,自定义事件传递操作)
  • 数据持久化(localStorage + watch 深度监听)
  • 列表渲染与 key 的使用(v-for 遍历,时间戳作为唯一 ID)
  • 计算属性(computed)处理派生数据(未完成数量、全选状态等)
  • 事件委派与事件转发(子组件事件通过父组件中转)

通过 TodoList 案例可串联组件开发全流程,核心思路如下:

  1. 组件拆分:按功能拆分为 TodoHeader(输入)、TodoList(列表)、TodoItem(项)、TodoFooter(统计)
  2. 数据管理:采用状态提升,将待办数据存放在根组件,子组件通过 props 接收或通过自定义事件修改
  3. 核心原则
    • v-model 绑定的值不能是 props 直接传递的内容
    • 父子通信优先用 props + 自定义事件
    • 跨组件通信可采用全局事件总线
  4. 本地存储:结合 localStorage 实现待办数据持久化,页面刷新后数据不丢失

结语

Vue 脚手架的核心知识点围绕工程结构、组件配置、通信方案三大维度展开。实际开发中,需根据项目规模和业务场景灵活选择技术方案:小型项目可简化配置,聚焦组件通信;大型项目则需规范工程结构,合理使用混入、插件提升代码复用性。掌握本文梳理的知识点,可轻松应对多数 Vue 开发场景,为后续学习 Vue3 打下坚实基础。

相关推荐
q***71852 小时前
海康威视摄像头ISUP(原EHOME协议) 摄像头实时预览springboot 版本java实现,并可以在浏览器vue前端播放(附带源码)
java·前端·spring boot
一只小阿乐2 小时前
vue3 使用v-model开发弹窗组件
javascript·vue.js·elementui
web加加2 小时前
vue3 +vite项目页面防f12,防打开控制台
前端·javascript·vue.js
A尘埃3 小时前
大模型应用python+Java后端+Vue前端的整合
java·前端·python
遥遥晚风点点3 小时前
Spark导出数据文件到HDFS
前端·javascript·ajax
克里斯蒂亚L4 小时前
开发一个计时器组件
前端·浏览器
克里斯蒂亚诺更新4 小时前
微信小程序 点击某个marker改变其大小
开发语言·前端·javascript
KYumii4 小时前
智慧判官-分布式编程评测平台
vue.js·spring boot·分布式·spring cloud·java-rabbitmq
天才奇男子4 小时前
从零开始搭建Linux Web服务器
linux·服务器·前端