vue3-vite-ts-pinia

Vue3 + vite + Ts + pinia + 实战 + 源码 +electron

仓库地址:https://gitee.com/szxio/vue3-vite-ts-pinia

视频地址:小满Vue3(课程导读)_哔哩哔哩_bilibili

课件地址:Vue3_小满zs的博客-CSDN博客

初始化Vue3项目

方式一

npm init vite@latest

image-20230903162122600

生成的目录结构

vite-demo
├── .vscode
│   └── extensions.json
├── public
│   └── vite.svg
├── src
│   ├── assets
│   │   └── vue.svg
│   ├── components
│   │   └── HelloWorld.vue
│   ├── App.vue
│   ├── main.ts
│   ├── style.css
│   └── vite-env.d.ts
├── README.md
├── index.html
├── package.json
├── tsconfig.json
├── tsconfig.node.json
└── vite.config.ts

启动

npm run dev

image-20230903162616723

方式二

npm init vue@latest

image-20230903162819151

生成的目录结构

vue-demo
├── .vscode
│   └── extensions.json
├── public
│   └── favicon.ico
├── src
│   ├── assets
│   │   ├── base.css
│   │   ├── logo.svg
│   │   └── main.css
│   ├── components
│   │   ├── __tests__
│   │   ├── icons
│   │   ├── HelloWorld.vue
│   │   ├── TheWelcome.vue
│   │   └── WelcomeItem.vue
│   ├── router
│   │   └── index.ts
│   ├── stores
│   │   └── counter.ts
│   ├── views
│   │   ├── AboutView.vue
│   │   └── HomeView.vue
│   ├── App.vue
│   └── main.ts
├── .eslintrc.cjs
├── .prettierrc.json
├── README.md
├── env.d.ts
├── index.html
├── package.json
├── tsconfig.app.json
├── tsconfig.json
├── tsconfig.node.json
├── tsconfig.vitest.json
├── vite.config.ts
└── vitest.config.ts

用这种方式生成的项目会全一点

启动

npm run dev

image-20230903163023078

自动生成路由

添加 gen-router.js 文件

