作为前端开发的核心框架之一,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 脚手架配置技巧
脚手架默认配置可通过命令查看,个性化需求则通过配置文件实现:
- 查看默认配置:执行
vue inspect > output.js,将默认配置输出到 output.js 文件 - 个性化配置:创建
vue.config.js文件自定义配置,官方文档参考Vue CLI 配置,核心配置方向包括代理、入口文件、静态资源等。
三、Vue 组件核心配置
3.1 ref 属性:DOM 与组件的引用桥梁
ref 用于给元素或组件注册引用信息,替代传统 DOM 操作中的 id,使用简单高效:
- 作用场景:获取 DOM 元素、调用子组件方法或访问子组件数据
- 使用方式:
- 标记目标:
<h1 ref="title">标题</h1>或<User ref="userComp"></User> - 访问目标:
this.$refs.title(获取 DOM 元素)、this.$refs.userComp(获取组件实例)
- 标记目标:
3.2 props 配置:父组件向子组件传值
props 是父向子通信的核心方式,支持数据验证,确保数据规范性:
-
基础用法:
-
父组件传值:
<User name="张三" age="20"></User> -
子组件接收:
- 简单接收:
props: ['name', 'age'] - 类型限制:
props: { name: String, age: Number } - 完整配置:包含类型、必要性、默认值
javascriptprops: { 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方法的对象:
-
插件定义:
javascriptexport 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 自定义事件:子组件向父组件传值
最常用的子向父通信方式,回调函数定义在父组件,子组件触发执行:
- 绑定方式:
- 模板绑定:
<User @sendMsg="handleMsg"></User> - 编程式绑定:通过 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 实例创建前配置
javascriptnew Vue({ beforeCreate() { Vue.prototype.$bus = this // 将Vue实例作为总线挂载到原型 }, render: h => h(App) }).$mount('#app') -
使用方式:
- 接收数据:在组件 mounted 中绑定事件
this.$bus.$on('msg', (data) => {}) - 发送数据:在任意组件中触发
this.$bus.$emit('msg', 数据) - 销毁事件:在组件 beforeDestroy 中解绑
this.$bus.$off('msg'),避免内存泄漏
- 接收数据:在组件 mounted 中绑定事件
步骤:
-
安装全局事件总线:
javascriptnew Vue({ ...... beforeCreate() { Vue.prototype.$bus = this //安装全局事件总线,$bus就是当前应用的vm }, ...... }) -
使用事件总线:
-
接收数据:A组件想接收数据,则在A组件中给$bus绑定自定义事件,事件的<span style="color:red">回调留在A组件自身。</span>
javascriptmethods(){ demo(data){......} } ...... mounted() { this.$bus.$on('xxxx',this.demo) } -
提供数据:
this.$bus.$emit('xxxx',数据)
-
-
最好在beforeDestroy钩子中,用$off去解绑<span style="color:red">当前组件所用到的</span>事件。
-

4.3 消息订阅与发布(pubsub):跨组件通信补充
第三方库实现的通信方案,适用于复杂组件结构,与全局总线功能类似:
- 使用步骤:
- 安装依赖:
npm i pubsub-js - 引入库:
import pubsub from 'pubsub-js' - 订阅消息:
this.pubId = pubsub.subscribe('msg', (msgName, data) => {}) - 发布消息:
pubsub.publish('msg', 数据) - 取消订阅:在 beforeDestroy 中执行
pubsub.unsubscribe(this.pubId)
- 安装依赖:
4.4 插槽:父组件向子组件传结构
特殊的通信方式,允许父组件向子组件指定位置插入 HTML 结构,分为三类:
-
默认插槽 :无名称的基础插槽,父组件直接插入内容
javascript<!-- 父组件 --> <Card><div>卡片内容</div></Card> <!-- 子组件 --> <template><div><slot>默认内容</slot></div></template> -
具名插槽 :给插槽命名,实现多位置精准插入
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> -
作用域插槽 :子组件向父组件传数据,父组件决定渲染结构(数据在子,结构在父)
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 实现请求转发:
-
简单代理(单目标):适合仅对接一个后端服务的场景
javascriptmodule.exports = { devServer: { proxy: "http://localhost:5000" // 后端服务地址 } }原理:请求前端不存在的资源时,自动转发到后端服务。
-
多代理配置(多目标):适合对接多个后端服务的场景
javascriptmodule.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. 功能说明
- 添加功能:在输入框输入内容,按回车添加到待办列表
- 状态切换:点击复选框可切换单个待办项的完成状态
- 全选功能:底部复选框可一键全选 / 取消全选所有待办项
- 删除功能:点击单个待办项后的 "×" 可删除该项
- 清除功能:点击 "清除已完成" 可删除所有已完成的待办项
- 统计功能:实时显示未完成的待办项数量
- 数据持久化:通过 localStorage 保存数据,页面刷新后数据不丢失
4. 核心技术点
- 组件化拆分与通信(props 传递数据,自定义事件传递操作)
- 数据持久化(localStorage + watch 深度监听)
- 列表渲染与 key 的使用(v-for 遍历,时间戳作为唯一 ID)
- 计算属性(computed)处理派生数据(未完成数量、全选状态等)
- 事件委派与事件转发(子组件事件通过父组件中转)
通过 TodoList 案例可串联组件开发全流程,核心思路如下:
- 组件拆分:按功能拆分为 TodoHeader(输入)、TodoList(列表)、TodoItem(项)、TodoFooter(统计)
- 数据管理:采用状态提升,将待办数据存放在根组件,子组件通过 props 接收或通过自定义事件修改
- 核心原则 :
- v-model 绑定的值不能是 props 直接传递的内容
- 父子通信优先用 props + 自定义事件
- 跨组件通信可采用全局事件总线
- 本地存储:结合 localStorage 实现待办数据持久化,页面刷新后数据不丢失
结语
Vue 脚手架的核心知识点围绕工程结构、组件配置、通信方案三大维度展开。实际开发中,需根据项目规模和业务场景灵活选择技术方案:小型项目可简化配置,聚焦组件通信;大型项目则需规范工程结构,合理使用混入、插件提升代码复用性。掌握本文梳理的知识点,可轻松应对多数 Vue 开发场景,为后续学习 Vue3 打下坚实基础。