vue3(基于vite)
配置环境
npm init vite@latest
yarn create vite
pnpm create vite
vite配置
npm install @types/node -D //安装node变量声明
路径别名
vite.config.ts
使用es6语法 因为node环境下默认不支持import.meta.url,所以需要通过fileURLToPath进行转化
import { fileURLToPath, URL } from 'node:url'
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)),
},
},
---vite.config.js----
import { defineConfig } from 'vite'
import path from 'path'
import vue from '@vitejs/plugin-vue'
// https://vitejs.dev/config/
export default defineConfig({
resolve: {
alias: [{ find: '@', replacement: path.resolve(__dirname, 'src') }]
},
server: {
host: true, // host设置为true才可以使用network的形式,以ip访问项目
port: 8080, // 端口号
open: true, // 自动打开浏览器
cors: true, // 跨域设置允许
strictPort: true, // 如果端口已占用直接退出
// 接口代理
proxy: {
"/api": {
// 本地 8000 前端代码的接口 代理到 8888 的服务端口
target: "http://localhost:8888/",
changeOrigin: true, // 允许跨域
rewrite: (path) => path.replace("/api/", "/"),
},
},
},
plugins: [vue()]
})
tsconfig.json
tsconfig.json
{
"compilerOptions": {
"target": "ESNext",
"useDefineForClassFields": true,
"module": "ESNext",
"moduleResolution": "node",
"strict": true,
"jsx": "preserve",
"resolveJsonModule": true,
"isolatedModules": true,
"esModuleInterop": true,
"lib": [
"ESNext",
"DOM"
],
"skipLibCheck": true,
"noEmit": true,
"baseUrl": "./",
"paths": {
"@/*": [
"src/*"
]
}
},
"include": [
"src/**/*.ts",
"src/**/*.d.ts",
"src/**/*.tsx",
"src/**/*.vue"
],
"references": [
{
"path": "./tsconfig.node.json"
}
]
}
//注意:为了防止未使用导入模块的警告
需要将
"noUnusedLocals": false,
//防止TS中通过变量存储key值读取对象的属性值时报错
"suppressImplicitAnyIndexErrors":true,
vite-demo项目框架
public: 存放静态资源,特点不会被vite编译
src/assets: 也可以存放静态资源,如图片,但是可以通过配置将其打包编译成base64,这样不浪费资源
vite.config.ts: vite的配置文件,可以配置打包的入口、出口,插件等等,注意,是node环境。
vite-env.d.ts: ts声明文件
注意:自己实现一个vue-cli的时候也需要这个声明,声明vue模块的类型
declare module "*.vue" {
import { DefineComponent } from "vue"
const component: DefineComponent<{}, {}, any>
export default component
}
index.html: webpack是通过js来作为入口文件的,index.js。但是vite是通过html作为入口文件,然后通过sctip type="module"进行导入。因此要注意,打包时的入口文件是index.html!!!
tsconfig.json:
| 字段 | 作用 | 案例 |
|---|---|---|
| "include" | 告诉ts哪些文件需要编译,比如vue文件里的ts代码需要编译。 | |
| "extends" | 继承对应的配置文件 | "@/vue/tsconfig/tsconfig.web.json" |
| "compilerOptions" | 用来配置路径别名。vite.config.ts中配置的路径别名是用于打包时查找文件,而tsconfig.json中是用于vscode代码提示。还可以用于eslint,使用es哪个版本等配置 | |
| "references" | tsconfig.json中配置一般是固定的,推荐在tsconfig.node.json中修改 |
强制使用pnpm包管理工具
团队开发项目的时候,需要统一包管理器工具,因为不同包管理器工具下载同一个依赖,可能版本不一样,
导致项目出现bug问题,因此包管理器工具需要统一管理!!!
在根目录创建scripts/preinstall.js文件,添加下面的内容
if (!/pnpm/.test(process.env.npm_execpath || '')) {
console.warn(
`\u001b[33mThis repository must using pnpm as the package manager ` +
` for scripts to work properly.\u001b[39m\n`,
)
process.exit(1)
}
配置命令
"scripts": {
"preinstall": "node ./scripts/preinstall.js"
}
**当我们使用npm或者yarn来安装包的时候,就会报错了。原理就是在install的时候会触发preinstall(npm提供的生命周期钩子)这个文件里面的代码。**
## vite环境变量的配置
创建三个文件
.env.development
.env.production
.env.test
<!---->
# 变量必须以 VITE_ 为前缀才能暴露给外部读取
NODE_ENV = 'development'
VITE_APP_TITLE = '硅谷甄选运营平台'
VITE_APP_BASE_API = '/dev-api'
**使用环境变量**
`import.meta.env`
## vite的优势:
①冷启动。vite启动时无需打包编译,不需要分析各个模块的依赖关系,可以直接启动服务器。只有浏览器请求相应模块时才会对相应模块进行打包编译。而webpack启动时,首先需要确定入口,从entry入口开始,递归解析所有的模块文件,调用loader进行加载处理,如果当前模块依赖于其他模块还需要对被依赖模块进行加载处理。处理完毕后得到转化内容和依赖关系,然后组装成一个个代码块chunk。之后再把chunk转化成文件添加到输出列表中
②热更新时,vite只需要对相应模块进行重编译,而webpack需要对当前模块和当前模块所依赖的模块进行重编译。
# svg图标的封装
## 安装插件
`npm install vite-plugin-svg-icons -D`
`npm install fast-glob`
vite.config.js中配置插件
import { createSvgIconsPlugin } from 'vite-plugin-svg-icons'
import path from 'path'
export default () => {
return {
plugins: [
createSvgIconsPlugin({
// Specify the icon folder to be cached
iconDirs: [path.resolve(process.cwd(), 'src/assets/icons')],
// Specify symbolId format
symbolId: 'icon-[dir]-[name]',
}),
],
}
}
main.ts中配置
import 'virtual:svg-icons-register'
## 使用
基础使用
```js
<template>
<div>
<h1>svg测试</h1>
<!-- 图表外层的容器,内部需要与use结合使用,通过style设置svg的样式 -->
<svg style="width: 30px; height: 30px">
<!-- xlink:href执行用哪一个图表,属性值务必#icon-图标名字 fill可以设置图标的颜色 -->
<use xlink:href="#icon-vue" fill="yellow"></use>
</svg>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, computed, onMounted, watch } from 'vue'
</script>
<style scoped></style>
虚拟DOM
虚拟DOM就是通过js生成一个抽象语法树AST。比如说ts-->js也需要 es6-->es5的babel-plugin也需要。 babel原理①解析 ②转换,对AST进行操作,babel-generator ③生成
diff算法: 如果没有key,那么新旧Vnode就会进行依次比对,判断是否相同(type和key),相同就比较下一个,不同就删除,重新新增。没法复用,所以性能很差。 有了key,采用的是从两端到中间的比较策略,首先进行一个前序对比算法,从头部开始依次比对是否是同一节点。如果不同的话就跳出,继续进行尾序比对算法,如果不同,跳出。(vue2还会头尾交叉再比,vue3取消了,vue3使用了最长递增子序列的优化),最后,如果新节点多了就新增,旧节点多了就删除。
vue2和vue3区别
①响应式原理不同。vue2采用的是数据劫持和发布订阅者模式实现响应式。首先设置一个Observer观察者,遍历data中的所有属性,然后通过Object.defineProeprty为其添加set和get方法,如果data对象中含有子对象,那么需要递归遍历子对象。
②设置一个compile模板解析器,解析模板中的变量并完成初始化渲染。当变量被调用的时候,就会触发get方法,同时会new一个watcher订阅者实例,然后将指令对应节点的更新函数绑定到watcher订阅者实例的update方法里,再将订阅者实例推到dep中
③dep。存放所有的订阅者,当变量被修改时,就会通过dep.notify通知调度中心,然后调度中心就会寻找到对应的订阅者触发update方法。
vue3采用的是proxy,但对于基本类型的数据,还是通过Object.defineProperty,对于引用类型的数据采用的是proxy。即reactive的原理。记住用Reflect,保证规范上下文完整。
export const reactive = <T extends object>(target: T) => {
return new Proxy(target, {
get(target, key, receiver) {
//为了保证上下文正确
let res = Reflect.get(target, key, receiver)
},
set(target, key, newValue, receiver) {
return Reflect.set(target, key, newValue, receiver)
}
})
}
创建一个副作用函数
let activeEffect
export const effect = (fn: Function) => {
const _effect = function () {
activeEffect = _effect
}
}
//调度中心targetMap
const targetMap = new WeakMap()
//订阅 只不过多加了一个映射,即target 和depsMap key才是type
export const track = (target, key) => {
let depsMap = targetMap.get(target)
//target是传入的对象,depsMap是对应对象的名称和订阅者数组的映射
if (!depsMap) {
depsMap = new Map()
targetMap.set(target, depsMap)
}
//找到对应的订阅者数组集合,如果没有的话就创建
let deps = depsMap.get(key)
if (!deps) {
deps = new Set()
depsMap.set(key, deps)
}
//收集副作用函数
deps.add(activeEffect)
}
//发布
export const trigger = (target, key) => {
const depsMap = targetMap.get(target)
//找到订阅者数组
const deps = depsMap.get(key)
deps.forEach(item => {
item()
})
}
class Dep {
watchers: Object
constructor() {
this.watchers = {}
}
on(type, callback) {
let depMap = this.watchers[type]
if (!depMap) {
this.watchers[type] = true
}
this.watchers[type].push(callback)
}
emit(type, ...args) {
if (!type) {
this.watchers[type] = []
}
this.watchers[type].forEach(item => {
item.call(null, ...args)
})
}
}
vue响应式的本质
数据与函数相关联。
数据要求: 响应式数据,且必须在函数中使用到
函数要求: render、computed、watchEffect、watch
总结:必须在特定函数中使用到响应式数据,这才能实现响应式。
案例①:
父组件中修改了props.count值,并不会导致子组件中模板的doubleCount重新渲染
const doubleCount=ref(props.count*2)
原因:props.count是响应式数据,但是它并没有在那四个特定函数中调用,doubleCount虽然也是响应式数据,但是它无法监听到props.count的改变。
案例②
实现了响应式。doubleCount是响应式数据,且在watchEffect中使用到,被修改了值,且在render函数中被调用,从而实现了响应式
const doubleCount = ref(0)
watchEffect(() => {
console.log('watchEffect')
doubleCount.value = props.count * 2
})
vue3的新特性
①重写双向数据绑定 v-model 用proxy替换object.defineProperty。通过遍历对象中的每个属性,然后通过Object.deinfePropery为对象的属性添加set和get方法,从而实现对象劫持。缺点:对象中新增的属性是没有响应式的,直接操作数组是没有响应式的,只能重写数组方法。
为什么需要reflect:① reflect可以修正proxy中的this指向问题。②reflect是为了规范返回的结果。
②composition api setup语法糖形式
③VDOM的优化。添加了静态标记,假如class类名是静态的,就添加静态标记,如果是变量就添加动态标记
④Tree-shaking的支持。在保持代码的正常运行下,去除掉冗余的代码。
⑤Fragment vue2中代码必须包含在根节点中,vue3支持fragment,可以有多个节点。底层原理其实就是多加了一个虚拟节点。
ref和reactive
ref全家桶
ref创建响应式变量,有两种定义类型的方法
① 直接定义好类型或者是接口
const Man=ref<M>({name:'zds'})
②用Ref官方提供的
import type { Ref } from 'vue'
const Man: Ref<M> = ref({ name: 'zds' })
isRef用来判断是否是ref变量
shallowRef:
用于浅层响应式,修改其属性是非响应式的。
只有到.value的时候是响应式的 如果m.value.name这样赋值就不是响应式的了。
ref可用于深层响应式。
但是可以这么写
const Man: Ref<M> = ref({ name: 'zds' })
const Man2: Ref<M> = shallowRef({ name: 'xqq' })
function change() {
Man2.value = { name: 'zds' } //这是响应式的
Man2.value.name='zds' //这是非响应式的
}
customRef
让用户自己实现一个ref响应式 可以用来实现一个简单的防抖
const text = useDebounceRef('hello', 500)
function useDebounceRef(value, delay) {
let timer = null
return customRef((track, trigger) => {
return {
get() {
track()
return value
},
set(newValue) {
clearTimeout(timer)
timer = setTimeout(() => {
console.log('触发了set')
value = newValue
trigger()
}, delay)
},
}
})
}
<template>
<div>
<h1>人物简介</h1>
<input type="text" v-model="text" />
{{ text }}
</div>
</template>
reactive全家桶
ref支持所有类型,reactive只支持引用类型。ref源码里对引用类型数据添加响应式,其实底层也是调用reactive
ref取值和赋值都需要+value 而reactive是不需要的
数组异步赋值问题
这是因为reactive proxy不能直接赋值,不然会破坏响应式对象
解决方案:①纯属组直接用push方法,配合解构
function add() {
setTimeout(() => {
let res = ['edg', 'rng', 'jdg']
list.push(...res)
}, 1000)
②包裹一层对象,这样的话只是修改了内部的属性,而不是破坏响应式。
type List = {
arr: string[]
}
let list = reactive<List>({
arr: []
})
function add() {
setTimeout(() => {
let res = ['edg', 'rng', 'jdg']
list.arr = res
}, 1000)
}
readonly
其实就是把每个变量变成只读的
shallowReactive
只有浅层的数据有响应式,即第一层
const newObj = reactive({
a: {
b: {
c: 'e'
}
}
})
newObj.a.b.c = '132' //没有响应式
newObj.a = { b: { c: 'x' } } //有响应式
获取组件实例
import {getCurrentInstance} from 'vue'
const instance = getCurrentInstance()
//instance会返回一个组件实例对象,vnode
toRef toRefs
作用:配合props的解构赋值。
在父传子中,一般通过props接受,可以配合{}进行解构,因为一般传递的都是reactive或者是ref响应式变量,如果直接解构会丢失响应式。
toRef
对于非响应式的对象,无论咋操作毫无用处,所以只能用于响应式对象。将响应式对象的某个属性提出来,然后通过.value能响应式的修改变量
import { ref, reactive, toRef } from 'vue'
const man = { name: 'zds', age: 20 }
//toRef只能修改响应式对象的值,非响应式毫无变化
//const man = reactive({ name: 'zds', age: 20 }) 这样即可
//说白了就是将某个属性取出来变成ref对象
const age = toRef(man, 'age')
function change() {
age.value = 12
console.log(man) //试图毫无变化,因为man是非响应式的
}
使用场景 hook(value) hook接受reactive响应式对象的某个属性,就可以通过toRef将某个属性值提出,单独修改,不需要传入整个对象了。
toRefs
其实,就是遍历对象中的所有属性,把每个属性都变成ref对象,最后返回。一般会配合解构使用。
原理:
const toRefs = <T extends object>(object: T) => {
const map: any = {}
for (let key in object) {
map[key] = toRef(object, key)
}
return map
}
如果不+toRefs,解构的话,name,age是没有响应式的,但是可以通过man.xxx来修改
toRaw
将响应式对象转化为普通对象
computed计算属性
只有computed依赖的值发生改变,它才重新计算,不然使用的是原来缓存的值
语法: const xx=computed(()=>{ return xx })
写法②
computed({
get() {
return 'xx'
},
set() {}
})
watch侦听器
语法: watch([v1,v2],(newValue,oldValue)=>{},{deep:true})
watch(message, (newValue, oldValue) => {
console.log(newValue, oldValue)
})
侦听多个数据
let message = ref<string>('小满11')
let message2 = ref<string>('打满')
watch([message, message2], (newValue, oldValue) => {
console.log(newValue, oldValue)
})
返回结果是数组形式
侦听对象 监听对象深层次数据需要开启深度监听
注意:监听到的newValue和oldValue是相同的。如果监听的是reactive数据,不需要开启deep:true,因为默认开启了
let message2 = ref({
foo: {
bar: {
name: 'zds'
}
}})
watch(
[message, message2],
(newValue, oldValue) => {
console.log(newValue)
},
{
deep: true //开启深度监听
}
)
只监听对象深层次某一属性 需要用回调函数方式
oldValue和newValue不同!!
watch(
() => message.foo.bar.name,
(newValue, oldValue) => {
console.log(newValue, oldValue)
}
)
options 配置项
deep:boolean 是否开启深度监听,ref定义的引用类型需要开启,reactive默认帮忙开启
immediate:boolean 是否开启立即监听,true就立刻触发一次
flush:"pre" 组件更新前调用| "sync" 同步执行 "post" 组件更新后执行
immediate监听的细节
分为以下几种情况:
| 监听目标 | immediate:false | immediate:true |
|---|---|---|
| 监听data中数据 | 第一次data中初始化值的时候不会调用,之后每修改一次调用一次。 | 第一次data中初始化值的时候立刻调用一次。由undefined==>初始值 |
| 直接监听props中数据 | 默认不会触发回调 | 会触发一次回调,效果类似于监听data中设置immediate:false,即newValue:传递的值,oldValue:自定义默认值 |
| 将props的值保存在data中,监听data | 第一次data中初始化值的时候不会调用,之后每修改一次调用一次 | 第一次data中初始化值的时候立刻调用一次。由undefined==>初始值 |
| 监听vuex中数据 | 不会直接触发回调,除非修改了vuex中对应数据 | 会触发一次回调,newValue即为vuex中存储数据。在mounted前调用 |
特殊情况①: 祖先组件传递值给父组件,父组件再添加一些数据传递给儿子组件。
immediate:false, 不会触发儿子组件的监听器回调immediate:true, 会触发一次监听器回调,类似于直接监听props中数据
特殊情况②: 父组件传递给子组件一个option,同时父组件发送请求,修改options中的值
-
immediate:false。只会触发一次回调,newValue:父组件发送请求后修改的值,oldValue:父组件传递给子组件的值 -
immediate:true。触发两次回调- 第一次回调: newValue:父组件传递给子组件的值,oldValue:默认值(null)
- 第二次回调:newValue:父组件发送请求后修改的值,oldValue:父组件传递给子组件的值。
echrats监听传递options,init初始化,就需要以此判断,比如特殊情况①,必须要immediate:true后,直接进行init。当然 this.$nextTick()是必须的
特殊情况②:可以设置immedaite:false了,因为只需要父组件发送请求后修改的值。至于不需要nextTick是因为一般在父组件mounted中发送请求,这时候子已经mounted结束了,DOM已经渲染完成
特殊情况③:父组件请求,子组件是通过动态组件动态创建的,这时候首个组件默认渲染的时候,就是类似于情况②,而其他组件如果通过props传递请求数据,那么就是情况①,因为它们组件都还未创建
watchEffect 高级侦听器
语法:watchEffect((before)=>{before()},{flush:'post'})
①非惰性的,即会立即执行一次,watch需要手动配置immediate
② 它不需要手动传入依赖对象,会自动收集函数内的数据作为依赖对象,然后当依赖变化后会重新执行回调函数。回调函数形参里还有个onInvalidate函数,当依赖变化后,它会优先执行它的回调。
watchEffect(oninvalidate => {
console.log(message.value)
oninvalidate(() => {
//这个回调被优先执行,比如可以清除副作用 就会优先执行一次before。但初始化的时候不会调用
console.log('before')
})
})
结果: 飞机 //首先会立即执行一次log
切换后 飞机 //会优先执行oninvalidate 输出 before 飞机
③ 可以停止监听 const stop=stopWatch()
const stop = watchEffect(oninvalidate => {
console.log(message.value)
oninvalidate(() => {
//这个回调被优先执行,比如可以清除副作用 就会优先执行一次before。但初始化的时候不会调用
console.log('before')
})
})
function stopWatch() {
stop()
}
④一般 flush再watchEffect中使用,因为它flush:'post',可以在页面渲染结束后调用,可以获取到最新的DOM元素
vue3生命周期
setup语法糖中:没有beforeCreate和created生命周期,但可以用setup来代替,且setup只会执行一次。setup优先于beforeCreated,created
setup onBeforeMount onMounted onBeforeUpdate onUpdated onBeforeUnmount onUnmounted
| 生命周期 |
|---|
| 父组件setup |
| 父组件onBeforeMount |
| 子组件setup |
| 子组件onBeforeMount |
| 子组件onMounted |
| 父组件onMounted |
组件通信
父传子:
通过v-bind 传递,子组件通过defineProps接受
①无ts模式
deinfeProps(['xx1','xx2'])
②ts模式
携带默认值 withDefault(defineProps<Iprops>(),{属性:默认值})
注意:复杂类型需要用函数形式
withDefaults(defineProps<Iprops>(), {
score: 0,
data: () => ({ name: "dzd", age: 12 }),
});
子传父:
通过事件触发,然后父组件中定义自定义事件接受
----简单写法-----
const emit = defineEmits(['onCCClick'])
function send() {
emit('onCCClick', '子传父')
}
配合ts写法
const emits = defineEmits<{
(e: 'update:modelValue', value: valueType): true
(e: 'change', value: valueType): true
}>()
注意:e跟着的是事件名
defineExpose
如果子组件想要暴露一些方法给父组件调用,在vue2中,父组件可以直接获取子组件实例,然后直接调用,但是vue3中,子组件必须通过defineExpose()暴露对应属性和方法
重点:组件内部数据对外是关闭的,如果想暴露给外部访问,需要通过defineExpose方法
父组件通过定义ref变量获取子组件,然后通过.value获取子组件实例。注意:只有在onMounted中可以获取到子组件实例,数据是响应式的。
注意:ref的值必须和定义的响应式变量名相同
//ts含类型写法
<water-fall ref="waterFallRef" />
import waterFall from './components/water-fall.vue'
const waterFallRef = ref<InstanceType<typeof waterFall>>()
onMounted(() => {
console.log(waterFallRef.value?.list)
})
v-model
(可以实现父子组件数据同步,其实说白了还是通过props传递值,父组件默认传递了一个名为modleValue的值,通过自定义事件update:modelValue修改父组件的值,默认直接定义好了一个update:modelValue的自定义事件)
①:相当于给子组件传递props[modelValue]=1000
②:同时在子组件上绑定了一个@update=‘updateModelValue’,用来更新modelValue,
所以只需要$emit('update:modelValue',新值)即可
写法二:想要自定义变量名称
这种写法,子组件$emits('update:pageNo',props.pageNo+1000)即可
useAttrs
可以用来接受父组件传递的属性和属性值。但是如果props接受后,$attrs就接受不到了
import { ref, reactive, useAttrs } from "vue";
let $attrs = useAttrs();
//可以获取到props的数据 比如props.value=1 $attrs.value就获取不到了。
兄弟组件通信
①状态提升的方式,A组件想要将数据传递给B组件,可以先通过自定义事件将数据给父组件,然后由父组件分发给B组件 ②使用mitt 本质上就是一个发布订阅。
模拟全局事件总线
vue3.0中,取消了vue的原型对象,改用了createApp()的方式创造组件实例,因此没有了this。如果想要实现类似功能,需要使用mitt插件
npm install mitt
pinia
全局状态管理,缺点在于无法持久性保存。 pinia使用文档
全局组件 局部组件
语法:app.component('组件名',Com)
局部组件import导入,就是不需要额外注册了 全局组件在main.ts中导入,注意:需要通过链式调用。因为每次都会返回app
通常,可以自定义一个全局组件注册插件,实现组件快速的全局注册
递归组件
常用于树型结构。通过v-if来结束递归
注意:递归调用时直接使用组件名即可,点击时要注意停止冒泡动作。所以必须要定义组件名
<template>
<div @click.stop="add" v-for="item in data" :key="item.name" class="tree">
<input type="checkbox" v-model="item.checked" /><span>{{ item.name }}</span>
<Tree v-if="item?.children?.length" :data="item.children" />
</div>
</template>
<script setup lang="ts">
import { ref, reactive, computed } from "vue";
interface Tree {
name: string;
checked: boolean;
children?: Tree[];
}
interface Iprop {
data: Tree[];
}
const props = defineProps<Iprop>();
</script>
<style scoped>
.tree {
margin-left: 10px;
}
</style>
动态组件
动态组件:让多个组件使用同一个挂载点,并进行动态切换。常用于tab切换
语法:<component :is='组件'>
注意:①vue2的时候是靠组件名称切换的,vue3是靠组件实例进行切换的,所以直接导入对应组件
②如果把组件实例直接放到reactive对象中,那么会报警告。这是因为reactive会对传入的对象进行代理,但是组件代理毫无用处,所以可以使用markRaw跳过代理
<template>
<div>
<button @click="change(item)" v-for="item in data" :key="item.name">
{{ item.name }}
</button>
<component :is="isCom"></component>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, computed,markRaw } from "vue";
import A from "./components/A.vue";
import B from "./components/B.vue";
const isCom = reactive(markRaw(A));
const data = reactive([
{
name: "A",
com: markRaw(A), //这样不会有警告,会跳过proxy
},
{
name: "B",
com: B,//这样会有警告
},
]);
function change(item: { name: string; com: any }) {
console.log(item.com);
isCom.value = item.com;
}
</script>
插槽
插槽其实就是子组件中提供给父组件的一个占位符,父组件可以往里面填充任何内容,替代 slot /slot 的位置
匿名插槽和具名插槽的语法:
<Dialog>
<template v-slot:header>我被插入到head中</template>
<template v-slot> 我被插入到main中 </template>
<template #footer> 我被插入到footer中</template>
</Dialog>
<header class="header">
<slot name="header" />
</header>
<main class="main">
<slot></slot>
</main>
<footer class="footer">
<slot name="footer" />
</footer>
作用域插槽,实现子传父
通过#slot='变量名' 父组件就可以获取到对应的值
动态插槽
异步组件
vue通过defineAsyncComponent()定义异步组件
defineAsyncComponent(()=>import('...vue'))
await
在es7 await可以直接在模块的最外层使用,这让整个模块就变成一个巨大的async函数。但也可以通过async包裹一个立即执行函数
Suspense
(suspense和telepot一样是vue3新增的内置组件) suspense可以用来配合异步组件做骨架屏.
需要配合异步组件来使用,有两个插槽,default是展示异步加载的组件,fallback用来展示骨架屏
<template>
<div>
<Suspense>
<template #default>
<Async />
</template>
<template #fallback> 加载的骨架屏 </template>
</Suspense>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, computed, defineAsyncComponent } from "vue";
const Async = defineAsyncComponent(() => import("./components/Async/index.vue"));
</script>
<style scoped></style>
优化打包
如果是直接import 同步导入组件的方式,那么打包出来的js文件会很大,即首次加载的白屏时间也很长。
如果用异步组件的方式,打包时会把没有用到的异步组件先拆出来,暂时不会打包到主包中,只有等用到该组件的时候才会加载,放到主包里。和路由懒加载差不多,都是用来优化首屏加载速度过慢的问题
默认的打包情况下,在构建整个组件树的过程种,组件和组件是
通过模块化直接导入的(import xx from '../xx.vue'),那么webpack在打包时就会将组件模块打包到一个js文件(如app.js)
如果打包时代码分包(比如定义异步组件,vue3通过defineAsyncComponent(()=>import('../xx.vue')))或者直接通过import()函数导入(它会返回一个promise)都会进行 分包,会被单独打包到一个js文件中
所以,对于一些首页加载时不需要立即使用的组件,可以单独对它们进行拆分,拆分成一些小的代码块chunk.js。
这些chunk.js会在被需要使用时,才会进行加载运行
Teleport
传送组件 类似于react的portal
它和Suspense一样,也是vue的内置组件。 作用:能够将定义的模板挂载到指定的DOM节点,不受父级style,v-show等属性影响,但data,prop数据依旧能够共用。
使用方法:
<Teleport to="body">
<Loading></Loading>
</Teleport>
keep-alive缓存组件
使用场景:不希望组件被重新渲染,或者处于性能考虑,避免多次重复渲染导致性能损耗。 当组件被keep-alive包裹的时候,那么初次进入onMounted-->onActived
退出后:deactived 再次进入后只会触发onActived
include:需要缓存的组件 exclude:不缓存的组件
自定义指令
需要注意的是,必须以vName开头,这样才能使用
vue2.0中还是通过bind insered实现,vue3.0中改成了类似生命周期
局部自定义指令
import {Directive} from 'vue'
<A v-move:aaa.xiaoman="{ background: 'red' }" />
对象形式写法:即类似于生命周期函数
const vMove: Directive = {
created() {
console.log('元素初始化调用')
},
beforeMount() {
console.log('指令绑定到元素后调用,只调用一次')
},
mounted(el: HTMLElement, dir: DirectiveBinding) {
console.log(el)
console.log(dir)
console.log('元素插入父级DOM调用')
//绑定的值在value里
el.style.background = dir.value.background
},
beforeUnmount() {
console.log('元素被移除后调用')
},
updated(){},//自定义传入的值发生改变就触发
unmounted() {
console.log('指令被移除后调用')
}
}
常用的就3个 mounted(){} ,updated(){}只要自定义传入的值发生改变就会触发,还有个卸载时beforeUnmounted
函数形式写法,相对来说更简单
函数形式简写 vMove:Directive=(el,dir)=>{}
案例 封装权限按钮组件是否展示的自定义指令
<template>
<div>
<button v-has-show="'shop:create'">创建</button>
<button v-has-show="'shop:edit'">编辑</button>
<button v-has-show="'shop:delete'">删除</button>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, computed, Directive } from 'vue'
//自定义指令实现不同权限下,按钮的隐藏
localStorage.setItem('userId', 'zds')
/* mock后端发回的权限数据 permission数组 */
const permission = ['zds:shop:edit', 'zds:shop:create', 'zds:shop:delete']
const userId = localStorage.getItem('userId') as string
const vHasShow: Directive = (el: HTMLElement, dir) => {
if (!permission.includes(userId + ':' + dir.value)) {
el.style.display = 'none'
}
}
</script>
<style scoped></style>
自定义hooks
本质上就是一个函数,暴露一个函数给组件进行使用。可以直接导入生命周期,在函数里使用。类似于setup函数
vue2.0中代码复用的方式主要是通过mixins混入,这样可以直接使用混入里面的变量和方法,但这会让代码逻辑看起来很混乱,变量来源不明确。
vue3官方常用的hooks import {useAttrs} from 'vue'。 这样就可以再组件中直接调用let attrs=useAttrs() 就可以获取到该组件拥有的属性值
案例:转化为base64
import { onMounted } from 'vue'
type Options = {
el: string
}
export default function (options: Options): Promise<{ baseURL: string }> {
//return new Promise其实主要是为了让外界可以通过.then传递数据
return new Promise(resolve => {
onMounted(() => {
console.log('自定义hook里的mounted')
let img: HTMLImageElement = document.querySelector(options.el) as HTMLImageElement
//需要等图片加载完毕后再转换
img.onload = () => {
resolve({ baseURL: base64(img) })
}
const base64 = (el: HTMLImageElement) => {
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')
canvas.width = el.width
canvas.height = el.height
//目标元素 开始x轴位置 y轴位置 宽度 高度
ctx?.drawImage(el, 0, 0, canvas.width, canvas.height)
//基于img绘制canvas,然后通过toDataURL返回base64
return canvas.toDataURL('image/png')
}
})
})
}
全局函数和变量
vue2.0中全局变量是直接挂载在vue的原型上。类似vue.prototype.$http=axios
vue3.0中使用app.config.globalProperties代替 然后定义变量和函数
使用 可以在任意位置直接使用$env
注意:这里会报警告,需要声明文件
type Filter = {
format<T>(str: T): string
}
// 声明要扩充@vue/runtime-core包的声明.
// 这里扩充"ComponentCustomProperties"接口, 因为他是vue3中实例的属性的类型.
declare module 'vue' {
export interface ComponentCustomProperties {
$filters: Filter
}
}
在setup js中获取其值
1. import {ref,reactive,getCurrentInstance} from 'vue'
1. const app = getCurrentInstance()
1. console.log(app?.proxy?.$filters.format('js'))
vue自定义插件
插件是自包含的代码,通常向 Vue 添加全局级功能。你如果是一个对象需要有install方法Vue会帮你自动注入到install 方法 你如果是function 就直接当install 方法去使用
有两种形式函数和对象,常用函数,必须实现install方法,install的参数会传递一个app根组件。注意导入的组件需要通过creatVNode转化为虚拟节点,再挂载
//定义插件有两种形式,一种对象,一种函数
/*
对象形式必须调用install 函数,它会回传一个app 即全局的那个app
*/
import type { App, VNode } from 'vue'
import { createVNode, render } from 'vue'
import Loading from './index.vue'
export default {
install(app: App) {
//将loading组件转化为vNode
const Vnode: VNode = createVNode(Loading)
render(Vnode, document.body)
//将vNode暴露出来的属性和方法直接挂载到全局的app里
app.config.globalProperties.$loading = {
show: Vnode.component?.exposed?.show,
hide: Vnode.component?.exposed?.hide
}
}
}
调用
/*
获取当前组件的实例
*/
const instance = getCurrentInstance()
instance?.proxy?.$loading.show()
案例:自定义一个myUse 传入不同插件调用
原理其实很简单,需要传入plugin,且plugin必须要有install方法,然后进行去重,最后调用plugin的install方法即可。
import type { App } from 'vue'
interface Use {
install: (app: App, ...options: any[]) => void
}
//传入的插件必须要有install方法,所以进行泛型约束
import { app } from '../../main'
const installList = new Set()
export function myUse<T extends Use>(plugin: T, ...options: any[]) {
if (installList.has(plugin)) {
console.error('不好意思,已经注册', plugin)
}
plugin.install(app, options)
installList.add(plugin)
}
Vue transition
动画组件 实现过渡动画
语法: 自定义 transition 过度效果,你需要对
transition组件的name属性自定义。并在css中写入对应的样式
<transition name='fade' > </transition>
v-enter-from:定义进入过渡的开始状态。在元素被插入之前生效,在元素被插入之后的下一帧移除。
v-enter-active:定义进入过渡生效时的状态。在整个进入过渡的阶段中应用,在元素被插入之前生效,在过渡/动画完成之后移除。这个类可以被用来定义进入过渡的过程时间,延迟和曲线函数。
v-enter-to:定义进入过渡的结束状态。在元素被插入之后下一帧生效 (与此同时 v-enter-from 被移除),在过渡/动画完成之后移除。
v-leave-from:定义离开过渡的开始状态。在离开过渡被触发时立刻生效,下一帧被移除。
v-leave-active:定义离开过渡生效时的状态。在整个离开过渡的阶段中应用,在离开过渡被触发时立刻生效,在过渡/动画完成之后移除。这个类可以被用来定义离开过渡的过程时间,延迟和曲线函数。
v-leave-to:离开过渡的结束状态。在离开过渡被触发之后下一帧生效 (与此同时 v-leave-from 被移除),在过渡/动画完成之后移除。
transition生命周期有八个
@before-enter="beforeEnter" //对应enter-from
@enter="enter"//对应enter-active
@after-enter="afterEnter"//对应enter-to
@enter-cancelled="enterCancelled"//显示过度打断
@before-leave="beforeLeave"//对应leave-from
@leave="leave"//对应enter-active
@after-leave="afterLeave"//对应leave-to
@leave-cancelled="leaveCancelled"//离开过度打断
当只用 JavaScript 过渡的时候,在 enter 和 leave 钩子中必须使用 done 进行回调
css3完整新特性
插槽选择器。想要修改插槽内容
动态CSS
可以通过v-bind( 变量)添加动态css
<template>
<div>
<button class="div" @click="change">鼎泰css</button>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, computed } from 'vue'
const style = ref<string>('red')
const change = () => {
myStyle.color = 'yellow'
}
//也可以用对象形式,但是需要加""
const myStyle = reactive({
color: 'red'
})
</script>
<style scoped>
.div {
color: v-bind('myStyle.color');
}
</style>
cssmodule
这种写法主要是为了写tsx,类似于react的时候使用
<template>
<div :class="zds.div">
123
<div :class="[zds.div, zds.bodder]">123</div>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, computed } from 'vue'
</script>
<style module="zds">
div {
color: red;
}
bodder {
border: 1px solid red;
}
</style>
scss中使用vue变量
通过:style=--content:${vue中的变量值}注意,必须以-- 开头
<main class="data-content" :style="`--content: ${content}`">
<div class="test">123</div> </main>
在对应的选择器,可以通过 var(--content)来接收使用,但总觉得不如直接使用scss变量好用
<style lang="scss" scoped>
//方式①直接使用var(--content)接收
.test {
color:var(--content);
}
//方式②scss变量
$string: var(--content);
.test {
color: $string;
}
</style>
vue环境变量
作用:让开发者区分不同的运行环境,来实现兼容开发和生产 npm run dev:开发环境 npm run build:生产环境
步骤 ①创建.env.development文件。自定义名称
跨域
跨域是因为浏览器的同源策略引起的,浏览器的同源策略限制了非同源的url向另一个url发起ajax请求,访问其DOM元素等等。
解决跨域的方式:
①:JSON跨域没因为script标签的src不会受到浏览器的同源策略的限制,将src替换成服务器的url同时传去一个callback函数名称,服务器返回对应的函数调用结果
②:cors跨域资源共享 一般是后端设置,Access-Control-Allow-Origin:origin
③poxy 反向代理
server: {
proxy: {
//它会截取/api之前的地址,替换成target
'/api': {
target: 'http://localhost:9999'
}
}
}
vue-router
配置
注意:需要导入RouteRecordRaw这个类型,它必须包含path和component还有name
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'
const routes: Array<RouteRecordRaw> = [{ path: '/', component: () => import('./components/Login.vue') }]
const router = createRouter({
history: createWebHistory(),
routes
})
export default router
任意路径匹配404
解决history路由404问题
//其实就是参数配合正则
{ //.表示任意字符 *重复任意次
path: '/:pathMatch(.*)',
component: () => import('@/views/NotFound/NotFound.vue'),
},
路由跳转
router-link
必须含有name属性,不然会报错,同时to改为对象形式了
编程式路由导航
import { useRouter } from 'vue-router'
const router = useRouter()
router.push({ path: '/home' })
路由传参
①query参数 跳转时,直接携带即可
router.push({path:'/home',query:{}})
②params参数
注意:params参数不能用path了,因为path里携带了变量,需要用name
router.push({ name: 'Home', params: { id: 'zds' } })
Vite配置
应用程序打包
注意:vite有一个很特殊的地方,它项目的入口文件不像webpack一样是个main.js|main.ts,反而是根目录下的一个index.html文件。
这种模式下,Vite 会将整个项目打包为一个或多个静态文件,包括 JavaScript、CSS、图片等资源,并生成一个可以通过静态文件服务器访问的构建输出目录。你可以使用
npm run build命令启动构建过程,然后将生成的文件部署到服务器上。
export default defineConfig({
outDir: 'dist', // 设置输出目录,默认为根目录下的 "dist"文件夹
assetsDir: 'assets', // 设置资源目录,默认为 "assets"
chunkSizeWarningLimit: 2000, // 设置分包大小警告的阈值,默认为 500kb
rollupOptions: {
input: path.resolve(__dirname, 'index.html'),//入口文件
output: {
entryFileNames: 'js/entry-[name]-[hash].js', // 设置入口文件名,可使用占位符
chunkFileNames: 'js/chuck-[name]-[hash].js', // 设置分包文件名,可使用占位符
assetFileNames: 'assets/[name]-[hash].[ext]', // 设置资源文件名,可使用占位符
},
},
})
库模式打包
库模式打包适用于开发和分享可复用的库组件,可以通过 npm 发布并供他人使用。
export default defineConfig({
build: {
lib: {
// Could also be a dictionary or array of multiple entry points
entry: resolve(__dirname, 'lib/main.js'),
name: 'MyLib',
// the proper extensions will be added
fileName: 'my-lib',
},
rollupOptions: {
// 确保外部化处理那些你不想打包进库的依赖
external: ['vue'],
output: {
// 在 UMD 构建模式下为这些外部化的依赖提供一个全局变量
globals: {
vue: 'Vue',
},
},
},
},
})
入口文件将包含可以由你的包的用户导入的导出:
// lib/main.js
import Foo from './Foo.vue'
import Bar from './Bar.vue'
export { Foo, Bar }
推荐在你库的 package.json 中使用如下格式:
{
"name": "my-lib",
"type": "module",
"files": ["dist"],
"main": "./dist/my-lib.umd.cjs",
"module": "./dist/my-lib.js",
"exports": {
".": {
"import": "./dist/my-lib.js",
"require": "./dist/my-lib.umd.cjs"
}
}
}
或者,如果暴露了多个入口起点:
{
"name": "my-lib",
"type": "module",
"files": ["dist"],
"main": "./dist/my-lib.cjs",
"module": "./dist/my-lib.js",
"exports": {
".": {
"import": "./dist/my-lib.js",
"require": "./dist/my-lib.cjs"
},
"./secondary": {
"import": "./dist/secondary.js",
"require": "./dist/secondary.cjs"
}
}
}