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 打下坚实基础。

相关推荐
JeffongTan3 小时前
在LWC中镶嵌VF Page获取用户IP
前端·javascript·salesforce
不cong明的亚子5 小时前
web live多屏互动平台
javascript·经验分享·音视频
陆枫Larry5 小时前
Astro 是什么
前端
是立不是利7 小时前
写给设计师的 CSS 博客美学指南
前端·css
dsyyyyy11017 小时前
JS复习,难点,易错点总结
开发语言·javascript·ecmascript
默_笙7 小时前
🏝 Docker 就是"房地产开发":从施工图纸到小区物业的容器化指南
前端·javascript
计算机魔术师7 小时前
Rohan Paul 谈用时间跨度衡量智能体能力,并引用 OpenAI 自动化研究实习生里程碑
前端
kyriewen7 小时前
我手写了个极简版 React Router——才搞懂 v6 为什么砍了这么多 API
前端·javascript·程序员
a1117768 小时前
Shirone 二次元博客主题 开源
前端·开源
IT_陈寒8 小时前
Python的切片赋值把我坑惨了,这不是bug是特性
前端·人工智能·后端