var fs = require('fs');
const readline = require('readline');
const os = require('os');const vueDir = './src/views/';fs.readdir(vueDir, function (err, files) {if (err) {console.log(err);return;}let routers = ``;// 对文件进行排序let sortFiles = files.sort((a,b)=>{return a.split("_")[0] - b.split("_")[0]});for (const filename of sortFiles) {if (filename.indexOf('.') < 0) {continue;}var [name, ext] = filename.split('.');if (ext != 'vue') {continue;}let routerName = nullconst contentFull = fs.readFileSync( `${vueDir}${filename}`, 'utf-8' );var match = /\<\!\-\-\s*(.*)\s*\-\-\>/g.exec(contentFull.split(os.EOL)[0]);if (match) {routerName = match[1];}routers += `  {path: '/${name === 'root' ? '' : encodeURIComponent(name)}',name:'${name}', component: ()=> import(/* webpackChunkName: "${name}" */ "@/views/${filename}") ${ routerName ? ',name: "' + routerName + '"' : ''} },\n`;}const result = `
import { createRouter, createWebHistory } from 'vue-router'
import Layout from '@/layout/index.vue'const router = createRouter({history: createWebHistory(import.meta.env.BASE_URL),routes: [{path: '/',name: 'index',component: Layout,redirect: '/index',children:[${routers}]},]
})export default router
`// console.log(result);fs.writeFile('./src/router/index.ts',result, 'utf-8',(err) => {if (err) throw err;});
});

修改 package.json 中的启动命令

"scripts": {"dev": "node gen-router.js &&  vite",
},

这样每次新建完一个文件后需要重启一下服务,然后会自动生成路由文件,配置菜单动态显示即可

Ref全家桶

ref

接受一个内部值并返回一个可变响应式的 ref 对象。ref 对象仅有一个 .value property,指向该内部值。

<template><div>{{ product }}</div><hr><button @click="change">点击</button>
</template><script setup lang="ts">import {ref} from "vue";const product = ref({id:"001",name:"小米手机"
})const change = () => {product.value.name = "华为手机"console.log(product)
}</script>

调试小技巧

我们打印 ref 对象时需要点开两层才能看到信息,如下

image-20230903201400943

可以打开 启用自定义格式化程序

image-20230903201422321

image-20230903201441480

之后打印就会直接展示具体的信息

image-20230903201520363

isRef

判断一个对象是否是响应式对象

import { ref, isRef } from "vue";const product = ref({id: "001",name: "小米手机"
})const change = () => {product.value.name = "华为手机"// isRef判断一个对象是否是响应式对象console.log(isRef(product)) // true
}

shallowRef

创建一个跟踪自身 .value 变化的 ref,但不会使其值也变成响应式的

import { ref, isRef, shallowRef } from "vue";const shaRef = shallowRef({price: 100
})const change = () => {// product.value.name = "华为手机"// isRef判断一个对象是否是响应式对象console.log(isRef(product)) // trueshaRef.value.price = 200console.log(shaRef.value);
}

上面的例子中页面不会发生变化

triggerRef

强制更新页面

import { ref, isRef, shallowRef, triggerRef } from "vue";const product = ref({id: "001",name: "小米手机"
})const shaRef = shallowRef({price: 100
})const change = () => {// product.value.name = "华为手机"// isRef判断一个对象是否是响应式对象console.log(isRef(product)) // trueshaRef.value.price = 200console.log(shaRef.value);triggerRef(shaRef)
}

需要传入一个要更新的对象

customRef

自定义一个ref响应式数据

import { customRef } from "vue";function myRef<T>(value: T) {return customRef((track, trigger) => {return {get() {track()return value},set(newVal) {value = newValtrigger()},}})
}const song1 = myRef("123")const change = () => {song1.value = "456"}

Reactive全家桶

Reactive

用来绑定复杂的数据类型 例如 对象 数组

源码中限定只能传入类型是Object的数据

image-20230903211342898

<template><div>{{ form }}</div><button @click="change">改变</button><hr><ul><li v-for="item in list.value">{{ item }}</li></ul><button @click="getList">获取</button>
</template><script setup lang="ts" name="Reactive">
import { reactive, } from 'vue';let form = reactive({name: "张三",age: 18
})
function change() {form.age++
}let list = reactive({value: ["lisi", "wangwu"]
})
function getList() {setTimeout(() => {let res = ["Anly", "Jack"]// 直接给reactive赋值会破坏原有的响应式list.value = resconsole.log(list);}, 1000);
}
</script>

Readonly

将一个对象设置为只读

import { reactive, readonly } from 'vue';let form = reactive({name: "张三",age: 18
})
let readOnlyForm = readonly(form)
function change() {readOnlyForm.age++
}

image-20230903212107449

shallowReactive

浅层的响应式

import { shallowReactive } from 'vue';let shaReactive = shallowReactive({a: {b: 123}
})
function chageSha() {shaReactive.a.b = 456 // 页面不会发生改变console.log(shaReactive); // 打印的数据发生改变
}

to系列全家桶

toRef

将对象中的某个属性变成响应式的

如果原始数据是非响应式的,则经过 toRef 之后也不会更新视图,但是数据会发生变化

<template><div>{{ student }}</div><div>likeRef:{{ likeRef }}</div><button @click="change">修改</button>
</template>
<script setup lang='ts'>
import { toRef } from "vue"const student = {name: "Jack",age: 18,like: "画画"
}let likeRef = toRef(student, "like")function change() {// 如果源数据是非响应式的,则经过toRef后也不会触发页面更新likeRef.value = "足球"console.log(student);console.log(likeRef);}</script>

如果源数据就是响应式的,则会触发页面更新

<template><div>{{ student }}</div><div>likeRef:{{ likeRef }}</div><button @click="change">修改</button>
</template>
<script setup lang='ts'>
import { toRef, reactive } from "vue"const student = reactive({name: "Jack",age: 18,like: "画画"
})let likeRef = toRef(student, "like")function change() {// 如果源数据是非响应式的,则经过toRef后也不会触发页面更新likeRef.value = "足球"console.log(student);console.log(likeRef);}</script>

toRefs

将对象的所有数据都变成响应式数据

import { toRef, toRefs, toRaw, ref, reactive } from "vue"const student = reactive({name: "Jack",age: 18,like: "画画",code: [1, 2]
})// 自实现toRefs
function myToRefs<T extends Object>(object: T) {let map: any = {}for (const key in object) {map[key] = toRef(object, key)}return map
}
function refs() {console.log(myToRefs(student)); // 打印结果如下图
}// 使用场景:对象解构
let { name, age, code } = toRefs(student)
function fun1() {name.value = "Tim"age.value = 16code.value.push(3)
}

myToRefs 打印结果

image-20230903223545771

toRaw

返回对象的原始信息

function fun2() {console.log(toRaw(student));
}

打印

image-20230903223501841

Vue3响应式源码实现

初始化项目结构

vue-proxy
├── effect.js
├── effect.ts
├── index.html
├── index.js
├── package.json
├── reactive.js
├── reactive.ts
└── webpack.config.js

reactive.ts

import { track, trigger } from "./effect"// 判断是否是对象
const isObject = (target) => target !== null && typeof target === "object"// 泛型约束只能传入Object类型
export const reactive = <T extends object>(target: T) => {return new Proxy(target, {get(target, key, receiver) {console.log(target);console.log(key);console.log(receiver);let res = Reflect.get(target, key, receiver)track(target, key)if (isObject(res)) {return reactive(res)}return res},set(target, key, value, receiver) {let res = Reflect.set(target, key, value, receiver)console.log(target, key, value);trigger(target, key)return res}})}

effect.ts

// 更新视图的方法
let activeEffect;
export const effect = (fn: Function) => {const _effect = function () {activeEffect = _effect;fn()}_effect()
}// 收集依赖
const targetMap = new WeakMap()
export const track = (target, key) => {let depsMap = targetMap.get(target)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(effect => effect())
}

测试

执行 tsc 转成 js 代码,没有 tsc 的全局安装 typescript

npm install typescript -g

新建 index.js,分别引入 effect.jsreactive.js

import { effect } from "./effect.js";
import { reactive } from "./reactive.js";let data = reactive({name: "lisit",age: 18,foor: {bar: "汽车"}
})effect(() => {document.getElementById("app").innerText = `数据绑定:${data.name} -- ${data.age} -- ${data.foor.bar}`
})document.getElementById("btn").addEventListener("click", () => {data.age++
})

新建index.html

<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Document</title>
</head><body><div id="app"></div><button id="btn">按钮</button>
</body>

然后再根目录执行

npm init -y

安装依赖

npm install webpack webpack-cli webpack-dev-server html-webpack-plugin -D

然后新建 webpack.config.js

const path = require("path")
const HtmlWebpakcPlugin = require("html-webpack-plugin")module.exports = {entry: "./index.js",output: {path: path.resolve(__dirname, "dist")},plugins: [new HtmlWebpakcPlugin({template: path.resolve(__dirname, "./index.html")})],mode: "development",// 开发服务器devServer: {host: "localhost", // 启动服务器域名port: "3000", // 启动服务器端口号open: true, // 是否自动打开浏览器},
}

执行命令启动项目

npx webpack serve

image-20230904232457817

image-20230904232522425

computed的简单使用

<template><div><table border width="600" cellspacing="0" cellpadding="0"><thead><th>名称</th><th>价格</th><th>数量</th><th>总价</th><th>操作</th></thead><tbody><tr v-for="(item,index) in choosList" style="text-align: center"><td>{{ item.name }}</td><td>{{ item.price }}</td><td>{{ item.count }}</td><td><el-button type="primary" @click="item.count--">-</el-button>{{ item.price * item.count }}<el-button type="primary" @click="item.count++">+</el-button></td><td><el-button type="danger" @click="remove">删除</el-button></td></tr></tbody><tfoot align="right"><tr><td colspan="5">总价:{{total}}</td></tr></tfoot></table></div>
</template><script setup>
import {reactive,computed} from "vue";let choosList = reactive([{name:"裤子",price:100,count:1,},{name:"衣服",price:200,count:1,},{name:"鞋子",price:300,count:1,},{name:"帽子",price:400,count:1,}
])
let total = computed(()=>{let total = 0;choosList.forEach(item=>{total += item.price * item.count;})return total;
})function remove(index){choosList.splice(index,1);
}
</script><style scoped></style>

image-20230910152223957

computed源码实现

effect.ts

// 更新视图方法
let activeEffect
export const effect = (fn:Function,options) => {console.log("effect触发")const _effect = function () {activeEffect = _effectreturn fn()}_effect.options = options_effect()return _effect
}// 依赖收集
const targetMap = new WeakMap()
export const track = (target, key) => {let depsMap = targetMap.get(key)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(effect => {if (effect.options.scheduler){effect.options.scheduler()}else{effect()}})
}

reactive.ts

import {track, trigger} from "./effect"
// 判断是否是对象类型
const isObject = (target) => typeof target === 'object' && target !== nullexport const reactive = (target) => {return new Proxy(target, {get(target, key, receiver) {console.log("reactive.get-",key)const res = Reflect.get(target, key, receiver)// 收集依赖track(target, key)// 递归return isObject(res) ? reactive(res) : res},set(target, key, value, receiver) {console.log("reactive.set-",key)const res = Reflect.set(target, key, value, receiver)// 触发依赖trigger(target, key)return res}})
}

computed.ts

import {effect} from  "./effect"export const myComputed = (getter:Function)=>{let _value = effect(getter,{scheduler:()=>{_dirty = true}})// 判断是否需要重新计算结果let _dirty = true// 缓存结果let catchValueclass ComputedRefImpl{get value(){if(_dirty){console.log("依赖发生变化时执行")catchValue = _value()_dirty = false}return catchValue}}return new ComputedRefImpl()
}

watch监听器

监听单属性值

let name = ref("李四")watch(name,(newValue,oldValue)=>{console.log(newValue,oldValue)
})

同时监听多个属性

let name = ref("李四")
let age = ref(20)watch([name,age],(newValue,oldValue)=>{console.log(newValue,oldValue)
})

image-20230910154510725

深度监听

let obj = ref({foo:{bar:{name:"张三"}}
})watch(obj,(newValue,oldValue)=>{console.log(obj.value.foo.bar.name)
},{deep:true, //  深度监听immediate:true, // 立即执行
})

监听对象中的某一个属性

let obj = ref({foo:{bar:{name:"张三",age:18}}
})// 监听某个属性是要传入一个函数来返回要监听的属性值
watch(()=>obj.value.foo.bar.age,(newValue,oldValue)=>{console.log(obj.value.foo.bar.age)
},{immediate:true
})

watchEffect

简介

watchEffect不需要传入任何参数,它是一个函数,当依赖变化时,这个函数就会执行,它内部会根据响应式数据的依赖关系,自动执行监听函数

使用

<template><el-input id="msg1" v-model="msg1" placeholder="placeholder"></el-input><el-input v-model="msg2" placeholder="placeholder"></el-input><el-button type="primary" @click="stopWatch">停止监听</el-button>
</template><script setup>
import {ref, watchEffect,nextTick} from 'vue'let msg1 = ref("msg1")
let msg2 = ref("msg2")// watchEffect不需要传入任何参数,它是一个函数,当依赖变化时,这个函数就会执行
// 它内部会根据响应式数据的依赖关系,自动执行监听函数
const stop = watchEffect(()=>{console.log(msg1.value)console.log(msg2.value)
})function stopWatch(){// 停止监听stop()
}</script>

BEM架构和Layout布局

Layout目录结构

layout
├── Content
│   └── index.vue
├── Header
│   └── index.vue
├── Menu
│   └── index.vue
├── css
│   └── bem.scss
└── index.vue

新建 bem.scss

$namespace: "zx" !default;
$block-sel:"-" !default;
$element-sel:"__" !default;
$modifier-sel:"--" !default;@mixin bfc{height:100%;overflow: hidden;
}@mixin b($block){// 拼接的结果为:zx-xxx$B:$namespace + $block-sel + $block;.#{$B}{@content;}
}@mixin e($element){// 拼接的结果为:zx-xxx__xxx$selector:&;@at-root {$E:$selector + $element-sel + $element;#{$E}{@content;}}
}@mixin m($modifier){// 拼接的结果为:zx-xxx--xxx$selector:&;@at-root {$M:$selector + $modifier-sel + $modifier;#{$M}{@content;}}
}

配置全局生效

import { fileURLToPath, URL } from 'node:url'import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueJsx from '@vitejs/plugin-vue-jsx'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'// https://vitejs.dev/config/
export default defineConfig({plugins: [vue(),vueJsx(),AutoImport({resolvers: [ElementPlusResolver()],}),Components({resolvers: [ElementPlusResolver()],}),],resolve: {alias: {'@': fileURLToPath(new URL('./src', import.meta.url))}},css: {preprocessorOptions: {// 配置全局CSSscss: {additionalData: "@import './src/layout_v2/css/bem.scss';"}}}
})

index.vue

<template><div class="zx-box"><div class="zx-box__menu"><Menu/></div><div class="zx-box__main"><Header/><Content/></div></div>
</template><script setup>import Menu from "@/layout_v2/Menu/index.vue";
import Header from "@/layout_v2/Header/index.vue";
import Content from "@/layout_v2/Content/index.vue";
</script><style scoped lang="scss">
@include b('box'){height: 100%;display: flex;@include e("menu"){width: 250px;height: 100%;border-right: 1px solid #ebebeb;}@include e("main"){flex: 1;display: flex;flex-direction: column;}
}</style>

Menu/index

<template><div>Menu</div>
</template><script setup></script>

Header/index.vue

<template>
<div class="zx-header">Header
</div>
</template><script setup>
</script><style scoped lang="scss">
@include b('header'){width: 100%;height: 60px;line-height: 60px;border-bottom: 1px solid #ccc;
}
</style>

Content/index.vue

<template><div class="zx-content"><div v-for="item in 50" class="zx-content__item">{{item}}</div></div>
</template><script setup></script><style scoped lang="scss">
@include b(content){height: 100%;overflow: auto;@include e(item){height: 60px;line-height: 60px;text-align: center;border-radius: 5px;border: 1px solid pink;margin: 10px;}
}
</style>

布局效果

image-20230913205111297

父子组件传值

简单使用

定义父组件

<template><div class="parent-box">父组件<div>子组件传过来的值:{{count}}</div><button @click="getSubInfo">获取子组件的所有属性和方法</button><SubComponent ref="subCom" :value="title" @changeCount="changeCount"/></div>
</template><script setup lang="ts">
import SubComponent from "@/components/SubComponent.vue";
import {ref} from "vue";let title = "给儿子传值";
let count = ref<number>()// 定义组件类型
let subCom = ref<InstanceType<typeof SubComponent>>()// 子组件触发的父组件方法
const changeCount = (newVal) => {count.value = newVal;
}const getSubInfo = () => {// 调用子组件的实例方法subCom.value.open()// 获取子组件的属性console.log(subCom.value.order)
}
</script><style scoped>
.parent-box{width: 300px;height: 300px;border: 1px solid #ccc;padding: 30px;
}
</style>

子组件

<template><div class="children-box">父组件传递的值: {{ value }}<button @click="changeParentCount">改变父组件的值</button></div>
</template><script setup lang="ts">
import {defineProps, defineEmits, defineExpose, ref} from 'vue';// 父组件传过来的值,带个问号表示可选
const props = defineProps<{value: string
}>()// JS中获取父组件传过来的值
console.log(props.value)// 点击按钮,触发父组件的自定义事件
const changeParentCount = () => {emit("changeCount", 10)
}// 触发父组件的自定义事件
const emit = defineEmits(["changeCount"])// 定义对外暴露的属性
let order = ref(10)
const open = () => {console.log("open")
}
// 使用defineExpose暴露出去
defineExpose({order,open
})
</script><style scoped>
.children-box{border: 1px solid #ccc;padding: 30px;
}
</style>

image-20230913222206507

实现瀑布流布局

父组件

<template><WaterfallFlow :list="list"/>
</template><script setup lang="ts">import WaterfallFlow from "@/components/WaterfallFlow.vue";
import {reactive} from "vue";
type listType = {height:number,color:string
}
// 随机生成100个高度和颜色的对象
let list = reactive<listType[]>([...Array.from({length:100},()=>({height:Math.floor(Math.random()*250)+50,color:`rgb(${Math.floor(Math.random()*255)},${Math.floor(Math.random()*255)},${Math.floor(Math.random()*255)})`}))])
</script>

子组件

<template><div class="wraps"><div v-for="item in list" class="item" :style="{left: item.left + 'px',top: item.top + 'px',height: item.height + 'px',backgroundColor: item.color,}"></div></div>
</template><script setup lang="ts">
import {defineProps, onMounted} from "vue"const props = defineProps<{list: any[]
}>()const initLayout = () => {// 上下左右间隙距离let margin = 10// 每个元素的宽度let elWidth = 120 + margin// 每行展示的列数let colNumber = Math.floor(document.querySelector(".app-content").clientWidth / elWidth)// 存放元素高度的listlet heightList = []// 遍历所有元素for (let i = 0; i < props.list.length; i++) {let el = props.list[i]// i小于colNumber表示第一行元素if(i < colNumber){el.top = 0el.left = elWidth * iheightList.push(el.height)}else{// 找出最小的高度let minHeight = Math.min(...heightList)// 找出最小高度的索引let minHeightIndex = heightList.indexOf(minHeight)// 设置元素的位置el.left = elWidth * minHeightIndexel.top = minHeight + margin// 更新高度集合heightList[minHeightIndex] = minHeight + el.height + margin}}
}// 监听app-content元素的宽度变化
window.onresize = () => {initLayout()
}onMounted(() => {initLayout()
})
</script><style scoped lang="scss">
.wraps{height: 100%;position: relative;.item{position: absolute;width: 120px;}
}
</style>

效果展示

image-20230913224830334

组件递归

实现一个如下的东西

image-20230915214800791

父组件

<template><TreeVue :treeData='treeData'/>
</template><script setup>import {reactive} from  "vue"
import TreeVue from "@/components/TreeVue.vue";let treeData = reactive([{label:"1",checked:false,children:[{label:"1-1",checked:false,},{label: "1-2",checked:true,}]},{label:"2",checked:false,children: [{label: "2-1",checked:false,children:[{label: "2-1-1",checked:false,children:[{label: "2-1-1-1",checked:false,}]}]}]},{label:"3",checked:false,}
])</script>

TreeVue.vue

<template><div v-for="item in treeData" style="margin-left: 15px" @click.stop="getCurrNode(item,$event)"><input type="checkbox" v-model="item.checked"/><span>{{item.label}}</span><TreeVue v-if="item.children" :tree-data="item.children"/></div>
</template><script setup>
import {defineProps} from "vue"
defineProps(["treeData"])const getCurrNode = (currNode,event) => {console.log(currNode)console.log(event)
}
</script>

控制台打印的东西

image-20230915214931139

动态组件

image-20230915222313132

<template><div style="display: flex;gap: 15px"><div v-for="(item,index) in tabData" :key="index"class="tab-item":class="{active:active === index}"@click="switchCom(item,index)"><div>{{item.tab}}</div></div></div><component :is="currCom"></component>
</template><script setup>
import {reactive, ref, shallowRef,markRaw} from "vue"
import ComA from "@/components/13/ComA.vue"
import ComB from "@/components/13/ComB.vue"
import ComC from "@/components/13/ComC.vue"// 使用shallowRef避免深层相应
let currCom = shallowRef(ComA)
let active = ref(0)let tabData = reactive([{tab:"组件A",// 使用markRaw使组件不会被vue进行响应式处理,提高性能com:markRaw(ComA)},{tab:"组件B",com:markRaw(ComB)},{tab:"组件C",com:markRaw(ComC)}
])const switchCom = (item,index) => {currCom.value = item.comactive.value = index
}
</script><style scoped>
.tab-item{padding: 5px 15px;border: 1px solid black;
}
.active{background-color: deepskyblue;
}
</style>

插槽

定义子组件

<template><div class="box"><div class="header"><slot name="header"></slot></div><div class="main"><!--默认插槽--><slot :link="link" :age="age"></slot></div><div class="footer"><slot name="footer"></slot></div></div>
</template>
<script setup lang='ts'>
import {ref} from "vue";const link = ref("Tome")
const age = ref(18)
</script><style scoped lang="scss">
// 父元素高度100%
.box{height: 100%;display: flex;flex-direction: column;
}
.header {height: 100px;background: pink;width: 100%;
}
.main{flex: 1;background-color: #c6e2ff;
}
.footer{height: 100px;background: blueviolet;width: 100%;
}
</style>

定义父组件

<template><Dialog><template #header>具名插槽-header</template><template #default="{link,age}">这是默认插槽{{link}} -- {{age}}</template><template #footer>具名插槽-footer</template></Dialog>
</template><script setup lang='ts'>
import Dialog from "@/components/14/Dialog.vue"
</script>

效果

image-20230917182606728

异步组件

添加骨架屏组件

Skeleton.vue

<template><el-skeleton style="--el-skeleton-circle-size: 100px"><template #template><el-skeleton-item variant="circle" /></template></el-skeleton><br /><el-skeleton />
</template>

效果是这个样子

image-20230917214033824

添加新闻组件

添加新闻数据,在 public 文件夹中添加 newinfo.json

[{"title": "秋粮陆续成熟 多措并举保粮食丰收","description": "眼下,从南到北,各地秋粮陆续成熟。人们全力以赴抓好秋粮生产,多措并举保粮食丰收。\n\n金秋时节,安徽水稻主产区无为市85万亩水稻进入收割期,当地组织机械作业服务队,帮助农民机耕机收,颗粒归仓。今年,安徽计划投入各类农机具240万台套,力争玉米、大豆、中晚稻机收水平达八成以上。","url": "https://baijiahao.baidu.com/s?id=1777244368223895628","image": "https://szx-bucket1.oss-cn-hangzhou.aliyuncs.com/picgo/img.png"}
]

引入 axios,请求这个文件

src/api/index.js

import axios from 'axios'export function getNewDataFun(){return axios("../public/newinfo.json")
}

编写组件 NewCar.vue

<template><div v-for="item in newData" class="new-box"><div class="image"><img :src="item.image" alt=""/></div><div class="content"><div class="title">{{item.title}}</div><div class="desc">{{item.description}}</div></div></div>
</template><script setup lang="ts">
import {onMounted, reactive, ref} from "vue";
import {getNewDataFun} from "@/api/index";type dataType = {title:string,description:string,url:string,image:string,
}const newData = ref<dataType[]>([])await getNewDataFun().then(res=>{setTimeout(()=>{newData.value = res.data},2000)
})
</script><style scoped lang="scss">
.new-box{display: flex;gap: 15px;.image{width: 200px;border-radius: 5px;overflow: hidden;img{width: 100%;}}.content{width: 80%;display: flex;flex-direction: column;gap: 10px;.title{font-weight: 700;font-size: 16px;}}
}
</style>

效果展示

image-20230917214242665

使用异步组件

Suspense 是vue内置的一个组件,有两个插槽

  • default:默认插槽,展示等待结果返回后的组件
  • fallback:等待过程中展示的组件
<template><div style="padding: 20px"><Suspense><template #default><NewCar/></template><template #fallback><Skeleton/></template></Suspense></div>
</template><script setup lang="ts">
import {defineAsyncComponent} from "vue"import Skeleton from "@/components/15/Skeleton.vue"const NewCar = defineAsyncComponent(()=>import("@/components/15/NewCar.vue"))
</script>

异步组件必须使用 defineAsyncComponent 函数来导入,接收一个回调函数

TelePore传送组件

自定义一个弹框组件

<template><div class="zx-dialog"><slot></slot><div class="zx-dialog__footer"><slot name="footer"></slot></div></div>
</template><style scoped lang="scss">
@include b("dialog"){width: 200px;height: 200px;position: absolute;left: 50%;top: 50%;margin-left: -100px;margin-top: -100px;border: 1px solid #ccc;background-color: #c6e2ff;@include e("footer"){position: absolute;bottom: 0;left: 0;right: 0;height: 50px;line-height: 50px;text-align: right;}
}
</style>

使用TelePore

父组件使用这个组件

<template><div class="box"><el-button type="primary" @click="switchDialog = !switchDialog">打开Dialog</el-button><!--使用teleport将组件渲染到body标签下面,避免受到父组件的position: absolute;定位影响--><teleport to="body" v-if="switchDialog"><MyDialog><template #default>弹框内容</template><template #footer><el-button @click="switchDialog = !switchDialog">关闭</el-button></template></MyDialog></teleport></div>
</template><script setup>
import {ref} from "vue"
import MyDialog from "@/components/MyDialog.vue";let switchDialog = ref(false);</script><style scoped>
.box{width: 100%;height: 50%;background-color: gold;position: absolute;}
</style>

效果

image-20230918075355087

KeepAlive

可以缓存组件内容

默认使用

切换组件显示后,组件内容不会丢失

<template><div><div><el-button type="primary" @click="switchFlag">切换组件</el-button></div><keep-alive><AliveA v-if="flag"/><AliveB v-else/></keep-alive></div>
</template><script setup>
import {ref} from "vue";
import AliveA from "@/components/AliveA.vue";
import AliveB from "@/components/AliveB.vue";let flag = ref(true)const switchFlag = () => {flag.value = !flag.value
}
</script>

includes

只缓存AliveA组件

<keep-alive :include="['AliveA']"><AliveA v-if="flag"/><AliveB v-else/>
</keep-alive>

exclude

不缓存AliveA组件

<keep-alive :exclude="['AliveA']"><AliveA v-if="flag"/><AliveB v-else/>
</keep-alive>

max

最多缓存的组件个数

<keep-alive :max="10"><AliveA v-if="flag"/><AliveB v-else/>
</keep-alive>

keep-alive的钩子函数

<script lang="ts" setup>
import { ref,onMounted,onActivated,onDeactivated,onUnmounted, } from 'vue'// 组件显示时只会触发一次
onMounted(()=>{console.log('mounted')
})// 组件显示时触发
onActivated(()=>{console.log('activated')
})
// 组件隐藏时触发
onDeactivated(()=>{console.log('deactivated')
})
// 被keepalive包裹时,组件销毁不会触发unmounted
onUnmounted(()=>{console.log('unmounted')
})

transition

基本用法

在进入/离开的过渡中,会有 6 个 class 切换。

v-enter-from:定义进入过渡的开始状态。在元素被插入之前生效,在元素被插入之后的下一帧移除。

v-enter-active:定义进入过渡生效时的状态。在整个进入过渡的阶段中应用,在元素被插入之前生效,在过渡/动画完成之后移除。这个类可以被用来定义进入过渡的过程时间,延迟和曲线函数。

v-enter-to:定义进入过渡的结束状态。在元素被插入之后下一帧生效 (与此同时 v-enter-from 被移除),在过渡/动画完成之后移除。

v-leave-from:定义离开过渡的开始状态。在离开过渡被触发时立刻生效,下一帧被移除。

v-leave-active:定义离开过渡生效时的状态。在整个离开过渡的阶段中应用,在离开过渡被触发时立刻生效,在过渡/动画完成之后移除。这个类可以被用来定义离开过渡的过程时间,延迟和曲线函数。

v-leave-to:离开过渡的结束状态。在离开过渡被触发之后下一帧生效 (与此同时 v-leave-from 被移除),在过渡/动画完成之后移除。

<template>
<div><el-button type="primary" @click="flag = !flag">切换</el-button><transition name="fade"><div class="box" v-if="flag"></div></transition>
</div>
</template><script setup>
import {ref} from "vue";let flag = ref(true)
</script><style scoped lang="scss">
.box{width: 200px;height: 200px;background-color: red;
}//开始过度.fade-enter-from{background:red;width:0px;height:0px;transform:rotate(360deg)}
//开始过度了.fade-enter-active{transition: all 1s ease;}
//过度完成.fade-enter-to{background:yellow;width:200px;height:200px;}
//离开的过度.fade-leave-from{width:200px;height:200px;transform:rotate(360deg)}
//离开中过度.fade-leave-active{transition: all 1s linear;}
//离开完成.fade-leave-to{width:0px;height:0px;}
</style>

结合animate

安装

npm install animate.css -D

官网中有很多动画示例 Animate.css | A cross-browser library of CSS animations.

<template><div class="root-box"><div class="app-menu"><Menu /></div><div class="app-content"><!-- 路由出口 --><!-- 路由匹配到的组件将渲染在这里 --><router-view v-slot="{ Component,route  }"><transition enter-active-class="animate__animated animate__fadeInUp"><!-- 这里加一个div是防止页面没有根组件时,动画失效 --><div :key="route.name" style="height: 100%"><component :is="Component" /></div></transition></router-view></div></div>
</template>
<script setup>
import Menu from './Menu.vue'
import 'animate.css';
// 设置所有动画的时间在0.3秒内完成
document.documentElement.style.setProperty('--animate-duration', '0.3s');
</script><style scoped lang="less">
.root-box {display: flex;width: 100%;height: 100vh;.app-menu {width: 200px;height: 100vh;background-color: #a18cd1;text-overflow: ellipsis;white-space: nowrap;overflow: auto;}.app-content {flex: 1;height: 100vh;background-color: white;overflow: auto;}
}
</style>

transtion生命周期

<transition @before-enter="beforeEnter" @enter="enter" @leave="leave"><div v-if="gsapFlag" class="gsap-box"></div>
</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"//离开过度打断

结合gsap

安装,官网:https://greensock.com/

npm install gsap

使用

html

<el-button type="primary" @click="gsapFlag = !gsapFlag">切换</el-button>
<transition @before-enter="beforeEnter" @enter="enter" @leave="leave"><div v-if="gsapFlag" class="gsap-box"></div>
</transition>

js

<script setup>
import gsap from "gsap";
import {ref} from "vue";let gsapFlag = ref(true)const beforeEnter = (el) => {console.log("显示之前")gsap.set(el,{width:0,height:0,background:"green"})
}
const enter = (el,done) => {gsap.to(el,{width:"200px",height:"200px",background:"red",rotate:"360dge",duration:1, // 动画时长,单位是秒onComplete:done, // 动画完成后的回调函数})
}
const leave = (el,done) => {gsap.to(el,{width:0,height:0,background:"green",rotate:"-360dge",duration:1, // 动画时长,单位是秒onComplete:done})
}

效果

appear属性

在 transtion 组件中添加 appear 可以在进入页面时就触发对应的样式代码

  • appear-class:初始样式
  • appear-to-class:结束样式
  • appear-active-class:动画曲线
<transition appear appear-class="" appear-to-class="" appear-active-class="animate__animated animate__rubberBand" name="fade"><div class="box" v-if="flag"></div>
</transition>

结合animate__animated实现一个进入页面就执行的一个动画效果

transition-group

在遍历数组的时候可以给每一个元素添加过度动画,生命周期和transition一致,我们结合animate来实现一个列表的动画效果

  <div><el-button type="primary" @click="add">add</el-button><el-button type="danger" @click="pop">pop</el-button></div><div class="warp"><transition-groupenter-active-class="animate__animated animate__bounceInLeft"leave-active-class="animate__animated animate__fadeOutRight"><div v-for="item in groupList" :key="item" class="item">{{item}}</div></transition-group></div>
import {ref,reactive} from "vue";
import "animate.css"const groupList = reactive([1,2,3,4,5])const add = () => {groupList.push(groupList.length + 1)
}
const pop = () => {groupList.pop()
}

动画效果

实现一个炫酷的动画效果

安装lodash库 Lodash 简介 | Lodash中文文档 | Lodash中文网 (lodashjs.com)

npm i --save lodash

实现代码

<div style="margin-top: 20px">平面动画过度效果</div>
<el-button type="primary" @click="shuffle">动画</el-button>
<div class="num-wrap"><transition-group move-class="move-class"><div v-for="item in numList" :key="item.id" class="num-item">{{item.value}}</div></transition-group>
</div>
import {ref,reactive} from "vue";
import _ from "lodash"let numList = ref(Array.apply(null, {length: 81}).map((_,index)=>{return {id:index,value:(index % 9) + 1}
}))const shuffle = () => {// shuffle 用来创建一个被打乱值的集合numList.value = _.shuffle(numList.value)
}
$numWidth:60px;.move-class{transition: all 1s ease;
}
.num-wrap{display: flex;flex-wrap: wrap;width: calc(#{$numWidth} * 9 + 5px * 8);gap: 5px;.num-item{width: $numWidth;height: $numWidth;line-height: $numWidth;text-align: center;border: 1px solid #ccc;}
}

实现效果

使用gsap实现数字滚动

<div style="margin-top: 20px;height: 2px">使用gsap实现数字滚动</div>
<el-input v-model="rolling.num" placeholder="placeholder" style="width: 200px"></el-input>
<h1>{{rolling.numRul.toFixed(0)}}
</h1>
import gsap from "gsap";
import {ref,reactive,watch} from "vue";let rolling = reactive({num:10,numRul:10
})
watch(()=>rolling.num,(newVal)=>{gsap.to(rolling,{numRul:newVal,duration:1,})
})

依赖注入provide和inject

爷爷组件

<template><h1>爷爷组件</h1><el-button type="primary" @click="setColor('red')">红色</el-button><el-button type="primary" @click="setColor('blue')">蓝色</el-button><el-button type="primary" @click="setColor('pink')">粉色</el-button><div class="box"></div><hr><ProvideA/><hr><ProvideB/></template><script setup lang="ts">
import {provide, inject, ref} from "vue"
import ProvideA from "@/components/ProvideA.vue";
import ProvideB from "@/components/ProvideB.vue";let color = ref("red")provide("color",color)const setColor = (c) => {color.value = c
}</script><style scoped>
.box{width: 100px;height: 100px;background-color: v-bind(color);
}
</style>

ProvideA

<template><div><h1>爸爸组件</h1><div class="box"></div></div>
</template><script setup lang="ts">
import {inject} from "vue"
import type {Ref} from "vue"
let color:Ref<string> = inject("color")
</script><style scoped>
.box{width: 100px;height: 100px;background-color: v-bind(color);
}
</style>

ProvideB

<template><div><h1>孙子组件</h1><div class="box"></div><button @click="setColor">设置粉色</button></div>
</template><script setup lang="ts">
import {inject} from "vue"
import type {Ref} from "vue"
let color:Ref<string> = inject("color")const setColor = () => {color.value = "pink"
}
</script><style scoped>
.box{width: 100px;height: 100px;background-color: v-bind(color);
}
</style>

实现效果

兄弟传参

Mitt

安装

npm install mitt
局部使用

添加一个JS文件导出

utils/mitt.js

import mitt from "mitt"
export default mitt()

使用,分别定义 A B两个组件

BusA

<template><div class="box">我是A组件<el-button type="primary" @click="changeFlag">改变</el-button></div>
</template><script setup>
import mitt from "../utils/mitt"
import {ref} from "vue";let flag = ref(false)
const changeFlag = () => {flag.value = !flag.valuemitt.emit("changeFlag",flag.value)
}</script>

BusB

<template><div class="box">我是B组件{{flag}}</div>
</template><script setup>
import mitt from "../utils/mitt";
import {ref,onBeforeUnmount} from "vue";
let flag = ref(false)mitt.on('changeFlag', data=>{flag.value = data
})onBeforeUnmount(()=>{mitt.off("changeFlag")
})
</script>

在父组件引入

<template><BusA/><BusB/>
</template><script setup>
import BusA from "@/components/BusA.vue";
import BusB from "@/components/BusB.vue";</script>

效果

全局使用

main文件添加

import './assets/main.css'
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import ElementPlus from 'element-plus'
import App from './App.vue'
import router from './router'
import 'element-plus/dist/index.css'
import zhCn from 'element-plus/dist/locale/zh-cn.min.js'
import 'dayjs/locale/zh-cn'+ import mitt from "mitt"
+ const Mitt = mitt()const app = createApp(App)
app.use(createPinia())
app.use(router)
app.use(ElementPlus, { locale: zhCn })
app.mount('#app')+ declare module 'vue'{
+   export interface ComponentCustomProperties {
+       $Bus: typeof Mitt
+   }
+ }
+ app.config.globalProperties.$bus = Mitt

文件内部通过从 vue 中导出 getCurrentInstance 方法获取当前实例获取定义的全局变量使用

BusA

<template><div class="box">我是A组件<el-button type="primary" @click="changeFlag">改变</el-button></div>
</template><script setup>
import {ref,getCurrentInstance} from "vue";let flag = ref(false)
let instance = getCurrentInstance()const changeFlag = () => {flag.value = !flag.valueinstance?.proxy?.$bus.emit("changeFlag",flag.value)
}
</script>

BusB

<template><div class="box">我是B组件{{flag}}</div>
</template><script setup>
import {ref, onBeforeUnmount, getCurrentInstance} from "vue";
let flag = ref(false)
let instance = getCurrentInstance()instance?.proxy?.$bus.on('changeFlag', data=>{flag.value = data
})onBeforeUnmount(()=>{instance?.proxy?.$bus.off("changeFlag")
})
</script>

手写Bus

class MyBus{constructor() {this.list = {}}emit(event, ...args){let funs = this.list[event]funs.forEach((fun) =>{fun.apply(this,args)})}on(event, callback){let funs = this.list[event]if(funs){funs.push(callback)}else{funs = [callback]}this.list[event] = funs}off(event){delete this.list[event]}
}
export default new MyBus()

jsx插件

安装

npm in stall @vitejs/plugin-vue-jsx -D

在 vite.config.js 中使用

import { fileURLToPath, URL } from 'node:url'import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueJsx from '@vitejs/plugin-vue-jsx'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'// https://vitejs.dev/config/
export default defineConfig({module:"es2022",plugins: [vue(),vueJsx(),AutoImport({resolvers: [ElementPlusResolver()],}),Components({resolvers: [ElementPlusResolver()],}),],resolve: {alias: {'@': fileURLToPath(new URL('./src', import.meta.url))}},css: {preprocessorOptions: {scss: {additionalData: "@import './src/layout_v2/css/bem.scss';"}}}
})

新建 JsxCom.tsx

import {defineComponent, reactive, ref} from "vue"
import {ElButton} from "element-plus"interface propType  {msg?:string
}export default defineComponent({props:{msg:String,},emits:[],setup(prop:propType,{emit,attrs,slots,expose}){let flag = ref(false)const chagneFlag = () => {flag.value = true}let list = reactive([1,2,3,4,5])return ()=> <>{/*遍历循环*/}{list.map(item => <h1>{item}</h1>)}<hr/>{/*按钮事件,使用οnclick={()=>chagneFlag()}*/}<ElButton type="primary" οnclick={()=>chagneFlag()}>改变这个值</ElButton>{flag.value && <h1>改变后的值</h1>}<hr/><div>父组件传递的值:{prop.msg}</div></>},
})

在vue中可以把这个当成普通的组件使用

<template><JsxCom msg="Hello Jsx"/>
</template>
<script setup>
import JsxCom from '../components/JsxCom'
</script>

页面效果

image-20230924150109742

自动引入插件

安装

npm istall unplugin-auto-import/vite

配置

import AutoImport from 'unplugin-auto-import/vite'export default defineConfig({module:"es2022",plugins: [vue(),vueJsx(),AutoImport({resolvers: [ElementPlusResolver()],imports: ['vue', 'vue-router'], // 自动引入vue,和vue-router相关dts: 'src/auto-imports.d.ts' // 自动生成的依赖文件}),Components({resolvers: [ElementPlusResolver()],}),],})

保存后查看 src/auto-imports.d.ts 内容

image-20230924150915291

里面自动的帮我们了引入

然后再组件中不需要手动的导入 vue,就可以使用vue中的各种声明

<template><el-button type="primary" @click="flag = !flag">buttonCont</el-button><div>{{flag}}</div>
</template>
<script setup>
let flag = ref(false)
</script>

v-model在组件中的使用

基本使用

vue3中在组件上绑定v-model时,默认的prop变成了modelValue

子组件 Vmodel

<template><div><el-input v-model="input" placeholder="placeholder" @input="changeValue" style="width: 200px"></el-input><el-button>关闭</el-button></div>
</template><script setup lang="ts">
import {defineProps,defineEmits} from "vue"const props = defineProps<{modelValue:string,
}>()
// 更新model绑定的值固定写法: update:modelValue
const emit = defineEmits(['update:modelValue'])let input = ref("")onMounted(()=>{input.value = props.modelValue
})const changeValue = (e) => {// 修改父组件的值emit('update:modelValue',e)
}</script>

父组件

<template>父组件的值:{{value}}<div class="box"><Vmodel v-model="value" /></div>
</template><script setup lang="ts">
import Vmodel from "@/components/Vmodel.vue";let value = ref("你好")
</script><style scoped>
.box{border: 2px solid black;padding: 30px;
}
</style>

绑定多个v-model

父组件

<template>父组件的值:{{value}}<el-button @click="isShow = !isShow">切换显示</el-button><div class="box"><Vmodel v-model="value" v-model:isShow="isShow"/></div>
</template><script setup lang="ts">
import Vmodel from "@/components/Vmodel.vue";let value = ref("你好")
let isShow = ref(true)
</script><style scoped>
.box{border: 2px solid black;padding: 30px;
}
</style>

子组件

<template><div v-if="isShow"><el-input v-model="input" placeholder="placeholder" @input="changeValue" style="width: 200px"></el-input><el-button @click="close">关闭</el-button></div>
</template><script setup lang="ts">
import {defineProps,defineEmits} from "vue"const props = defineProps<{modelValue:string,isShow:boolean
}>()
// 更新model绑定的值固定写法: update:modelValue
const emit = defineEmits(['update:modelValue','update:isShow'])let input = ref("")onMounted(()=>{input.value = props.modelValue
})const changeValue = (e) => {// 修改父组件的值emit('update:modelValue',e)
}const close = () => {emit("update:isShow",false)
}
</script>

自定义指令

自定义指令的声明周期

<template><div class="box" v-resize="onResize"></div>
</template><script setup lang="ts">
import { Directive } from 'vue'const onResize = () => {console.log("宽高变化")
}// 声明一个局部自定义指令,必须以v开头
const vResize:Directive = {created(){console.log("created")},beforeMount(){console.log("beforeMount")},mounted(...arg){console.log("mounted")console.log(arg)},beforeUpdate(){console.log("beforeUpdate")},updated(){console.log("updated")},beforeUnmount(){console.log("beforeUnmount")},unmounted(){console.log("unmounted")}
}
</script><style scoped>
.box{height: 100%;background-color: #f5f5f5;
}
</style>

在任意一个钩子函数头能拿到自定义指令绑定的参数,我们通过打印 arg 看看参数有什么

image-20230924171641529

我们利用这两个参数实现监听元素宽高变化的指令,当元素宽高发生变化时调用绑定的函数

mounted(el,bindings){console.log("mounted")// 监听元素宽高变化const resizeObserver = new ResizeObserver(entries => {let width = entries[0].contentRect.width;let height = entries[0].contentRect.height;console.log(`元素宽度:${width},元素高度:${height}`)bindings.value()});resizeObserver.observe(el);
},

修改 mounted 钩子的内容,通过observe 观察 el,然后调用 bindings.value

自定义指令的简写方式

我们也可以通过函数的方式来自定义指令

<template><el-button v-has-show="'order:add'" type="primary">新增</el-button><el-button v-has-show="'order:update'" type="warning">修改</el-button><el-button v-has-show="'order:delete'" type="danger">删除</el-button>
</template><script setup lang="ts">
import { Directive } from 'vue'
//permission
localStorage.setItem('userId','songzx')//mock后台返回的数据
const permission = [// 'songzx:order:add','songzx:order:update','songzx:order:delete'
]const userId = localStorage.getItem('userId') as stringconst vHasShow:Directive<HTMLElement,string> = (el,binding)=>{if(!permission.includes(`${userId}:${binding.value}`)){// 直接移除这个元素,比使用 el.style.display = 'none' 更安全el.remove()}
}</script>

上面的例子是一个按钮级别权限的demo

鼠标拖动元素案例

<template><div class="root"><div class="box" v-move><div class="header"></div></div></div></template><script setup lang="ts">
import { Directive } from 'vue'const vMove:Directive<HTMLElement> = (el)=>{let moveEl:HTMLElement = el.querySelector(".header")const mousedown = (e:MouseEvent)=> {// 鼠标按下时获取当前鼠标的位置和移动物体相对于浏览器的位置let X = e.x - el.offsetLeftlet Y = e.y - el.offsetTop// 移动const move = (e:MouseEvent)=>{// 在移动物体时,需要减去偏移量el.style.left = e.clientX - X + "px"el.style.top = e.clientY - Y + "px"}document.addEventListener("mousemove", move)document.addEventListener("mouseup", ()=>{document.removeEventListener("mousemove", move)})}// 鼠标按下头部时触发moveEl.addEventListener("mousedown",mousedown)
}</script><style scoped lang="scss">
.root{position: relative;
}
.box{position: absolute;width: 200px;height: 200px;left: 100px;top: 100px;border: 2px solid black;.header{width: 100%;height: 20px;background-color: black;}
}
</style>

图片懒加载案例

<template><div class="root"><img v-for="item in arr" v-lazy="item" style="width: 100%;"/></div></template><script setup lang="ts">
import { Directive,DirectiveBinding } from 'vue'// import.meta.glob 引入目标路径中的所有文件,返回一个对象,默认使用module引入
// 添加了eager: true,则变成同步引入
let imgList = import.meta.glob("../assets/images/*.*",{eager: true})
// 得到所有图片地址
let arr = Object.values(imgList).map(item=>item.default)// 自定义懒加载指令
const vLazy:Directive = async (el:HTMLImageElement,binding:DirectiveBinding)=>{// 先给一个默认值const def = await import("../assets/logo.svg")el.src = def.default// 判断元素是否在可视范围内const  intersection = new IntersectionObserver((e)=>{// 判断是否在可视范围内if(e[0].intersectionRatio > 0){setTimeout(()=>{el.src = binding.value},500)intersection.unobserve(el)}})intersection.observe(el)
}</script><style scoped lang="scss">
.root{width: 361px;height: 800px;overflow: auto;
}
</style>

自定义Hook

好用的第三方库

vueuse

npm i @vueuse/core

网址:Get Started | VueUse — 开始使用 |Vueuse

图片转base64

新建 useImgToBase64.ts

import {onMounted} from 'vue'type optionsType = {el:String
}export default function (options:optionsType):Promise<string>{return new Promise((resolve, reject) =>{onMounted(()=>{let img:HTMLImageElement = document.querySelector(options.el)img.onload = ()=>{resolve(toBase64(img))}const toBase64 = (img:HTMLImageElement) => {let canvas:HTMLCanvasElement = document.createElement('canvas')let ctx:CanvasRenderingContext2D = canvas.getContext('2d')canvas.width = img.widthcanvas.height = img.heightctx.drawImage(img, 0, 0, canvas.width, canvas.height)return canvas.toDataURL("image/jpeg")}})})
}

使用

<template><img src="../assets/images/1.jpeg"/>
</template><script setup lang="ts">
import useImgToBase64 from "@/utils/useImgToBase64";useImgToBase64({el:"img"}).then(res=>{console.log(res)
})
</script>

image-20230924222116173

自定义Vite库并发布到NPM

封装useResize

用于监听绑定元素的宽高变化,当元素宽高发生变化时触发回调并获取最新的宽高

新建项目

结合上面学到的 Hook 和 自定义指令封装一个监听元素宽高变化的指令,并发布到 npm

项目结构

useResize            
├── src              
│   └── index.ts     
├── README.md        
├── index.d.ts       
├── package-lock.json
├── package.json     
├── tsconfig.json    
└── vite.config.ts

src/index.ts

import type {App} from "vue";/*** 自定义Hook* @param el* @param callback*/
const weakMap = new WeakMap<HTMLElement, Function>();
const resizeObserver = new ResizeObserver((entries) => {for (const entry of entries) {const handle = weakMap.get(entry.target as HTMLElement);handle && handle(entry)}
})function useResize(el: HTMLElement, callback: Function) {if (weakMap.get(el)) {return}weakMap.set(el, callback)resizeObserver.observe(el)
}/*** 定义vite插件时,vue会在底层调用插件的install方法* @param app*/
function install(app: App) {app.directive('resize', {mounted(el: HTMLElement, binding: { value: Function }) {useResize(el, binding.value)}})
}useResize.install = installexport default useResize

vite.config.ts

import {defineConfig} from "vite"export default defineConfig({build:{lib:{// 打包入口文件entry:"src/index.ts",// namename:"useResize"},rollupOptions:{// 忽略打包的文件external:['vue'],output:{globals:{useResize:"useResize"}}}}
})

index.d.ts

declare const useResize:{(element:HTMLElement, callback:Function):voidinstall:(app:any) => void
}export default useResize

package.json

{"name": "v-resize-songzx","version": "1.0.0","description": "","main": "dist/v-resize-songzx.umd.js","module": "dist/v-resize-songzx.mjs","scripts": {"test": "echo \"Error: no test specified\" && exit 1","build": "vite build"},"keywords": [],"author": "songzx","files": ["dist","index.d.ts"],"license": "ISC","devDependencies": {"vue": "^3.3.4"},"dependencies": {"vite": "^4.4.9"}
}

pachage.json 文件属性说明:

  • name:对应打包后生成的包名,也就是上传到npm上面的包名,不能包含数字和特殊符号
  • version:包的版本号
  • main:对应打包后的 umd.js 文件,在使用 app.use 时会访问使用文件
  • module:使用import、require等方式引入时会使用 mjs 文件
  • files:指定那些文件需要上传
打包
npm run build
登录npm
npm login

image-20230924231941434

发布
npm publish

image-20230924232715692

打开 npm 网站,搜索查看是否发布成功

image-20230925090849623

使用自己的库

安装
npm i v-resize-songzx
使用方式一

全局注册 v-resze 指令

main.ts 引入

import useResize from "v-resize-songzx";const app = createApp(App)app.use(useResize)
app.mount('#app')
<template><div class="resize" v-resize="getNewWH"></div>
</template><script setup lang="ts">
const getNewWH = (e) => {console.log(e.contentRect.width, e.contentRect.height);
}</script><style scoped>
/*把一个元素设置成可以改变宽高的样子*/
.resize {resize: both;width: 200px;height: 200px;border: 1px solid;overflow: hidden;
}
</style>
使用方式二

使用Hook的方式

<template><div class="resize"></div>
</template><script setup lang="ts">import useResize from "v-resize-songzx";onMounted(() => {useResize(document.querySelector(".resize"), e => {console.log(e.contentRect.width, e.contentRect.height);})
})</script><style scoped>
/*把一个元素设置成可以改变宽高的样子*/
.resize {resize: both;width: 200px;height: 200px;border: 1px solid;overflow: hidden;
}
</style>

定义全局变量和方法

main.ts 中添加

import dayjs from "dayjs"
import mitt from "mitt"const Mitt = mitt()// 定义全局变量
app.config.globalProperties.$bus = Mitt
app.config.globalProperties.$BaseUrl = 'http://localhost'
app.config.globalProperties.$formatDate = (date: Date) => dayjs(date).format('YYYY-MM-DD HH:mm:ss')// 定义声明文件
declare module 'vue' {export interface ComponentCustomProperties {$bus: typeof Mitt,$BaseUrl: string,$formatDate: Date}
}

在任何组件中都可以使用

<template><div>{{ $BaseUrl }}</div>
</template><script setup lang="ts">
import {getCurrentInstance} from 'vue'
// 获取当前实例
const instance = getCurrentInstance()console.log(instance.proxy.$BaseUrl) //=> http://localhost
console.log(instance.proxy.$formatDate(new Date())) //=> 2023-09-25 13:51:23</script>

自定义插件之全局Loading

ElementPlus的默认全局Loading

如果完整引入了 Element Plus,那么 app.config.globalProperties 上会有一个全局方法$loading,同样会返回一个 Loading 实例。

名称说明类型默认
targetLoading 需要覆盖的 DOM 节点。 可传入一个 DOM 对象或字符串; 若传入字符串,则会将其作为参数传入 document.querySelector以获取到对应 DOM 节点string / HTMLElementdocument.body
bodyv-loading 指令中的 body 修饰符booleanfalse
fullscreenv-loading 指令中的 fullscreen 修饰符booleantrue
lockv-loading 指令中的 lock 修饰符booleanfalse
text显示在加载图标下方的加载文案string
spinner自定义加载图标类名string
background遮罩背景色string
customClassLoading 的自定义类名string
指令的方式使用
<template><div class="box" v-loading="isLoading">content</div><el-button type="primary" @click="showDivLoading">显示loading</el-button>
</template><script setup lang="ts">
// 显示局部loading
let isLoading = ref(false)const showDivLoading = () => {isLoading.value = !isLoading.value
}</script><style scoped>
.box {width: 200px;height: 200px;border: 1px solid;
}
</style>
函数式调用
<template><el-button type="primary" @click="showLoading">showLoading</el-button>
</template><script setup lang="ts">
import {getCurrentInstance} from 'vue'
// 获取当前实例
const {proxy} = getCurrentInstance()// 显示全局loading
const showLoading = () => {const loading = proxy.$loading()setTimeout(() => {loading.close()}, 2000)
}
</script>

自定义全局Loading

我们自己动手来实现一个和ElementPlus的Loading,同时支持函数调用和指令调用

添加MyLoading.vue
<template><transition enter-active-class="animate__animated animate__fadeIn"leave-active-class="animate__animated animate__fadeOut"><div class="root-box" v-if="show"><div class="wrap"><img src="../assets/images/loading.gif"/></div></div></transition>
</template><script setup>
let show = ref(false)const showLoading = () => {show.value = true
}
const hideLoading = (callback) => {show.value = falsecallback && setTimeout(() => callback(), 500)
}defineExpose({show,showLoading,hideLoading
})</script><style scoped lang="scss">
.animate__animated.animate__fadeIn {--animate-duration: 0.5s;
}.animate__animated.animate__fadeOut {--animate-duration: 0.5s;
}.root-box {position: absolute;left: 0;top: 0;bottom: 0;right: 0;margin: 0;background-color: rgba(255, 255, 255, 0.9);z-index: 2000;display: flex;justify-content: center;align-items: center;.wrap {width: 100px;height: 100px;display: flex;justify-content: center;align-items: center;overflow: hidden;img {width: 100%;transform: scale(2.5);}}
}
</style>
添加MyLoading.ts
import type {App, VNode,} from "vue"
import {createVNode, render, cloneVNode} from "vue"
import MyLoading from "@/components/MyLoading.vue"export default {install(app: App) {// 使用vue底层的createVNode方法将组件渲染为虚拟节点const VNode: VNode = createVNode(MyLoading)// 使用render函数将组件挂载到body中render(VNode, document.body)// 定义全局方法设置组件的显示和隐藏app.config.globalProperties.$showLoading = VNode.component?.exposed.showLoadingapp.config.globalProperties.$hideLoading = VNode.component?.exposed.hideLoadingconst weakMap = new WeakMap()// 自定义Loading指令app.directive("zx-loading", {mounted(el) {if (weakMap.get(el)) return//  记录当前绑定元素的positionweakMap.set(el, window.getComputedStyle(el).position)},updated(el: HTMLElement, binding: { value: Boolean }) {const oldPosition = weakMap.get(el);// 如果不是position: relative或者absolute,就设置为relative// 这里的目的是确保loading组件正确覆盖当前绑定的元素if (oldPosition !== 'absolute' && oldPosition !== 'relative') {el.style.position = 'relative'}// 克隆一份loading元素,// 作用是当页面上有多个zx-loading时,每个dom都维护一份属于自己的loading,不会冲突const newVNode = cloneVNode(VNode)// 挂载当前节点render(newVNode, el)// 判断绑定的值if (binding.value) {newVNode.component?.exposed.showLoading()} else {newVNode.component?.exposed.hideLoading(() => {// 还原布局方式el.style.position = oldPosition})}}})}
}

在上面的文件中定义了两个全局函数和一个自定义指令

  • $showLoading:全局显示一个Loading
  • $hideLoading:关闭全局的Loading
  • zx-loading:自定义指令
在main.ts中挂载

main.ts 中去挂载我们自定义的 Loading

import {createApp} from 'vue'
import MyLoading from "@/utils/MyLoading";const app = createApp(App)
// 引入自定义的全局Loading
app.use(MyLoading)app.mount('#app')
使用方法一:函数式使用

调用全局方法弹出Loading

<template><!--自定义全局loading--><el-button type="primary" @click="showMyLoading">显示自定义的全局loading</el-button>
</template><script setup lang="ts">
import {getCurrentInstance} from 'vue'
// 获取当前实例
const {proxy} = getCurrentInstance()// 全局显示自定义loading
const showMyLoading = () => {proxy.$showLoading()setTimeout(() => {proxy.$hideLoading()}, 2000)
}
</script>

image-20230925171920861

使用方法二:指令式使用
<template><div><!--自定义的loading指令使用--><div class="box" v-zx-loading="isLoading">指令的方式使用</div><el-button type="primary" @click="showDivLoading">显示loading</el-button><!--自定义的loading指令使用-->      <div class="parent"><div class="child" v-zx-loading="childLoading"></div></div><el-button type="primary" @click="showChildLoading">显示childLoading</el-button></div>
</template><script setup lang="ts">
// 显示局部loading
let isLoading = ref(false)
const showDivLoading = () => {isLoading.value = !isLoading.value
}const childLoading = ref(false)
const showChildLoading = () => {childLoading.value = !childLoading.value
}
</script><style scoped lang="scss">
.box {width: 200px;height: 200px;border: 1px solid;
}.parent {position: relative;width: 300px;height: 300px;border: 1px solid;padding: 30px;.child {position: absolute;right: 30px;bottom: 30px;width: 200px;height: 200px;border: 1px solid;}
}
</style>

image-20230925172100385

use函数源码实现

添加 MyUse.ts

import type {App} from "vue"
import {app} from "@/main"// 定义一个接口,声明install方法必传
interface Use {install: (app: App, ...options: any[]) => void
}const installList = new Set()export default function myUse<T extends Use>(plugin: T, ...options: any[]) {// 判断这个插件是否已经注册过了,如果注册过了则报错if (installList.has(plugin)) {console.error("Plugin already installed")return}// 调用插件身上的install方法,并传入main.ts导出的appplugin.install(app, ...options)installList.add(plugin)
}

使用自定义的myUse方法注册我们自定义的Loading

import {createApp} from 'vue'// 自定义全局Loading
import MyLoading from "@/utils/MyLoading";
// 自定义app.use方法
import myUse from "@/utils/MyUse";export const app = createApp(App)
// 引入自定义的全局Loading
myUse(MyLoading)app.mount('#app')

CSS选择器

:deep

使用 :deep() 将选择器包裹起来可以将第三方库的样式进行修改

<template><div><el-input placeholder="placeholder" v-model="name"/></div>
</template><script setup>
let name = ref("")
</script><style scoped lang="scss">
.el-input{:deep(.el-input__inner) {background-color: red;}
}
</style>

image-20230925223153879

:slotted

使用 :slotted() 将插槽中的类名包裹起来,可以修改插槽中的元素样式

SlotTestCom.vue

<template><div>父组件<slot></slot></div>
</template><style scoped>
:slotted(.msg) {font-weight: bold;color: red;
}
</style>
<SlotTestCom><div class="msg">私人订制DIV</div>
</SlotTestCom>

image-20230927103314117

:global

使用 :global() 用于设置全局样式

:global(div){font-size: 17px;color: #222222;
}

全局设置div的样式

css中使用v-bind

let color = ref("pink")
// 随机一个颜色
const randomColor = () => {color.value = `rgb(${Math.random() * 255},${Math.random() * 255},${Math.random() * 255})`
}

使用 v-bind() 将JS中变量包裹起来即可使用

.el-input {width: 300px;:deep(.el-input__inner) {background-color: v-bind(color);}
}

Vue3集成Tailwind CSS

官网地址Tailwind CSS 中文文档 - 无需离开您的HTML,即可快速建立现代网站。

安装

npm install -D tailwindcss@latest postcss@latest autoprefixer@latest

生成配置文件

npx tailwindcss init -p

修改配置文件 tailwind.config.js

2.6版本

module.exports = {purge: ['./index.html', './src/**/*.{vue,js,ts,jsx,tsx}'],theme: {extend: {},},plugins: [],
}

3.0版本

module.exports = {content: ['./index.html', './src/**/*.{vue,js,ts,jsx,tsx}'],theme: {extend: {},},plugins: [],
}

新建 index.css 并在 main.ts 中引入

@tailwind base;
@tailwind components;
@tailwind utilities;

image-20230927132455491

基础使用

详细类名见文档:https://www.tailwindcss.cn/docs/font-family

<template><div class="h-full flex justify-center items-center bg-teal-400"><div class="text-8xl text-rose-700font-bold text-white">Hello Word</div></div>
</template>

image-20230927132522243

nextTick

vue 中更新DOM操作是异步的,但是JS程序是同步的,所以当遇到操作DOM时可能会出现延迟更新的情况,vue 也给了一个解决方案,就是可以将操作 DOM 的代码放在 nextTick 中执行,nextTick 会执行一个 Promise 函数去更新DOM,来实现同步更新DOM的操作

这样做的好处是可以提高程序性能,例如执行一个for循环,每次循环会改变变量的值,然后吧这个变量输出到页面上。用一个watch去监听这个变量,watch函数并不会触发多次,而是只会执行一次

下面是一个小案例

<template><div class="box" ref="box"><div class="item" v-for="(item,index) in msgList" :key="index">{{ item.msg }}</div></div><el-input v-model="msg" style="width: 200px"/><el-button type="primary" @click="send">发送</el-button>
</template><script setup lang="ts">
import {nextTick, ref, reactive} from 'vue'let msgList = reactive([{msg: "Hello world"}
])
let msg = ref("")
let box = ref<HTMLDivElement>()const send = () => {msgList.push({msg: msg.value})nextTick(() => {// 发送完消息后自动滚动到底部box.value.scrollTop = box.value.scrollHeight})
}
</script><style scoped lang="scss">
.box {width: 300px;border: 2px solid #ddd;height: 400px;overflow: auto;.item {height: 30px;line-height: 30px;padding-left: 1em;background-color: #dddddd;margin: 2px;}
}
</style>

Vue3开发安卓和IOS

参照博客:https://xiaoman.blog.csdn.net/article/details/131507483

安装安卓开发工具

image-20230927161024413

image-20230927161057832

image-20230927161139275

image-20230927161213097

image-20230927161303242

安装完成后打开

image-20230927161352447

首次运行需要安装一些SDK

image-20230927161623550

ionic安装

npm install -g @ionic/cli

初始化项目

ionic start app tabs --type vue
  • app 项目名称
  • tabs 使用的预设
  • –type vue 使用的是vue就写vue,react就写react

image-20230927163158065

image-20230927165645993

启动项目

npm run dev

image-20230927165717819

打包和构建

先执行打包命令

npm run build

再执行构建命令,将程序打包成Android包

ionic capacitor copy android

运行成功后会自动多一个android文件夹

image-20230927165845466

image-20230927165905649

然后运行下面命令进行预览

ionic capacitor open android

会自动打开安卓编辑器

等待项目加载完成后,点击绿色的箭头即可启动

image-20230927171133698

H5适配

添加meat信息

<meta name="viewport" content="width=device-width, initial-scale=1.0">

清除默认样式

<style>html,body,#app{height: 100%;overflow: hidden;}*{padding: 0;margin: 0;}
</style>

圣杯布局

<template><div class="header"><div></div><div></div><div></div></div>
</template><style scoped lang="scss">
.header{width: 100%;height: 50px;line-height: 50px;display: flex;div:nth-child(1),div:nth-child(3){width: 100px;background-color: deepskyblue;}div:nth-child(2){flex: 1;background-color: pink;}
}
</style>
image-20231007222439590

使用postCSS将px单位转成vh和vw

百分比是相对于父元素

vw和vh相对于视口

编写postCSS插件

新建 plugins/PxToVwVh.ts

import {Plugin} from "postcss"let Options = {defaultWidth: 390,defaultHeight: 844,
}
interface OptionsTypes {defaultWidth?:number,defaultHeight?:number,
}export function PxToVwVh(options:OptionsTypes=Options):Plugin{let opt = Object.assign({}, options)return {postcssPlugin:"px-to-vw-vh",// 钩子函数Declaration(node){if(node.value.includes("px")){const num = parseFloat(node.value)if(node.prop.includes("width")){node.value = `${((num / opt.defaultWidth) * 100).toFixed(2)}vw`}else if(node.prop.includes("height")){node.value = `${((num / opt.defaultHeight) * 100).toFixed(2)}vh`}}}}
}

tsconfig.node.json 中引入

{"extends": "@tsconfig/node18/tsconfig.json","include": ["vite.config.*","vitest.config.*","cypress.config.*","nightwatch.conf.*","playwright.config.*","plugins/**/*"],"compilerOptions": {"composite": true,"module": "ESNext","moduleResolution": "Bundler","types": ["node"],"noImplicitAny": false}
}
  • include中添加 plugins/**/*
  • noImplicitAny 允许隐式的使用any

使用插件

vite.config.ts 中使用

import { fileURLToPath, URL } from 'node:url'import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import {PxToVwVh} from "./plugins/PxToVwVh";// https://vitejs.dev/config/
export default defineConfig({plugins: [vue(),],css: {postcss: {plugins: [PxToVwVh()]},},resolve: {alias: {'@': fileURLToPath(new URL('./src', import.meta.url))}}
})

效果展示

我们通过编写插件,实现了将PX单位转换成相对于视口,这样保证了在不同尺寸的屏幕上都会有一个相同的展示布局

image-20231007224908253

image-20231007224848623

全局控制字体大小

设置全局CSS变量

:root{--font-size:16px;
}

然后全局可以通过 var(–font-size) 使用

<template><div class="header"><div>返回</div><div>H5适配</div><div>取消</div></div><button @click="changeFontSize(15)">默认</button><button @click="changeFontSize(24)">大</button><button @click="changeFontSize(36)">特大</button>
</template><script setup>import {onMounted} from "vue";onMounted(()=>{document.documentElement.style.setProperty("--font-size",localStorage.getItem("fontSize") || "16px")
})const changeFontSize = (size) => {document.documentElement.style.setProperty("--font-size",size + 'px')localStorage.setItem("fontSize",size + 'px');
}
</script><style scoped lang="scss">
.header{width: 100%;height: 50px;line-height: 50px;display: flex;text-align: center;font-size: var(--font-size);div:nth-child(1),div:nth-child(3){width: 100px;background-color: deepskyblue;}div:nth-child(2){flex: 1;background-color: pink;}
}button{margin-right: 10px;
}
</style>

点击按钮可以实现字体大小切换

image-20231007230159198

unoCss原子化

官网:https://unocss.dev/

什么是css原子化?

CSS原子化的优缺点

1.减少了css体积,提高了css复用

2.减少起名的复杂度

3.增加了记忆成本 将css拆分为原子之后,你势必要记住一些class才能书写,哪怕tailwindcss提供了完善的工具链,你写background,也要记住开头是bg

安装

npm i -D unocss

配置插件

// vite.config.ts
import UnoCSS from 'unocss/vite'
import { defineConfig } from 'vite'export default defineConfig({plugins: [UnoCSS(),],
})

创建一个 uno.config.js 文件

// uno.config.js
import { defineConfig } from 'unocss'export default defineConfig({// 自定义规则rules:[["red",{ color:"red",'font-size':"25px" }]]
})

main.ts 文件中添加

// main.ts
import 'virtual:uno.css'

使用

直接在页面中使用类名即可

<div class="red">Hello Word
</div>

image-20231008221631938

动态配置类名

rules: [[/^m-(\d+)$/, ([, d]) => ({ margin: `${Number(d) * 10}px` })],['flex', { display: "flex" }]
]

使用

<div class="red m-10">Hello Word
</div>

image-20231008222106144

使用预设

修改 uno.config.js

// uno.config.js
import { defineConfig,presetIcons,presetAttributify,presetUno } from 'unocss'export default defineConfig({// 自定义规则rules:[[/^m-(\d+)$/, ([, d]) => ({ margin: `${Number(d) * 10}px` })],["red",{ color:"red",'font-size':"25px" }],],// 使用预设presets:[presetIcons(),presetAttributify(),presetUno()]
})
  • presetIcons 这个是图标

  • presetAttributify 这个是美化CSS

  • presetUno 预设(实验阶段)是一系列流行的原子化框架的 通用超集,包括了 Tailwind CSS,Windi CSS,Bootstrap,Tachyons 等。

    例如,ml-3(Tailwind),ms-2(Bootstrap),ma4(Tachyons),mt-10px(Windi CSS)均会生效。

使用图标

在官网中找到自己需要的图标:https://icones.js.org/

然后选中后安装

image-20231008223505900

查看页面路径上的单词,然后安装

npm i -D @iconify-json/svg-spinners

点击某个要使用的图标,复制类名即可

image-20231008223618270

<div class="i-svg-spinners-bars-fade font-size-50px color-pink"></div>

image-20231008224316603

Vue编译宏

首先vue版本必须是3.3及以上版本

子组件

<template><el-button type="primary" @click="add">添加</el-button><ul><li v-for="item in props.nameList">{{item}}</li></ul>
</template><script setup lang="ts">
import {defineProps,defineOptions,defineEmits,defineSlots} from "vue"// defineProps,可以定义类型
const props = defineProps<{nameList:string[]
}>()const add = () => {emit("addName",'Tome')
}
// defineEmits,可以定义事件
// 第一个参数是事件名称,第二个参数是事件参数类型,问号表示可选
const emit = defineEmits<{(event:'addName',args?:any):void
}>()// defineOptions常用来定义组件名字
defineOptions({name:"DefineComponents"
})</script>

父组件

<template><DefineComponents :nameList="nameList" @addName="addName"/>
</template><script setup lang="ts">
import DefineComponents from "@/components/DefineComponents.vue";let nameList:string[] = reactive(["张三","李四", "王五"])const addName = (args) => {nameList.push(args)
}</script>
函数名称含义
defineProps接收父组件传递过来的参数
defineEmits定义事件名称
defineOptions配置组件名称和其他信息

Vue环境变量

在项目根目录新建两个文件,分别表示开发环境配置、生成环境配置

注意:设置环境变量时必须以 VITE_ 开头,否则不生效

.env.development

# .env.development
VITE_API=http://localhost:8080

.env.production

# .env.production
VITE_API=/prod-api

修改 package.json 中的运行命令,在启动dev是设置mode是development,表示读取开发环境配置,名称可以自定义,但是要和上面新建的配置文件后缀名保持一致

"scripts": {"dev": "vite --mode development",
},

然后在 vue 文件中通过下面方式获取配置项

console.log(import.meta.env)

image-20231015105913888

这里是开发环境,读取到的 VITE_API 是 http://localhost:8080

然后打包项目,再看一下打印结果

image-20231015110224421

vite.config.ts 中获取环境变量时通过如下方式获取

import { defineConfig,loadEnv } from 'vite'let {VITE_API} = loadEnv(process.env.NODE_ENV,process.cwd())console.log(VITE_API)

控制台会打印出定义的环境变量

image-20231015110925175

Webpack从0到1构建Vue3工程

项目结构

webpack-vue            
├── config             
│   ├── webpack.dev.js 
│   └── webpack.prod.js
├── src                
│   ├── App.vue        
│   └── Child.vue      
├── index.html         
├── main.js            
├── package.json       
└── pnpm-lock.yaml

package.json

{"name": "webpack-vue","version": "1.0.0","description": "","main": "index.js","scripts": {"test": "echo \"Error: no test specified\" && exit 1","build": "webpack --config config/webpack.prod.js","dev": "webpack serve --config config/webpack.dev.js"},"keywords": [],"author": "","license": "ISC","dependencies": {"@vue/compiler-sfc": "^3.3.4","clean-webpack-plugin": "^4.0.0","css-loader": "^6.8.1","friendly-errors-webpack-plugin": "^1.7.0","html-webpack-plugin": "^5.5.3","less": "^4.2.0","less-loader": "^11.1.3","style-loader": "^3.3.3","typescript": "^5.2.2","vue": "^3.3.4","vue-loader": "^17.3.0","webpack": "^5.89.0","webpack-cli": "^5.1.4","webpack-dev-server": "^4.15.1"}
}

webpack.dev.js

const path = require("path")
const HtmlWebpackPlugin =  require("html-webpack-plugin");
const {CleanWebpackPlugin} = require("clean-webpack-plugin");
const {VueLoaderPlugin} = require("vue-loader");module.exports = {mode:"development",entry: "./main.js",output: {filename: "js/[name].[contenthash:10].js",path: path.resolve(__dirname, "dist")},module: {rules: [{test:/\.vue$/,use: "vue-loader"},{test: /\.css$/, //解析cssuse: ["style-loader", "css-loader"],},{test:/\.less/,use: ["style-loader","css-loader", "less-loader"],}]},resolve: {alias: {"@/": path.resolve(__dirname, './src') // 别名},extensions: ['.js', '.json', '.vue', '.ts', '.tsx'] //识别后缀},plugins: [new CleanWebpackPlugin(),new VueLoaderPlugin(),new HtmlWebpackPlugin({template: "./index.html",}),],devServer: {port: 8088,open: true,host: "localhost",historyApiFallback: true, // 解决vue-router刷新404问题proxy: {"/api": {changeOrigin: true,pathRewrite: {"^/api": ""}}}}
}

webpack.prod.js

const path = require("path")
const HtmlWebpackPlugin =  require("html-webpack-plugin");
const {CleanWebpackPlugin} = require("clean-webpack-plugin");
const {VueLoaderPlugin} = require("vue-loader");module.exports = {mode:"production",entry: "./main.js",output: {filename: "js/[name].[contenthash:10].js",path: path.resolve(__dirname, "../dist")},module: {rules: [{test:/\.vue$/,use: "vue-loader"},{test: /\.css$/, //解析cssuse: ["style-loader", "css-loader"],},{test:/\.less/,use: ["style-loader","css-loader", "less-loader"],}]},resolve: {alias: {"@": path.resolve(__dirname, './src') // 别名},extensions: ['.js', '.json', '.vue', '.ts', '.tsx'] //识别后缀},plugins: [new CleanWebpackPlugin(),new VueLoaderPlugin(),new HtmlWebpackPlugin({template: "./index.html",}),],
}

Vite性能优化

打包优化

vite.config.js 添加 build 配置项

import { fileURLToPath, URL } from 'node:url'import { defineConfig,loadEnv } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueJsx from '@vitejs/plugin-vue-jsx'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
import unocss from 'unocss/vite'let {VITE_API} = loadEnv(process.env.NODE_ENV,process.cwd())console.log(VITE_API)// https://vitejs.dev/config/
export default defineConfig({module:"es2022",plugins: [vue(),vueJsx(),AutoImport({resolvers: [ElementPlusResolver()],imports: ['vue', 'vue-router'],dts: 'src/auto-imports.d.ts'}),Components({resolvers: [ElementPlusResolver()],}),unocss(),],resolve: {alias: {'@': fileURLToPath(new URL('./src', import.meta.url))}},css: {preprocessorOptions: {scss: {additionalData: "@import './src/layout_v2/css/bem.scss';"}}},build:{minify:"esbuild", // esbuild打包速度最快,terser 打包体积最小cssCodeSplit:true,// 拆分CSS文件chunkSizeWarningLimit:2000, // 单文件超过2000kb警告assetsInlineLimit:1024*10, // 静态资源文件低于10KB时自动转Base64}
})

Pinia

安装

npm install pinia

在 main.ts 中引入

import {createApp} from 'vue'
import {createPinia} from 'pinia'export const app = createApp(App)
app.use(createPinia())app.mount('#app')

基本使用

userInfoStore.js

import {defineStore} from 'pinia'export const useUserInfoStore = defineStore('userInfo', {state: () => {return {name: "李斯特",age: 18}},getters: {userMsg() {return this.name + '---' + this.age}},actions: {setName(newName) {console.log(this.name)this.name = newName}}
})

actions 中的函数也是支持异步的,this 指向指向的是 state 中返回的对象地址,所以可以通过this来获取到 state 中的属性值

vue文件中使用方法

<template><div><ul><li>{{ userInfoStore.name }}</li><li>{{ userInfoStore.age }}</li><li>{{ userInfoStore.userMsg }}</li></ul><el-button type="primary" @click="change">change</el-button></div>
</template><script setup>
import {useUserInfoStore} from "@/stores/userInfoStore";const userInfoStore = useUserInfoStore()const change = () => {userInfoStore.setName("张三丰")
}</script>

Pinia的一些API

  • $reset 重置数据
  • $subscribe 监听数据变化
  • $onAction 监听 action 数据变化
import {useUserInfoStore} from "@/stores/userInfoStore";const userInfoStore = useUserInfoStore()const change = () => {userInfoStore.setName("张三丰")
}// $reset 重置数据
const reset = () => {userInfoStore.$reset()
}// $subscribe 监听数据变化
userInfoStore.$subscribe((mutation, state) =>{console.log(mutation, state)
})// $onAction 监听 action 数据变化
userInfoStore.$onAction((action, state) =>{console.log(action, state)
})

Pinia持久化缓存

安装

npm install pinia-plugin-persistedstate

配置

import {createApp} from 'vue'
import {createPinia} from 'pinia'
import PiniaPluginPersistedstate from "pinia-plugin-persistedstate"export const app = createApp(App)
// 配置Pinia并设置持久化缓存
const Pinia = createPinia()
Pinia.use(PiniaPluginPersistedstate)app.use(Pinia)
app.mount('#app')

然后在需要设置持久化缓存的pinia文件中开启persist配置

import {defineStore} from 'pinia'export const useUserInfoStore = defineStore('userInfo', {state: () => {return {name: "李斯特",age: 18}},getters: {userMsg() {return this.name + '---' + this.age}},actions: {setName(newName) {console.log(this.name)this.name = newName}},// 开启数据持久化persist: true
})

效果展示

它原理是将pinia数据保存到 localStorage 缓存中,刷新页面后优先从缓存中读取,如果缓存中没有则再从代码中读取

Echarts展示地图

效果图

image-20231019201753090

安装

npm install echarts

默认安装的是 5.x 版本

在这个版本中的引入方式必须是下面这种方法

import * as echarts from 'echarts'

源码

首先要下载好地图数据 china.js

下载地址:https://szx-bucket1.oss-cn-hangzhou.aliyuncs.com/picgo/china.js,下载到本地使用即可

地图实现源码

<template><div class='h-full flex justify-center items-center'><div id='mapDom' class='h-full w-full'></div></div>
</template><script setup>
import { onMounted } from 'vue'
import * as echarts from 'echarts'
import '../assets/china'
import { getCityPositionByName } from '@/assets/cityPostion'// 模拟10条数据
let mockData = [{ name: '北京', value: 500 },{ name: '天津', value: 200 },{ name: '河南', value: 300 },{ name: '广西', value: 300 },{ name: '广东', value: 300 },{ name: '河北', value: 300 },
]onMounted(() => {let data = mockData.map(i => {let cityPosition = getCityPositionByName(i.name).valuereturn {name: i.name,value: cityPosition.concat(i.value),}})let initMap = echarts.init(document.querySelector('#mapDom'))initMap.setOption({backgroundColor: 'transparent', // 设置背景色透明// 必须设置tooltip: {show: false,},// 地图阴影配置geo: {map: 'china',// 这里必须定义,不然后面series里面不生效tooltip: {show: false,},label: {show: false,},zoom: 1.03,silent: true, // 不响应鼠标时间show: true,roam: false, // 地图缩放和平移aspectScale: 0.75, // scale 地图的长宽比itemStyle: {borderColor: '#0FA3F0',borderWidth: 1,areaColor: '#070f71',shadowColor: 'rgba(1,34,73,0.48)',shadowBlur: 10,shadowOffsetX: -10,shadowOffsetY: 10,},select: {disabled: true,},emphasis: {disabled: true,areaColor: '#00F1FF',},// 地图区域的多边形 图形样式 阴影效果// z值小的图形会被z值大的图形覆盖top: '10%',left: 'center',// 去除南海诸岛阴影 series map里面没有此属性regions: [{name: '南海诸岛',selected: false,emphasis: {disabled: true,},itemStyle: {areaColor: '#00000000',borderColor: '#00000000',},}],z: 1,},series: [// 地图配置{type: 'map',map: 'china',zoom: 1,tooltip: {show: false,},label: {show: true, // 显示省份名称color: '#ffffff',align: 'center',},top: '10%',left: 'center',aspectScale: 0.75,roam: false, // 地图缩放和平移itemStyle: {borderColor: '#3ad6ff', // 省分界线颜色  阴影效果的borderWidth: 1,areaColor: '#17348b',opacity: 1,},// 去除选中状态select: {disabled: true,},// 控制鼠标悬浮上去的效果emphasis: { // 聚焦后颜色disabled: false, // 开启高亮label: {align: 'center',color: '#ffffff',},itemStyle: {color: '#ffffff',areaColor: '#0075f4',// 阴影效果 鼠标移动上去的颜色},},z: 2,data: data,},{type: 'scatter',coordinateSystem: 'geo',symbol: 'pin',symbolSize: [50, 50],label: {show: true,color: '#fff',formatter(value) {return value.data.value[2]},},itemStyle: {color: '#e30707', //标志颜色},z: 2,data: data,},],})
})
</script>

cityPostion.js 文件代码,这个文件主要是通过省份名称获取经纬度

const positionArr = [{ name: '北京', value: ['116.3979471', '39.9081726'] },{ name: '上海', value: ['121.4692688', '31.2381763'] },{ name: '天津', value: ['117.2523808', '39.1038561'] },{ name: '重庆', value: ['106.548425', '29.5549144'] },{ name: '河北', value: ['114.4897766', '38.0451279'] },{ name: '山西', value: ['112.5223053', '37.8357424'] },{ name: '辽宁', value: ['123.4116821', '41.7966156'] },{ name: '吉林', value: ['125.3154297', '43.8925629'] },{ name: '黑龙江', value: ['126.6433411', '45.7414932'] },{ name: '浙江', value: ['120.1592484', '30.265995'] },{ name: '福建', value: ['119.2978134', '26.0785904'] },{ name: '山东', value: ['117.0056', '36.6670723'] },{ name: '河南', value: ['113.6500473', '34.7570343'] },{ name: '湖北', value: ['114.2919388', '30.5675144'] },{ name: '湖南', value: ['112.9812698', '28.2008247'] },{ name: '广东', value: ['113.2614288', '23.1189117'] },{ name: '海南', value: ['110.3465118', '20.0317936'] },{ name: '四川', value: ['104.0817566', '30.6610565'] },{ name: '贵州', value: ['106.7113724', '26.5768738'] },{ name: '云南', value: ['102.704567', '25.0438442'] },{ name: '江西', value: ['115.8999176', '28.6759911'] },{ name: '陕西', value: ['108.949028', '34.2616844'] },{ name: '青海', value: ['101.7874527', '36.6094475'] },{ name: '甘肃', value: ['103.7500534', '36.0680389'] },{ name: '广西', value: ['108.3117676', '22.8065434'] },{ name: '新疆', value: ['87.6061172', '43.7909393'] },{ name: '内蒙古', value: ['111.6632996', '40.8209419'] },{ name: '西藏', value: ['91.1320496', '29.657589'] },{ name: '宁夏', value: ['106.2719421', '38.4680099'] },{ name: '台湾', value: ['120.9581316', '23.8516062'] },{ name: '香港', value: ['114.139452', '22.391577'] },{ name: '澳门', value: ['113.5678411', '22.167654'] },{ name: '安徽', value: ['117.2757034', '31.8632545'] },{ name: '江苏', value: ['118.7727814', '32.0476151'] },
]export function getCityPositionByName(name) {return positionArr.find(item => item.name === name)
}

Vue-Router

安装

npm install vue-router

安装完成后检查一下安装的版本是否是 4.x 版本,确保在 vue3 中可以使用

image-20231022094941965

定义路由和404

新建 router/index.js

import {createRouter,createWebHashHistory} from "vue-router"const router = createRouter({// 定义路由模式:哈希模式history:createWebHashHistory(),routes:[{path:"/",component:()=>import("../views/home.vue")},{path:"/about",component:()=>import("../views/about.vue")},// 匹配404页面,当所有路径都匹配不到时,就跳转到404{path: "/:pathMatch(.*)",component: ()=>import("../views/404.vue"),},]
})// 导出路由
export default router

注册路由

main.js

import { createApp } from 'vue'
import App from './App.vue'
import router from "./router"const app = createApp(App)app.use(router)app.mount('#app')

定义路由出口

App.vue

<template><router-view/>
</template>

image-20231022100705985

路由跳转

方式一:router-link

<router-link class="mr-10" to="/">home</router-link>
<router-link to="/about">about</router-link>

router-link是vue-router内置的组件,通过to属性定义要跳转的地址,属性值要和路由中的 path 相对应

方式二:通过js的方式跳转

定义两个按钮,点击按钮实现跳转

<button class="mr-10" @click="toPath('/')">home</button>
<button @click="toPath('/about')">about</button>

js方法

import {useRouter} from "vue-router"const router = useRouter()const toPath = (url) => {router.push({path:url})
}

控制路由返回与前进

定义两个按钮分别实现返回和前进

<button class="mr-10" @click="back()">返回</button>
<button class="mr-10" @click="advance()">前进</button>

实现两个方法

const back = () => {// 方式一// router.go(-1)// 方式二router.back()
}const advance = () => {router.go(1)
}

replace

默认通过 push 的方式跳转会留下历史记录。如果不想留下历史记录,可以通过 replace 这种方法跳转。

例如在登录成功后就可以使用 replace 来跳转

在 router-link 标签上添加 replace 属性

<router-link replace class="mr-10" to="/">home</router-link>
<router-link replace class="mr-10" to="/about">about</router-link>

或者通过 router.replace

const toPath = (url) => {router.replace({path:url})
}

这种跳转方式不会留下历史记录

路由传参

通过添加 query 参数来实现传参

const toPath = (url) => {router.push({path:url,query:{id:1,name:"李四",}})
}

通过如下方法接收路由参数

<template>我是详情页,接收到的路由参数是:{{route.query}}
</template><script setup>
import {useRoute} from "vue-router";const route = useRoute()console.log(route.query)</script>

image-20231022102319527

接收到到的是一个对象

动态URL

我们也可以将参数作为页面URL的一部分

首先定义路由

注意:

这里要多定义一个参数:name,动态路由跳转时,需要通过 name 来跳转

使用 /dyDetail/:xxx/:xxx 这种方式定义动态参数名称

{path:"/dyDetail/:id/:name",name:"DyDetail",component:()=>import("../views/dyDetail.vue")
},

添加跳转方法

const toDyDetail = () => {router.push({// 这里使用name来跳转,name名称也要和路由中定义的name一致name:"DyDetail",// 这里传递的属性名必须和路由中定义的参数名一致params:{id:"1",name:"张三"}})
}

获取动态路由参数方法,通过 route.params 方法获取

<template><div>id:{{route.params.id}}</div><div>name:{{route.params.name}}</div>
</template><script setup>
import {useRoute} from "vue-router";const route = useRoute()console.log(route.params)</script>

image-20231022103152777

这里观察地址栏中的显示方式,直接将参数获取url的一部分来显示

路由嵌套

定义路由

{path:"/system",component:()=>import("../views/system/index.vue"),children:[{path:"menu",component:()=>import("../views/system/menu.vue")},{path:"role",component:()=>import("../views/system/role.vue")},]
}

system/index.vue

<template><div class="parent"><button @click="toPath('menu')">菜单管理</button><button @click="toPath('role')">角色管理</button></div><router-view/>
</template><script setup>
import {useRouter} from "vue-router";const router = useRouter()
const toPath = (url) => {router.push({path:`/system/${url}`})
}</script><style scoped>
.parent{height: 45px;background-color: pink;display: flex;gap: 15px;align-items: center;justify-content: center;
}
</style>

跳转到子路由时,需要加上父路由地址

image-20231022104829861

重定向

{path:"/system",// 重定向到第一个子菜单redirect:"/system/menu",component:()=>import("../views/system/index.vue"),children:[{path:"menu",component:()=>import("../views/system/menu.vue")},{path:"role",component:()=>import("../views/system/role.vue")},]
}

路由守卫

全局前置路由守卫

// 全局前置路由守卫
router.beforeResolve((to,from,next)=>{console.log(to) // 去哪个页面console.log(from) // 从哪个页面来next() // 下一步,必须要写,否则无法跳转
})

全局后置路由守卫

// 全局后置路由守卫
router.afterEach((to,from)=>{console.log(to) // 去哪个页面console.log(from) // 从哪个页面来
})

局部路由守卫

{path:"menu",component:()=>import("../views/system/menu.vue"),// 局部前置路由守卫beforeEnter:((to,from,next)=>{console.log(to,'局部前置路由守卫')console.log(from,'局部前置路由守卫')next()})
},

滚动行为

import {createRouter,createWebHashHistory} from "vue-router"const router = createRouter({// 定义路由模式:哈希模式history:createWebHashHistory(),// 滚动模式scrollBehavior:(to,from,savedPosition)=>{if(savedPosition){// 如果有滚动的位置,则重新回到之前滚动的位置return savedPosition}else{// 否则页面滚动到顶部return {x:0,y:0}}},routes:[{path:"/",component:()=>import("../views/home.vue")},{path:"/about",component:()=>import("../views/about.vue")},{path:"/detail",component:()=>import("../views/detail.vue")},]
})// 导出路由
export default router

动态路由

在后台管理系统中常见的场景,根据不同的角色,显示不同的菜单

编写方法,根据不同的账号名,返回不同的菜单

export function getDynamicRouting(name){return new Promise((resolve,reject)=>{// root角色登录if(name === "admin"){resolve([{path:"/about",component:"about.vue"},{path:"/detail",component:"detail.vue"},{path:"/system",redirect:"/system/menu",component:"system/index.vue",children:[{path:"menu",component:"system/menu.vue",},{path:"role",component:"system/role.vue"},],},])}// 普通人员登录if(name === "tome"){resolve([{path:"/about",component:"about.vue"},{path:"/detail",component:"detail.vue"},])}})
}

login.vue

登录成功后根据返回的路由信息,添加路由

<template><div><input placeholder="请输入账号" v-model="name"/><input placeholder="请输入密码" type="password" v-model="pwd"/><button @click="login">登录</button></div>
</template><script setup>
import {ref} from "vue";
import router from "../router"
import {getDynamicRouting} from "../../mock/mockRouter.js";let name = ref("")
let pwd = ref("")const login = () => {getDynamicRouting(name.value).then(routers=>{let dyRouter = setDyRouter(routers)// 只需要添加一级路由信息即可dyRouter.forEach(rootRouter=>{router.addRoute(rootRouter)})})
}const setDyRouter = (routers,parentPath) => {routers.forEach(item=>{item.component = import(`../views/${item.component}`)if(!item.path.startsWith("/")){item.path = `${parentPath}/${item.path}`}if(item.children){setDyRouter(item.children,item.path)}})return routers
}
</script>

测试

首先用admin登录,然后点击菜单管理可以正常返回

image-20231022203258691

然后刷新页面,使用tome登录,然后点击菜单管理发现是404

image-20231022203424279

上面的例子只是简单的实现了一个动态路由,实际开发中,我们会根据接口返回的路由数据渲染不同的菜单来显示

MarkDown语法高亮

安装

npm install marked highlight.js --save
or
pnpm add marked highlight.js --save

注册

import './assets/main.css'
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import router from './router'
import highlight from 'highlight.js'
import "highlight.js/styles/atom-one-dark.css"const app = createApp(App)app.use(createPinia())
app.use(router)app.directive("highlight",function(el){let blocks = el.querySelectorAll('pre code');blocks.forEach((block)=>{highlight.highlightBlock(block);})
})app.mount('#app')

使用

<div v-highlight v-html='content'></div><script>
import { marked } from 'marked'
const content = ref("")
// 需要使用marked方法吧语法转成html页面
content = marked(content)
</script>

效果

image-20231028152820833

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/123366.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【计算机网络笔记】DNS报文格式

DNS 提供域名到主机IP地址的映射  域名服务的三大要素&#xff1a;  域&#xff08;Domain&#xff09;和域名(Domain name)&#xff1a; 域指由地 理位置或业务类型而联系在一起的一组计算机构 成。  主机&#xff1a;由域名来标识。域名是由字符和&#xff08;或&a…

【多线程面试题十】、说一说notify()、notifyAll()的区别

文章底部有个人公众号&#xff1a;热爱技术的小郑。主要分享开发知识、学习资料、毕业设计指导等。有兴趣的可以关注一下。为何分享&#xff1f; 踩过的坑没必要让别人在再踩&#xff0c;自己复盘也能加深记忆。利己利人、所谓双赢。 面试官&#xff1a;说一说notify()、notify…

pdf转jpg的方法【ps和工具方法】

pdf转jpg的方法&#xff1a; 1.photoshop办法&#xff1a; pdf直接拖入ps中&#xff0c;另存为*.Jpg文件即可 另外注意的时候&#xff0c;有时候别人给你pdf文件中包含你需要的jpg文件&#xff0c;千万不要截图进入ps中&#xff0c;直接把文件拖入ps中&#xff0c;这样的文件…

皮卡丘RCE靶场通关攻略

皮卡丘RCE靶场通关攻略 文章目录 皮卡丘RCE靶场通关攻略RCE(remote command/code execute)概述远程系统命令执行启动环境漏洞练习第一关exec "ping"第二关 exec "eval" RCE(remote command/code execute)概述 RCE漏洞&#xff0c;可以让攻击者直接向后台服…

el -table 多层级嵌套

只要你后端可以查到数据这个层级可以无限嵌套 这里用了懒加载&#xff0c;每次点击的时候将当前点击的父级id作为查询条件&#xff0c;向后端发送请求&#xff0c;来获取他子级的数据&#xff0c;并不是将所有数据查出来拼接返回的。 前端代码 <el-table:data"dataLis…

基于Ubuntu20.04安装ROS系统

文章目录 一、ROS简介二、ROS安装三、ROS安装测试四、安装问题解决1. sudo rosdepc init&#xff1a;找不到命令2. ERROR: cannot download default sources list from...3. Command roscore not found...4. Resource not found: roslaunch... 一、ROS简介 ROS是用于编写机器人…

C# 递归算法使用简介_常用整理

一、递归简介 递归算法是一种直接或者间接调用自身函数或者方法的算法。 递归算法的实质是把问题分解成规模缩小的同类问题的子问题&#xff0c;然后递归调用方法来表示问题的解。递归算法对解决一大类问题很有效&#xff0c;它可以使算法简洁和易于理解。 递归本质是循环&a…

Spring-声明式事务

声明式事务 一、简介1、准备工作2、测试 二、声明式事务概念1、编程式事务2、声明式事务3、基于注解的声明式事务1.测试无事务情况2.加入事务①Transactional注解标识的位置②事务属性&#xff1a;只读③事务属性&#xff1a;超时④事务属性&#xff1a;回滚策略⑤事务属性&…

文心一言 VS 讯飞星火 VS chatgpt (123)-- 算法导论10.4 4题

四、用go语言&#xff0c;对于一个含 n个结点的任意有根树&#xff0c;写出一个 O(n)时间的过程&#xff0c;输出其所有关键字该树以左孩子右兄弟表示法存储。 文心一言&#xff1a; 在计算机科学中&#xff0c;左孩子右兄弟表示法是一种用于表示树状结构的方法&#xff0c;其…

解决Maven打包Nacos时插件报错

Maven打包指令&#xff1a;mvn clean install -Dmaven.test.skiptrue -Dcheckstyle.skiptrue 在执行时报错&#xff0c;报错信息如下&#xff1a; Failed to execute goal org.xolstice.maven.plugins:protobuf-maven-plugin:0.6.1:compile (default) on project nacos-consist…

软考下午第一题 案列分析

期待分值 10&#xff0c;前三问12左右分&#xff0c;最后一题2、3分左右&#xff0c;重点在于拿下前面三题。 小心谨慎&#xff0c;不要大意。 数据流图 外部系统 数据存储 加工&#xff08;&#xff09;process 数据流 第二小题 说明给出存储名称&#xff0c;就使用该名称&…

走进国产机器人领军品牌华数机器人,共探数字化变革魔力

近日&#xff0c;纷享销客举办的“一院两司服务对接会暨走进纷享销客【数字化标杆】游学示范基地活动”在佛山顺利举行&#xff0c;本期活动走进华中数控旗下品牌、国家级专精特新“小巨人”企业华数机器人&#xff0c;特邀佛山华数机器人有限公司常务副总经理杨林、纷享销客广…

【vue3】样式穿透、完整新特性、动态css、css-module

一、样式穿透 vue2里面使用 /deep/ vue3里面使用 :deep() :deep(.el-input__inner){background-color: red; }二、完整新特性 :slotted() //parent.vue <template><div><p>这是父级</p><span></span><A><p class"red"…

高级深入--day44

Scrapy 和 scrapy-redis的区别 Scrapy 是一个通用的爬虫框架&#xff0c;但是不支持分布式&#xff0c;Scrapy-redis是为了更方便地实现Scrapy分布式爬取&#xff0c;而提供了一些以redis为基础的组件(仅有组件)。 pip install scrapy-redis Scrapy-redis提供了下面四种组件&a…

Unable to find GatewayFilterFactory with name TokenRelay

目录 问题分析解决方案参考文档开源项目微服务商城项目前后端分离项目 问题分析 Spring Cloud Gateway 网关作为代理资源服务器&#xff0c;需要将 JWT 传递给下游资源服务器&#xff0c;下面是网关的配置 spring:cloud:gateway:discovery:locator:enabled: true # 启用服务发…

.NET、VUE利用RSA加密完成登录并且发放JWT令牌设置权限访问

后端生成公钥私钥 使用RSA.ToXmlString(Boolean) 方法生成公钥以及私钥。 RSACryptoServiceProvider rSA new(); string pubKey rSA.ToXmlString(false);//公钥 string priKey rSA.ToXmlString(true);//私钥 后端将生成的公钥发送给前端 创建一个get请求&#xff0c;将…

光流估计(二) FlowNet 系列文章解读

在上篇文章中&#xff0c;我们学习并解了光流&#xff08;Optical Flow&#xff09;的一些基本概念和基本操作&#xff0c;但是传统的光流估计方法计算比较复杂、成本较高。近些年来随着CNN卷积神经网络的不断发展和成熟&#xff0c;其在各种计算机视觉任务中取得了巨大成功&am…

【剑指offer|图解|双指针】移除元素 + 合并两个有序数组

&#x1f308;个人主页&#xff1a;聆风吟 &#x1f525;系列专栏&#xff1a;数据结构、算法模板、汇编语言 &#x1f516;少年有梦不应止于心动&#xff0c;更要付诸行动。 文章目录 &#x1f4cb;前言一. ⛳️移除元素二. ⛳️合并两个有序数组&#x1f4dd;全文总结 &#…

vue3后台管理系统

项目创建及代码规范化开发 vue脚手架创建项目 安装vue脚手架 npm install-g vue/cli npm update -g vue/cli终端输入vue create 项目名称 即可进入模板选择 //利用vue-cli创建项目 进入模板选择 Vue CLI v5.0.8 ? Please pick a preset:Default ([Vue 3] babel, eslint)De…

2011-2021年“第四期”数字普惠金融与上市公司匹配(根据城市匹配)/上市公司数字普惠金融指数匹配数据

2011-2021年“第四期”数字普惠金融与上市公司匹配&#xff08;根据城市匹配&#xff09;/上市公司数字普惠金融指数匹配数据 1、时间&#xff1a;2011-2021年 指标&#xff1a;指标&#xff1a;股票代码、年份、行政区划代码、行业名称、行业代码、所属省份、所属城市、数字…