01. 组件化思想
当我们面对一个复杂问题的时候,常见的、高效的做法就是对复杂问题进行拆分,
将复杂问题拆分成一个个小的、简单的问题,
逐一解决小问题,再将处理好的小问题整合到一起,
如此解决复杂问题。

在开发中,我们也是用同样的思路去完成一个大的项目:
-
将一个整体功能代码拆分成一个个小的功能块,每个功能块完成属于自己这部分独立的功能,
-
一个个功能块拆分后,就可以像搭建积木一下来搭建我们的项目。
这样做的好处除了降低开发的难度、提高代码复用率以外,也利于项目的管理和维护。
无论是前端三大框架(Vue、React、Angular),还是跨平台方案的Flutter,甚至是移动端都在转向组件化开发,包括小程序的开发也是采用组件化开发的思想。
02. 组件化基础
前面我们都是用Vue的createApp方法,传入一个对象来创建app的。
<script>
// 1.创建app
const app = Vue.createApp({
// data: option api
data() {
return {
message: "Hello Vue"
}
},
})
// 2.挂载app
app.mount("#app")
</script>
实际上,我们也可以先定义App对象,再将对象传入。
这个对象本质上就是一个组件,也就是我们应用程序的根组件。
<script>
// 1.组件: App组件(根组件)
const App = {
data() {
return {
message: "Hello Vue"
}
}
}
// 1.创建app
const app = Vue.createApp(App)
// 2.挂载app
app.mount("#app")
</script>
任何的应用都可以被抽象成一棵组件树。

03. 注册组件
3.1 全局组件
全局组件注册后,在任何其他的组件中都可以使用的组件;
需要注意的是,全局组件只要注册了,无论有没有使用到这个组件,在使用类似于webpack打包工具去打包项目时,都会对组件进行打包。
在开发中,很少注册全局组件。
示例:注册一个全局组件,用到app.component。
<body>
<div id="app">
<!-- 1.内容一: -->
<product-item></product-item>
<!-- 2.内容二: -->
<product-item></product-item>
<!-- 3.内容三: -->
<product-item></product-item>
</div>
<!-- 组件product-item的模板 -->
<template id="item">
<div class="product">
<h2>我是商品</h2>
<div>商品图片</div>
<div>商品价格: <span>¥9.9</span></div>
<p>商品描述信息, 9.9秒杀</p>
</div>
</template>
<script src="../lib/vue.js"></script>
<script>
/*
1.通过app.component(组件名称, 组件的对象)
2.在App组件的模板中, 可以直接使用product-item的组件
*/
// 1.组件: App组件(根组件)
const App = {}
// 2.创建app
const app = Vue.createApp(App)
// 3.注册一个全局组件
// 第一个参数定义组件的名称:product-item;第二参数传入一个对象。
app.component("product-item", {
template: "#item" // 此处也可以直接书写template
})
// 2.挂载app
app.mount("#app")
</script>
</body>
3.2 局部组件
局部组件,只有在注册的组件中才能使用的组件;
通过components属性选项来进行注册;

比如之前的App组件中,我们有data、computed、methods等选项了,事实上还可以有一个components选项;
该components选项对应的是一个对象,对象中的键值对是 组件的名称: 组件对象;
示例:注册局部组件
<body>
<div id="app">
<product-item></product-item>
<product-item></product-item>
<product-item></product-item>
</div>
<template id="product">
<div class="product">
<h2>{{title}}</h2>
<p>商品描述, 限时折扣, 赶紧抢购</p>
<p>价格: {{price}}</p>
<button>收藏</button>
</div>
</template>
<script>
// 1.创建app
const ProductItem = {
template: "#product",
data() {
return {
title: "我是product的title",
price: 9.9
}
}
}
// 1.1.组件打算在哪里被使用
const app = Vue.createApp({
// components: option api
components: {
ProductItem,
}
},
// data: option api
data() {
return {
message: "Hello Vue"
}
}
})
// 2.挂载app
app.mount("#app")
</script>
</body>
3.3 组件名称规范
在通过app.component注册一个组件的时候,第一个参数是组件的名称,定义组件名的方式有两种:
- 方式一:使用kebab-case(短横线分割符)
当使用 kebab-case (短横线分隔命名) 定义一个组件时,引用这个自定义元素时也必须使用 kebab-case,
例如
- 方式二:使用PascalCase(驼峰标识符)
当使用 PascalCase (首字母大写命名) 定义一个组件时,在引用这个自定义元素时两种命名法都可以使用。
也就是说
04. Vue CLI脚手架
4.1 开发模式
目前我们使用vue的过程都是在html文件中,通过template编写自己的模板、脚本逻辑、样式等。
这样的开发模式在面对复杂项目的时候会显得很臃肿,不利于维护。
所以在真实开发中,我们会通过一个后缀名为 .vue 的single-file components (单文件组件) 来解决,
并且可以使用webpack或者vite或者rollup等构建工具来对其进行处理。
每一个.vue文件有自己的template、script、style等标签,维护自己的代码逻辑:

如果我们想要使用这一的SFC的.vue文件,比较常见的是两种方式:
-
方式一:使用Vue CLI来创建项目,项目会默认帮助我们配置好所有的配置选项,可以在其中直接使用.vue文件;
-
方式二:自己使用webpack或rollup或vite这类打包工具,对其进行打包处理;
4.2 Vue CLI脚手架
脚手架其实是建筑工程中的一个概念,在我们软件工程中也会将一些帮助我们搭建项目的工具称之为脚手架。
Vue的脚手架就是Vue CLI,本质上是一个npm包。
CLI是Command-Line Interface, 翻译为命令行界面.
我们可以通过CLI选择项目的配置和创建出我们的项目;
Vue CLI已经内置了webpack相关的配置,我们不需要从零来配置;
安装Vue CLI
建议全局安装,这样任何时候都可以通过vue命令来创建项目:
npm install @vue/cli -g
查看Vue CLI版本
vue --version
升级Vue CLI
npm update @vue/cli -g
通过Vue命令创建项目
vue create 项目名称
4.3 vue create 项目过程
- 选择预设:
- Vue2预设
- Vue3预设
- 手动模式
babel是一个将ES6语法转化成ES5的工具,eslint是做语法检查。

- 选择需要的特性

- 选择Vue版本

- 是否将配置信息存放到单独的文件中

- 是否将当前的配置保存为【预设】,可方便下次直接创建

- 选择一个包管理工具

- 项目创建中

- 启动项目,在项目所在目录的终端下,输入
npm run server,即可启动项目。

- 项目的目录结构:

- babel.config.js babel是ES6转换ES5的组件,这是他所需的配置文件(一般不需要动)。
- package.json 项目所需的包的版本信息。
- package-lock.json 保存项目所需的包细节以及包的依赖等信息。
- node-modules 项目安装依赖包的文件保存的地方。例如:npm install axios
axios包会保存在此目录、信息也会写在 package.json、package-lock.json中- src
- main.js 项目的启动 npm run serve ,用户访问时程序的入门。
- App.vue 主组件
- components 子组件
- assets 静态文件(自己的静态文件,会被压缩和合并)
- public 【此目录下的文件直接被复制到dist/目录下,一般放不动的数据,引入第三方】
- index.html 主HTML文件(模板引擎)(主界面)
- favicon.icon 图标
- README.md 项目说明文档
4.4 另一种创建项目方式
使用 vue create 项目名称 的方式去创建项目,默认的打包工具是webpack;
我们也可以使用以下的命令创建项目,其打包工具是vite。
这种方式正逐渐成为主流,因为它的打包速度非常快。
npm init vue@latest
项目结构:

启动项目:
npm run dev
05. 组件间的通信

