组件
组件的定义
组件允许我们将UI划分为独立的、可重用的部分,并可以对每个部分进行单独的思考。在实际应用中,组件常常被组织成层层嵌套的树状结构:

这和我们嵌套HTML元素的方式类似,Vue实现了自己的组件模型,使我们可以在每个组件内封装自定义内容与逻辑。
<body>
<div id="root">
<pl-navbar></pl-navbar>
</div>
<script src="../vue.js"></script>
<script>
const app = Vue.createApp({
data(){
return{
}
},
})
// 定义组件
app.component("pl-navbar",{
// 模板(使用键盘上1前方的`符号进行包裹)
template:`
<nav>
<ul>
<li>首页</li>
<li>新闻中心</li>
<li>产品大全</li>
</ul>
</nav>
`
})
app.mount("#root")
</script>
</body>
预览结果如下:

注意:由于HTML中不区分英文字母大小写,所以定义组件名称时可以用驼峰式命名
plNavbar
,但在使用组件时,需要通过-
连接的名称pl-navbar
。或者组件名、变量名均用不带任何字符的小写字母
全局组件和局部组件
<body>
<div id="root">
<pl-navbar></pl-navbar>
<pl-sidebar></pl-sidebar>
</div>
<script src="../vue.js"></script>
<script>
const app = Vue.createApp({
data(){
return{
}
},
})
// 定义全局组件
app.component("pl-navbar",{
// 模板(使用键盘上1前方的`符号进行包裹)
template:`
<nav style="background:yellow;">
<ul>
<li v-for="item in datalist">
{{item}}
</li>
</ul>
</nav>
`,
data(){
return{
datalist:["首页","新闻中心","产品大全"]
}
}
})
// 全局组件定义
app.component("pl-sidebar",{
template:`
<aside>
我是侧边栏
<pl-button></pl-button>
</aside>
`,
// 局部组件定义
components:{
"pl-button":{
template:`
<div style="background:red;">
<button style="background:red;">联系</button>
</div>
`,
// watch,computed,methods
}
}
})
app.mount("#root")
</script>
</body>

上面的例子是将所有组件都写入在HTML中,而Vue在实际使用时,会将组件提取到单独的组件文件中,即*.vue
文件,英文Single-File Component,简称SFC
,单文件组件。这个是一个特殊的文件格式,将一个Vue组件的模板、逻辑与样式封装在单个文件中。
(前面的例子中,都是将vue代码写入到html文件中,并引入vue的CDN文件进行解释,而单文件组件,需要新建*.vue
文件,而浏览器默认无法识别vue文件,也无法通过引入vue的CDN文件的形式,需要进行vue环境的安装,见本小节的附录)
组件间的通信
父传子
语法格式:
父组件绑定数据传入
v-bind:attr1="xxxx" v-bind:attr2="yyyy"
子组件声明接收
props:["attr1","attr2"]
父组件:App.vue
<template>
<div class="root">
<Navbar left="返回" title="我的产品列表页" right="首页"></Navbar>
</div>
</template>
<script>
import Navbar from './components/Navbar.vue'
import Layout from './components/Layout.vue'
export default{
components:{
Navbar,
Layout
}
}
</script>
子组件:Navbar.vue
<template>
<div>
<button>{{left}}</button>
{{title}}
<button>{{right}}</button>
</div>
</template>
<script>
export default{
props:["title","left","right"]
}
</script>
预览页面:

