这里仅的记录我在使用 vue 过程中踩过的坑和常用的代码板子

本文标题间毫无逻辑关系,纯粹想到哪记到哪

关于组合式 API

<script setup> 这块语法糖甜到我根本找不到方向:单文件组件 | Vue.js (vuejs.org)

defineProps 与 defineEmits

  • 父子组件传参的写法有了很大的变化我们需要使用definePropsdefineEmits代替原来的propscontext.emit
  • 要注意definePropsdefineEmits都是只在<script setup>中才能使用的编译器宏
  • 该模式下,使用宏时无需import可以直接使用
  • 下面例子演示了父子组件相互传递信息的过程
  • 父组件 ==> 子组件
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
父组件
<template>
<SonView :cont="content" />
</template>

<script setup>
import { reactive } from 'vue'
import SonView from './SonView.vue'

const content = reactive({
name: 'tom',
age: 18,
score: 92,
})
</script>

子组件
<script setup>
const props = defineProps({
cont: {
type: Object,
required: true,
},
})

console.log(props.cont.name)
</script>

自此子组件可使用props.cont访问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
父组件
<template>
<div class="container">
<SonButton @change="add" />
<div>{{ val }}</div>
</div>
</template>

<script setup>
import SonButton from './SonButton.vue'
import { ref } from 'vue'

let val = ref(0)
const add = () => {
val.value++
}
</script>

子组件
<template>
<button @click="func">AddVal</button>
</template>

<script setup>
const emit = defineEmits(['change'])
const func = () => {
emit('change')
}
</script>

点击位于子组件的按钮改变父组件中的数据

defineExpose

  • 因为使用<script setup>后不需要 return了,但这样会出现一些问题
  • 使用 <script setup> 的组件是默认关闭的,也即通过模板 ref 或者 $parent 链获取到的组件的公开实例,不会暴露任何在 <script setup> 中声明的绑定
  • 为了在 <script setup> 组件中明确要暴露出去的属性,使用 defineExpose 编译器宏
  • 注意时机!:父组件需要在 onMounted生命周期内或通过触发事件来获取
  • 下面例子演示了父组件通过ref从子组件获取数据的过程
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>
<SonButton ref="sonref" />
<button @click="func">get</button>
</template>

<script setup>
import SonButton from '../components/SonButton.vue'
import { ref, onMounted } from 'vue'

let sonref = ref('')
// 第一种方法
onMounted(() => {
console.log(sonref.value.val)
})
// 第二种方法
const func = () => {
console.log(sonref.value.val)
sonref.value.fun()
}
</script>

子组件
<template>
<button type="button" class="btn btn-info" @click="func">AddVal</button>
</template>

<script setup>
import { ref } from 'vue'

const val = ref(100)
const fun = () => {
console.log(1000)
}
defineExpose({
val,
fun,
})
</script>

定义组件 name

  • 配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
npm i unplugin-vue-define-options -D

// vite.config.ts
import { defineConfig } from 'vite'
import DefineOptions from 'unplugin-vue-define-options/vite'
import vue from '@vitejs/plugin-vue'

export default defineConfig({
plugins: [
vue(),
DefineOptions()
]
})

// tsconfig.json
{
"compilerOptions": {
"types": ["unplugin-vue-define-options/macros-global"]
}
}
  • 使用
1
2
3
4
5
6
7
8
9
<template>
<div>this is a card</div>
</template>

<script setup lang="ts">
defineOptions({
name: 'card',
})
</script>

好用的库们

Bootstrap

1
2
3
4
5
npm install bootstrap --save

// App.vue
import 'bootstrap/dist/css/bootstrap.css'
import 'bootstrap/dist/js/bootstrap'

IconPark

碰巧最近有使用 icon 的需求的时候阿里的 iconfont 挂掉了,于是试了试这个网站

缺点就是图标种类广度有点不足,样式也不够多样,不过大差不差也能用,还能自定义颜色

1
2
3
4
npm install @icon-park/vue-next --save

// App.vue
import '@icon-park/vue-next/styles/index.css'
  • 使用

    • 从网站复制 vue 代码即可
    • 注意使用时要单独引入,引入的命名方式是kebab-caseCamelCase
1
2
3
4
5
6
7
<template>
<github-one theme="outline" size="44" fill="#000000" />
</template>

<script setup>
import { GithubOne } from '@icon-park/vue-next' // 注意引入的命名方式
</script>

ElementPlus

需要注意的是 vue3 要引入的是 element-plus

