Vue3 + vite + Ts + pinia + 实战 + 源码 +electron
仓库地址:https://gitee.com/szxio/vue3-vite-ts-pinia
视频地址:小满Vue3(课程导读)_哔哩哔哩_bilibili
课件地址:Vue3_小满zs的博客-CSDN博客
初始化Vue3项目 方式一
生成的目录结构
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 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
启动
方式二
生成的目录结构
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 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
用这种方式生成的项目会全一点
启动
自动生成路由 添加 gen-router.js
文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 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 = null const 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 ` fs.writeFile('./src/router/index.ts' ,result, 'utf-8' , (err) => { if (err) throw err; }); });
修改 package.json
中的启动命令
1 2 3 "scripts": { "dev": "node gen-router.js && vite", },
这样每次新建完一个文件后需要重启一下服务,然后会自动生成路由文件,配置菜单动态显示即可
Ref全家桶 ref 接受一个内部值并返回一个可变响应式的 ref 对象。ref 对象仅有一个 .value
property,指向该内部值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 <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 对象时需要点开两层才能看到信息,如下
可以打开 启用自定义格式化程序
之后打印就会直接展示具体的信息
isRef 判断一个对象是否是响应式对象
1 2 3 4 5 6 7 8 9 10 11 12 import { ref, isRef } from "vue" ;const product = ref({ id: "001" , name: "小米手机" }) const change = () => { product.value.name = "华为手机" console .log(isRef(product)) }
shallowRef 创建一个跟踪自身 .value
变化的 ref,但不会使其值也变成响应式的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import { ref, isRef, shallowRef } from "vue" ;const shaRef = shallowRef({ price: 100 }) const change = () => { console .log(isRef(product)) shaRef.value.price = 200 console .log(shaRef.value); }
上面的例子中页面不会发生变化
triggerRef 强制更新页面
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 import { ref, isRef, shallowRef, triggerRef } from "vue" ;const product = ref({ id: "001" , name: "小米手机" }) const shaRef = shallowRef({ price: 100 }) const change = () => { console .log(isRef(product)) shaRef.value.price = 200 console .log(shaRef.value); triggerRef(shaRef) }
需要传入一个要更新的对象
customRef 自定义一个ref响应式数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 import { customRef } from "vue" ;function myRef <T >(value: T ) { return customRef((track, trigger ) => { return { get ( ) { track() return value }, set (newVal ) { value = newVal trigger() }, } }) } const song1 = myRef("123" )const change = () => { song1.value = "456" }
Reactive全家桶 Reactive 用来绑定复杂的数据类型 例如 对象 数组
源码中限定只能传入类型是Object的数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 <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 = res console.log(list); }, 1000); } </script>
Readonly 将一个对象设置为只读
1 2 3 4 5 6 7 8 9 10 import { reactive, readonly } from 'vue' ;let form = reactive({ name: "张三" , age: 18 }) let readOnlyForm = readonly(form)function change ( ) { readOnlyForm.age++ }
shallowReactive 浅层的响应式
1 2 3 4 5 6 7 8 9 10 11 import { shallowReactive } from 'vue' ;let shaReactive = shallowReactive({ a: { b: 123 } }) function chageSha ( ) { shaReactive.a.b = 456 console .log(shaReactive); }
to系列全家桶 toRef 将对象中的某个属性变成响应式的
如果原始数据是非响应式的,则经过 toRef 之后也不会更新视图,但是数据会发生变化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 <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>
如果源数据就是响应式的,则会触发页面更新
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 <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 将对象的所有数据都变成响应式数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 import { toRef, toRefs, toRaw, ref, reactive } from "vue" const student = reactive({ name: "Jack" , age: 18 , like: "画画" , code: [1 , 2 ] }) 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 = 16 code.value.push(3 ) }
myToRefs 打印结果
toRaw 返回对象的原始信息
1 2 3 function fun2 ( ) { console .log(toRaw(student)); }
打印
Vue3响应式源码实现 初始化项目结构 1 2 3 4 5 6 7 8 9 vue-proxy ├── effect.js ├── effect.ts ├── index.html ├── index.js ├── package.json ├── reactive.js ├── reactive.ts └── webpack.config.js
reactive.ts 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 import { track, trigger } from "./effect" const isObject = (target ) => target !== null && typeof target === "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 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 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
1 npm install typescript -g
新建 index.js
,分别引入 effect.js
和 reactive.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 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
1 2 3 4 5 6 7 8 9 10 11 12 13 <!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 >
然后再根目录执行
安装依赖
1 npm install webpack webpack-cli webpack-dev-server html-webpack-plugin -D
然后新建 webpack.config.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 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 , }, }
执行命令启动项目
computed的简单使用 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 <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>
computed源码实现 effect.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 let activeEffectexport const effect = (fn:Function ,options ) => { console .log("effect触发" ) const _effect = function ( ) { activeEffect = _effect return 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 import {track, trigger} from "./effect" const isObject = (target ) => typeof target === 'object' && target !== null export 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 import {effect} from "./effect" export const myComputed = (getter:Function )=> { let _value = effect(getter,{ scheduler:()=> { _dirty = true } }) let _dirty = true let catchValue class ComputedRefImpl { get value (){ if (_dirty){ console .log("依赖发生变化时执行" ) catchValue = _value() _dirty = false } return catchValue } } return new ComputedRefImpl() }
watch监听器 监听单属性值 1 2 3 4 5 let name = ref("李四" )watch(name,(newValue,oldValue )=> { console .log(newValue,oldValue) })
同时监听多个属性 1 2 3 4 5 6 let name = ref("李四" )let age = ref(20 )watch([name,age],(newValue,oldValue )=> { console .log(newValue,oldValue) })
深度监听 1 2 3 4 5 6 7 8 9 10 11 12 13 14 let obj = ref({ foo:{ bar:{ name:"张三" } } }) watch(obj,(newValue,oldValue )=> { console .log(obj.value.foo.bar.name) },{ deep:true , immediate:true , })
监听对象中的某一个属性 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 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不需要传入任何参数,它是一个函数,当依赖变化时,这个函数就会执行,它内部会根据响应式数据的依赖关系,自动执行监听函数
使用 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 <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目录结构
1 2 3 4 5 6 7 8 9 10 layout ├── Content │ └── index.vue ├── Header │ └── index.vue ├── Menu │ └── index.vue ├── css │ └── bem.scss └── index.vue
新建 bem.scss
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 $namespace : "zx" !default;$block-sel :"-" !default;$element-sel :"__" !default;$modifier-sel :"--" !default;@mixin bfc{ height :100% ; overflow : hidden; } @mixin b($block ){ $B :$namespace + $block-sel + $block ; .#{$B }{ @content ; } } @mixin e($element ){ $selector :&; @at-root { $E :$selector + $element-sel + $element ; #{$E }{ @content ; } } } @mixin m($modifier ){ $selector :&; @at-root { $M :$selector + $modifier-sel + $modifier ; #{$M }{ @content ; } } }
配置全局生效
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 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' export default defineConfig({ 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';" } } } })
index.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 <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
1 2 3 4 5 6 7 8 9 <template> <div> Menu </div> </template> <script setup> </script>
Header/index.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 <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>
布局效果
父子组件传值 简单使用 定义父组件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 <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>
子组件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 <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>
实现瀑布流布局 父组件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 <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>
子组件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 <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) // 存放元素高度的list let heightList = [] // 遍历所有元素 for (let i = 0; i < props.list.length; i++) { let el = props.list[i] // i小于colNumber表示第一行元素 if(i < colNumber){ el.top = 0 el.left = elWidth * i heightList.push(el.height) }else{ // 找出最小的高度 let minHeight = Math.min(...heightList) // 找出最小高度的索引 let minHeightIndex = heightList.indexOf(minHeight) // 设置元素的位置 el.left = elWidth * minHeightIndex el.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>
效果展示
组件递归 实现一个如下的东西
父组件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 <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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <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>
控制台打印的东西
动态组件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 <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.com active.value = index } </script> <style scoped> .tab-item{ padding: 5px 15px; border: 1px solid black; } .active{ background-color: deepskyblue; } </style>
插槽 定义子组件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 <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>
定义父组件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 <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>
效果
异步组件 添加骨架屏组件 Skeleton.vue
1 2 3 4 5 6 7 8 9 <template> <el-skeleton style="--el-skeleton-circle-size: 100px"> <template #template> <el-skeleton-item variant="circle" /> </template> </el-skeleton> <br /> <el-skeleton /> </template>
效果是这个样子
添加新闻组件 添加新闻数据,在 public 文件夹中添加 newinfo.json
1 2 3 4 5 6 7 8 [ { "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
1 2 3 4 5 import axios from 'axios' export function getNewDataFun ( ) { return axios("../public/newinfo.json" ) }
编写组件 NewCar.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 <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>
效果展示
使用异步组件 Suspense
是vue内置的一个组件,有两个插槽
default:默认插槽,展示等待结果返回后的组件
fallback:等待过程中展示的组件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 <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传送组件 自定义一个弹框组件 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 <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 父组件使用这个组件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 <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>
效果
KeepAlive 可以缓存组件内容
默认使用 切换组件显示后,组件内容不会丢失
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 <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组件
1 2 3 4 <keep-alive :include="['AliveA']"> <AliveA v-if="flag"/> <AliveB v-else/> </keep-alive>
exclude 不缓存AliveA组件
1 2 3 4 <keep-alive :exclude="['AliveA']"> <AliveA v-if="flag"/> <AliveB v-else/> </keep-alive>
max 最多缓存的组件个数
1 2 3 4 <keep-alive :max="10"> <AliveA v-if="flag"/> <AliveB v-else/> </keep-alive>
keep-alive的钩子函数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 <script lang="ts" setup> import { ref,onMounted,onActivated,onDeactivated,onUnmounted, } from 'vue' onMounted(()=> { console .log('mounted' ) }) onActivated(()=> { console .log('activated' ) }) onDeactivated(()=> { console .log('deactivated' ) }) 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 被移除),在过渡/动画完成之后移除。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 <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 安装
1 npm install animate.css -D
官网中有很多动画示例 Animate.css | A cross-browser library of CSS animations.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 <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生命周期 1 2 3 <transition @before-enter="beforeEnter" @enter="enter" @leave="leave"> <div v-if="gsapFlag" class="gsap-box"></div> </transition>
1 2 3 4 5 6 7 8 @before-enter="beforeEnter" @enter="enter" @after-enter="afterEnter" @enter-cancelled="enterCancelled" @before-leave="beforeLeave" @leave="leave" @after-leave="afterLeave" @leave-cancelled="leaveCancelled"
结合gsap 安装,官网:https://greensock.com/
使用
html
1 2 3 4 <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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 <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:动画曲线
1 2 3 <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来实现一个列表的动画效果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <div> <el-button type="primary" @click="add">add</el-button> <el-button type="danger" @click="pop">pop</el-button> </div> <div class="warp"> <transition-group enter-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>
1 2 3 4 5 6 7 8 9 10 11 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)
实现代码
1 2 3 4 5 6 7 8 9 <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>
1 2 3 4 5 6 7 8 9 10 11 12 13 14 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 = () => { numList.value = _.shuffle(numList.value) }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 $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实现数字滚动 1 2 3 4 5 <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>
1 2 3 4 5 6 7 8 9 10 11 12 13 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 爷爷组件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 <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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 <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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 <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 安装
局部使用 添加一个JS文件导出
utils/mitt.js
1 2 import mitt from "mitt" export default mitt()
使用,分别定义 A B两个组件
BusA
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <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.value mitt.emit("changeFlag",flag.value) } </script>
BusB
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 <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>
在父组件引入
1 2 3 4 5 6 7 8 9 10 <template> <BusA/> <BusB/> </template> <script setup> import BusA from "@/components/BusA.vue"; import BusB from "@/components/BusB.vue"; </script>
效果
全局使用 main文件添加
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <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.value instance?.proxy?.$bus.emit("changeFlag",flag.value) } </script>
BusB
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 <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 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 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插件 安装
1 npm in stall @vitejs/plugin-vue-jsx -D
在 vite.config.js 中使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 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' 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 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/> {/*按钮事件,使用onclick={()=>chagneFlag()}*/} <ElButton type="primary" onclick={()=>chagneFlag()}>改变这个值</ElButton> {flag.value && <h1>改变后的值</h1>} <hr/> <div>父组件传递的值:{prop.msg}</div> </> }, })
在vue中可以把这个当成普通的组件使用
1 2 3 4 5 6 <template> <JsxCom msg="Hello Jsx"/> </template> <script setup> import JsxCom from '../components/JsxCom' </script>
页面效果
自动引入插件 安装
1 npm istall unplugin-auto-import/vite
配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 import AutoImport from 'unplugin-auto-import/vite' export default defineConfig({ module :"es2022" , plugins: [ vue(), vueJsx(), AutoImport({ resolvers: [ElementPlusResolver()], imports: ['vue' , 'vue-router' ], dts: 'src/auto-imports.d.ts' }), Components({ resolvers: [ElementPlusResolver()], }), ], })
保存后查看 src/auto-imports.d.ts
内容
里面自动的帮我们了引入
然后再组件中不需要手动的导入 vue,就可以使用vue中的各种声明
1 2 3 4 5 6 7 8 9 10 <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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 <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>
父组件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 <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 父组件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 <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>
子组件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 <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>
自定义指令 自定义指令的声明周期 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 <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 看看参数有什么
我们利用这两个参数实现监听元素宽高变化的指令,当元素宽高发生变化时调用绑定的函数
1 2 3 4 5 6 7 8 9 10 11 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
自定义指令的简写方式 我们也可以通过函数的方式来自定义指令
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 <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 string const vHasShow:Directive<HTMLElement,string> = (el,binding)=>{ if(!permission.includes(`${userId}:${binding.value}`)){ // 直接移除这个元素,比使用 el.style.display = 'none' 更安全 el.remove() } } </script>
上面的例子是一个按钮级别权限的demo
鼠标拖动元素案例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 <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.offsetLeft let 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>
图片懒加载案例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 <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
网址:Get Started | VueUse — 开始使用 |Vueuse
图片转base64 新建 useImgToBase64.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 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.width canvas.height = img.height ctx.drawImage(img, 0 , 0 , canvas.width, canvas.height) return canvas.toDataURL("image/jpeg" ) } }) }) }
使用
1 2 3 4 5 6 7 8 9 10 11 <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>
自定义Vite库并发布到NPM 封装useResize 用于监听绑定元素的宽高变化,当元素宽高发生变化时触发回调并获取最新的宽高
新建项目 结合上面学到的 Hook 和 自定义指令封装一个监听元素宽高变化的指令,并发布到 npm
项目结构
1 2 3 4 5 6 7 8 9 useResize ├── src │ └── index.ts ├── README.md ├── index.d.ts ├── package-lock.json ├── package.json ├── tsconfig.json └── vite.config.ts
src/index.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 import type {App} from "vue" ;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) } function install (app: App ) { app.directive('resize' , { mounted (el: HTMLElement, binding: { value: Function } ) { useResize(el, binding.value) } }) } useResize.install = install export default useResize
vite.config.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 import {defineConfig} from "vite" export default defineConfig({ build:{ lib:{ entry:"src/index.ts" , name:"useResize" }, rollupOptions:{ external:['vue' ], output:{ globals:{ useResize:"useResize" } } } } })
index.d.ts
1 2 3 4 5 6 declare const useResize:{ (element:HTMLElement, callback :Function ):void install:(app:any ) => void } export default useResize
package.json
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 { "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
发布
打开 npm 网站 ,搜索查看是否发布成功
使用自己的库 安装
使用方式一 全局注册 v-resze 指令
main.ts
引入
1 2 3 4 5 6 import useResize from "v-resize-songzx" ;const app = createApp(App)app.use(useResize) app.mount('#app' )
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 <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的方式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 <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
中添加
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 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 } }
在任何组件中都可以使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <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 实例。
名称
说明
类型
默认
target
Loading 需要覆盖的 DOM 节点。 可传入一个 DOM 对象或字符串; 若传入字符串,则会将其作为参数传入 document.querySelector
以获取到对应 DOM 节点
string
/ HTMLElement
document.body
body
同 v-loading
指令中的 body
修饰符
boolean
false
fullscreen
同 v-loading
指令中的 fullscreen
修饰符
boolean
true
lock
同 v-loading
指令中的 lock
修饰符
boolean
false
text
显示在加载图标下方的加载文案
string
—
spinner
自定义加载图标类名
string
—
background
遮罩背景色
string
—
customClass
Loading 的自定义类名
string
—
指令的方式使用 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 <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>
函数式调用 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <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 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 <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 = false callback && 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 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 import type {App, VNode,} from "vue" import {createVNode, render, cloneVNode} from "vue" import MyLoading from "@/components/MyLoading.vue" export default { install (app: App ) { const VNode: VNode = createVNode(MyLoading) render(VNode, document .body) app.config.globalProperties.$showLoading = VNode.component?.exposed.showLoading app.config.globalProperties.$hideLoading = VNode.component?.exposed.hideLoading const weakMap = new WeakMap () app.directive("zx-loading" , { mounted (el ) { if (weakMap.get(el)) return weakMap.set(el, window .getComputedStyle(el).position) }, updated (el: HTMLElement, binding: { value: Boolean } ) { const oldPosition = weakMap.get(el); if (oldPosition !== 'absolute' && oldPosition !== 'relative' ) { el.style.position = 'relative' } 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
1 2 3 4 5 6 7 8 import {createApp} from 'vue' import MyLoading from "@/utils/MyLoading" ;const app = createApp(App)app.use(MyLoading) app.mount('#app' )
使用方法一:函数式使用 调用全局方法弹出Loading
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <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>
使用方法二:指令式使用 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 <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>
use函数源码实现 添加 MyUse.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import type {App} from "vue" import {app} from "@/main" 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 } plugin.install(app, ...options) installList.add(plugin) }
使用自定义的myUse方法注册我们自定义的Loading
1 2 3 4 5 6 7 8 9 10 11 12 13 import {createApp} from 'vue' import MyLoading from "@/utils/MyLoading" ;import myUse from "@/utils/MyUse" ;export const app = createApp(App)myUse(MyLoading) app.mount('#app' )
CSS选择器 :deep 使用 :deep() 将选择器包裹起来可以将第三方库的样式进行修改
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <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>
:slotted 使用 :slotted() 将插槽中的类名包裹起来,可以修改插槽中的元素样式
SlotTestCom.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 <template> <div> 父组件 <slot></slot> </div> </template> <style scoped> :slotted(.msg) { font-weight: bold; color: red; } </style>
1 2 3 <SlotTestCom> <div class="msg">私人订制DIV</div> </SlotTestCom>
:global 使用 :global() 用于设置全局样式
1 2 3 4 :global (div){ font-size: 17px ; color : #222222 ; }
全局设置div的样式
css中使用v-bind 1 2 3 4 5 let color = ref("pink" )const randomColor = () => { color.value = `rgb(${Math .random() * 255 } ,${Math .random() * 255 } ,${Math .random() * 255 } )` }
使用 v-bind() 将JS中变量包裹起来即可使用
1 2 3 4 5 6 7 8 .el-input { width : 300px ; :deep (.el-input__inner) { background-color: v-bind (color); } }
Vue3集成Tailwind CSS 官网地址Tailwind CSS 中文文档 - 无需离开您的HTML,即可快速建立现代网站。
安装 1 npm install -D tailwindcss@latest postcss@latest autoprefixer@latest
生成配置文件
修改配置文件 tailwind.config.js
2.6版本
1 2 3 4 5 6 7 module .exports = { purge: ['./index.html' , './src/**/*.{vue,js,ts,jsx,tsx}' ], theme: { extend: {}, }, plugins: [], }
3.0版本
1 2 3 4 5 6 7 module .exports = { content: ['./index.html' , './src/**/*.{vue,js,ts,jsx,tsx}' ], theme: { extend: {}, }, plugins: [], }
新建 index.css 并在 main.ts 中引入
1 2 3 @tailwind base;@tailwind components;@tailwind utilities;
基础使用 详细类名见文档:https://www.tailwindcss.cn/docs/font-family
1 2 3 4 5 <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>
nextTick
vue 中更新DOM操作是异步的,但是JS程序是同步的,所以当遇到操作DOM时可能会出现延迟更新的情况,vue 也给了一个解决方案,就是可以将操作 DOM 的代码放在 nextTick 中执行,nextTick 会执行一个 Promise 函数去更新DOM,来实现同步更新DOM的操作
这样做的好处是可以提高程序性能,例如执行一个for循环,每次循环会改变变量的值,然后吧这个变量输出到页面上。用一个watch去监听这个变量,watch函数并不会触发多次,而是只会执行一次
下面是一个小案例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 <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
安装安卓开发工具
安装完成后打开
首次运行需要安装一些SDK
ionic安装 1 npm install -g @ionic/cli
初始化项目 1 ionic start app tabs --type vue
app 项目名称
tabs 使用的预设
–type vue 使用的是vue就写vue,react就写react
启动项目
打包和构建 先执行打包命令
再执行构建命令,将程序打包成Android包
1 ionic capacitor copy android
运行成功后会自动多一个android文件夹
然后运行下面命令进行预览
1 ionic capacitor open android
会自动打开安卓编辑器
等待项目加载完成后,点击绿色的箭头即可启动
H5适配 添加meat信息 1 <meta name ="viewport" content ="width=device-width, initial-scale=1.0" >
清除默认样式 1 2 3 4 5 6 7 8 9 10 <style > html ,body ,#app { height : 100% ; overflow : hidden; } *{ padding : 0 ; margin : 0 ; } </style >
圣杯布局 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 <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 >
使用postCSS将px单位转成vh和vw
百分比是相对于父元素
vw和vh相对于视口
编写postCSS插件 新建 plugins/PxToVwVh.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 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
中引入
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 { "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
中使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 import { fileURLToPath, URL } from 'node:url' import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' import {PxToVwVh} from "./plugins/PxToVwVh" ;export default defineConfig({ plugins: [ vue(), ], css: { postcss: { plugins: [ PxToVwVh() ] }, }, resolve: { alias: { '@' : fileURLToPath(new URL('./src' , import .meta.url)) } } })
效果展示 我们通过编写插件,实现了将PX单位转换成相对于视口,这样保证了在不同尺寸的屏幕上都会有一个相同的展示布局
全局控制字体大小 设置全局CSS变量
1 2 3 :root { --font-size :16px ; }
然后全局可以通过 var(–font-size) 使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 <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>
点击按钮可以实现字体大小切换
unoCss原子化 官网:https://unocss.dev/
什么是css原子化? CSS原子化的优缺点
1.减少了css体积,提高了css复用
2.减少起名的复杂度
3.增加了记忆成本 将css拆分为原子之后,你势必要记住一些class才能书写,哪怕tailwindcss提供了完善的工具链,你写background,也要记住开头是bg
安装
配置插件
1 2 3 4 5 6 7 8 9 import UnoCSS from 'unocss/vite' import { defineConfig } from 'vite' export default defineConfig({ plugins: [ UnoCSS(), ], })
创建一个 uno.config.js
文件
1 2 3 4 5 6 7 8 9 10 import { defineConfig } from 'unocss' export default defineConfig({ rules:[ ["red" ,{ color :"red" ,'font-size' :"25px" }] ] })
在 main.ts
文件中添加
1 2 import 'virtual:uno.css'
使用 直接在页面中使用类名即可
1 2 3 <div class ="red" > Hello Word </div >
动态配置类名 1 2 3 4 rules: [ [/^m-(\d+)$/ , ([, d] ) => ({ margin : `${Number (d) * 10 } px` })], ['flex' , { display : "flex" }] ]
使用
1 2 3 <div class ="red m-10" > Hello Word </div >
使用预设 修改 uno.config.js
1 2 3 4 5 6 7 8 9 10 11 12 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/
然后选中后安装
查看页面路径上的单词,然后安装
1 npm i -D @iconify-json/svg-spinners
点击某个要使用的图标,复制类名即可
1 <div class ="i-svg-spinners-bars-fade font-size-50px color-pink" > </div >
Vue编译宏
首先vue版本必须是3.3及以上版本
子组件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 <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>
父组件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <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
1 2 VITE_API =http://localhost:8080
.env.production
修改 package.json
中的运行命令,在启动dev是设置mode是development,表示读取开发环境配置,名称可以自定义,但是要和上面新建的配置文件后缀名保持一致
1 2 3 "scripts": { "dev": "vite --mode development", },
然后在 vue 文件中通过下面方式获取配置项
1 console .log(import .meta.env)
这里是开发环境,读取到的 VITE_API 是 http://localhost:8080
然后打包项目,再看一下打印结果
在 vite.config.ts
中获取环境变量时通过如下方式获取
1 2 3 4 5 6 import { defineConfig,loadEnv } from 'vite' let {VITE_API} = loadEnv(process.env.NODE_ENV,process.cwd())console .log(VITE_API)
控制台会打印出定义的环境变量
Webpack从0到1构建Vue3工程 项目结构
1 2 3 4 5 6 7 8 9 10 11 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 { "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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 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$/ , use: ["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 , proxy: { "/api" : { changeOrigin: true , pathRewrite: { "^/api" : "" } } } } }
webpack.prod.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 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$/ , use: ["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 配置项
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 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)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" , cssCodeSplit:true , chunkSizeWarningLimit:2000 , assetsInlineLimit:1024 *10 , } })
Pinia 安装
在 main.ts 中引入
1 2 3 4 5 6 7 import {createApp} from 'vue' import {createPinia} from 'pinia' export const app = createApp(App)app.use(createPinia()) app.mount('#app' )
基本使用 userInfoStore.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 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文件中使用方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 <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 数据变化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 import {useUserInfoStore} from "@/stores/userInfoStore" ;const userInfoStore = useUserInfoStore()const change = () => { userInfoStore.setName("张三丰" ) } const reset = () => { userInfoStore.$reset() } userInfoStore.$subscribe((mutation, state ) => { console .log(mutation, state) }) userInfoStore.$onAction((action, state ) => { console .log(action, state) })
Pinia持久化缓存 安装
1 npm install pinia-plugin-persistedstate
配置
1 2 3 4 5 6 7 8 9 10 11 import {createApp} from 'vue' import {createPinia} from 'pinia' import PiniaPluginPersistedstate from "pinia-plugin-persistedstate" export const app = createApp(App)const Pinia = createPinia()Pinia.use(PiniaPluginPersistedstate) app.use(Pinia) app.mount('#app' )
然后在需要设置持久化缓存的pinia文件中开启persist配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 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展示地图 效果图
安装
默认安装的是 5.x 版本
在这个版本中的引入方式必须是下面这种方法
1 import * as echarts from 'echarts'
源码 首先要下载好地图数据 china.js
下载地址:https://szx-bucket1.oss-cn-hangzhou.aliyuncs.com/picgo/china.js,下载到本地使用即可
地图实现源码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 <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).value return { 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
文件代码,这个文件主要是通过省份名称获取经纬度
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 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 安装
安装完成后检查一下安装的版本是否是 4.x 版本,确保在 vue3 中可以使用
定义路由和404 新建 router/index.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 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" ) }, { path: "/:pathMatch(.*)" , component: ()=> import ("../views/404.vue" ), }, ] }) export default router
注册路由
main.js
1 2 3 4 5 6 7 8 9 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
1 2 3 <template> <router-view/> </template>
路由跳转 方式一:router-link
1 2 <router-link class="mr-10" to="/">home</router-link> <router-link to="/about">about</router-link>
router-link是vue-router内置的组件,通过to属性定义要跳转的地址,属性值要和路由中的 path 相对应
方式二:通过js的方式跳转
定义两个按钮,点击按钮实现跳转
1 2 <button class="mr-10" @click="toPath('/')">home</button> <button @click="toPath('/about')">about</button>
js方法
1 2 3 4 5 6 7 8 9 import {useRouter} from "vue-router" const router = useRouter()const toPath = (url ) => { router.push({ path:url }) }
控制路由返回与前进 定义两个按钮分别实现返回和前进
1 2 <button class="mr-10" @click="back()">返回</button> <button class="mr-10" @click="advance()">前进</button>
实现两个方法
1 2 3 4 5 6 7 8 9 10 11 const back = () => { router.back() } const advance = () => { router.go(1 ) }
replace 默认通过 push 的方式跳转会留下历史记录。如果不想留下历史记录,可以通过 replace 这种方法跳转。
例如在登录成功后就可以使用 replace 来跳转
在 router-link 标签上添加 replace 属性
1 2 <router-link replace class ="mr-10" to ="/" > home</router-link > <router-link replace class ="mr-10" to ="/about" > about</router-link >
或者通过 router.replace
1 2 3 4 5 const toPath = (url ) => { router.replace({ path:url }) }
这种跳转方式不会留下历史记录
路由传参 通过添加 query 参数来实现传参
1 2 3 4 5 6 7 8 9 const toPath = (url ) => { router.push({ path:url, query:{ id:1 , name:"李四" , } }) }
通过如下方法接收路由参数
1 2 3 4 5 6 7 8 9 10 11 12 <template > 我是详情页,接收到的路由参数是:{{route.query}} </template > <script setup > import {useRoute} from "vue-router" ;const route = useRoute()console .log(route.query)</script >
接收到到的是一个对象
动态URL 我们也可以将参数作为页面URL的一部分
首先定义路由
注意:
这里要多定义一个参数:name,动态路由跳转时,需要通过 name 来跳转
使用 /dyDetail/:xxx/:xxx 这种方式定义动态参数名称
1 2 3 4 5 { path:"/dyDetail/:id/:name" , name:"DyDetail" , component:()=> import ("../views/dyDetail.vue" ) },
添加跳转方法
1 2 3 4 5 6 7 8 9 10 11 const toDyDetail = () => { router.push({ name:"DyDetail" , params:{ id:"1" , name:"张三" } }) }
获取动态路由参数方法,通过 route.params 方法获取
1 2 3 4 5 6 7 8 9 10 11 12 13 <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 >
这里观察地址栏中的显示方式,直接将参数获取url的一部分来显示
路由嵌套 定义路由
1 2 3 4 5 6 7 8 9 10 11 12 13 14 { 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 <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>
跳转到子路由时,需要加上父路由地址
重定向 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 { 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" ) }, ] }
路由守卫 全局前置路由守卫
1 2 3 4 5 6 router.beforeResolve((to,from ,next )=> { console .log(to) console .log(from ) next() })
全局后置路由守卫
1 2 3 4 5 router.afterEach((to,from )=> { console .log(to) console .log(from ) })
局部路由守卫
1 2 3 4 5 6 7 8 9 10 { path:"menu" , component:()=> import ("../views/system/menu.vue" ), beforeEnter:((to,from ,next )=> { console .log(to,'局部前置路由守卫' ) console .log(from ,'局部前置路由守卫' ) next() }) },
滚动行为 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 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
动态路由 在后台管理系统中常见的场景,根据不同的角色,显示不同的菜单
编写方法,根据不同的账号名,返回不同的菜单
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 export function getDynamicRouting (name ) { return new Promise ((resolve,reject )=> { 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
登录成功后根据返回的路由信息,添加路由
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 <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登录,然后点击菜单管理可以正常返回
然后刷新页面,使用tome登录,然后点击菜单管理发现是404
上面的例子只是简单的实现了一个动态路由,实际开发中,我们会根据接口返回的路由数据渲染不同的菜单来显示
MarkDown语法高亮 安装 1 2 3 npm install marked highlight.js --save or pnpm add marked highlight.js --save
注册 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 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' )
使用 1 2 3 4 5 6 7 8 <div v-highlight v-html ='content' > </div > <script > import { marked } from 'marked' const content = ref("" )content = marked(content) </script >
效果