其中父组件中的
<Navbar left="返回" title="我的产品列表页" right="首页"></Navbar>
可以做如下改写:
<template>
<div class="root">
<Navbar v-bind="propnav"></Navbar>
</div>
</template>
<script>
import Navbar from './components/Navbar.vue'
import Layout from './components/Layout.vue'
export default{
data(){
return{
propnav:{
left:'返回',
title:'我的产品列表页',
right:'首页'
}
}
},
components:{
Navbar,
Layout
}
}
</script>
所有的props
都遵循着单向绑定原则,props
因父组件的更新而变化,自然地将新的状态向下流往子组件,而不会逆向传递。
属性验证
在props
接收时可以指定特定的类型或指定范围值
<template>
<div>
<button v-show="leftshow">{{left}}</button>
{{title}}
<button v-show="rightshow">{{right}}</button>
</div>
</template>
<script>
export default{
props:{
title:String,
left:[String,Number],
right:{
required:true, // 必传,不设置成必传。不传这个值就不会经过校验器进行正确性校验
// 校验器
Validator(value){
return ['success','waring','danger'].includes(value)
}
},
leftshow:{
type:Boolean,
default:true // 设置默认值
},
rightshow:Boolean
}
}
</script>
子传父
父组件App.vue
<template>
<div class="root">
<Navbar left="返回" :title="title" right="首页"
:leftshow="true" :rightshow="false"
></Navbar>
<!-- <Navbar v-bind="propnav"></Navbar> -->
<button @click="plClick">click</button>
<Layout @event="$event => plEvent($event)"></Layout>
</div>
</template>
<script>
import Navbar from './components/Navbar.vue'
import Layout from './components/Layout.vue'
export default{
data(){
return{
title:"我的产品列表页",
propnav:{
// left:'返回',
mytitle:'我的产品列表页',
// right:'首页'
}
}
},
methods:{
plClick(){
this.title="我的产品列表页-第一页"
},
plEvent(data){
console.log("app-event",data)
}
},
components:{
Navbar,
Layout
}
}
</script>
子组件Layout.vue
<template>
<div>
Layout-child
<button @click="plClick">click</button>
</div>
</template>
<script>
export default{
data(){
return{
childtitle:"child-1111"
}
},
methods:{
plClick(){
// console.log(this.childtitle)
this.$emit("event",this.childtitle)
}
}
}
</script>
<style scoped>
div{
background: green;
}
</style>
上面的例子中,父组件中定义的@event
为自定义事件,名称可以任意取名,@自定义事件名="表达式或方法"
;子组件触发自定义事件$emit("自定义事件名")
$refs
- ref如果绑定在dom节点上,得到的就是原生dom节点
- ref如果绑定在组件上,得到的就是组件对象,可以实现通信功能
例子:
父组件App.vue
<template>
<div>
app-<button @click="plClick()">click</button>
<!-- 原生dom -->
<input ref="myinput"/>
<div ref="mydiv"></div>
<!-- 新建一个子组件Child.vue -->
<Child ref="mychild"></Child>
</div>
</template>
<script>
// 引入子组件Child.vue
import Child from './Child.vue'
export default{
// 注册子组件
components:{
Child
},
methods:{
plClick(){
// 原生dom
console.log(this.$refs.myinput)
console.log(this.$refs.mydiv)
// 组件,可以获取到子组件的对象及属性
console.log(this.$refs.mychild.childtitle)
console.log(this.$refs)
// 修改子组件属性值
this.$refs.mychild.childtitle = "22222"
}
}
}
</script>
子组件Child.vue
<template>
<div>
child-{{childtitle}}
</div>
</template>
<script>
export default{
data(){
return {
childtitle:"11111"
}
}
}
</script>
$parent
和$root
$parent
父组件,$root
根组件
例子:
根组件 App.vue
<template>
<div>
app
<!-- 这里引入了一个组件Aparent,即Aparent是当前组件App的子组件 -->
<Apartent></Apartent>
</div>
</template>
<script>
import Apartent from './Apartent.vue';
export default{
data(){
return{
title:"根组件root"
}
},
components:{
Apartent
}
}
</script>
App的子组件Aparent
<template>
<div>
parent
<!-- 这里引入了一个组件BChild,即BChild是当前组件Apartent的子组件 -->
<BChild/>
</div>
</template>
<script>
import BChild from './BChild.vue';
export default{
data(){
return{
title:"父组件parent"
}
},
components:{
BChild
}
}
</script>
Aparent组件的子组件BChild
<template>
<div>
child-<button @click="plClick">click</button>
</div>
</template>
<script>
export default{
methods:{
plClick(){
console.log(this.$parent.title)
console.log(this.$root.title)
}
}
}
</script>

小节:父子组件间的通信有如下方法:
- 父组件向子组件传递数据,
props
属性绑定;- 子组件向父组件传递数据,子组件触发父组件方法,通过vue实例方法
vm.$emit
实现- 使用
$refs
,实现父组件访问子组件的数据和方法- 使用
$parent
,实现子组件访问父组件的数据和方法
订阅发布模式
在项目中新建一个store.js
export default {
datalist:[],
// 订阅者
subscribe(cb){
this.datalist.push(cb)
console.log(this.datalist)
},
// 发布者
publish(value){
this.datalist.forEach(cb=>cb(value))
}
}
通过引入订阅发布模式实现组件间的数据传递
Navbar.vue
<script>
import store from './store'
export default {
// inject:["navTitle","app"]
//生命周期-mounted()
data(){
return {
title:"首页"
}
},
mounted(){
//订阅,,,,
store.subscribe((value)=>{
console.log("我被触发了",value)
this.title = value
})
}
}
</script>
TabbarItem.vue
<script>
import store from './store';
export default {
props:["item"],
// inject:["navTitle","app"],
methods:{
plClick(){
// console.log(this.navTitle)
store.publish(this.item)
}
}
}
</script>
动态组件
App.vue
<template>
<div>
<Navbar />
<!-- <Home></Home>
<List></List>
<Center></Center> -->
<!-- 使用keep-alive或KeepAlive实现缓存当前数据(比如输入框中输入内容,切换到其他tab,再切换回来,数据仍显示), -->
<!-- include表示包含哪些组件,exclude表示排除哪些组件 -->
<!-- component实现动态组件 -->
<keep-alive include="Home,List">
<component :is="which"></component>
</keep-alive>
<Tabbar/>
</div>
</template>
<script>
import Navbar from './Navbar.vue';
import Tabbar from './Tabbar.vue';
import Home from './views/Home.vue';
import List from './views/List.vue';
import Center from './views/Center.vue';
import store from './store';
export default {
data(){
return {
navTitle:"首页",
which:"Home"
}
},
provide(){
return {
navTitle:this.navTitle,
app:this
}
},
mounted(){
let obj = {
"首页":"Home",
"列表":"List",
"我的":"Center"
}
store.subscribe((value)=>{
this.which = obj[value]
})
},
components:{
Navbar,
Tabbar,
Home,
List,
Center
}
}
</script>
Home.vue
<template>
<div>
Home
<input/>
</div>
</template>
<script>
export default{
name:"Home"
}
</script>
List.vue和Center.vue与Home.vue的内容类似。

