vue.config.js
const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
transpileDependencies: true,
configureWebpack: {
resolve: {
// 配置路径别名
// @是已经配置好的路径别名: 对应的是src路径
alias: {
"utils": "@/utils"
}
}
}
})
此文件中可以添加webpack的配置,如上代码,写入一个对象即可
其中alias是为路径起别名,如上配置后,在其他代码页面中路径写utils那么就是src下的utils
@是内置的别名,代表项目src下的路径
jsconfig文件的作用(了解)
tips:其中vue.config.js是配置webpack的,当你对vue给你配置的webpack等打包配置不满意,你就可以在这里自己配置。
1-jsconfig的作用是为了让vscode我们的编辑器,更好的给我们代码提示。
这里的@是什么意思?是内置@为src的底层代码吗?
不是的,我脑子不好使,这里想了半天;
@的内置底层代码肯定不是在这,而是在vue源码里面,这里只是个配置文件,
众所周知jsconfig的作用是为了让vscode我们的编辑器,更好的给我们代码提示,那这里的paths是什么作用?
当我们在vue.config.js中设置了别名,但是当我们写代码路径的时候,vscode并不知道我们设置了别名,如我们写utils,vscode就不会提示utils文件下有哪些子文件,导致我们需要自己手动输入子文件名称。
当我们在下图中的path告诉vscode,utils代表什么路径,它就会提示utils有哪些子文件了。
{
"compilerOptions": {
"target": "es5",
"module": "esnext",
"baseUrl": "./",
"moduleResolution": "node",
"paths": {
"@/*": [
"src/*"
]
},
"lib": [
"esnext",
"dom",
"dom.iterable",
"scripthost"
]
}
}
jsconfig.json我们一般不修改,使用默认即可。
如上代码,target是告诉vscode当前项目代码最终打包为es5;
module是让当前模块化语法规则遵循最新;
baseurl就是当前项目所有页面代码的路径默认./是算从哪里开始(如代码paths中的src就是以此json文件为相对路径开始);
lib就是告诉vscode我写的代码可能包含哪些语法,便于vscode给予我们代码提示。
修改配置文件后,需要重新运行项目才能生效,修改代码不需要重新运行会热更新。
为什么main.js的template无法编译?
import { createApp } from 'vue' // 不支持template选项
// import { createApp } from 'vue/dist/vue.esm-bundler' // compile代码
import App from './App.vue' // vue-loader: template -> createVNode过程
import "./utils/abc/cba/nba/index"
/**
* 1.jsconfig.json的演练
* 作用: 给VSCode来进行读取, VSCode在读取到其中的内容时, 给我们的代码更加友好的提示.
* 2.引入的vue的版本
* 默认vue版本: runtime, vue-loader完成template的编译过程
* vue.esm-bundler: runtime + compile, 对template进行编译
*
* 3.补充: 单文件Vue style是有自己的作用域
* style -> scoped
* 4.补充: vite创建一个Vue项目
*/
// 元素 -> createVNode: vue中的源码来完成
// 导入的vue源码必须包含compile的代码
// const App = {
// template: `<h2>Hello Vue3 App</h2>`,
// data() {
// return {}
// }
// }
createApp(App).mount('#app')
如上代码,如果我们不是导入的app对象组件,而是在本页面自己const声明写根组件的app对象,那么对象里面就需要用到template,就会出现如下问题
1-为什么呢?
因为从vue导入的creatApp方法无法解析编译template,需要从vue/dist/vue.esm-bundler导入
2-为什么呢?如下是元素转换为真实dom的过程
我在上一篇博客中学习了diff算法,就知道了虚拟dom和真实dom,但是我们只知道元素如div会转换为vnode,然后虚拟节点vnode可以转换为虚拟dom或者转换为其他平台的控件什么的。
但是在元素如div转换为虚拟vnode的时候,有一个过程我不知道,那就是上图的通过compile编译为createVNode()函数,再变成虚拟vnode。
3-所以导入的vue源码,必须包含能compile的代码才能解析template;vue/dist/vue.esm-bundler才有compile,vue里面是没有compile的;
4-那么为什么我在app.vue或者其他vue组件内,导入vue就行了,也没有compile啊?
因为在其他组件中(通常是.vue后缀),其实不是通过导入的vue源码实现编译的(它也实现不了,因为没有compile编译功能),而是通过webpack或者其他打包软件里面的vue-loader来实现编译的。(框架前置课学了,webpack打包里面有什么css-loder等等,我也记不太清了但是知道有这种东西)
插件
vscode的vue插件,可以给我们代码提示,提升开发效率
1-vetur:大家在使用vue2的时候很受欢迎,也支持vue3,但是在编写vue3的一些新特性中会提示语法错误。
2-volar(Vue Language Features):现在2022基本上都vue3开发,使用这个即可。
3-path intellisense,给予更好的代码提示
组件中style的作用域
众所周知,webpack是从main.js为入口开始打包代码(我们也可以在webpack配置里指定出入口),然后main.js里面有根组件,根组件下有一个组件树,它们都是.vue文件,都有自己独自的style,那它们是怎么生效的呢?
如上图结构,不管是根组件app.vue还是AppHeader.vue和AppContent.vue,只要导入了,不分是谁注册了谁,它们写的style都是全局的,所有组件都生效。
如果是这样的话,那么就十分难以维护,如写一个p标签的css,不同组件肯定需要不同的样式,那么最后必然样式冲突,我们自己都不知道生效的谁。怎么办呢?
在各自组件的style中加上scoped属性即可把当前css作用域限定在当前页面中,它是怎么实现的呢?
<style scoped>
p{color: aqua;}
</style>
底层原理就是,当我们加上scoped后,其实vue自动帮我们给css需要生效的标签加上了一个独有属性data-v-xxxxx,把我们的css也多加了一层属性选择(交集选择器)。
scope(范围)
快捷生成代码片段
https://snippet-generator.app/
我们原先刚学vue的时候也设置了代码片段,生成键也是vue,不会和这个冲突吗?
原先我们配置的是html.json,现在配置的是vue文件的json,生效对应的文件不同,自然不会冲突。
vue项目的vite创建
方式一:我们当前常用的是通过vue的脚手架,也就是VueCLI通过命令 vue create demo ,就能创建一个名字为demo的项目(底层是基于webpack打包)
方式二:npm init vue @latest,npm就自动帮我们安装了一个本地工具:create-vue,可以通过create-vue为我们生成项目。(基于vite打包,是未来趋势,主要因为打包的很快,他不会自动安装依赖,需要我们自己npm install)
注意:第一次接触到npm init是在框架前置课中学webpack,这个命令是生成package.json的,只有先npm init生成了package.json,然后install webpack才会生成node_modules文件夹
怎么package.json还在自己生成?不是自动有吗?
这是我原先单独学webpack的时候需要的,我们通过vueCLI生成的项目是自动有package.json的
其中npm安装了vueCLI,也就自动有了webpack功能,就像方式二安装了create-vue,也就自动有了vite
方式二这个项目没有名字?
我们通过命令后,它会询问一些问题,名字,需要的东西如ty,router等等,我们填上去即可
这个方式我现在只是了解,在我学完小程序框架和vue几个项目后,会详细学习vite并深入工程化学习。
注意我们运行项目的时候,这里是npm run vite,通过vite打包,这个在框架前置课也学了。
我们原先通过脚手架生成的项目一般是serve,通过webpack打包
组件间的通信
vue组件的嵌套关系
通俗来讲,比如一个列表组件,我们在多处使用,但是不是显示同样的数据,这个数据如何处理传递。就是学习组件通信的原因。
父组件传递子组件
如上图,父组件可以通过给予组件属性,传递数据到子组件,子组件可以通过props数组接收数据,并在插值语法中使用。
数组语法的弊端:
1-没有默认值
2-没有类型规范检查
所以我们需要学props的对象写法
如果我们在使用组件的时候,父组件没有传递值过来,默认的值就会显示上去。
required的为必传项,不传就会报警告,所以一般必传项没有默认值,上图中只是一种语法参考。
type一般有:string,number,boolean,arry,object,date,function,symbol
问题:如下我不管怎么传,都提示我传过来的是字符串,为什么?
<AppHeader name="zdq" gender="man" age="20"></AppHeader>
<AppHeader name="tyl" gender="man" age="21"></AppHeader>
解决:
<AppHeader name="zdq" gender="man" :age="20"></AppHeader>
<AppHeader name="tyl" gender="man" :age="21"></AppHeader>
加冒号的,说明后面的是一个变量或者表达式;没加冒号的后面就是对应的字符串字面量!
https://blog.csdn.net/fifteen718/article/details/80329558
细节:对象类型的其他写法
如果父组件传递过来的是一个对象或者数组,那么默认值我们必须写成如上的一个函数,函数再返回对象
至于为什么就是一些底层机制了,这里不研究了。
注意:这里父组件给子组件传递了一个对象或数据后,对象在子组件的props内被接收,它并没有创建一个新对象,而是和父组件共用同一个对象,也就是指向同一个引用。
所以子组件修改了这个对象后,父组件内的此对象也会被修改
// 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"
}
}
}
意思就是在父组件传值的时候可以写小驼峰也可以写短横线,在子组件写props的时候必须小驼峰
非props的attribute
<show-info name="why" :age="18" :height="1.88"address="广州市" abc="cba" class="active" />
这段代码对应上一段代码段
当我们在父组件赋值后,但是在子组件的props没有设置接收,那么就会添加到子组件的根节点中(也就是子组件中最外层的div,我写的时候子组件没写div结果是没被添加)
当我们不希望继承,就可以在组件的script里禁用
<script>
export default {
inheritAttrs: false,
}
</script>
如果有多个根节点,那么就会报错,我们需要通过$attrs手动告诉它哪个div需要绑定
应用场景的也不知道,coderwhy老师讲了什么我先记下来,后续可能用到。
子组件传递给父组件
一般场景:子组件发生了一个事件,父组件需要响应。
1-如上是一个加减计数器,当子组件的“加”按钮被点击,会触发事件,然后事件会执行this.$emit()方法,把当前事件传递到根组件中(别管咋来的,$开头一般都是内置的方法,它就是会这样传)
2-其中add为事件名称,可以随便写,我们在根组件对应即可,第二个是需要传递的参数,如上参数传到根组件,根组件对应响应的事件函数可以接收到count,然后声明使用
methods:{
addBtnCkick(count){
this.counter+=count
}
},
记住根组件这里的addBtnCkick可以接收到子组件的count的值,但是根组件写coun也可以,随便写什么名字,这里是接收值然后重新声明了一个coun变量,而不是直接把子组件的count拿过来用了。
methods:{
addBtnCkick(){
this.counter+=count
}
},
报错,因为根本不知道count是什么,并没有定义变量来接收子组件的count值
我们只是习惯把这个变量写的和子组件传递过来变量值的变量名相同,如count
正确如下:
methods:{
addBtnCkick(count){
this.counter+=count
}
},
所以这里也必须写一个变量名,因为函数的形参其实就是一种变量的声明,作用域为这个函数作用域,你不写的话就是 没有参数,自然就报错
3-然后在根组件写事件监听,监听这个add事件(和监听click什么的一样,只是click是浏览器用户操作事件,这个add是来自子组件的自定义事件)
4-最后执行上图的addbtnclick函数完成需要的响应。
自定义事件的参数与验证
一般验证很少用,这是vue3新增的,我们即使写,也只写数组写法(这样在根组件监听的时候有代码提示,而且如果两个组件不是一个人开发,另一个人可以通过emits数组快速找到这里有哪些自定义事件)
export default {
// 1.emits数组语法
emits: ["add","sub"],
// 2.emmits对象语法
// emits: {
// add: function(count) {
// if (count <= 10) {
// return true
// }
// return false
// }
// },
}
如上写了emits数组后,在根组件监听这些自定义事件的时候,打一个a,vscode就会提示add,方便不同组件不同人开发时的使用。(否则另一个人不知道用的组件有哪些事件,还得去源码仔细看)
组件通信案例
报错:
一定要注意代码规范,一个空格报错,找的头脑发热。
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>{{ item }}</span>
</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"/>
<!-- <tab-control :titles="['流行', '最新', '优选']"/> -->
<!-- 2.展示内容 -->
<h1>{{ pageContents[currentIndex] }}</h1>
</div>
</template>
<script>
import TabControl from './TabControl.vue'
export default {
components: {
TabControl
},
data() {
return {
pageContents: [ "衣服列表", "鞋子列表", "裤子列表" ],
currentIndex: 0
}
},
methods: {
tabItemClick(index) {
console.log("app:", index)
this.currentIndex = index
}
}
}
</script>
<style scoped>
</style>
注意:
第一:
methods: {
itemClick(index) {
this.currentIndex = index
this.$emit("tabItemClick", index)
}
}
这是从TabControl组件中发出的自定义tabItemClick,自然只能在这个组件中标签中监听到,如下
<template>
<div class="app">
<!-- 1.tab-control -->
<tab-control :titles="['衣服', '鞋子', '裤子']"
@tab-item-click="tabItemClick"/>
<!-- <tab-control :titles="['流行', '最新', '优选']"/> -->
<!-- 2.展示内容 -->
<h1>{{ pageContents[currentIndex] }}</h1>
</div>
</template>
你不能说给上述代码中的h1也加个tabItemClick自定义监听事件,根本监听不到。
第二:
<h1>{{ pageContents[currentIndex] }}</h1>
我们可以看到这里插值语法用的是变量currentIndex,而不是index,为什么呢?
因为index的作用域只在methods,它没有写在data中,是无法被外面直接使用的,否则会报错。
插槽Slot
插槽的作用
简而言之就是当我们自己封装一个组件,如上的导航组件,其他人使用的时候,可能要的效果与控件是不同的,所以我们不能写死一个地方是button或者input,而是留下一个slot插槽让使用者自己来决定。
插槽的基本使用
message.vue
<template>
<h2>{{ title }}</h2>
<div class="content">
<slot>
<p>我是默认插槽内容, 哈哈哈</p>
</slot>
</div>
</template>
<script>
export default {
props: {
title: {
type: String,
default: "我是title默认值"
}
}
}
</script>
<style scoped>
</style>
如上,当我们封装一个组件,可以把内容通过slot标签形成插槽,其中slot标签内可以写默认内容,也就是当使用者不插入东西的时候,就显示,如上的p标签;当使用者自己插入元素就会替换默认内容p标签。
<template>
<div class="app">
<!-- 1.内容是button -->
<show-message title="哈哈哈">
<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>
如上是使用者插槽的语法,我们在组件标签内写入需要插入的一个元素即可(这里只有一个最基本的插槽)
如果我们有多个元素对应多个插槽,那么这些元素不会被逐个对应,而是都会被插入到一个插槽内
我们理想的是,button对应插槽一,span对应插槽二,a对应插槽三,但实际上它会把所有插槽加上三个元素。
那么如何实现多个插槽不同内容呢?那就是下面的具名插槽。
具名插槽/多个插槽的使用
为什么多个插槽也叫具名插槽,因为面对多个插槽,我们一般会给它起具体名字,让它知道元素是要插入到哪个槽点。
一:赋予槽点名字,如下的name=‘left’
二:使用者给插槽插入元素的语法,我们需要用template标签包裹需要插入槽点的标签,然后通过v-slot确定是插入哪一个槽点。(v-slot语法是官方规定的)
注意:
1-v-slot是写在template里面的,不是写在button或者span什么的里面。
2-赋予name的插槽,只有当使用者通过v-slot指定后才会被插,这也是它的作用所在。
如果使用者给予的元素没有通过v-slot指定,那么都被插入到default插槽。
动态插槽
也就是我不确定这个元素标签如button需要被插入到哪个槽点,就可以通过一个变量去动态修改它,随着一些事件响应让它变化。
一般都是插槽地点都是固定的,所以动态插槽应用场景较少。
v-slot的语法糖
渲染作用域
也就是说,一个vue文件里面用到的数据,如在插值语法中使用的数据,只能是当前文件下的props或者data里面的数据,不能跨文件使用。
作用域插槽的使用( 难点)
需求:
子组件
<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)">
<slot :item="item" abc="cba">
<span>{{ item }}</span>
</slot>
</div>
</template>
如上案例,我们插槽了“button哈哈”给子组件,但是子组件里面的遍历出的三个button全部变成了“button哈哈”,如果我们需要它们的内容不同应该怎么办呢?
1-在app.vue通过插值语法修改需要插槽过去的内容
没用因为,你在这里再怎么修改,最终插值过去的还是相同内容的死数据
2-利用父传子,让子组件通过props接收到需要动态修改的数据,然后修改
没用,因为在子组件插槽内修改的都为插槽默认数据,最终会被父组件插槽过来的内容替换。
怎么办呢?
上述案例是需要拿到子组件遍历出的item,然后对应动态修改在插槽内。
其中有一个这样的插槽语法,我们把需要从子组件传递出去的数据变量写在slot标签内,然后在父组件通过
v-slot:xx=“props”接收,其中props就是一个对象,名字可以自己定义。
注意:
:item=“item”是声明一个需要传递到父组件prop对象的属性,等于当前子组件的item变量
abc:“cba”是声明一个需要传递到父组件prop对象的属性,等于当前子组件随便写的一个字符串cba
前面加了冒号就是一个表达式,不加冒号就是字符串字面量
有的东西说不明白哈哈,这博客我自己复习用的,我能理解就成,大家还是得自己看视频
是不是可以具名槽点也能实现这个效果?
不能,完全不是一个东西了,你还得仔细研究一下概念,上面其实是一个元素对一的槽点,只是遍历了三次,而不是具名槽点的多个元素对多个槽点。
单个默认插槽的缩写
我们统一都写模板template,防止出错。