上面的嵌套逻辑如下,它们存在如下关系:
- App组件是Header、Main、Footer组件的父组件;
- Main组件是Banner、ProductList组件的父组件;
在开发过程中,我们会经常遇到需要组件之间相互进行通信:
- 比如App可能使用了多个Header,每个地方的Header展示的内容不同,那么我们就需要使用者传递给Header一些数据,让其进行展示;
- 又比如我们在Main中向服务器一次性请求了Banner数据和ProductList数据,那么就需要传递给它们来进行展示;
- 也可能是子组件中发生了事件,需要由父组件来完成某些操作,那就需要子组件向父组件传递事件;
父子组件之间如何进行通信呢?
-
父组件传递给子组件:通过props属性;
-
子组件传递给父组件:通过$emit触发事件;

5.1 父组件传递子组件
在开发中很常见的就是父子组件之间通信,比如父组件有一些数据,需要子组件来进行展示。
这个时候我们可以通过props来完成组件之间的通信;
什么是Props呢?
Props是你可以在组件上注册一些自定义的attribute;
父组件给这些attribute赋值,子组件通过attribute的名称获取到对应的值;
Props有两种常见的用法:
-
方式一:字符串数组,数组中的字符串就是attribute的名称;
-
方式二:对象类型,对象类型我们可以在指定attribute名称的同时,指定它需要传递的类型、是否是必须的、默认值等等;
App.vue:在父组件中,通过属性来传递值。
在属性中,以
:age="30"的形式传入值,则传入的是数字。因为v-bind后面引号里的是js代码。
<template>
<!-- 1.展示why的个人信息 -->
<show-info name="why" :age="18" :height="1.88" />
<!-- 2.展示kobe的个人信息 -->
<show-info name="kobe" :age="30" :height="1.87" />
<!-- 3.展示默认的个人信息 -->
<show-info :age="100" show-message="哈哈哈哈"/>
</template>
<script>
import ShowInfo from './ShowInfo.vue'
export default {
components: {
ShowInfo
}
}
</script>
<style scoped>
</style>
showInfo.vue:
<template>
<div class="infos">
<h2>姓名: {{ name }}</h2>
<h2>年龄: {{ age }}</h2>
<h2>身高: {{ height }}</h2>
<h2>Message: {{ showMessage }}</h2>
</div>
<div class="others" v-bind="$attrs"></div>
</template>
<script>
export default {
// 如果不希望组件的根元素继承attribute,可以使用下面的配置:
// inheritAttrs: false,
// 1.props数组语法(不常用)
// 弊端: 1> 不能对类型进行验证 2.没有默认值的
// props: ["name", "age", "height"]
// 2.props对象语法(必须掌握)
props: {
name: {
type: String,
default: "我是默认name"
},
age: {
type: Number,
required: true, // 是否必须传值,如果为true,则默认值失效。
default: 0
},
height: {
type: Number,
default: 2
},
// 重要的原则: 数组、对象类型写默认值时, 需要编写default的函数, 函数返回默认值
friend: {
type: Object,
default() {
return { name: "james" }
}
},
hobbies: {
type: Array,
default: () => ["篮球", "rap", "唱跳"]
},
// 命名使用小驼峰,
// 父组件中,属性传值可以使用短横杠【-】:<show-info :age="100" show-message="哈哈哈哈"/>
showMessage: {
type: String,
default: "我是showMessage"
}
}
}
</script>
type支持的类型:
- String
- Number
- Boolean
- Array
- Object
- Date
- Function
- Symbol
type也支持多个值,以数组的形式:
name: {
type: [String,Number]
default: "我是默认name"
},
对象类型,或者数组类型,其默认值必须是一个函数。由函数来返回对象。
当我们传递给一个组件某个属性,但是该属性并没有定义对应的props或者emits时,就称之为 非Prop的Attribute。
常见的包括class、style、id属性等;
当组件有单个根节点时,非Prop的Attribute将自动添加到根节点的Attribute中;没有根节点,则会报警告,提示必须指定要绑定到哪一个标签上。
如果我们不希望组件的根元素继承attribute,可以在组件中设置 inheritAttrs: false。
示例:showInfo.vue:
<template>
<!-- address="广州市" abc="cba" class="active" 等属性,并没有定义在props中 -->
<show-info name="why" :age="18" :height="1.88"
address="广州市" abc="cba" class="active" />
</template>
<script>
import ShowInfo from './ShowInfo.vue'
export default {
components: {
ShowInfo
}
}
</script>
<style scoped>
</style>
App.vue:
<template>
<div class="infos">
<h2>姓名: {{ name }}</h2>
<h2>年龄: {{ age }}</h2>
<h2>身高: {{ height }}</h2>
</div>
<div class="others" v-bind="$attrs"></div>
</template>
<script>
export default {
// 如果不希望组件的根元素继承attribute,可以使用下面的配置:
inheritAttrs: false,
props: {
name: {
type: String,
default: "我是默认name"
},
age: {
type: Number,
required: true, // 是否必须传值,如果为true,则默认值失效。
default: 0
},
height: {
type: Number,
default: 2
},
}
}
</script>

