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

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

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

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

初始化Vue3项目

方式一

1
npm init vite@latest

image-20230903162122600

生成的目录结构

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
npm run dev

image-20230903162616723

方式二

1
npm init vue@latest

image-20230903162819151

生成的目录结构

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

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

启动

1
npm run dev

image-20230903163023078

自动生成路由

添加 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
`



// console.log(result);
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 对象时需要点开两层才能看到信息,如下

image-20230903201400943

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

image-20230903201422321

image-20230903201441480

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

image-20230903201520363

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 = "华为手机"
// isRef判断一个对象是否是响应式对象
console.log(isRef(product)) // true
}

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 = () => {
// product.value.name = "华为手机"
// isRef判断一个对象是否是响应式对象
console.log(isRef(product)) // true

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 = () => {
// product.value.name = "华为手机"
// isRef判断一个对象是否是响应式对象
console.log(isRef(product)) // true

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的数据

image-20230903211342898

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++
}

image-20230903212107449

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]
})

// 自实现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 打印结果

image-20230903223545771

toRaw

返回对象的原始信息

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

打印

image-20230903223501841

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"

// 泛型约束只能传入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.jsreactive.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 init -y

安装依赖

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, // 是否自动打开浏览器
},
}

执行命令启动项目

1
npx webpack serve

image-20230904232457817

image-20230904232522425

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>

image-20230910152223957

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 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

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)
})

image-20230910154510725

深度监听

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){
// 拼接的结果为: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;
}
}
}

配置全局生效

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'

// 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

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>

布局效果

image-20230913205111297

父子组件传值

简单使用

定义父组件

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>

image-20230913222206507

实现瀑布流布局

父组件

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>

效果展示

image-20230913224830334

组件递归

实现一个如下的东西

image-20230915214800791

父组件

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>

控制台打印的东西

image-20230915214931139

动态组件

image-20230915222313132

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>

效果

image-20230917182606728

异步组件

添加骨架屏组件

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>

效果是这个样子

image-20230917214033824

添加新闻组件

添加新闻数据,在 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>

效果展示

image-20230917214242665

使用异步组件

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>

效果

image-20230918075355087

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')
})
// 被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 被移除),在过渡/动画完成之后移除。

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-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/

1
npm install gsap

使用

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
npm i --save lodash

实现代码

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 = () => {
// 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

安装

1
npm install 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'

// 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

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>

页面效果

image-20230924150109742

自动引入插件

安装

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'], // 自动引入vue,和vue-router相关
dts: 'src/auto-imports.d.ts' // 自动生成的依赖文件
}),
Components({
resolvers: [ElementPlusResolver()],
}),
],

})

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

image-20230924150915291

里面自动的帮我们了引入

然后再组件中不需要手动的导入 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 看看参数有什么

image-20230924171641529

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

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

1
npm i @vueuse/core

网址: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>

image-20230924222116173

自定义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";

/**
* 自定义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

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
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:指定那些文件需要上传

打包

1
npm run build

登录npm

1
npm login

image-20230924231941434

发布

1
npm publish

image-20230924232715692

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

image-20230925090849623

使用自己的库

安装

1
npm i v-resize-songzx

使用方式一

全局注册 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) {
// 使用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

1
2
3
4
5
6
7
8
import {createApp} from 'vue'
import MyLoading from "@/utils/MyLoading";

const app = createApp(App)
// 引入自定义的全局Loading
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>

image-20230925171920861

使用方法二:指令式使用

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>

image-20230925172100385

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"

// 定义一个接口,声明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

1
2
3
4
5
6
7
8
9
10
11
12
13
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() 将选择器包裹起来可以将第三方库的样式进行修改

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>

image-20230925223153879

: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>

image-20230927103314117

: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

生成配置文件

1
npx tailwindcss init -p

修改配置文件 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;

image-20230927132455491

基础使用

详细类名见文档: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>

image-20230927132522243

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

安装安卓开发工具

image-20230927161024413

image-20230927161057832

image-20230927161139275

image-20230927161213097

image-20230927161303242

安装完成后打开

image-20230927161352447

首次运行需要安装一些SDK

image-20230927161623550

ionic安装

1
npm install -g @ionic/cli

初始化项目

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

image-20230927163158065

image-20230927165645993

启动项目

1
npm run dev

image-20230927165717819

打包和构建

先执行打包命令

1
npm run build

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

1
ionic capacitor copy android

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

image-20230927165845466

image-20230927165905649

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

1
ionic capacitor open android

会自动打开安卓编辑器

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

image-20230927171133698

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>
image-20231007222439590

使用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";

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

效果展示

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

image-20231007224908253

image-20231007224848623

全局控制字体大小

设置全局CSS变量

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>

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

image-20231007230159198

unoCss原子化

官网:https://unocss.dev/

什么是css原子化?

CSS原子化的优缺点

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

2.减少起名的复杂度

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

安装

1
npm i -D unocss

配置插件

1
2
3
4
5
6
7
8
9
// vite.config.ts
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
// uno.config.js
import { defineConfig } from 'unocss'

export default defineConfig({
// 自定义规则
rules:[
["red",{ color:"red",'font-size':"25px" }]
]
})

main.ts 文件中添加

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

使用

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

1
2
3
<div class="red">
Hello Word
</div>

image-20231008221631938

动态配置类名

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>

image-20231008222106144

使用预设

修改 uno.config.js

1
2
3
4
5
6
7
8
9
10
11
12
// uno.config.js
import { defineConfig,presetIcons,presetAttributify,presetUno } from 'unocss'

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

  • presetAttributify 这个是美化CSS

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

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

使用图标

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

然后选中后安装

image-20231008223505900

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

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

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

image-20231008223618270

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

image-20231008224316603

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
# .env.development
VITE_API=http://localhost:8080

.env.production

1
2
# .env.production
VITE_API=/prod-api

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

1
2
3
"scripts": {
"dev": "vite --mode development",
},

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

1
console.log(import.meta.env)

image-20231015105913888

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

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

image-20231015110224421

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)

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

image-20231015110925175

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$/, //解析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

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$/, //解析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)

// 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

安装

1
npm install 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("张三丰")
}

// $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持久化缓存

安装

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)
// 配置Pinia并设置持久化缓存
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展示地图

效果图

image-20231019201753090

安装

1
npm install 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

安装

1
npm install vue-router

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

image-20231022094941965

定义路由和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")
},
// 匹配404页面,当所有路径都匹配不到时,就跳转到404
{
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>

image-20231022100705985

路由跳转

方式一: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.go(-1)

// 方式二
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>

image-20231022102319527

接收到到的是一个对象

动态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来跳转,name名称也要和路由中定义的name一致
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>

image-20231022103152777

这里观察地址栏中的显示方式,直接将参数获取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>

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

image-20231022104829861

重定向

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)=>{
// 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

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

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登录,然后点击菜单管理可以正常返回

image-20231022203258691

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

image-20231022203424279

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

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("")
// 需要使用marked方法吧语法转成html页面
content = marked(content)
</script>

效果

image-20231028152820833