这里仅的记录我在使用 vue 过程中踩过的坑和常用的代码板子
本文标题间毫无逻辑关系,纯粹想到哪记到哪
关于组合式 API
<script setup> 这块语法糖甜到我根本找不到方向:单文件组件 | Vue.js (vuejs.org)
defineProps 与 defineEmits
- 父子组件传参的写法有了很大的变化我们需要使用
defineProps和defineEmits代替原来的props和context.emit
- 要注意
defineProps和defineEmits都是只在<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
import { defineConfig } from 'vite' import DefineOptions from 'unplugin-vue-define-options/vite' import vue from '@vitejs/plugin-vue'
export default defineConfig({ plugins: [ vue(), DefineOptions() ] })
{ "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
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
import '@icon-park/vue-next/styles/index.css'
|
-
使用
- 从网站复制 vue 代码即可
- 注意使用时要单独引入,引入的命名方式是
kebab-case转CamelCase
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
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) { 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
import 'uno.css'
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
|
路由跳转
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' }) }
|
1 2 3 4 5 6 7 8 9 10 11
| const router = useRouter()
router.go(-1) router.go(0) router.go(1)
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
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', { state: () => { return { count: 2, name: 'mary', age: 19, } }, actions: { func() { this.count++ }, }, getters: { testCount(state) { return state.count * 2 }, }, })
|
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>
|
该方法能将Store的数据转化为转换为响应式数据
1 2
| const st = testStore() let { count, age } = storeToRefs(st)
|
1 2 3 4 5 6 7 8 9 10 11 12 13
| import { createPinia } from 'pinia' const pinia = createPinia()
export default pinia
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中进行配置!!!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| 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, }, }, }, })
|
1 2 3 4 5 6 7 8 9 10 11 12 13
| export default defineConfig({ server: { proxy: { '/api/': { target: 'http://127.0.0.1:8060/', rewrite: (path) => path.replace(/^\/api/, '/api'), changeOrigin: true, ws: true, }, }, }, })
|
工具类
Base64转url展示图片
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 }
|