1.1 组件化简介
1.1.1 概述
将实现页面某一部分功能的结构、样式和逻辑封装成为一个整体,使其高内聚,低耦合,达到分治与复用的目的。在前端范畴,我们可以用下面的这张图来简单地理解组件化:
这样看起来,组件化前端开发就像造一辆车,我们将轮子、发动机、悬挂、车身车门等等各部分组装成一辆车,轮子、发动机就是组件,车就是最终产品。我们将页头、侧边栏、页脚、内容区等等组件拼装起来组成了我们的页面。
1.1.2 意义
在谈到组件化的意义时,很多人的看法都是组件化的目的是复用。在某一篇博客看到另一种想法,分而治之,我比较这一种想法,良好地组件化以后的组件,会表现出高内聚低耦合的特征,这会给我们带来好处;组件之间不会相互影响,能有效减少出现问题时定位和解决问题的时间;组件化程度高的页面,具有清晰的页面组织和高可读性的 HTML 结构代码,组件之间的关系一目了然;组件化会强迫开发人员划清各个组件的功能边界,使得开发出的功能更加健壮;所以分而治之才是组件化的意义所在,复用只是它的副作用。同时我们还有很多其他方式都可以做到复用,这并不是组件化的专利。
1.1.3 组件化与模块化
模块化是一种处理复杂系统分解成为更好的可管理模块的方式。它可以通过在不同组件设定不同的功能,把一个问题分解成多个小的独立、互相作用的组件,来处理复杂、大型的软件。似乎在后端领域,组件化和模块化说的是同一件事。但在我的理解中,前端领域的组件化和模块化是两个概念。先说结论:组件化是从产品功能角度进行分割,模块化是从代码实现角度进行分割,模块化是组件化的前提和基础。
当我们将一段代码写成一个模块的时候,它有可能是一个函数、一个对象或者其他什么做了一件单一事情的东西,我们将它做成模块是因为它完成了一个单一的功能,并且这个功能很多地方都可能用得到。而当一个组件被从产品中抽象出来,它有时候就只是一个模块,但有时候却有相对复杂的实现,它就可能会有多个模块。我们说一个日期选择器是一个组件,但实现它的时候,我们分成了计算模块、渲染模块、用户输入响应模块等等模块来实现。一个单一产品功能的实现,可能是由多个模块来实现的。这样理解起来,其实可以说组件化是更粗粒度的模块化,它是在产品功能上的模块化。
1.2 组件注册
1.2.1 全局组件
使用 Vue.component("组件名称", { })
进行组件注册,全局组件注册后,任何 Vue 实例都可以使用;组件其实也是一个 Vue 实例,因此它在定义时也有:data、methods、生命周期函数等。不同的是组件不会与页面的元素绑定,否则就无法复用了,因此没有 el 属性。但是组件渲染需要 html 模板,所以增加了 template 属性,值就是 HTML 模板,模板中只能有一个根标签。data 必须是一个函数,不再是一个对象。因此组件复用时各个组件之间互补影响。
<div id="app">
<first></first>
</div>
<script>
Vue.component("first",{
// 使用 ` 将 HTML 代码包裹起来,可以不用加换行符,使用 " 需要加换行符
template: `
<div>
<h1>China---{{year}}---{{msg}}</h1>
</div>
`,
data() {
return {
msg: "大好河山",
year: 2019
}
}
});
var app = new Vue({
el:"#app"
});
</script>
1.2.3 局部组件
局部组件只有在当前注册它的 Vue 实例中使用
☞ 语法
// 注册组件
var component_name = {
template:'',
data(){
return {}
}
};
// 引用组件
var app = new Vue({
el:"#app",
components:{
component_name: first // 将定义的对象注册为组件
component_name // 组件名与对象名一致时可以简写
}
})
☞ 示例
<div id="app">
<!-- 组件复用 -->
<first></first>
<first></first>
<first></first>
</div>
<script>
var first = {
template:'<button v-on:click="count++">{{ count }} 次</button>',
data(){
return {
count:0
}
}
}
var app = new Vue({
el:"#app",
components: {
first
}
});
</script>
1.3 单文件组件
1.3.1 概述
显然,组件注册的两种写法都是在同一个页面完成的,而且没有 CSS 样式等,这样的组件是没有意义的,因此我们需要将“组件”抽离开来。Vue 单文件组件每个单文件组件的后缀名都是 .vue 优化了传统全局组件的一些弊端(模板缺乏高亮、没有 css 等) 每一个 Vue 单文件组件都由 template、script、style 三部分组成。
<template>
<!-- 组件代码区域 -->
</template>
<style scoped>
/* 样式代码区域 */
</style>
<script>
// js 代码区域
</script>
1.3.2 编写一个组件
由于每个组件都是独立的,要想被引用,需要将其暴露出来,使用 export default { }
导出默认成员,使用 export A
可以按需导出。引用组件的那一方需要使用 import 接收名称 from "模块路径"
将其到导入本页面。使用 import { A, B } from "模块路径"
可以按需导入。
<template>
<div>
<span class="font">{{msg}}</span>
<button @click="fun">组件中的按钮</button>
</div>
</template>
<style scoped>
.font {
font-size: 24px;
color: #42b983;
}
</style>
<script>
// 只能存在一个默认暴露,内容与 Vue 示例类似
export default {
// 组件的名称
name: 'myComponent',
// 数据,与 Vue 实例类似
data() {
return {
msg: "Hello World",
}
},
// 方法,与 Vue 实例类似
methods: {
fun() {
alert("我是组件的方法")
}
}
}
// 单独暴露方法
export function funC() {
alert("这是组件中 funC() 方法")
}
</script>
如上所示,一个简单的组件就写好了,export default {} 中可以有 Vue 实例中除 el 属性的所有类容。如果我们只想要抽取方法,不想要页面,此时我们虽然可以写在 Vue 单文件组件中,但是非常不方便,我们可以将其写到 JS 文件中。
export function funA() {
alert("这是 js 中 funA() 方法")
}
export function funB() {
alert("这是 js 中 funB() 方法")
}
一般我们使用 Vue 进行开发需要抽取的东西就只有组件和方法了,下面我们就可以将 MyComponent.vue 组件和 Fun.js 引入到页面中。页面也不再是熟知的 HTML,而是之前用到的 Vue 单文件组件。因此我们称引用组件的那一方为父组件,被引用的那一方为子组件。下面我们在 views 新建 vue 单文件组件编写我们的代码。Vue 中路径有个新的写法 @/
表示 src 目录下。
<template>
<div>
<my-component></my-component>
<button @click="onA">按钮A</button>
<button @click="onB">按钮B</button>
<button @click="onC">按钮C</button>
</div>
</template>
<style scoped>
</style>
<script>
// 引入文件
import MyComponent from "@/components/MyComponent";
import {funC} from "../components/MyComponent";
import {funA, funB} from "@/components/MyFun";
export default {
// 局部注册组件
components: {
myComponent: MyComponent,
},
data() {
return {}
},
methods: {
onA() {
funA();
},
onB() {
funB();
},
onC() {
funC();
}
}
}
</script>
启动项目,打开 http://localhost:8080/#/test
,哎?怎么啥也没有,难道是哪里写错了。发现在 views 文件夹中还有两个文件,我们来访问试试,发现http://localhost:8080/#/home
也没有东西而 http://localhost:8080/#/about
有东西。
产生上述问题的原因是 Vue 通过路由来找页面,而我们没有配置路由,所以找不到页面,那为什么自带的 Home.vue 也找不到呢?这是因为该页面配置的路径并不是 /home
,查看路由可以发现 Home.vue 配置的路径是 /
,所以直接使用 http://localhost:8080/#/
就可以找到了。路由这里后面后详细说,这里只需要在 /route/index.js
中配好就可以了。参考 About、Home 的配置,我们配置好 Test 的路由,就可以访问到页面了。
import Vue from 'vue'
import VueRouter from 'vue-router'
import Home from '../views/Home.vue'
Vue.use(VueRouter)
const routes = [
{
path: '/',
name: 'Home',
component: Home
},
{
path: '/about',
name: 'About',
// route level code-splitting
// this generates a separate chunk (about.[hash].js) for this route
// which is lazy-loaded when the route is visited.
component: () => import(/* webpackChunkName: "about" */ '../views/About.vue')
},
{
path: '/test',
name: 'Test',
// route level code-splitting
// this generates a separate chunk (about.[hash].js) for this route
// which is lazy-loaded when the route is visited.
component: () => import(/* webpackChunkName: "about" */ '@/views/Test')
}
]
const router = new VueRouter({
routes
})
export default router
1.4 组件之间的通信
1.4.1 父向子通信
父组件发送信息的形式是以属性的形式绑定值到子组件身上。 然后子组件用属性 props 接收,使用 props 向子组件传递数据,首先要在子组件中定义子组件能接受的 props,然后在父组件中子组件的自定义元素上将数据传递给它。
☞ 子组件(MyComponent.vue)
<template>
<div>
<span class="font">{{msg}}</span>
<h1 class="font">{{title}}</h1>
<span class="font">{{text}}</span>
</div>
</template>
<style scoped>
.font {
font-size: 24px;
color: #42b983;
}
</style>
<script>
export default { // 一个 vue 中只能存在一个
name: 'myComponent',
props: ['title', 'text'],
data() {
return {
msg: "Hello World",
}
},
methods: {
}
}
</script>
☞ 父组件(Test.vue)
<template>
<div>
<my-component :title="title" :text="text"></my-component>
</div>
</template>
<style scoped>
</style>
<script>
import MyComponent from "@/components/MyComponent";
export default {
components: {
myComponent: MyComponent,
},
data() {
return {
title: "中国",
text: "Java 开发者",
}
},
methods: {
}
}
</script>
1.4.2 子向父通信
但是如果子组件需要向父组件传递数据,则不能通过 props,Vue 2 中强调“单项数据流”,所谓“单向数据流”,即是数据的变动只能由外向内传递,而不能由内向外传递。组件只能将从 props 传递进来的数据进行使用,不能对其进行修改。我们唯一能做的,就是在子组件想要修改数据时,发送事件通知父组件修改。父组件通过监听子组件发送的这个事件,来决定需要做什么。 即:子组件不能直接向父组件传值,我们可以通过操作父组件的方法来实现通信。子组件用 $emit() 触发事件,父组件使用 v-on 来监听事件。
☞ 子组件(MyComponent.vue)
<template>
<div>
子组件的数值:<span class="font">{{num}}</span>
<br/>
<button @click="fun">加</button>
</div>
</template>
<style scoped>
.font {
font-size: 24px;
color: #42b983;
}
</style>
<script>
export default {
name: 'myComponent',
props: ['num'],
data() {
return {
}
},
methods: {
fun() {
this.$emit("add-num");
}
}
}
</script>
☞ 父组件(Test.vue)
<template>
<div>
父组件的数值:{{num}}
<br/>
<my-component :num="num" @add-num="add"></my-component>
</div>
</template>
<style scoped>
</style>
<script>
import MyComponent from "@/components/MyComponent";
export default {
components: {
myComponent: MyComponent,
},
data() {
return {
num: 10,
}
},
methods: {
add() {
this.num++;
}
}
}
</script>
1.4.3 兄弟组件通信
兄弟组件之间传递数据需要借助于中间桥梁,通过中间桥梁传递数据,中间桥梁一般是空的 Vue 实例。 传递数据方,通过 hub.$emit(方法名,参数)
触发事件。接收数据方,通过在 mounted 钩子中用 hub.$on(方法名,(参数) => {}) 监听事件
。销毁事件通过 hub.$off(方法名)
进行销毁,销毁之后无法进行传递数据。
☞ 中间桥梁(hub.js)
import Vue from "vue";
export default new Vue();
☞ 组件1(MyComponent1.vue)
<template>
<div>
子组件1的数值:<span class="font">{{numa}}</span>
  