组件中的v-model
未应用组件中的v-model
的例子
App.vue
<template>
<div>
{{plvalue}}
<!-- 属性值的动态绑定 v-model -->
<!-- <input type="text" v-model="plvalue"> -->
<!-- 属性值的动态绑定,v-bind,由于v-bind是单向绑定,需要在增加事件监听 -->
<!-- <input type="text" :value="plvalue" @input="plInput"> -->
<!-- 也可以通过直接将表达式写在监听事件中 -->
<!-- <input type="text" :value="plvalue" @input="plvalue=$event.target.value"> -->
<!-- 使用子组件Field,plvalue与data()中的plvalue对应,是用户名的属性值 -->
<!-- @plevent进行监听子组件(这里@plevent是监听事件名,“plEvent”是监听方法名) -->
<Field label="用户名" :value="plvalue" @plevent="plEvent"/>
<button @click="plRegister">注册</button>
<button @click="plReset">重置</button>
</div>
</template>
<script>
// 引入自定义组件Field
import Field from './Field.vue'
export default {
data() {
return {
plvalue:"aaaa"
}
},
// 注册子组件Field
components: {
Field
},
methods: {
// plEvent被触发即value有新的值从子组件传递过来,赋值给父组件的plvalue
plEvent(value){
console.log(value)
this.plvalue = value
},
// plInput(event) {
// console.log(event.target.value)
// this.plvalue = event.target.value
// },
plRegister() {
console.log("register",this.plvalue)
},
plReset() {
console.log("reset")
this.plvalue=""
}
}
}
</script>
子组件Field.vue
<template>
<div style="background: yellow;">
<label>{{label}}</label>:
<!-- 动态绑定value值,value来自props中的value,由父组件传递过来 -->
<!-- @input监听输入框,当输入框的value有变化时,触发plInput方法 -->
<input :type="type" :value="value" @input="plInput">
</div>
</template>
<script>
export default {
data(){
return {
myvalue:""
}
},
methods:{
// 当input框的value值发生变化就会触发plInput方法,通过$emit传递给父组件的plevent进行接收
plInput(event){
console.log(event.target.value)
this.$emit("plevent",event.target.value)
}
},
props:{
label:{
type:String,
default:""
},
type:{
type:String,
default:"text"
},
// 增加一项父组件中需要支持的value类型,进行接收
value:{
type:String,
default:""
}
}
}
</script>
- 在父组件中引入子组件Field
- 增加注册和重置按钮,来操作label的属性值value
- 在script中的data中返回value值
- 动态绑定value值
- 在子组件中的props中增加value属性值的接收验证
- 接收到的value值传给子组件中input绑定的属性值value
- 子组件的input输入框的值有变化时要传给父组件,在子组件的input增加监听事件plInput,并在methods中增加plInput()方法,获取到输入框属性值的内容
event.target.value
- 在父组件中增加对子组件的监听事件plEvent
- 当子组件的input属性value有变化时,会触发子组件的监听事件plInput,由plInput通过
this.$emit("plevent",event.target.value)
传递给父组件的监听事件plEvent,父组件的plEvent会将子组件传递过来的value值重新赋值给前端的plvalue
下面是在组件上绑定v-model实现同样功能的例子:
App.vue
<template>
<div>
{{plvalue}}
<!-- 将v-model应用在Field组件上,子组件props中的value属性的名称需要写成modelValue -->
<Field label="用户名" v-model="plvalue"/>
<button @click="plRegister">注册</button>
<button @click="plReset">重置</button>
</div>
</template>
<script>
// 引入自定义组件Field
import Field from './Field.vue'
export default {
data() {
return {
plvalue:"aaaa"
}
},
// 注册子组件Field
components: {
Field
},
methods: {
// plEvent被触发即value有新的值从子组件传递过来,赋值给父组件的plvalue
plEvent(value){
console.log(value)
this.plvalue = value
},
plRegister() {
console.log("register",this.plvalue)
},
plReset() {
console.log("reset")
this.plvalue=""
}
}
}
</script>
Field.vue
<template>
<div style="background: yellow;">
<label>{{label}}</label>:
<!-- v-model绑定组件时,value属性的名称需要写成modelValue -->
<input :type="type" :value="modelValue" @input="plInput">
</div>
</template>
<script>
export default {
data(){
return {
myvalue:""
}
},
methods:{
plInput(event){
console.log(event.target.value)
// v-model绑定组件时,传递给父组件时需要将plvalue改写成update:modelValue
this.$emit("update:modelValue",event.target.value)
}
},
props:{
label:{
type:String,
default:""
},
type:{
type:String,
default:"text"
},
// v-model绑定组件时,value属性的名称需要写成modelValue
modelValue:{
type:String,
default:""
}
}
}
</script>
异步组件
之前写的代码,都是在首次访问时,一起全部加载完成。若文件较大较多时,会出现页面加载慢的问题,这时需要对组件进行异步加载处理,当用到时才请求加载。
<script>
import { defineAsyncComponent } from 'vue';
import Navbar from './Navbar.vue';
import Tabbar from './Tabbar.vue';
// import Home from './views/Home.vue';
// import List from './views/List.vue';
// import Center from './views/Center.vue';
import store from './store';
export default {
data(){
return {
navTitle:"首页",
which:"Home"
}
},
provide(){
return {
navTitle:this.navTitle,
app:this
}
},
mounted(){
let obj = {
"首页":"Home",
"列表":"List",
"我的":"Center"
}
store.subscribe((value)=>{
this.which = obj[value]
})
},
components:{
Navbar,
Tabbar,
Home:defineAsyncComponent(()=>import("./views/Home.vue")),
List:defineAsyncComponent(()=>import("./views/List.vue")),
Center:defineAsyncComponent(()=>import("./views/Center.vue"))
}
}
</script>
之前的代码都是将组件一起全都导入,这里需要通过使用defineAsyncComponent()
方法进行包裹导入,这样在不请求的组件不会加载,只有访问了才加载。
注:若异步加载的组件文件较大较慢,可以在defineAsyncComponent()
的基础上增加加载loading及错误提示等。
附录:安装Vue
安装vue有两种方法:
Vue CLI (不再推荐)
Vue CLI
已经进入维护阶段,到2023年12月31日将停止维护。