1
2
3
4
5
6
npm install element-plus --save

// main.js中引入
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
app.use(ElementPlus)
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
<template>
<div class="box">
<div class="login-container">
<el-button :plain="true" v-if="false">error</el-button>
<el-button text v-if="false"></el-button>
<h2 style="color:white">登录</h2>
<el-form :rules="rules" :model="user" ref="form">
<el-form-item prop="username">
<el-input
placeholder="请输入用户名"
:prefix-icon="UserFilled"
class="item"
v-model="user.username" />
</el-form-item>
<el-form-item prop="password">
<el-input
placeholder="请输入密码"
:prefix-icon="Lock"
class="item"
v-model="user.password"
show-password
@paste.capture.prevent="handlePaste" />
</el-form-item>
<div class="link">
<router-link :to="{ path: '/todo/register' }" class="link"
>没有用户名?请注册</router-link
>
</div>
<el-form-item>
<el-button type="primary" class="item" @click="submit"
>登录</el-button
>
</el-form-item>
</el-form>
</div>
</div>
</template>

<script setup>
import { UserFilled, Lock } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
import axios from 'axios'
import { reactive, ref, toRaw } from 'vue'
import { useRouter } from 'vue-router'
import { countStore } from '@/stores/countStore'
const user = reactive({
username: '',
password: '',
})

const rules = reactive({
username: [
{ required: true, message: 'Please input Name', trigger: 'blur' },
],
password: [
{
required: true,
message: 'Please input Password',
trigger: 'blur',
},
],
})

const form = ref('')
const router = useRouter()
const store = countStore()

const submit = () => {
form.value.validate((valid) => {
if (valid) {
axios({}).then((resp) => {
if (resp.data === null) {
ElMessage({
showClose: true,
message: 'Oops, 用户名或密码错误',
type: 'error',
})
} else {
router.push({ path: '' })
}
})
} else {
ElMessage({
showClose: true,
message: '请求超时',
type: 'warning',
})
return false
}
})
}
</script>

<style scoped>
.box {
display: flex;
height: 100vh;
justify-content: center;
align-items: center;
background-color: mediumslateblue;
}

.login-container {
width: 300px;
}

.item {
width: 100%;
height: 45px;
}

h2 {
text-align: center;
margin-bottom: 15px;
}

.link {
color: indigo;
text-align: right;
}
</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
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
父组件
<template>
<div class="box">
<el-dialog
v-model="profile"
title="上传头像"
width="30%"
draggable
v-if="uploadview">
<profilePhoto @change="upload" />
</el-dialog>
</div>
</template>
<script setup>
const profile = ref(false)
const uploadview = ref(true)
const upload = () => {
router.go(0)
ElMessage.success('上传成功')
}
</script>

子组件
<template>
<div class="box">
<el-upload
class="avatar-uploader"
:show-file-list="false"
action="/api/setphoto"
:on-success="handleAvatarSuccess"
:headers="{ token: token }"
:before-upload="beforeAvatarUpload"
:auto-upload="false"
ref="uploadRef"
:on-change="changeAvatar"
:data="{ userid: userid }">
<img v-if="imageUrl" :src="imageUrl" class="avatar" />
<el-icon v-else class="avatar-uploader-icon">
<Plus />
</el-icon>
</el-upload>
<br />
<el-button type="primary" plain @click="submitUpload">上传</el-button>
</div>
</template>

<script setup>
import { ref, defineEmits } from 'vue'
import { ElMessage } from 'element-plus'
import { Plus } from '@element-plus/icons-vue'
import * as imageConversion from 'image-conversion'

const imageUrl = ref('')
const token = localStorage.getItem('token')
const uploadRef = ref()
const userid = localStorage.getItem('userid')
const emit = defineEmits(['change'])

const handleAvatarSuccess = () => {
emit('change')
}

const beforeAvatarUpload = (rawFile) => {
if (rawFile.type !== 'image/jpeg') {
ElMessage.error('Avatar picture must be JPG format!')
return false
} else if (rawFile.size / 1024 / 1024 > 1) {
ElMessage.error('Avatar picture size can not exceed 1MB!')
return false
} else if (rawFile.size / 1024 / 1024 > 0.05) {
// 图像压缩至 50KB
let myImg = new Promise((resolve) => {
imageConversion.compressAccurately(rawFile, 50).then((res) => {
resolve(res)
})
})
return myImg
}
return true
}

const changeAvatar = (uploadFile) => {
imageUrl.value = URL.createObjectURL(uploadFile.raw)
}

