vue3 笔记
基本知识
因为是引用类型,语法上可以修改,并会影响父组件,只不过 eslint 校验不通过,此时只需要使用 vscode 修复下错误
1 2
| // eslint-disable-next-line vue/no-mutating-props
|
1-vite 基本使用
vue 项目脚手架工具,需要自己额外配置 ,所以在单纯学习 vue3 语法会使用它
vite 基本使用:
- 先下载 yarn create vite
- 创建项目
yarn create vite-app 项目名称
- 安装依赖
yarn
- 启动项目
yarn dev
总结:使用 vite 创建项目学习 vue3 语法,使用 vue-cli 创建项目正式开发。
vue2 使用选项 API
vue3 使用组合组合 API
2-setup 函数
组合 API 的起点,生命周期在 vue2 的 beforeCreate 之前执行,模板所需要的数据函数,需要在 setup
返回
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| <template> <div class="container"> <h1 @click="say()">{{msg}}</h1> //模板使用 </div> </template>
export default { setup () { const msg = 'hi vue3' // 数据 const say = () => { // 函数 console.log(msg) } return { msg , say} //return返回 },
|
总结:setup
组件初始化之前执行,它返回的数据和函数可在模版使用。
3-生命周期
vue3.0 生命周期钩子函数
setup
创建实例前
onBeforeMount
挂载 DOM 前
onMounted
挂载 DOM 后
onBeforeUpdate
更新组件前
onUpdated
更新组件后
onBeforeUnmount
卸载销毁前
onUnmounted
卸载销毁后
总结: 组合 API 的生命周期钩子有 7 个,可以多次使用同一个钩子,
4-reactive 函数
reactive 是一个函数,它只能定义一个复杂数据类型(数组和对象),成为响应式数据。
reactive 将数组值置空
1 2
| reactive([]) list.length = 0
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| <div>{{obj.name}}</div> <div>{{obj.age}}</div> <button @click="updateName">修改数据</button>
<script> import { reactive } from 'vue' //按需导入
setup () { const obj = reactive({ name: 'ls', age: 18 })
// 修改名字 const updateName = () => { console.log('updateName') obj.name = 'zs' }
return { obj ,updateName} }
|
5-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 27 28 29 30
| <template> <div class="container">{{ name }} <button @click="updateName">修改数据</button></div> </template> <script> import { reactive, toRef } from 'vue' export default { name: 'App', setup() { // 1. 响应式数据对象 const obj = reactive({ name: 'ls', age: 10, }) console.log(obj) // 2. 模板中只需要使用name数据 // 注意:从响应式数据对象中解构出的属性数据,不再是响应式数据 // let { name } = obj 不能直接解构,出来的是一个普通数据 const name = toRef(obj, 'name') // console.log(name) const updateName = () => { console.log('updateName') // toRef转换响应式数据包装成对象,value存放值的位置 name.value = 'zs' }
return { name, updateName } }, } </script> <style scoped lang="less"></style>
|
使用场景:有一个响应式对象数据,但是模版中只需要使用其中一项数据。
6-toRefs 函数
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 29 30 31 32 33 34 35 36 37
| <template> <div class="container"> <div>{{ name }}</div> <div>{{ age }}</div> <button @click="updateName">修改数据</button> </div> </template> <script> import { reactive, toRef, toRefs } from 'vue' export default { name: 'App', setup() { // 1. 响应式数据对象 const obj = reactive({ name: 'ls', age: 10, }) console.log(obj) // 2. 解构或者展开响应式数据对象 // const {name,age} = obj // console.log(name,age) // const obj2 = {...obj} // console.log(obj2) // 以上方式导致数据就不是响应式数据了 const obj3 = toRefs(obj) console.log(obj3)
const updateName = () => { // obj3.name.value = 'zs' obj.name = 'zs' }
return { ...obj3, updateName } }, } </script> <style scoped lang="less"></style>
|
使用场景:剥离响应式对象(解构|展开),想使用响应式对象中的多个或者所有属性做为响应式数据。
7-ref 函数
ref 函数,常用于简单数据类型定义为响应式数据
- 再修改值,获取值的时候,需要.value
- 在模板中使用 ref 申明的响应式数据,可以省略.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
| <template> <div class="container"> <div>{{ name }}</div> <div>{{ age }}</div> <button @click="updateName">修改数据</button> </div> </template> <script> import { ref } from 'vue' export default { name: 'App', setup() { // 1. name数据 const name = ref('ls') const updateName = () => { name.value = 'zs' } // 2. age数据 const age = ref(10)
return { name, age, updateName } }, } </script>
|
使用场景:
- 当你明确知道需要的是一个响应式数据 对象 那么就使用 reactive 即可
- 其他情况使用 ref
8-computed 函数
定义计算属性,计算属性不能修改。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| <template> <div class="container"> <div>今年:{{ age }}岁</div> <div>后年:{{ newAge }}岁</div> </div> </template> <script> import { computed, ref } from 'vue' export default { name: 'App', setup() { // 1. 计算属性:当你需要依赖现有的响应式数据,根据一定逻辑得到一个新的数据。 const age = ref(16) // 得到后年的年龄 const newAge = computed(() => { // 该函数的返回值就是计算属性的值 return age.value + 2 }) return { age, newAge } }, } </script>
|
给 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
| <template> <div class="container"> <div>今年:{{ age }}岁</div> <div>后年:{{ newAge }}岁</div> <!-- 使用v-model绑定计算属性 --> <input type="text" v-model="newAge" /> </div> </template> <script> import { computed, ref } from 'vue' export default { name: 'App', setup() { // 1. 计算属性:当你需要依赖现有的响应式数据,根据一定逻辑得到一个新的数据。 const age = ref(16)
// 计算属性高级用法,传人对象 const newAge = computed({ // get函数,获取计算属性的值 get() { return age.value + 2 }, // set函数,当你给计算属性设置值的时候触发 set(value) { age.value = value - 2 }, }) return { age, newAge } }, } </script>
|
目的:让计算属性支持双向数据绑定。 给 computed 传入对象,get 获取计算属性的值,set 监听计算属性改变。
9-watch 函数
watch 函数,是用来定义侦听器的
基本用法
1 2 3
| watch(count, (newVal,oldVal)=>{ console.log(newVal,oldVal) })
|
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
| const obj = reactive({ name: 'ls', age: 10, brand: { id: 1, name: '宝马' } }) const updateName = () => { obj.name = 'zs' } const updateBrandName = () => { obj.brand.name = '奔驰' } // 2. 监听一个reactive数据 watch(obj, ()=>{ console.log('数据改变了') })
watch(()=>obj.brand, ()=>{ console.log('brand数据改变了') },{ // 5. 需要深度监听 deep: true, // 6. 想默认触发 immediate: true })
// 3. 监听多个数据的变化 // watch([count, obj], ()=>{ // console.log('监听多个数据改变了') // })
// 4. 此时监听对象中某一个属性的变化 例如:obj.name // 需要写成函数返回该属性的方式才能监听到 // watch(()=>obj.name,()=>{ // console.log('监听obj.name改变了') // })
return {count, add, obj, updateName, updateBrandName} } } </script>
|
10-ref 属性
掌握使用 ref 属性绑定 DOM 或组件
1-先定义一个空的响应式数据 ref 定义的
2- setup 中返回该数据,你想获取那个 dom 元素,在该元素上使用 ref 属性绑定该数据即可。
1 2 3 4 5 6
| <div ref="dom">我是box</div>
const dom = ref(null) onMounted(()=>{ console.log(dom.value) // <div ref="dom">我是box</div> })
|
获取 v-for 遍历的元素
定义一个空数组,接收所有的 LI
定义一个函数,往空数组 push DOM
1 2 3 4 5 6 7 8 9 10 11 12 13
| <!-- 被遍历的元素 --> <ul> <li v-for="i in 4" :key="i" :ref="setDom">第{{i}}LI</li> </ul>
const domList = [] const setDom = (el) => { domList.push(el) } onMounted(()=>{ console.log(domList) }) return {dom, setDom}
|
11-父子通讯
掌握使用 props 选项和 emits 选项完成父子组件通讯
1.父传子:
1 2 3
| <Son :money="money" />
setup () { const money = ref(100) return { money } }
|
子组件:
1 2
| export default { name: 'Son', // 子组件接收父组件数据使用props即可 props: { money: { type: Number, default: 0 } }, setup (props) { // 获取父组件数据money console.log(props.money) }
|
2.子传父:
1 2 3 4 5 6 7 8
| <h1>子组件</h1> <p>{{money}}</p> <button @click="changeMoney">花50元</button>
// 子组件接收父组件数据使用props即可 props: { money: { type: Number, default: 0 } }, // props 父组件数据 // emit 触发自定义事件的函数 setup (props, {emit}) { // 获取父组件数据money console.log(props.money) // 向父组件传值 const changeMoney = () => { // 消费50元 // 通知父组件,money需要变成50 emit('change-money', 50) } return {changeMoney} } }
|
父组件:
1 2 3 4 5 6 7 8 9 10 11 12 13
| <h1>父组件</h1> <p>{{money}}</p> <hr> <Son :money="money" @change-money="updateMoney" />
setup () { const money = ref(100) const updateMoney = (newMoney) => { money.value = newMoney } return { money , updateMoney} } }
|
在 vue3.0 的时候,使用 v-model:money="money"
即可
1 2
| <!-- <Son :money="money" @update:money="updateMoney" /> --> <Son v-model:money="money" />
|
总结:
- 父传子:在 setup 种使用 props 数据
setup(props){ // props就是父组件数据 }
- 子传父:触发自定义事件的时候 emit 来自
setup(props,{emit}){ // emit 就是触发事件函数 }
- 在 vue3.0 中
v-model
和 .sync
已经合并成 v-model
指令
12-依赖注入
目标:掌握使用 provide 函数和 inject 函数完成后代组件数据通讯
父组件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| <h1>父组件 {{money}} <button @click="money=1000">发钱</button> <Son />
setup () { const money = ref(100)
const changeMoney = (saleMoney) => { console.log('changeMoney',saleMoney) money.value = money.value - saleMoney } // 将数据提供给后代组件 provide provide('money', money) // 将函数提供给后代组件 provide provide('changeMoney', changeMoney)
return { money } }
|
子组件
1 2 3 4 5
| <h2>子组件 {{money}}</h2> <GrandSon />
import { inject } from 'vue' setup () { // 接收祖先组件提供的数据 const money = inject('money') return { money } }
|
孙组件
1 2 3 4 5 6
| <h3>孙组件 {{money}} <button @click="fn">消费20</button></h3>
import { inject } from 'vue' setup () { const money = inject('money') // 孙组件,消费50,通知父组件App.vue组件,进行修改 // 不能自己修改数据,遵循单选数据流原则,大白话:数据谁定义谁修改 const changeMoney = inject('changeMoney') const fn = () => { changeMoney(20) } return {money, fn}
|
总结:
- provide 函数提供数据和函数给后代组件使用
- inject 函数给当前组件注入 provide 提供的数据和函数
- inject 拿到的数据为 ref,所以要加 value
13-v-model 语法糖
目标:掌握 vue3.0 的 v-model 语法糖原理
父组件
1 2 3 4
| <!-- <Son :modelValue="count" @update:modelValue="count" /> --> <Son v-model="count" />
setup () { const count = ref(10) return { count } }
|
子组件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| <h2>子组件 {{modelValue}} <button @click="fn">改变数据
props: { modelValue: { type: Number, default: 0 } }, setup (props, {emit}) { const fn = () => { // 改变数据 emit('update:modelValue', 100) } return { fn } }
|
总结: vue3.0 封装组件支持 v-model 的时候,父传子:modelValue
子传父 @update:modelValue
组合式 API(setup)
1-侦听器
属性:{ immediate: true } 立即执行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| const user = reactive({ name: 'tom' })
watch(user, (value) => { console.log(value) })
watch(user.name, (value) => { console.log(value) })
watch(() => user.name, (value) => { console.log(value); })
user.name = 'jake'
第三参数:立即执行 { immediate: true }
|
2-计算属性
1 2 3 4 5 6 7 8
| // 构建指示点的数组 const indicatorData = computed(() => { const arr = [] for (let i = 0; i < props.sliders.length; i++) { arr.push(i) } return arr })
|
3-注册插件
components/library/index.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| import XtxSkeleton from './xtx-skeleton.vue'
export default { install(app) { // 在app上进行扩展,app提供 component directive 函数 // 如果要挂载原型 app.config.globalProperties 方式 // 注册全局组件 app.component('xtx-skeleton', XtxSkeleton) app.component('xtx-carousel', XtxCarousel) app.component('xtx-more', XtxMore)
// 自定义指令 defineDirective(app) } }
|
4-路由(重点)
1 2 3
| 当前路由路径: route.path 当前路由参数: route.params 当前路由查询参数: route.query
|
1-路由配置
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 25 26 27
| import { createRouter, createWebHashHistory } from 'vue-router' import store from '@/store'
const Layout = () => import('@/views/Layout')
const routes = [ { path: '/', name: 'Layout', component: Layout, children: [ { path: '/', name: 'home', component: home } ] }
export default router
|
3-创建路由实例
挂载路由规则
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| const router = createRouter({ / 模糊匹配,只要路由第一层相同就会加上active样式 linkExactActiveClass: 'active', history: createWebHashHistory(), routes, scrollBehavior() { return { left: 0, top: 0 } } })
|
路由切换时 控制滚动条 scrollBehavior()
4-路由导航守卫
使用场景:做页面权限 某些页面需要做登录
1 2 3 4 5 6 7 8 9 10 11 12 13
| router.beforeEach((to, from, next) => { if (!store.state.user.profile.token && to.path.startsWith('/member')) { next({ path: '/login', query: { redirectUrl: encodeURIComponent(to.fullPath), }, }) } next() })
|
encodeURIComponent() 转换 uri 编码
解码:decodeURIComponent(游览器会自动解析,一般不需要手动解码)
2-封装 axios 请求
创建一个新的 axios 实例
请求拦截器,如果有 token 进行头部携带
响应拦截器:1. 剥离无效数据 2. 处理 token 失效
导出一个函数,调用当前的 axsio 实例发请求,返回值 promise
创建utils/request.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| import axios from 'axios' import store from '@/store' import router from '@/router'
export const baseURL = 'https://apipc-xiaotuxian-front.itheima.net/'
const request = axios.create({ baseURL, timeout: 5000 })
2- 请求拦截器,如果有token进行头部携带
3. 响应拦截器:1. 剥离无效数据 2. 处理token失效
4. 导出一个函数,调用当前的axsio实例发请求,返回值promise
|
2– 请求拦截器
添加 token 请求头
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| request.interceptors.request.use( (config) => { const { profile } = store.state.user
if (profile.token) { config.headers.Authorization = `Bearer ${profile.token}` } return config }, (err) => { return Promise.reject(err) } )
|
3–响应拦截器
处理权限不够保存当前地址,并跳转到登录页面
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
|
request.interceptors.response.use( (res) => res.data, (err) => { if (err.response && err.response.status === 401) { store.commit('user/setUser', {}) const fullPath = encodeURIComponent(router.currentRoute.value.fullPath)
router.push('/login?redirectUrl=' + fullPath) } return Promise.reject(err) } )
|
router.currentRoute.value.fullPath 当前路由地址
(router.currentRoute 是 ref 响应式数据)
加码 encodeURIComponent( )
4-导出一个函数,调用当前的 axsio 实例发请求,返回值 promise
1 2 3 4 5 6 7 8 9
| export default (url, method, submitData) => { return request({ url, method, [method.toLowerCase() === 'get' ? 'params' : 'data']: submitData, }) }
|
5–使用
1 2 3 4 5 6
| 1 - 导入 import http from '@/utils/request'
export const getSpecsAndSkus = (skuId) => { return http(`/goods/sku/${skuId}`, 'get') }
|
使用场景二
1 2 3 4 5
|
export const findCommentsList = (params, id) => { return http(`https://mock.boxuegu.com/mock/1175/goods/${id}/evaluate/page`, 'get', params) }
|
5-动态组件
1 2
| <!-- currentTab 改变时组件也改变 --> <component :is="tabs[currentTab]"></component>
|
在上面的例子中,被传给 :is
的值可以是以下几种:
当使用 <component :is="...">
来在多个组件间作切换时,被切换掉的组件会被卸载。我们可以通过强制被切换掉的组件仍然保持“存活”的状态。
6-KeepAlive
缓存动态组件
7-Transition 组件
过渡样式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| <Transition name="down"> <div>包裹要加的样式</div> </Transition>
<style scoped lang="less"> // 动画的样式 .down { &-enter { &-from { transform: translateY(-75px); opacity: 0; }
&-active { transition: all 0.5s; }
&-to { transform: none; opacity: 1; } } }
|
8-vuex
store/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
| import { createStore } from 'vuex'
import createPersistedstate from 'vuex-persistedstate'
import cate from './modules/cate'
export default createStore({ modules:{ cate }, plugins:[ createPersistedstate({ storage: window.localStorage, paths: ['app','user ,'permission'] }), // 需要sessionStorage存储起来的模块 createPersistedstate({ storage: window.sessionStorage, paths: ['platform', 'upload'] }) ]
|
store/modules
下的各个模块
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| export default createStore({ state: { username: 'zs' }, mutations: { getData(state, payload) { }, actions: { }, getters: { }
|
功能
1-less 封装样式函数
src/assets/style/mixins.less
1 2 3 4 5 6 7 8 9
| // 鼠标经过移阴影动画 .hoverShadow() { transition: all .5s;
&:hover { transform: translate3d(0, -3px, 0); box-shadow: 0 3px 8px rgba(0, 0, 0, 0.2); } }
|
使用
1 2 3 4 5 6 7 8
| 全局使用 li { width: 306px; height: 406px; background: #f0f9f4; // 使用mixinLess中的函数 .hoverShadow(); }
|
2-数据懒加载(滚动加载)
hook/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 { useIntersectionObserver } from '@vueuse/core'
export const useLazyData = (target, callback) => { const { stop } = useIntersectionObserver( target, ([{ isIntersecting }]) => { if (isIntersecting) { stop()
callback && callback() } } ) }
|
使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| 导入ref import { ref } from 'vue' 导入api import { findNew } from '@/api/home' 导入数据懒加载函数 import { useLazyData } from '@/hook'
1-要监听的dom对象 const newGoodsRef = ref(null) <div class="home-new" ref="newGoodsRef">
useLazyData(newGoodsRef, () => { findNew().then(({ result }) => { newList.value = result console.log(result) }) })
|
3-组件插槽
1 2 3 4 5 6 7 8 9 10
| 创建组件 <div class="container"> <div class="head"> <h3>{{ props.title }}<small>{{ props.subTitle }}</small></h3> <!-- 具名插槽-> <slot name="right" /> </div> <!-- 内容部分默认插槽 --> <slot /> </div>
|
使用
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 HomePanel from '../com/home-panel.vue' 导入
<HomePanel title="新鲜好物" subTitle="新鲜出炉 品质靠谱"> <!-- 顶部的右边插槽 --> <template #right> <xtx-more></xtx-more> </template> <Transition name="fade"> <!-- 默认插槽 --> <ul class="goods-list" v-if="newList && newList.length"> <li v-for="item in newList" :key="item.id"> <RouterLink :to="`/product/${item.id}`"> <img :src="item.picture" alt=""> <p class="name ellipsis">{{ item.name }}</p> <p class="price">¥{{ item.price }}</p> </RouterLink> </li> </ul> <!-- 骨架屏组件 --> <HomeSkeleton v-else bg="#f0f9f4"></HomeSkeleton> </Transition> </HomePanel>
|
总结具名插槽使用<template #right>或
4-批量自动导入注册组件
src/components/library/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
| 批量导入需要使用一个函数 require.context(dir,deep,matching) 参数:1. 目录 2. 是否加载子目录 3. 加载的正则匹配 const importFn = require.context('./', false, /\.vue$/)
export default { install(app) { importFn.keys().forEach(key => { const component = importFn(key).default
const comName = key.slice(2, -4)
app.component(comName, component) })
defineDirective(app) } }
|
5-v-model 语法糖
父组件
1
| <xtx-checkbox v-model="sortParams.inventory">仅显示有货商品</xtx-checkbox>
|
v-model 双向绑定
子组件
方式 1(useVModel)
使用 useVModel 实现双向数据绑定 v-model 指令
使用 props 接收 modelValue
使用 useVModel 来包装 props 中的 modelValue 属性数据
在使用 checked.value 就是使用父组件数据
在使用 checked.value = ‘数据’ 赋值,触发 emit(‘update:modelValue’, ‘数据’)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| <script setup> import { defineProps, defineEmits } from 'vue' import { useVModel } from '@vueuse/core' const props = defineProps({ modelValue: { type: Number, default: 1, }, }) const emit = defineEmits(['update:modelValue']) // 使用useVmodel函数实现双向数据绑定,num是一个ref声明的响应式数据 const num = useVModel(props, 'modelValue', emit)
function changeNum(step) { let newNum = num.value + step if (newNum < props.min) newNum = props.min if (newNum > props.max) newNum = props.max num.value = newNum } </script>
|
方式 2(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
| <div class="xtx-checkbox" @click="changeChecked()"> <i v-if="checked" class="iconfont icon-checked"></i> <i v-else class="iconfont icon-unchecked"></i>
import { ref, defineProps, watch, defineEmits } from 'vue' const props = defineProps({ modelValue: { type: Boolean, default: false } })
const emit = defineEmits(['update:modelValue']) const checked = ref(null)
watch(() => props.modelValue, (newVal) => { checked.value = newVal }, { immediate: true })
const changeChecked = () => { emit('update:modelValue', !checked.value) }
|
6-h 函数(创建虚拟 dom)
- h(标签, {属性},内容)
- h(标签, {属性},[可以继续嵌套 h()])
- createVNode(标签, {属性},内容)
- createVNode(标签, {属性},[可以继续嵌套 createVNode()])
h 函数:第一个参数 标签名字 第二个参数 标签属性对象 第三个参数 子节点
1 2
| dymanicItems.push(h('i', { class: 'iconfont icon-angle-right' })) h('div', { class: 'xtx-bread' }, dymanicItems)
|
使用
注意:setup 写法不可用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| <script> import { h } from 'vue' export default { name: 'xtx-bread', // 创建组件内容 render() { // 获取动态插槽 const items = this.$slots.default() const dymanicItems = [] items.forEach((item, index) => { dymanicItems.push(item) // 最后一级不需要加 > if (index < items.length - 1) { dymanicItems.push(h('i', { class: 'iconfont icon-angle-right' })) } }) // h函数:第一个参数 标签名字 第二个参数 标签属性对象 第三个参数 子节点 return h('div', { class: 'xtx-bread' }, dymanicItems) } } </script>
|
7-组件挂载原型对象
- h(标签, {属性},内容)
- h(标签, {属性},[可以继续嵌套 h()])
- createVNode(标签, {属性},内容)
- createVNode(标签, {属性},[可以继续嵌套 createVNode()])
以 message 提示框为列
1.封装组件
2.封装 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
| createVNode:创建虚拟dom import { createVNode, render } from 'vue'
// 创建一个真实dom const div = document.createElement('div') div.setAttribute('class', 'xtx-message-container') 追加到body内 document.body.appendChild(div)
将函数导出 export default ({ type, text, time = 2000 }) => { // 1.以xtx-message.vue组件为虚拟dom的模板创建虚拟dom节点 const vnode = createVNode(XtxMessage, { type, text }) // 2.将虚拟dom节点挂载到真实的dom中进行显示 render(vnode, div) setTimeout(() => { render(null, div) }, time) }
|
2.5 可以传递函数(控制创建销毁)
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
| export default ({ title, text }) => { return new Promise((resolve, reject) => { const submitCallback = () => { resolve()
render(null, div) }
const cancelCallback = () => { render(null, div)
reject(new Error('已经取消')) }
const vnode = createVNode(XtxConfirm, { title, text, submit: submitCallback, cancel: cancelCallback, }) render(vnode, div) }) }
const props = defineProps({ title: { type: String, default: '温馨提示', }, text: { type: String, default: '您确认从购物车删除该商品吗?', }, submit: { type: Function, }, cancel: { type: Function, }, })
|
3.导入挂载
- 如果要挂载原型 app.config.globalProperties 方式
1 2 3 4
| import Message from './message' install(app) { app.config.globalProperties.$message = Message }
|
4. 使用方法一 获取 app 原型
1 2 3 4 5 6
| 获取实例函数 import { getCurrentInstance } from 'vue' const { proxy: app } = getCurrentInstance() // 获取原型对象 使用 app.$message({ type: 'error', text: err.response.data.message || '登录失败', time: 3000 })
|
4.5 使用方法二
1 2 3 4 5
| 1 直接导入message.js import Message from '@/components/library/message'
2 使用 Message({ type: 'success', text: '添加购物车成功' })
|
8-监听滚动条
导入@vueuse/core 内的 useIntersectionObserver
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| <div class="xtx-infinite-loading" ref="target">
import { useIntersectionObserver } from '@vueuse/core' const target = ref(null) 绑定dom useIntersectionObserver( target, ([{ isIntersecting }])=>{ if(isIntersecting){ emit('load')要触发的事件 } }, { threshold: 0 } )
|
2 数据懒加载的封装
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
| import { useIntersectionObserver } from '@vueuse/core'
export const useLazyData = (target, callback) => { const { stop } = useIntersectionObserver( target, ([{ isIntersecting }]) => { if (isIntersecting) { stop()
callback && callback() } } ) }
使用方法
import { useLazyData } from '@/hook'
const newList = ref([])
const newGoodsRef = ref(null) useLazyData(newGoodsRef, () => { findNew().then(({ result }) => { newList.value = result }) })
|
9-获取其他地址数据
1-获取非封装地址数据
2-做在内存中做缓存
3-封装 promise 函数
地址数据 https://yjy-oss-files.oss-cn-zhangjiakou.aliyuncs.com/tuxian/area.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
| <script setup> await getCityData() 这里调用 </script> <script> const getCityData = () => { // 这个位置可能有异常操作,封装成promise return new Promise((resolve, reject) => { if (window.cityData) { // console.log('读缓存') // 有缓存 resolve(window.cityData); } else { // console.log('调接口') // 无缓存 const url = "https://yjy-oss-files.oss-cn-zhangjiakou.aliyuncs.com/tuxian/area.json"; axios.get(url).then((res) => { // 数据保存在内存缓存中 window.cityData = res.data; resolve(window.cityData); }); } }); }; </script>
|
10-vee-validate(表单验证)
1
| 1-安装 "vee-validate": "4.0.3"
|
版本号过高有影响
1
| 2-导入 import { Form, Field } from 'vee-validate'
|
Field 特性(as)
当将组件名称变成 Field 后就变成了文本输入框,如果希望保持原来组件的特性,使用 as 属性指向组件名称即可
1 2
| !-- 复选框组件:, --> <Field as="xtx-checkbox" name="isAgree" v-model="form.isAgree" />
|
Field 替换的是组件,as 指向组件名,可保留其特性
3-使用
- Form 替换 form 元素 Field 替换 input 元素
- ref 绑定 Form 元素(用于清空信息,验证规则)
- 给 Form 提供验证规则 :validation-schema=”schema”
- Field 给 name 绑定验证属性 name=”account”
- Form 下作用域插槽解构出 errors v-slot=”{ errors }”
- 通过 errors.属性 拿到错误信息 errors.account
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| <Form ref="loginForm" class="form" :validation-schema="schema" v-slot="{ errors }" autocomplete="off" > <div class="form-item"> <div class="input"> <i class="iconfont icon-user"></i> <Field :class="{ error: errors.account }" type="text" v-model="form.account" name="account" placeholder="请输入用户名或手机号" /> </div> <!-- 如果验证失败,错误显示在此处,验证通过不显示--> <div class="error" v-if="errors.account"><i class="iconfont icon-warning" /> {{ errors.account }}</div> </div> </Form>
|
4-验证规则
src/config/index
下创建验证规则
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| export const myFormValidate = { account(value) { if (!value) return '请输入用户名' if (!/^[a-zA-Z]\w{5,19}$/.test(value)) return '字母开头且6-20个字符' return true }, password(value) { if (!value) return '请输入密码' if (!/^\w{6,24}$/.test(value)) return '密码是6-24个字符' return true }, isAgree(value) { if (!value) return '请勾选同意用户协议' return true }, }
|
4.1-导入并使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| import { myFormValidate } from '@/config'
const schema = { account: myFormValidate.account, password: myFormValidate.password, mobile: myFormValidate.mobile, code: myFormValidate.code, isAgree: myFormValidate.isAgree, }
const form = reactive({ isAgree: true, account: 'lau0400810121', password: '123456', mobile: null, code: null, })
|
5-验证表单
1 2 3 4 5 6 7 8 9
| async function login() { const valid = await loginForm.value.validate() if (valid) { } else { } }
|
- 验证失败的信息清空:loginForm.value.resetForm()
- 验证整体表单:const valid = await loginForm.value.validate()(注意:异步操作)
6-验证单条数据
1 2 3
| 直接从验证规则内拿到 该验证规则,通过返回值判断 const mobileValid = schema.mobile(from.mobile) if (mobileValid === true) {ok}
|
11-定时器
- useIntervalFn(回调函数,执行间隔,是否立即开启)
- pause()关闭定时器
- resume()启动定时器
导入
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| import { useIntervalFn } from '@vueuse/core'
const { pause, resume } = useIntervalFn( () => { time.value-- if (time.value <= 0) { pause() time.value = 60 } }, 1000, { immediate: false } ) 记得在组件销毁时将其暂停 onUnmounted(() => { pause() })
|
12-第三方登录 qq
第一步:参考文档:
1 2 3 4
| # 测试用appid # 100556005 # 测试用redirect_uri # http://www.corho.com:8080/#/login/callback
|
本地测试方法
1 2 3
| windows 1. 找到 C:\Windows\System32\drivers\etc 下hosts文件 2. 在文件中加入 127.0.0.1 www.corho.com
|
1-在 vue.config.js 添加
1 2 3 4 5 6
| # 这个是设置外部扩展,模块为qc变量名为QC,导入qc将不做打包。 configureWebpack: { externals: { qc: 'QC' } },
|
2-这里要加 appid
public/index.html
1 2 3 4 5
| <script type="text/javascript" src="http://qzonestyle.gtimg.cn/qzone/openapi/qc_loader.js" data-callback="true" data-appid="你自己的appid" data-redirecturi="你自己的回调地址" charset="utf-8"> </script>
例子:<script src="http://connect.qq.com/qc_jssdk.js" data-appid="100556005" data-redirecturi="http://www.corho.com:8080/#/login/callback"></script>
|
3-拿到链接(否则会跳出新窗口)
1 2 3 4 5 6 7 8 9 10 11 12 13
| // 导入QQ登录模块 // import QC from 'qc' // QQ登录 // onMounted(() => { // QC.Login({ // btnId: 'qqLoginBtn' // }) // })
<!-- QQ登录 会跳别的页面 --> <!-- <span id="qqLoginBtn"> </span> --> span改为如下 <!-- 实现当前页面跳转 --> <a href="https://graph.qq.com/oauth2.0/show?which=Login&display=pc&client_id=100556005&response_type=token&scope=all&redirect_uri=http%3A%2F%2Fwww.corho.com%3A8080%2F%23%2Flogin%2Fcallback" > <img src="https://qzonestyle.gtimg.cn/qzone/vas/opensns/res/img/Connect_logo_7.png" alt="" /> </a>
|
4-扫码后跳回页面
默认跳到login/callback
路由下(待验证),所以要配置路由
需要一个回调页面:
路由规则 src/router/index.js
1 2 3
| const LoginCallback = () => import('@/views/login/callback') { path: '/login', component: Login }, + { path: '/login/callback', component: LoginCallback }
|
三个组件
1.回调页面(公共组件) 2.已注册未绑定 3.未绑定有账户
src/views/login/callback.vue
回调页面(判断有无绑定,有无账户)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| import QC from 'qc'
const unionId = ref(null)
if (QC.Login.check()) {
QC.Login.getMe(async openid => {
unionId.value = openid try{ const { result } = await userQQLogin(openid, 6)
}catch(err){ }
|
4.2 若有账户,未绑定
1 2 3 4 5 6 7 8
| import QC from 'qc'
if (QC.Login.check()) { QC.api('get_user_info').success(res=>{
avatar.value = res.data.figureurl_2 nickname.value = res.data.nickname })
|
总结:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| import QC from 'qc'
<span id="qqLoginBtn"></span> onMounted(() => { QC.Login({ btnId: 'qqLoginBtn' }) })
if(QC.Login.check())
QC.Login.getMe(async openid => { unionId.value = openid
QC.api('get_user_info').success(res=>{ avatar.value = res.data.figureurl_2 nickname.value = res.data.nickname })
|
13-Promise 函数
❎
内部方法
在 Promise 对象内部,有一些函数可以使用:
- ✅
then(onFulfilled, onRejected)
- 注册 Promise 成功或失败时的回调函数。
- ✅
catch(onRejected)
- 注册 Promise 失败时的回调函数。
finally(onFinally)
- 注册 Promise 状态变化时的回调函数,无论它是成功还是失败。
- ✅
Promise.resolve(value)
- 返回一个已经解决的 Promise ,它立即完成并返回给定的值。
- ✅
Promise.reject(reason)
- 返回一个已拒绝的 Promise ,将其拒绝原因设置为给定的原因。
- ✅
Promise.all(iterable)
- 接受一个可迭代对象,并返回一个新的 Promise ,该 Promise 在所有输入 Promise 都已成功解决时解决,或在任何一个 Promise 拒绝时拒绝。
Promise.race(iterable)
- 接受一个可迭代对象,并返回一个新的 Promise ,该 Promise 解决或拒绝时,该可迭代对象中的第一个 Promise 决定其结果。
一、 Promise 对象有三种状态,他们分别是:
pending: 等待中,或者进行中,表示还没有得到结果
resolved(Fulfilled): 已经完成,表示得到了我们想要的结果,可以继续往下执行
rejected: 也表示得到结果,但是由于结果并非我们所愿,因此拒绝执行
1 2 3 4 5 6 7 8 9 10 11
| updateCart(context, good) { return new Promise((resolve, reject) => { if (context.rootState.user.profile.token) { } else { context.commit('updateCart', good) resolve('ok') } }) }
|
链式调用
1 2 3 4 5 6 7 8 9 10 11
| const skuIds = context.getters.validList.map((item) => item.skuId) checkAllCart({ selected: status, ids: skuIds, }) .then(() => { return getCartList() }) .then((data) => { context.commit('setCartList', data.result) })
|
promise 的异常穿透
(1)当使用 promise 的 then 链式调用时,可以在最后指定失败的回调。
(2)前面任何操作出 了异常,都会传到最后失败的回调中处理。
.all()函数使用
循环执行异步操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| const skuIds = context.state.lists.map(item => item.skuId) const promiseAll = context.state.lists.map(item => getNewCartGoods(item.skuId))
// Promise.all()执行,确保按照顺序执行 Promise.all(promiseAll).then(data => { // 同步更新 data.forEach((item, index) => { console.log(item) context.commit('updateCart', { skuId: skuIds[index], ...item }) }) resolve() })
|
14-调用子组件的方法
- 父组件:ref 绑定子组件
- 父组件:addressAddCom.value.open() 调用子组件方法
- 子组件:定义 open 方法
- 子组件:使用 defineExpose()向外暴露
父组件:
1 2 3 4 5 6 7 8 9
| <xtx-button @click="openAddDialog">添加地址</xtx-button> <AddressAdd ref="addressAddCom"></AddressAdd>
const addressAddCom = ref(null) function openAddDialog() { addressAddCom.value.open() }
|
子组件:
1 2 3 4 5 6 7 8 9 10 11 12 13
| import { ref, defineExpose } from 'vue' const dialogVisible = ref(false)
function open() { dialogVisible.value = true }
defineExpose({ open, })
|
15-第三方支付(支付宝)
总结:
- 点击支付,从后台提供的支付链接+回调地址+订单 ID
- 后台服务发起支付—–等待结果—-修改订单状态,回跳道 PC 前台结果页
- PC 前台在结果页获取回调的参数(i 订单 d),查询订单支付状态,展示支付结果
回调地址
1 2 3 4 5 6 7 8
| export const alipayRedirectUrl = 'http://www.corho.com:8080/#/member/callback'
const redirect = encodeURIComponent(alipayRedirectUrl)
ocation.href = baseURL + `/pay/aliPay?redirect=${redirect}&orderId=${route.query.id}`
|
在做一个回调页面
16-mock 数据
文档: http://mockjs.com/
目标:模拟 /my/test
接口,随机返回点数据。
Mock 有一个拦截 axios 的作用,肯定优先匹配 mock 中的 api,如果 mock 中没有,就请求真实的 api
基本使用步骤:
- 安装
- 配置
src/mock/index.js
1 2 3 4 5 6 7
| import Mock from 'mockjs'
Mock.setup({ timeout: '500-1000', })
|
- 导入
src/main.js
1 2 3
| import 'normalize.css' import '@/assets/styles/common.less' + import './mock'
|
- 模拟接口,拦截请求
1 2 3 4 5 6 7
|
Mock.mock(/\/my\/test/, 'get', () => { return { msg: '请求测试接口成功', result: [] } })
|
- 生成随机数据
1 2 3 4 5 6 7
| Mock.mock('@integer(0,7)')
Mock.mock({ id: '@id', name: '@ctitle(2,4)', })
|
具体规则:http://mockjs.com/examples.html
完整代码
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
| import Mock from 'mockjs'
import qs from 'qs'
Mock.setup({ timeout: '500-1000', })
Mock.mock(/\/member\/collect/, 'get', (config) => { const queryString = config.url.split('?')[1] const queryObject = qs.parse(queryString) const items = [] for (let i = 0; i < +queryObject.pageSize; i++) { items.push( Mock.mock({ id: '@id', name: '@ctitle(10,20)', desc: '@ctitle(4,10)', price: '@float(100,200,2,2)', picture: "@image('50*50','#FF83FA','#FCFCFC','png','mono')", }) ) } return { msg: '获取收藏商品成功', result: { counts: 35, pageSize: queryObject.pageSize, page: queryObject.page, items, }, } })
|
17-jsx 语法
使用场景:在父组件内渲染子组件
vue3 使用 jsx 语法需要下载
1
| yarn add -D @vue/cli-plugin-babel
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| setup(){ 这里写方法变量 }, rebder(){ const nav = ( <nav> {dynamicPanels.map((item) => { return (<a onClick={() => this.changeTab(item.props.name)} className={this.activeName === item.props.name ? "active" : ""} href="javascript:;">{item.props.label} </a>); })} </nav> );
return <div className="xtx-tabs">{[nav, dynamicPanels]}</div>;
}
|
注意事项:如果子组件插槽是循环遍历出来的,this.$slots.default();那么该方法获取到的子组件在children内
vueuse/core(工具库)
import { onClickOutside } from ‘@vueuse/core’
1-dom 失焦
监听绑定的 dom 之外的点击事件
onClickOutside (dom,()=>{})
1 2 3 4 5 6 7
| <div class="cart-sku" ref="target">
// 鼠标点击attrs这个dom外部时隐藏sku区域 const target = ref(null) onClickOutside(target, () => { hide() })
|