如果禁用了attribute继承,我们还是可以通过 $attrs来访问所有的 非props的attribute;
<h2 :class="$attrs.class">姓名: {{ name }}</h2>
5.2 子组件传递父组件
当子组件有一些事件发生的时候,比如在组件中发生了点击,父组件需要切换内容,此时就需要子组件传递信息给父组件。
我们如何完成上面的操作呢?
-
首先,在子组件中定义好在某些情况下触发的事件名称;
-
其次,在父组件中以v-on的方式传入要监听的事件名称,并且绑定到对应的方法中;
-
最后,在子组件中发生某个事件的时候,根据事件名称触发对应的事件;
以下示例实现一个计数功能:

App.vue
<template>
<div class="app">
<h2>当前计数: {{ counter }}</h2>
<!-- 1.自定义add-counter, 并且监听内部的add事件 -->
<add-counter @add="addBtnClick"></add-counter>
<!-- 2.自定义sub-counter, 并且监听内部的sub事件 -->
<sub-counter @sub="subBtnClick"></sub-counter>
</div>
</template>
<script>
import AddCounter from './AddCounter.vue'
import SubCounter from './SubCounter.vue'
export default {
components: {
AddCounter,
SubCounter
},
data() {
return {
counter: 0
}
},
methods: {
addBtnClick(count) {
this.counter += count
},
subBtnClick(count) {
this.counter -= count
}
}
}
</script>
AddCounter.vue
<template>
<div class="add">
<button @click="btnClick(1)">+1</button>
<button @click="btnClick(5)">+5</button>
<button @click="btnClick(10)">+10</button>
</div>
</template>
<script>
export default {
methods: {
btnClick(count) {
console.log("btnClick:", count)
// 让子组件发出去一个自定义事件:
// 第一个参数自定义的事件名称
// 第二个参数是传递的参数
this.$emit("add", count)
}
}
}
</script>
SubCounter.vue
<template>
<div class="sub">
<button @click="btnClick(1)">-1</button>
<button @click="btnClick(5)">-5</button>
<button @click="btnClick(10)">-10</button>
</div>
</template>
<script>
export default {
emits: ["add"], // 写明传递出去的函数名,方便查看与代码提示
methods: {
btnClick(count) {
this.$emit("sub", count)
}
}
}
</script>

在子组件中,定义vue对象时,有一个emits属性。
emits的数组写法:
- 可以方便查看该组件都定义了哪些自定义事件,
- 如果有emits,父组件在书写代码时,也会有事件名称提示。
emmits对象语法:
- 事件名称对应的是函数,当触发这个事件时,先执行此函数。
- 常用于:在参数被传递出去前先做验证
<script>
export default {
// 1.emits数组语法
emits: ["add"],
// 2.emmits对象语法
// emits: {
// add: function(count) {
// if (count <= 10) {
// return true
// }
// return false
// }
// },
methods: {
btnClick(count) {
console.log("btnClick:", count)
// 让子组件发出去一个自定义事件
// 第一个参数自定义的事件名称
// 第二个参数是传递的参数
this.$emit("add", 100)
}
}
}
</script>