const submitUpload = () => {
uploadRef.value.submit()
}
</script>

<style scoped>
.avatar-uploader .avatar {
width: 178px;
height: 178px;
display: block;
}
</style>

<style>
.box {
text-align: center;
}

.avatar-uploader .el-upload {
border: 1px dashed var(--el-border-color);
border-radius: 6px;
cursor: pointer;
position: relative;
overflow: hidden;
transition: var(--el-transition-duration-fast);
}

.avatar-uploader .el-upload:hover {
border-color: var(--el-color-primary);
}

.el-icon.avatar-uploader-icon {
font-size: 28px;
color: #8c939d;
width: 178px;
height: 178px;
text-align: center;
}
</style>

Unocss

  • 安装
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
npm install unocss @unocss/preset-uno @unocss/preset-attributify  -D

// main.js
import 'uno.css'

// vie.config.js
import Unocss from 'unocss/vite'
import { presetUno, presetAttributify } from 'unocss'
plugins: [
Unocss({
presets: [
presetUno(),
presetAttributify()
]
})
]

动态组件

  • 动态组件是很必要而且很香的
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>
<button v-for="(index, item) in groups" :key="index" @click="func(item)">
{{ item }}
</button>
<component :is="groups[current]"></component>
</template>

<script setup>
import ComA from '../components/ComA.vue'
import ComB from '../components/ComB.vue'
import ComC from '../components/ComC.vue'
import { ref } from 'vue'

const groups = {
ComA,
ComB,
ComC,
}

const current = ref('ComA')

const func = (it) => {
current.value = it
}
</script>
  • 使用 keep-alive 取消切换组件时 DOM 的销毁,用于保留状态
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
父组件使用 keep-alive
<template>
<keep-alive>
<ComA />
</keep-alive>
</template>

<script setup>
import ComA from '../components/ComA.vue'
</script>

子组件能够持续计数
<template>
<div>{{ val }}</div>
</template>

<script setup>
import { ref } from 'vue'

let val = ref(0)
setInterval(() => {
val.value++
}, 200)
</script>

关于路由

路由懒加载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import { createRouter, createWebHistory } from 'vue-router'
import HomeView from '../views/HomeView.vue'

const routes = [
{
path: '/',
name: 'home',
component: HomeView,
},
{
path: '/About',
name: 'about',
component: () => import('../views/About.vue'),
},
]

const router = createRouter({
history: createWebHistory(process.env.BASE_URL),
routes,
})

export default router

路由跳转

  • 使用 router-link
1
<router-link :to="{ name: 'xxx' }"> 点我跳转 </router-link>
  • push:选项式 API — 在选项式 API methods 中使用
1
2
3
4
5
6
7
<script>
methods: {
func: function() {
this.$router.push('/xxx')
}
}
</script>
  • push:组合式 API 中使用 — 需要引入 useRouter
1
2
3
4
5
6
7
8
<script setup>
import { useRouter } from 'vue-router'

const router = useRouter()
const func = () => {
router.push('/xxx')
}
</script>

404

  • 使用正则表达式匹配已有的路由,匹配失败的话就 404 喽
1
2
3
4
5
6
7
8
9
{
path: '/404',
name: '404',
component: NotFoundView
},
{
path: '/:catchAll(.*)',
redirect: '/404'
}

其他

  • 获取当前路由路径,可作为判据
1
2
3
4
5
6
7
8
9
// 方法一
window.location.pathname === '/xxx'

// 方法二
const route = useRoute()
const router = useRouter()
if (route.path !== `/xxx`) {
router.push({ name: 'xxx' })
}
  • goback
1
2
3
4
5
6
7
8
9
10
11
const router = useRouter()

// go: 原页面表单中的内容会丢失;
router.go(-1) // 后退+刷新
router.go(0) // 刷新
router.go(1) // 前进

// back: 原页表表单中的内容会保留;在返回界面传递参数;
router.back() // 后退
router.back(0) // 刷新
router.back(1) // 前进

Pinia

Pinia 是未来的趋势。它解决了 Vuex 很多的问题,Pinia 取消了 Vuex 的 Mutations,并且在 Action 中同时支持同步和异步;并且支持 Ts 和组合式 API 的语法。

https://pinia.web3doc.top/

  • 安装
1
2
3
4
5
npm install pinia

// main.js
import { createPinia } from 'pinia'
.use(createPinia())
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 { defineStore } from 'pinia'

