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
生成的目录结构
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
方式二
npm init vue@latest
生成的目录结构
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
自动生成路由
添加 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 = 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
`
// 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 对象时需要点开两层才能看到信息,如下
可以打开 启用自定义格式化程序
之后打印就会直接展示具体的信息
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)) // true
shaRef.value.price = 200
console.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)) // true
shaRef.value.price = 200
console.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 = newVal
trigger()
},
}
})
}
const song1 = myRef("123")
const change = () => {
song1.value = "456"
}
Reactive全家桶
Reactive
用来绑定复杂的数据类型 例如 对象 数组
源码中限定只能传入类型是Object的数据
<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
将一个对象设置为只读
import { reactive, readonly } from 'vue';
let form = reactive({
name: "张三",
age: 18
})
let readOnlyForm = readonly(form)
function change() {
readOnlyForm.age++
}
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 = 16
code.value.push(3)
}
myToRefs 打印结果
toRaw
返回对象的原始信息
function fun2() {
console.log(toRaw(student));
}
打印
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.js
和 reactive.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
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>
computed源码实现
effect.ts
// 更新视图方法
let activeEffect
export 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
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
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监听器
监听单属性值
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)
})
深度监听
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: {
// 配置全局CSS
scss: {
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>
布局效果
父子组件传值
简单使用
定义父组件
<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>
实现瀑布流布局
父组件
<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)
// 存放元素高度的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>
效果展示
组件递归
实现一个如下的东西
父组件
<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>
控制台打印的东西
动态组件
<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>
插槽
定义子组件
<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>
效果
异步组件
添加骨架屏组件
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>
效果是这个样子
添加新闻组件
添加新闻数据,在 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>
效果展示
使用异步组件
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>
效果
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-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>
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.value
mitt.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.value
instance?.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>
页面效果
自动引入插件
安装
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
内容
里面自动的帮我们了引入
然后再组件中不需要手动的导入 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 看看参数有什么
我们利用这两个参数实现监听元素宽高变化的指令,当元素宽高发生变化时调用绑定的函数
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 string
const 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.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>
图片懒加载案例
<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.width
canvas.height = img.height
ctx.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>
自定义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 = install
export default useResize
vite.config.ts
import {defineConfig} from "vite"
export default defineConfig({
build:{
lib:{
// 打包入口文件
entry:"src/index.ts",
// name
name:"useResize"
},
rollupOptions:{
// 忽略打包的文件
external:['vue'],
output:{
globals:{
useResize:"useResize"
}
}
}
}
})
index.d.ts
declare const useResize:{
(element:HTMLElement, callback:Function):void
install:(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
发布
npm publish
打开 npm 网站,搜索查看是否发布成功
使用自己的库
安装
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 实例。
名称 | 说明 | 类型 | 默认 |
---|---|---|---|
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 | — |
指令的方式使用
<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 = 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
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.showLoading
app.config.globalProperties.$hideLoading = VNode.component?.exposed.hideLoading
const weakMap = new WeakMap()
// 自定义Loading指令
app.directive("zx-loading", {
mounted(el) {
if (weakMap.get(el)) return
// 记录当前绑定元素的position
weakMap.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>
使用方法二:指令式使用
<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
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导出的app
plugin.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>
: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>
: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;
基础使用
详细类名见文档: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>
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
安装安卓开发工具
安装完成后打开
首次运行需要安装一些SDK
ionic安装
npm install -g @ionic/cli
初始化项目
ionic start app tabs --type vue
- app 项目名称
- tabs 使用的预设
- –type vue 使用的是vue就写vue,react就写react
启动项目
npm run dev
打包和构建
先执行打包命令
npm run build
再执行构建命令,将程序打包成Android包
ionic capacitor copy android
运行成功后会自动多一个android文件夹
然后运行下面命令进行预览
ionic capacitor open android
会自动打开安卓编辑器
等待项目加载完成后,点击绿色的箭头即可启动
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>
使用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单位转换成相对于视口,这样保证了在不同尺寸的屏幕上都会有一个相同的展示布局
全局控制字体大小
设置全局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>
点击按钮可以实现字体大小切换
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>
动态配置类名
rules: [
[/^m-(\d+)$/, ([, d]) => ({ margin: `${Number(d) * 10}px` })],
['flex', { display: "flex" }]
]
使用
<div class="red m-10">
Hello Word
</div>
使用预设
修改 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/
然后选中后安装
查看页面路径上的单词,然后安装
npm i -D @iconify-json/svg-spinners
点击某个要使用的图标,复制类名即可
<div class="i-svg-spinners-bars-fade font-size-50px color-pink"></div>
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)
这里是开发环境,读取到的 VITE_API 是 http://localhost:8080
然后打包项目,再看一下打印结果
在 vite.config.ts
中获取环境变量时通过如下方式获取
import { defineConfig,loadEnv } from 'vite'
let {VITE_API} = loadEnv(process.env.NODE_ENV,process.cwd())
console.log(VITE_API)
控制台会打印出定义的环境变量
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$/, //解析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, // 解决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$/, //解析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 配置项
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展示地图
效果图
安装
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).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
文件代码,这个文件主要是通过省份名称获取经纬度
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 中可以使用
定义路由和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>
路由跳转
方式一: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>
接收到到的是一个对象
动态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>
这里观察地址栏中的显示方式,直接将参数获取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>
跳转到子路由时,需要加上父路由地址
重定向
{
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登录,然后点击菜单管理可以正常返回
然后刷新页面,使用tome登录,然后点击菜单管理发现是404
上面的例子只是简单的实现了一个动态路由,实际开发中,我们会根据接口返回的路由数据渲染不同的菜单来显示
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>