<button @click="fun">让 2 号组件 + 1</button>
</div>
</template>
<style scoped>
.font {
font-size: 24px;
color: #42b983;
}
</style>
<script>
import hub from "@/components/hub";
export default {
name: 'myComponent1',
data() {
return {
numa: 0,
}
},
mounted () {
// 此处需要使用箭头函数,否侧 this 不代表组件
hub.$on("myComponent1",(value) => {
this.numa += value;
})
},
methods: {
fun() {
hub.$emit("myComponent2",1);
}
},
}
</script>
☞ 组件2(MyComponent2.vue)
<template>
<div>
子组件2的数值:<span class="font">{{numb}}</span>
  
<button @click="fun">让 1 号组件 + 2</button>
</div>
</template>
<style scoped>
.font {
font-size: 24px;
color: #42b983;
}
</style>
<script>
import hub from "@/components/hub";
export default {
name: 'myComponent2',
data() {
return {
numb: 0,
}
},
mounted() {
hub.$on("myComponent2",(value) => {
this.numb += value;
})
},
methods: {
fun() {
debugger
hub.$emit("myComponent1",2);
}
},
}
</script>
☞ 父组件(Test.vue)
<template>
<div>
<my-component1 ></my-component1>
<my-component2 ></my-component2>
</div>
</template>
<style scoped>
</style>
<script>
import MyComponent1 from "@/components/MyComponent1";
import MyComponent2 from "@/components/MyComponent2";
export default {
components: {
myComponent1: MyComponent1,
myComponent2: MyComponent2,
},
data() {
return {
}
},
methods: {
}
}
</script>