export const testStore = defineStore('test', {
// 相当于 data
state: () => {
return {
count: 2,
name: 'mary',
age: 19,
}
},
// 相当于 methods
actions: {
func() {
this.count++
},
},
// 相当于 computed 调用时可直接当作属性
getters: {
testCount(state) {
return state.count * 2
},
},
})
  • 使用Store
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<template>
<div>
<button @click="add">click to add</button>
</div>
</template>

<script setup>
import { testStore } from '../store/TestStore'
import ChangeVal from './ChangeVal.vue'

const store = testStore()

const add = () => {
store.count++
}
</script>
  • storeToRefs

该方法能将Store的数据转化为转换为响应式数据

1
2
const st = testStore()
let { count, age } = storeToRefs(st)
  • js文件中使用Store (如路由页面中)
1
2
3
4
5
6
7
8
9
10
11
12
13
// 创建文件 store.js
import { createPinia } from 'pinia'
const pinia = createPinia()

export default pinia

// main.js 引入方式修改为
import pinia from '@/stores/store'
.use(pinia)

// 使用时
import pinia from '@/stores/store'
const store = countStore(pinia)

Axios

  • 安装
1
npm install axios --save
  • 使用
1
2
3
4
5
6
7
axios({
method:
url:
headers:
data:
}).then(res => {
})
  • 二次封装
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
import axios from 'axios'
import { ElNotification } from 'element-plus'
import { InfoStore } from '@/stores/InfoStore'
import pinia from '@/stores/store'

const infoStore = InfoStore(pinia)

const req1 = axios.create({
timeout: 10000,
withCredentials: true,
})

req1.interceptors.request.use((config) => {
config.headers.token = infoStore.token
return config
})

req1.interceptors.response.use((res) => {
if (res.data.code === 400) {
ElNotification({
title: res.data.msg,
type: 'warning',
})
return Promise.reject(res.data)
}
return res.data
})

const req2 = axios.create({
timeout: 5000,
withCredentials: true,
})

req2.interceptors.response.use((res) => {
if (res.data.code === 400) {
ElNotification({
title: res.msg,
type: 'warning',
})
// 强制退出登录
infoStore.clearInfo()
return Promise.reject(res.data)
}
return res.data
})

export { req1, req2 }

配置跨域

注意后续要在nginx中进行配置!!!

  • webpack
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// vue.config.js
const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
transpileDependencies: true,
devServer: {
proxy: {
'/api': {
target: 'http://127.0.0.1:8060',
pathRewrite: { '^/api': '' },
ws: true,
changeOrigin: true,
},
},
},
})
  • vite
1
2
3
4
5
6
7
8
9
10
11
12
13
// vite.config.js
export default defineConfig({
server: {
proxy: {
'/api/': {
target: 'http://127.0.0.1:8060/',
rewrite: (path) => path.replace(/^\/api/, '/api'),
changeOrigin: true,
ws: true,
},
},
},
})

工具类

Base64url展示图片

1
2
3
4
5
6
7
8
9
10
export function convert_to_url(string) {
const byteCharacters = atob(string)
const byteArrays = []
for (let i = 0; i < byteCharacters.length; i++) {
byteArrays.push(byteCharacters.charCodeAt(i))
}
const blob = new Blob([new Uint8Array(byteArrays)], { type: 'image/jpeg' })
const imageUrl = URL.createObjectURL(blob)
return imageUrl
}

AES加解密

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
npm install crypto-js --save

// 加密
import CryptoJS from 'crypto-js'
import pinia from '@/stores/store'
import { InfoStore } from '@/stores/InfoStore'

const infoStore = InfoStore(pinia)

const key = 'z2Se46GTYl6ajK2KXTp3XpZG5/ZzLJgT'

export function aesEncrypt() {
let keyHex = CryptoJS.enc.Utf8.parse(key)
const email = infoStore.email
let encrypted = CryptoJS.AES.encrypt(email, keyHex, {
mode: CryptoJS.mode.ECB,
padding: CryptoJS.pad.Pkcs7
})
return encrypted.toString()
}

export function SHA256Encrypt(pwd) {
return CryptoJS.SHA256(pwd).toString()
}

// 解密
export function aesDecrypt(str, key) {
let keyHex = CryptoJS.enc.Utf8.parse(key)
let decrypted = CryptoJS.AES.decrypt(str, keyHex, {
mode: CryptoJS.mode.ECB,
padding: CryptoJS.pad.Pkcs7
})
let result_value = decrypted.toString(CryptoJS.enc.Utf8)
return result_value
}