Vue ClI
是一个基于Vue.js
进行快速开发的完整系统,提供:
- 通过
@vue/cli
实现的交互式的项目脚手架; - 通过
@vue/cli
+@vue/cli-service-global
实现的零配置原型开发; - 一个运行时依赖(
@vue/cli-service
),该依赖:- 可升级;
- 基于
webpack
构建,并带有合理的默认配置; - 可以通过项目内的配置文件进行配置;
- 可以通过插件进行扩展;
- 一个丰富的官方插件集合,集成了前端生态中最好的工具;
- 一套完全图形化的创建和管理Vue.js项目的用户界面
安装:
npm install -g @vue/cli
注意,有可能会出现权限不够的问题,报错信息如下:
npm ERR! code EACCES
npm ERR! syscall mkdir
npm ERR! path /Users/laobai/.npm/_cacache/content-v2/sha512/a7/b1
npm ERR! errno EACCES
npm ERR!
npm ERR! Your cache folder contains root-owned files, due to a bug in
npm ERR! previous versions of npm which has since been addressed.
npm ERR!
npm ERR! To permanently fix this problem, please run:
npm ERR! sudo chown -R 501:20 "/Users/laobai/.npm"
npm ERR! A complete log of this run can be found in:
npm ERR! /Users/laobai/.npm/_logs/2023-07-16T10_49_31_741Z-debug-0.log
目录‘path /Users/laobai/.npm’权限不够,根据提示执行“sudo chown -R 501:20 “/Users/laobai/.npm””即可解决。
sudo chown -R 501:20 "/Users/laobai/.npm"
确认已安装成功
vue -V
@vue/cli 5.0.8
创建vue项目
vue create vue_test01
选择Default([Vue 3] babel, eslint)
进行安装 (其中babel
是为了使低端浏览器(IE浏览器,其他特低版本其他浏览器(电脑端和手机端))兼容ES6,将ES6转成ES5)
创建好项目后,可以在查看项目目录中的package.json
确认项目信息
{
"name": "vue_test01",
"version": "0.1.0",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint"
},
"dependencies": {
"core-js": "^3.8.3",
"vue": "^3.2.13"
},
"devDependencies": {
"@babel/core": "^7.12.16",
"@babel/eslint-parser": "^7.12.16",
"@vue/cli-plugin-babel": "~5.0.0",
"@vue/cli-plugin-eslint": "~5.0.0",
"@vue/cli-service": "~5.0.0",
"eslint": "^7.32.0",
"eslint-plugin-vue": "^8.0.3"
}
}
可以在命令行中,执行下面的命令启动服务
npm run serve
> vue_test01@0.1.0 serve
> vue-cli-service serve
INFO Starting development server...
DONE Compiled successfully in 801ms 05:35:28
App running at:
- Local: http://localhost:8080/
- Network: http://192.168.18.199:8080/
Note that the development build is not optimized.
To create a production build, run npm run build.
通过http://localhost:8080/
访问项目页面

Vite(推荐)
Vite
(法语意为“快速的”,发音/vit/
)是一种新型前端构建工具,能够显著提升前端开发体验,它主要由两部分组成:
- 一个开发服务器,它基于原生ES模块提供了丰富的内建功能,如速度快到惊人的模块热更新(HMR);
- 一套构建指令,它使用Roollup打包你的代码,并且它是预配置的,可输出用于生产环境的高度优惠过的静态资源。
通过Vite创建vue项目
npm create vite@latest
或者
npm init vite@latest
创建时,若本地还没有vite会提示进行下载
npm create vite@latest
✔ Project name: … vue_test02
✔ Select a framework: › Vue
✔ Select a variant: › TypeScript
Scaffolding project in /Users/laobai/TempProjects/vue_test02...
Done. Now run:
cd test_vue02
npm install
npm run dev
创建了项目,并没有安装插件及工具,按照提示进入到项目目录,执行npm install
进行安装
cd test_vue02
npm install
added 43 packages, and audited 44 packages in 19s
4 packages are looking for funding
run `npm fund` for details
found 0 vulnerabilities
可以在查看项目目录中的package.json
确认项目信息
{
"name": "vue_test02",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"vue": "^3.3.4"
},
"devDependencies": {
"@vitejs/plugin-vue": "^4.2.3",
"vite": "^4.4.5"
}
}
可以在命令行中,执行下面的命令启动服务
npm run dev
> test_vue@0.0.0 dev
> vite
VITE v4.4.4 ready in 373 ms
➜ Local: http://localhost:5202/
➜ press h to show help
20:23:52 [vite] vite.config.ts changed, restarting server...
通过http://localhost:5202/
访问项目页面
