前端 Vue3 框架 | 地址 |
---|---|
(一)基础语法 | https://blog.csdn.net/weixin_42771853/article/details/129956268 |
(二)组件化基础 | https://blog.csdn.net/weixin_42771853/article/details/129957080 |
(三)Composition API | https://blog.csdn.net/weixin_42771853/article/details/129957511 |
(四)Vue3 全家桶 router、vuex、pinia、axios | https://blog.csdn.net/weixin_42771853/article/details/129957595 |
组件化是Vue、React、Angular的核心思想:
<div id="app">
<HomeNav></HomeNav>
<home-nav></home-nav>
<product-item></product-item>
<product-item></product-item>
<product-item></product-item>
</div>
<template id="nav">
<h2>我是应用程序的导航</h2>
</template>
<template id="product">
<div class="product">
<h2>{{title}}</h2>
<p>商品描述, 限时折扣, 赶紧抢购</p>
<p>价格: {{price}}</p>
<button @click="favarItem">收藏</button>
</div>
</template>
<script src="../lib/vue.js"></script>
<script>
// 1.创建app
const app = Vue.createApp({
// data: option api
data() {
return {}
},
})
// 2.注册全局组件
app.component("product-item", {
template: "#product",
data() {
return {
title: "我是商品Item",
price: 9.9
}
},
methods: {
favarItem() {
console.log("收藏了当前的item")
}
}
})
app.component("HomeNav", {
template: "#nav"
})
// 3.挂载app
app.mount("#app")
</script>
<div id="app">
<home-nav></home-nav>
<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>
<template id="nav">
<div>-------------------- nav start ---------------</div>
<h1>我是home-nav的组件</h1>
<product-item></product-item>
// 在此组件中加components属性才可使用
<div>-------------------- nav end ---------------</div>
</template>
<script src="../lib/vue.js"></script>
<script>
const ProductItem = {
template: "#product",
data() {
return {
title: "我是product的title",
price: 9.9
}
}
}
// 1.创建局部组件
const app = Vue.createApp({
// components: option api
components: {
ProductItem,
HomeNav: {
template: "#nav",
components: {
ProductItem
}
}
},
// data: option api
data() {
return {
message: "Hello Vue"
}
}
})
// 2.挂载app
app.mount("#app")
</script>
在真实开发中,我们可以通过一个后缀名为 .vue 的single-file components (单文件组件) 来解决,并且可以使用 webpack或者vite或者rollup等构建工具来对其进行处理
想要使用这一的SFC的.vue文件,比较常见的是两种方式:
Vue的脚手架就是Vue CLI:
npm install @vue/cli -g
npm update @vue/cli -g
Vue create 项目的名称
node_modules: 安装的所有依赖包
public: public目录存放的是一些公用文件
---favicon.ico 图标
---index.html 打包webpack时所需要的的HTML 模板
src: 存放vue项目的源代码
--assets: 资源文件,比如存放css,图片等资源
--components: 组件文件夹
--APP.vue 根组件
--main.js 项目的入口文件
.browserslistrc: 设置目标浏览器,进行浏览器适配
.gitignore: git的忽略文件
babel.config.js: babel的配置
jsconfig.json: 给vscode进行读取,vscode在读取到其中的内容时,给我们代码更加友好的提示
package-lock.json: 项目包的锁定文件,npm install 可以通过package-lock文件来检测lock中包的版本是否和package.json中一致 ---一致,会优先查找缓存,不一致,就会重新构建依赖关系
package.json: npm配置文件,记录这你项目的名称,版本号,项目描述,也记录项目所依赖的其他库的信息和依赖库的版本号
README.md: 项目说明(描述)
vue.config.js: vue 的配置文件
import { createApp } from "vue";
import App from "./components/App.vue";
// import ProductItem from "./components/ProductItem.vue";
const app = createApp(App);
// 全局注册
// app.component("product-item", ProductItem);
app.mount("#app");
<template>
<h2>{{ title }}</h2>
<h2>当前计数:{{ counter }}</h2>
<button @click="decrement">-</button>
<button @click="increment">+</button>
<product-item></product-item>
</template>
<script>
// 局部注册
import ProductItem from "./ProductItem.vue"
export default {
components: {
ProductItem,
},
data() {
return {
title: "我还是标题",
counter: 0,
};
},
methods: {
decrement() {
this.counter--;
},
increment() {
this.counter++;
},
},
}
</script>
<style>
h2 {
color: red;
}
</style>
<template>
<div class="product">
<h2>我是商品</h2>
<div>商品图片</div>
<div>
商品价格: <span>¥{{ price }}</span>
</div>
</div>
</template>
<script>
export default {
data() {
return {
price: 9.9,
count: 0,
};
},
}
</script>
<style></style>
给vscode进行读取,vscode在读取到其中的内容时,给我们代码更加友好的提示
每个vue文件中的css:
此时对title的样式只对此vue文件中的.title适用,其他vue文件中不适用
npm init vue@latest
对组件进行拆分,拆分成一个个功能的小组件:
上面的嵌套逻辑如下,它们存在如下关系:
在开发过程中,我们会经常遇到需要组件之间相互进行通信:
父子组件之间通信,比如父组件有一些数据,需要子组件来进行展示:
Props有两种常见的用法:
type
:age="18"
required
default
type 的类型
Prop 的大小写命名
// App.vue
<template>
<!-- 1.展示why的个人信息 -->
<!-- 如果当前的属性是一个非prop的attribute, 那么该属性会默认添加到子组件的根元素上 -->
<show-info name="why" :age="18" :height="1.88"
address="广州市" abc="cba" class="active" />
<!-- 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 :class="$attrs.class">姓名: {{ name }}</h2>
<h2>年龄: {{ age }}</h2>
<h2>身高: {{ height }}</h2>
<h2>Message: {{ showMessage }}</h2>
</div>
<div class="others" v-bind="$attrs"></div>
</template>
<script>
export default {
// inheritAttrs: false,
// 作用: 接收父组件传递过来的属性
// 1.props数组语法
// 弊端: 1> 不能对类型进行验证 2.没有默认值的
// props: ["name", "age", "height"]
// 2.props对象语法(必须掌握)
props: {
name: {
type: String,
default: "我是默认name"
},
age: {
type: Number,
required: true,
default: 0
},
height: {
type: Number,
default: 2
},
// 重要的原则: 对象类型写默认值时, 需要编写default的函数, 函数返回默认值
friend: {
type: Object,
default() {
return { name: "james" }
}
},
hobbies: {
type: Array,
default: () => ["篮球", "rap", "唱跳"]
},
showMessage: {
type: String,
default: "我是showMessage"
}
}
}
</script>
<style scoped>
</style>
inheritAttrs: false
v-bind="$attrs"
、:class="$attrs.address"
当子组件有一些事件发生的时候,比如在组件中发生了点击,父组件需要切换内容; 子组件有一些内容想要传递给父组件:
// 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 {
// 1.emits数组语法
emits: ["add"],
// 2.emmits对象语法(了解)
// emits: {
// add: function(count) {
// if (count <= 10) {
// return true
// }
// return false
// }
// },
methods: {
btnClick(count) {
// 让子组件发出去一个自定义事件
// 第一个参数自定义的事件名称
// 第二个参数是传递的参数
this.$emit("add", 100)
}
}
}
</script>
<style scoped>
</style>
<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>
<style scoped>
</style>
假如我们定制一个通用的导航组件 - NavBar
定义插槽slot:
// App.vue
<template>
<div class="app">
<!-- 1.内容是button -->
<show-message>
<button>我是按钮元素</button>
</show-message>
<!-- 2.内容是超链接 -->
<show-message>
<a href="#">百度一下</a>
</show-message>
<!-- 3.内容是一张图片 -->
<show-message>
<img src="@/img/kobe02.png" alt="">
</show-message>
<!-- 4.内容没有传递 -->
<show-message></show-message>
</div>
</template>
<script>
import ShowMessage from './ShowMessage.vue'
export default {
components: {
ShowMessage
}
}
</script>
<style scoped>
</style>
// ShowMessage.vue
<template>
<h2>哈哈哈哈哈</h2>
<div class="content">
<slot>
<p>我是默认内容, 哈哈哈</p>
</slot>
</div>
</template>
<script>
export default {}
</script>
<style scoped>
</style>
当有多个插槽时,默认情况下每个插槽都会获取到我们插入的内容来显示。希望达到的效果是插槽对应的显示,这个时候我们就可以使用 具名插槽:
可以通过 v-slot:[dynamicSlotName]
方式动态绑定一个名称
// App.vue
<template>
<nav-bar>
<template v-slot:left>
<button>{{ leftText }}</button>
</template>
// 语法糖写法
<template #center>
<span>内容</span>
</template>
<template #right>
<a href="#">登录</a>
</template>
</nav-bar>
<!-- nav-bar只给一个插槽传入数据 -->
<nav-bar>
<template v-slot:[position]>
<a href="#">注册</a>
</template>
</nav-bar>
<button @click=" position = 'left' ">左边</button>
<button @click=" position = 'center' ">中间</button>
<button @click=" position = 'right' ">右边</button>
</template>
<script>
import NavBar from './NavBar.vue'
export default {
components: {
NavBar
},
data() {
return {
position: "center",
leftText: "返回"
}
}
}
</script>
<style scoped>
</style>
// NavBar.vue
<template>
<div class="nav-bar">
<div class="left">
<slot name="left">left</slot>
</div>
<div class="center">
<slot name="center">center</slot>
</div>
<div class="right">
<slot name="right">right</slot>
</div>
</div>
<div class="other">
<slot name="default"></slot>
</div>
</template>
<script>
export default {}
</script>
<style scoped>
.nav-bar {
display: flex;
height: 44px;
line-height: 44px;
text-align: center;
}
.left {
width: 80px;
background-color: orange;
}
.center {
flex: 1;
background-color: skyblue;
}
.right {
width: 80px;
background-color: aquamarine;
}
</style>
在Vue中有渲染作用域的概念:
阶段案例练习改进版:
// TabControl.vue
<template>
<div class="tab-control">
<template v-for="(item, index) in titles" :key="item">
<div class="tab-control-item"
:class="{ active: index === currentIndex }"
@click="itemClick(index)">
// 选项列表中的内容形式span希望可以在外部更改为其他形式(如button),故引入slot插槽
// 将子组件的内容item、abc通过slot传至父组件
<slot :item="item" abc="cba">
<span>{{ item }}</span>
</slot>
</div>
</template>
</div>
</template>
<script>
export default {
props: {
titles: {
type: Array,
default: () => [],
},
},
data() {
return {
currentIndex: 0,
};
},
emits: ["tabItemClick"],
methods: {
itemClick(index) {
this.currentIndex = index;
this.$emit("tabItemClick", index);
},
},
};
</script>
<style scoped>
.tab-control {
display: flex;
height: 44px;
line-height: 44px;
text-align: center;
}
.tab-control-item {
flex: 1;
}
.tab-control-item.active {
color: red;
font-weight: 700;
}
.tab-control-item.active span {
border-bottom: 3px solid red;
padding: 8px;
}
</style>
// App.vue
<template>
<div class="app">
<!-- 1.tab-control -->
<tab-control :titles="['衣服', '鞋子', '裤子']"
@tab-item-click="tabItemClick"/>
<!-- 2.展示内容 -->
<h1>{{ pageContents[currentIndex] }}</h1>
<!-- 1.tab-control: button -->
<tab-control :titles="['衣服', '鞋子', '裤子']"
@tab-item-click="tabItemClick">
// 从子组件中接收到内容放入props中
<template v-slot:default="props">
<button>{{ props.item }}</button>
</template>
</tab-control>
<!-- 2.tab-control: a元素(重要) -->
<tab-control :titles="['衣服', '鞋子', '裤子']"
@tab-item-click="tabItemClick">
// 从子组件中接收到内容放入props中
<template #default="props">
<a href="#">{{ props.item }}</a>
</template>
</tab-control>
</div>
</template>
<script>
import TabControl from './TabControl.vue'
export default {
components: {
TabControl
},
data() {
return {
pageContents: [ "衣服列表", "鞋子列表", "裤子列表" ],
currentIndex: 0
}
},
methods: {
tabItemClick(index) {
this.currentIndex = index
}
}
}
</script>
<style scoped>
</style>
主要有两种方式:
一些深度嵌套的组件,子组件想要获取父组件的部分内容,可以使用 Provide 和 Inject :
如果Provide中提供的一些数据是来自data,那么我们可能会想要通过this来获取:
如果修改了names之后,之前在provide中引入的 this.names.length 本身并不是响应式的:
在Banner.vue中触发事件:eventBus.emit("事件名",参数)
在App.vue中监听事件:eventBus.on("事件名",回调函数)
// HomeBanner.vue
<template>
<div class="banner">
<button @click="bannerBtnClick">banner按钮</button>
</div>
</template>
<script>
import eventBus from './utils/event-bus'
export default {
methods: {
bannerBtnClick() {
console.log("bannerBtnClick")
eventBus.emit("whyEvent", "why", 18, 1.88)
}
}
}
</script>
// App.vue
<template>
<div class="app">
<h2>App Message: {{ message }}</h2>
<home></home>
<button @click="isShowCategory = false">是否显示category</button>
<category v-if="isShowCategory"></category>
</div>
</template>
<script>
import eventBus from './utils/event-bus'
import Home from './Home.vue'
import Category from './Category.vue'
export default {
components: {
Home,
Category
},
data() {
return {
message: "Hello App",
isShowCategory: true
}
},
created() {
// fetch()
// 事件监听
eventBus.on("whyEvent", (name, age, height) => {
console.log("whyEvent事件在app中监听", name, age, height)
this.message = `name:${name}, age:${age}, height:${height}`
})
}
}
</script>
某些情况下我们希望取消掉之前注册的函数监听:
// Category.vue
<template>
<div>
<h2>Category</h2>
</div>
</template>
<script>
import eventBus from './utils/event-bus'
export default {
methods: {
whyEventHandler() {
console.log("whyEvent在category中监听")
}
},
created() {
eventBus.on("whyEvent", this.whyEventHandler)
},
unmounted() {
console.log("category unmounted")
<!-- 取消事件 -->
eventBus.off("whyEvent", this.whyEventHandler)
}
}
</script>
某些情况下,我们在组件中想要直接获取到元素对象或者子组件实例:
// App.vue
<template>
<div class="app">
<!-- 绑定ref属性 -->
<h2 ref="title" class="title" :style="{ color: titleColor }">
{{ message }}
</h2>
<button ref="btn" @click="changeTitle">修改title</button>
<banner ref="banner" />
</div>
</template>
<script>
import Banner from "./Banner.vue";
export default {
components: {
Banner,
},
data() {
return {
message: "Hello World",
titleColor: "red",
};
},
methods: {
changeTitle() {
// 1.获取h2/button元素
console.log(this.$refs.title);
console.log(this.$refs.btn);
// 2.获取banner组件: 组件实例
console.log(this.$refs.banner); // Proxy(Object) {bannerClick: ƒ, …}
// 2.1.在父组件中可以主动的调用子组件的对象方法
this.$refs.banner.bannerClick();
// 2.2.获取banner组件实例, 获取banner template中的所有元素(单根)
console.log(this.$refs.banner.$el);
// 2.3.如果banner template是多个根, 拿到的是第一个node节点
// 注意: 开发中不推荐一个组件的template中有多个根元素
// console.log(this.$refs.banner.$el.nextElementSibling)
// 3.组件实例还有两个属性(了解):
console.log(this.$parent); // 获取父组件 null
console.log(this.$root); // 获取根组件 Proxy(Object) {changeTitle: ƒ, …}
},
},
};
</script>
想要实现了一个功能:点击一个tab-bar,切换不同的内容显示
可以使用动态组件的方式实现
// App.vue
<template>
<div class="app">
<div class="tabs">
<template v-for="item in tabs" :key="item">
<button :class="{ active: currentTab === item }"
@click="itemClick(item)">
{{ item }}
</button>
</template>
</div>
<div class="view">
<!-- 做法: 动态组件 component -->
<!-- is中的组件需要来自两个地方: 1.全局注册的组件 2.局部注册的组件 -->
<!-- 传值 -->
<component :is="currentTab"
name="why"
:age="18"
@homeClick="homeClick">
</component>
</div>
</div>
</template>
<script>
import Home from './views/Home.vue'
import About from './views/About.vue'
import Category from './views/Category.vue'
export default {
components: {
Home,
About,
Category
},
data() {
return {
tabs: ["home", "about", "category"],
currentTab: "home"
}
},
methods: {
itemClick(tab) {
this.currentTab = tab
},
homeClick(payload) {
console.log("homeClick:", payload)
}
}
}
</script>
<style scoped>
.active {
color: red;
}
</style>
// Home.vue
<template>
<div>
<h2>Home组件: {{ name }} - {{ age }}</h2>
<button @click="homeBtnClick">homeBtn</button>
</div>
</template>
<script>
export default {
// 与父组件进行传值
props: {
name: {
type: String,
default: ""
},
age: {
type: Number,
default: 0
}
},
emits: ["homeClick"],
methods: {
homeBtnClick() {
this.$emit("homeClick", "home")
}
}
}
</script>
keep-alive有一些属性:
include 和 exclude prop 允许组件有条件地缓存:
name
选项对于缓存的组件来说,再次进入时,我们是不会执行created或者mounted等生命周期函数的:
// App.vue
<template>
<div class="app">
<div class="tabs">
<template v-for="item in tabs" :key="item">
<button :class="{ active: currentTab === item }"
@click="itemClick(item)">
{{ item }}
</button>
</template>
</div>
<div class="view">
<!-- include: 组件的名称来自于组件定义时 name 选项 -->
<keep-alive include="home,about">
<component :is="currentTab"></component>
</keep-alive>
</div>
</div>
</template>
<script>
import Home from './views/Home.vue'
import About from './views/About.vue'
import Category from './views/Category.vue'
export default {
components: {
Home,
About,
Category
},
data() {
return {
tabs: ["home", "about", "category"],
currentTab: "home"
}
},
methods: {
itemClick(tab) {
this.currentTab = tab
},
homeClick(payload) {
console.log("homeClick:", payload)
}
}
}
</script>
<style scoped>
.active {
color: red;
}
</style>
// Home.vue
<template>
<div>
<h2>Home组件</h2>
<h2>当前计数: {{ counter }}</h2>
<button @click="counter++">+1</button>
</div>
</div>
</template>
<script>
export default {
// 需有name,与include配对
name: "home",
data() {
return {
counter: 0
}
},
created() {
console.log("home created")
},
unmounted() {
console.log("home unmounted")
},
// 对于保持keep-alive组件, 监听有没有进行切换
// keep-alive组件进入活跃状态
activated() {
console.log("home activated")
},
deactivated() {
console.log("home deactivated")
}
}
</script>
提供了一个函数:defineAsyncComponent
vue也支持在组件上使用v-model
为了我们的MyInput组件可以正常的工作,这个组件内的必须:
// App.vue
组件和组件之间有时候会存在相同的代码逻辑,我们希望对相同的代码逻辑进行抽取
如果Mixin对象中的选项和组件对象中的选项发生了冲突,合并规则:(以组件对象为优先)
如果组件中的某些选项,是所有的组件都需要拥有的,那么这个时候我们可以使用全局的mixin: