vue3学习笔记

380 阅读13分钟
  • 近期,整理下vue3的学习笔记,仅供参考!

vue3 + ts + vite 学习

vue2 和 vue3 的区别:

  1. 数据更新方式 v2 采用 Object.defineProperty, v3 使用 proxy
  2. tree-shaking
  3. fragments
  4. diff 算法不同
  5. 支持 ts

配置环境

npm init vite@latest 创建项目 在 vite-env.d.ts 中配置声明文件,ts 本身不能识别.vue 的文件,可能需要手动配置

vue3 基础

响应式数据:

ref

  支持所有类型,此对象只有一个指向其内部值的属性 .value。
  ```js
  ref() // 需要使用.value的形式
  reactive()
  shallowRef() // 浅层,.value外部状态管理,替换整个个对象
  triggerRef() //  强制触发依赖于一个浅层 ref 的副作用,

  // 防抖使用
  customRef((track, trigger) => {
      get() {
          track()
      },
      set() {
          trigger()
      }
  })

  // 可以使用ref来绑定一个dom, 读取dom属性
  <div ref="dom">this is a dom!!</div>
  const dom = ref<HTMLDivElement>()
  console.log(dom.value?.innerText)

reactive

只支持引用类型 Array Object Map Set 不需要使用.value 形式修改值, 直接修改

  • reactive 中使用 proxy 不能直接赋值,否则会破坏响应式结构,解决方案: 数组可以使用 push + 解构; 对象中添加一个数组属性
      let list1 = list.push(...res)   // 法1
      let list2 = reactive<{ arr: string[] }>({ arr: [] }) // 法2
    

readonly 只读属性

let obj = reactive({ name: 'wxt' })
const read = readonly(obj)

const add = () => {
  read.name = '2323' // 此时两者都为wxt, 不可修改; 若obj.name修改,则两者都变为2323, reactive会影响readonly属性
  console.log(obj, read)
}

shallowReactive 个浅层响应式对象里只有根级别的属性是响应式的, 会被 reactive 的属性影响,注意:若根元素发生 reactive 变化,此时深层次的也会发生变化

toRef toRefs, toRaw

toRef 基于响应式对象上的一个属性,创建一个对应的 ref。toRef(object, key), 使用 reactive + .value 使用

  • 使用场景: useSomeFeature(toRef(props, 'foo')) 作为函数的参数传入

toRefs 解构不失响应式属性, 功能:每个属性都是使用 toRef() 创建的响应式对象const {a, b} = toRefs(oo)

toRaw 返回由 reactive()、readonly()、shallowReactive() 或者 shallowReadonly() 创建的代理对应的原始对象。

vue3 响应式原理

// effect 函数---副作用函数
let activeEffect: Function // 闭包,保存副作用函数

export const effect = (fn: Function) => {
  const _effect = function () {
    activeEffect = _effect
    fn()
  }

  _effect()
}

const targetMap = new WeakMap()
// track 追踪
export const track = (target: object, key: any) => {
  // 第一层
  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)
}

// trigger 触发
export const trigger = (target: object, key: any) => {
  const depsMap = targetMap.get(target)
  const deps = depsMap.get(key)

  deps.forEach((effect: Function) => effect())
}

// reactive函数
const isObject = (target: any) => target != null && typeof target == 'object'

export const reactive = <T extends object>(target: T): T => {
return new Proxy(target, {
  get(target, key, receiver) {
    const res = Reflect.get(target, key, receiver) as object
    track(target, key)

    if (isObject(res)) {
      return reactive(res)
    }

    return res
  },
  set(target, key, value, receiver) {
    const res = Reflect.set(target, key, value, receiver)

    trigger(target, key)

    return res
  },
})
}

// 具体使用

const user = reactive({
        name: 'zs',
        age: 12,
        fool: {
          bar: {
            text: 'ddfjdi ',
          },
        },
      })

  effect(() => {
    document.querySelector(
      '#app'
    ).innerText = `${user.name} -- ${user.age} ---${user.fool.bar.text}`
  })

  setTimeout(() => {
    user.name = '大马纳'
    setTimeout(() => {
      user.fool.bar.text = 'you are'
    }, 1000)
  }, 2000)

computed 的使用

两种写法:

  1. 选项式写法(可读,可写)

    const fullName = computed<string>({
      get() {
        return firstName.value + '-' + lastName.value
      },
      set(val) {
        ;[firstName.value, lastName.value] = val.split('-')
      },
    })
    
  2. 组合函数写法(设置为只读属性,不可写)

const fullName = computed<string>(() => firstName.value + '-' + lastName.value)

watch 数据源可以是一个 ref (包括计算属性)、一个响应式对象、一个 getter 函数、或多个数据源组成的数组

watch(x, (newVal, oldVal) => {})

watch(
  () => x.d,
  (newVal, oldVal) => {}
)
watch([x, y], ([newVal, newVal2]) => {})

watch(x, (val) => {}, { immediate: true, deep: true, flush: 'pre' })

// pre 组件更新之前调用  sync同步执行  post组件更新后执行

-- reactive 对象默认执行深度监听 ;ref 对象要设置 deep,但是 newVal, oldVal 相同,慎用

watchEffect 高级监听

  1. 副作用可以用来清理无效的副作用
  2. 返回值是一个用来停止该副作用的函数
  3. 不加参数,则默认监听立即执行,直接在回调函数中使用即可
const stop = watchEffect(
  (onCleanup) => {
    // let m:HTMLInputElement = document.querySelector('#btn') as HTMLInputElement
    // console.log(m, 'm')
    console.log(message1.value, message2.value)
    onCleanup(() => console.log('before')) // 总是先执行
  },
  {
    // flush: 'post',
  }
)

生命周期

  onBeforeMount,
  onMounted,
  onBeforeUpdate,
  onUpdated,
  onBeforeUnmount,
  onUnmounted

  - 其中 setup 默认就包括 vue2 中的(beforeCreate, created)

  - 父子组件的生命周期执行顺序: 父 onMounted 之前执行子组件的 onMounted

layout 布局,sass, scoped (element ui)

sass 常用语法

  1. 嵌套规则

    .a {
      .b {
    
      }
    }
    
  2. 变量使用 $main-color: 'red

  3. 混入

    @mixin xx{}  ===>  @include xx
    @mixin xx($a, $b) {}  ====> @include xx(m, n)
    
    
  4. @at-root 独立出当前的父级元素,自己独立开始

  5. 插值语句#{xx}

    $name: foo;
    $attr: border;
    p.#{name} {
      #{attr}-color: blue;
    }
    

父子组件传值

  1. defineProps -- 使用默认值

    const props = withDefaults(defineProps<{ txt: string }>(), {
      txt: '阳光开朗大的孩',
    })
    
  2. defineEmits --- 子传父

    const emit = defineEmits<{
      (e: 'on-click', arr: number[], name: string): void
      (e: 'on-change', name: string): void
    }>()
    
    // 在子组件中发送
    run() {
      emit('on-click', xx, xx)
    }
    
    // 在父组件中接收
    <Son  @on-click = "handle"/>
    const handle = (p1:xx, p2:xx) {
      console.log(p1, p2)
    }
    
  3. defineExpose 将子组件的属性暴露给父组件

    • 当父组件通过模板引用的方式获取到当前组件的实例(ref)--> xxx.value 没有值

    • 办法: 子组件暴露出来

      const a = 1
      const b = ref(2)
      
      defineExpose({
        a,
        b,
      })
      

全局组件、局部组件、递归组件

  - 全局组件: 页面中出现频率高的业务组件
  - 局部组件: 页面中多模块,每个模块一个组件
  - 递归组件: menu菜单

可选链操作符?和??在 vue 中的使用

a.children?.length // ?前面返回 undefined 隐式转换为 false a.children?.length?.xxxx ?? [] // ?? 左侧是 undefined 或 null, 返回 ?? 右侧的值

动态组件

  • 常用于 tab 栏的切换操作

    用法

    • 注册的组件名(shallowRef, 只监听到.value)

      import { ref, reactive, shallowRef } from 'vue'
      const componentId = shallowRef(A)
      const activeIndex = ref(0)
      const list = reactive([
        {
          name: 'comA',
        },
        {
          name: 'comB',
        },
        {
          name: 'comC',
        },
      ])
      
      const change = (item, index) => {
        componentId.value = item
        activeIndex.value = index
      }
      
      // 第二个ts
      import A from './components/A.vue'
      import B from './components/B.vue'
      import C from './components/C.vue'
      
      export default {
        components: {
          comA: A,
          comB: B,
          comC: C,
        },
      }
      

      导入的组件对象 (使用 markRaw, shallowRef 避免组件的监听)

      import A from './components/A.vue'
      import B from './components/B.vue'
      import C from './components/C.vue'
      
      import { ref, reactive, markRaw, shallowRef } from 'vue'
      
      const componentId = shallowRef(A)
      const activeIndex = ref(0)
      
      const list = reactive([
        {
          name: 'comA',
          component: markRaw(A),
        },
        {
          name: 'comB',
          component: markRaw(B),
        },
        {
          name: 'comC',
          component: markRaw(C),
        },
      ])
      
      const change = (item, index) => {
        componentId.value = item.component
        cosnole.log(componentId.value, 'dddd')
        activeIndex.value = index
      }
      

插槽使用

  • 使用场景: 要为子组件传递一些模板片段,让子组件在它们的组件中渲染这些片段。
  1. 默认插槽 #default
  2. 具名插槽 #header
  3. 作用域插槽 v-slot={a, b} -- 可以将子组件的数据传递给父组件
  4. 动态插槽 #[dynamicSlot]
<A>
    <template #content>
      <div>bjfidjfi</div>
    </template>
  </A>

异步组件 --- 提升性能(当页面出现白屏的时候,可以出现提示信息)

  <Suspense>
    <template #default>
      <AsyncPage></AsyncPage>  // 当数据成功加载的时候展示
    </template>
    <template #fallback>  // loading时候
      <Skeleon></Skeleon>
    </template>
  </Suspense>

  // 引入异步租价你
  const AsyncPage = defineAsyncComponent(() => import('./components/Async.vue'))

内置组件 Teleport

-- 它可以将一个组件内部的一部分模板“传送”到该组件的 DOM 结构外层的位置去, 如果使用定位的时候可以考虑使用!

<Teleport to="body" :disabled="true">
      <div v-if="open" class="modal">
        <p>Hello from the modal!</p>
        <button @click="open = false">Close</button>
      </div>
</Teleport>

内置组件 keep-alive --- 用缓存提升用户体验

  • 应用场景: 当切换不同的组件,之前表单的输入数据被清除(想要保存之前的状态)
// 属性有include, exclude, max
;<keep-alive exclude="Async">
  <Async v-if="open" />
  <Skeleon v-else />
</keep-alive>

// 在切换过程中缓存组件的数据
onActivated(() => {
  console.log('a 从缓存中重新插入')
})

onDeactivated(() => {
  console.log('a 移除并进入缓存')
})

动画组件 结合 animate.css 使用 , 结合 gsap 使用--- 具体看文档使用

  • name

  • 自定义类名 + 默认类名

  • 结合 animate.css 使用

  • xxx-enter-from 和 xx-leave-to 样式通常相同, xxx-leave-to 和 xxx-leave-from 通常为现在的样式

  • 生命周期(8 个)

      <Transition
        @before-enter="onBeforeEnter"
        @enter="onEnter"
        @after-enter="onAfterEnter"
        @enter-cancelled="onEnterCancelled"
        @before-leave="onBeforeLeave"
        @leave="onLeave"
        @after-leave="onAfterLeave"
        @leave-cancelled="onLeaveCancelled"
        >
        <p v-if="show"></p>  // 外部通过控制show的值来控制显示
      </Transition>
    
  • transition-group 用来对一组数据进行操作

兄弟组件通信(A 和 B 组件通信)

  1. 通过父组件作为中转, A 触发操作修改值,通过属性传递给 B 组件, 数据定义在父组件中
  2. 结合 vue2 中使用的 eventBus,自己封装一个 bus
// 类似vue2中event bus 实现组件通信, emit触发,on监听
type BusClass<T> = {
  emit: (name: T) => void
  on: (name: T, callback: Function) => void
}

type BusParams = string | number | symbol
type List = {
  [key: BusParams]: Array<Function>
}

class Bus<T extends BusParams> implements BusClass<T> {
  list: List
  constructor() {
    this.list = {}
  }

  // 触发
  emit(name: T, ...args: Array<any>) {
    let events: Array<Function> = this.list[name]
    events.forEach((event) => event.apply(this, args))
  }

  // 监听
  on(name: T, callback: Function) {
    let fn: Array<Function> = this.list[name] || []
    fn.push(callback)
    this.list[name] = fn
  }
}

export default new Bus()

// 具体使用
xxx.$bus.emit('defineEventName', 待传递的值)
xxx.$bus.on('defineEventName', (val) => 执行的操作)
  • 在 A 组件对应位置触发 emit 并传值, 在 B 组件中响应位置开启监听

    1. 可以使用全局变量注入,将 bus 应用到全局
    app.config.globalProperties.$bus = bus
    
    // 在<script setup lang='ts'> </script>使用, 要使用proxy解构
    import { getCurrentInstance, ref } from 'vue'
    const { proxy } = getCurrentInstance()
    console.log(proxy.$bus)
    

mitt 插件 --- 可以实现 bus 的功能 npm install mitt -S

// 全局挂载

import mitt from 'mitt'

const Mit = mitt()

//TypeScript注册
// 由于必须要拓展ComponentCustomProperties类型才能获得类型提示
declare module 'vue' {
  export interface ComponentCustomProperties {
    $Bus: typeof Mit
  }
}

const app = createApp(App)

//Vue3挂载全局API
app.config.globalProperties.$Bus = Mit

app.mount('#app')


// 常用属性
xxx.emit('xx', xx)
xxx.on('xx', () => {})  // 监听
xxx.off('xx', xx)) // 清除监听
xxx.$bus.all.clear()  // 清除所有监听

tsx & vite 插件的使用

-- 便于组件灵活操作

npm install @vitejs/plugin-vue-jsx -D

-- defineComponent 类型推导的使用, 结合 tsx 语法使用

// 1. tsx语法
import { ref } from 'vue'
let v = ref<string>('')
const renderDom = (props: Props, content: any) => {
  return (
    <>
      <input v-model={v.value} type="text" />
      <div onClick={clickTap.bind(this, content)}>{v.value}</div>
      // content.emit('xxx', ddfdf)\
    </>
  )
}


// 2. 函数语法
import { ref, defineComponent } from 'vue'

const renderDom = defineComponent({
  props: {
    xxx: String,
  },
  emits: ['on-click'],
  setup(props, {emit}) {
    let v = ref<string>('')
    return (
      <>
        <input v-model={v.value} type="text" />
        <div  onClick={clickTap.bind(this, v.value)}>{v.value}</div>
        //() => handle(item)
      </>
    )
  },
})

import { defineComponent } from 'vue'

// 3.选项语法
const renderDom = defineComponent({
  data() {
    return {
      test: 343,
    }
  },
  render() {
    return (
      <>
        <input v-model={this.test} type="text" />
        <div>{this.test}</div>
      </>
    )
  },
})

export default renderDom
  ```

tsx 中常见的语法

  1. v-bind 变为 {}
  2. ref 使用,不能直接获取值,需要 xx.value 才可以在页面中展示
  3. 支持 v-show, 不支持 v-if,可以 支持三元表达式
  4. 不支持 v-for, 用 map 来代替
  5. 绑定事件 以 on 开头,事件名称首字母大写 onClick= {handle} onClick={()=>{}}
  6. 使用 prop 和 emits (setup 和 tsx 函数写法中,注意区别)
  7. slots 使用
const myslots = {
    default: () => <div>默认值我是默认的</div>,
  }
<A v-slots={myslots} test={'good'}></A>
// 或者 <A test={'good'}>{myslots}</A>

// 使用插槽
const A = (_, { slots }) => {
  return (
    <>
      <div>{slots.default ? slots.default() : '默认值'}</div>
    </>
  )
}

vue3 自动引入安装插件

npm i -D unplugin-auto-import/vite 会自动引入 vue3 中需要用到 watch,computed ...

  • 然后再 vite.config.ts 中使用插件
  AutoImport({
    imports: ['vue'],
    dts: 'src/auto-import.d.ts',
  }),

组件中 v-model 双向绑定,以及修饰符的使用

-- 结合 props 和 emits 操作, 可以绑定多个 v-modle

  <B v-model="show" v-model:text.isBt="text"></B>

  const props = defineProps<{
      modelValue: boolean // 默认值
      text: string
      textModifiers?: {  // 注意使用的固定格式xxModifiers: {}
        isBt: boolean
      }
}>()

const emits = defineEmits(['update:modelValue', 'update:text'])

const changeInput = (e: Event) => {
  const t = e.target as HTMLInputElement
  emits('update:text', props.textModifiers?.isBt ? t.value + '变态' : t.value)
}

自定义指令 directives

  • 直接通过js操作dom
import { Directive, DirectiveBinding } from 'vue'

type Dir = {
  background: string
}

// 自定义指令的钩子函数
const vColor: Directive = {
    // mounted(...args: Array<any>) {},
    created() {},
    beforeMount() {},
    // 通过指定泛型,可在代码中获取相关提示
    mounted(el: HTMLElement, dir: DirectiveBinding<Dir>) {
      el.style.background = dir.value.background
    },
    beforeUpdate() {},

    updated() { },

    beforeUnmount() {},

    unmount(){},
}

// 简写, 可以使用函数的形式, 常使用updated和mounted钩子函数
// 鉴权的使用
<button v-has-show="'shop-edit'">编辑</button>
<button v-has-show="'shop-delete'">删除</button>

      const permissions = ['wx1213:shop-create', 'wx1213:shop-delete'] // 模拟后端返回的数据

      // 自定义指令
      const vHasShow: Directive<HTMLElement, DirectiveBinding> = (el, binding) => {
        let isShow = permissions.includes(
          localStorage.getItem('userId') + ':' + binding.value
        )
        if (!isShow) {
          el.style.display = 'none'
        }
  }
  • 简写模式:不关系其他钩子函数的时候,可以使用函数模式实现
  • 常见的应用场景: 鉴权管理,判断用户是否具有相关权限(比如创建、编辑、删除权限)

  • 实践 1: 利用指定实现拖拽的效果

    // 注意盒子要设置为absolute模式, 具体使用直接加指令v-move即可,不需传入参数
    import { Directive } from 'vue'
    const vMove: Directive<any, void> = (el: HTMLElement) => {
      let moveEl = el.firstElementChild as HTMLDivElement // 获取待移动元素
      const mouseDown = (e: MouseEvent) => {
        let X = e.clientX - el.offsetLeft // 计算鼠标在外层元素的相对位置
        let Y = e.clientY - el.offsetTop
        const move = (event: MouseEvent) => {
          el.style.left = event.clientX - X + 'px' // 减去相对位置,获得外层元素的位置
          el.style.top = event.clientY - Y + 'px'
        }
        // 2. 抬起  3. 移动
        document.addEventListener('mouseup', () => {
          document.addEventListener('mousemove', move)
        })
      }
      // 在移动元素中监听鼠标操作  1. 按下
      moveEl.addEventListener('mousedown', mouseDown)
    }
    
    • 实践 2: 图片懒加载 (当移动到可视区的时候,图片加载出)

      • 利用 js 中 IntersectionObserver 接口

        import type { Directive } from 'vue'
        // 静态加载所有图片模块  import.meta.glob 类似于之前的() => import xxxx
        const images: Record<string, { default: string }> = import.meta.globEager(
          './assets/imgs/*'
        )
        let arr = Object.values(images).map((v) => v.default)
        
        const vLazy: Directive<HTMLImageElement, string> = async (
          el,
          binding
        ) => {
          let url = await import('./assets/vue.jpeg')
          el.src = url.default
        
          // IntersectionObserver  异步观察目标元素与其祖先元素或顶级文档视口
          let observer = new IntersectionObserver((entries) => {
            if (entries[0].intersectionRatio > 0 && entries[0].isIntersecting) {
              setTimeout(() => {
                el.src = binding.value
              }, 2000)
              observer.unobserve(el)
            }
          })
          observer.observe(el)
        }
        

自定义 hooks

  • 主要用来处理复用代码逻辑的封装, 类似 vue2 中 mixins

    VueUse 用 hooks 写的开源库 vueuse.org/guide/

  • vue3 本身提供的 hooks,如 useAttrs, useSlots

  • hooks 的写法可以使用组合式函数来写,可以使用的相关的生命周期,以及 ref 和 reactive 等属性, 最后通过返回 promise 回调供外层调用。

    import { useAttrs } from 'vue'
    console.log(useAttrs()) // 获取组件中所有动态属性对象
    
  • 实践: 实现图片转 base64 编码

    // 自定义hooks
    type Options = {
      el: string
    }
    export default (options: Options) => {
      return new Promise((resolve) => {
        // 通过promise 返回即可
        onMounted(() => {
          let img: HTMLImageElement = document.querySelector(
            options.el
          ) as HTMLImageElement
          img.onload = () => {
            resolve(getBase64(img))
          }
        })
    
        const getBase64 = (el: HTMLImageElement) => {
          const canvas = document.createElement('canvas')
          const ctx = canvas.getContext('2d')
          canvas.width = el.width
          canvas.height = el.height
          ctx?.drawImage(el, 0, 0, canvas.width, canvas.height)
          return canvas.toDataURL('image/jpg')
        }
      })
    }
    // 调用
    import useBase64 from './hooks/index'
    useBase64({ el: '#img' }).then((res) => console.log(res))
    
    • hooks 和 自定义指令综合案例: 检测 dom 尺寸的变化大小 (npm 包的封装 ---- 小满大神实在是太强大了!!!)

      export const useResize = (el: HTMLElement, callback: Function): void => {
        let resize = new ResizeObserver((entries) => {
          callback(entries[0].contentRect) // 将尺寸变化信息传出给外层的回调函数
        })
      
        resize.observe(el)
      }
      
      import type { App } from 'vue'
      
      // 使用自定义指令
      const install = (app: App) => {
        app.directive('resize', {
          mounted(el, binding) {
            // binding.value中传入回调函数
            useResize(el, binding.value)
          },
        })
      }
      useResize.install = install //  相当于实现了一个插件的封装
      

全局函数和变量

  1. 定义变量

    app.config.globalProperties.$msg = 'this is pension 退休老实人'
    app.config.globalProperties.$filters = {
      format<T>(str: T) {
        return 'xdfd' + str
      },
    }
    
  2. 补充扩展声明

    type Filter = {
      format<T>(str: T): string
    }
    declare module 'vue' {
      interface ComponentCustomProperties {
        $msg: string
        $filters: Filter
      }
    }
    
  3. 使用

// <div>{{ $msg }}</div>  模板中直接用
import { getCurrentInstance } from 'vue'
const app = getCurrentInstance()
console.log(app?.proxy?.$msg)

自定义 vue 插件

  • 实际上就是通过全局变量,将响应的 vue 组件挂载

    import { VNode, createVNode, render, App } from 'vue'
    import Loading from './index.vue'
    export default {
    install(app: App) {
      const vNode: VNode = createVNode(Loading) // 生成虚拟dom
      render(vNode, document.body) //render 把我们的Vnode 生成真实DOM 并且挂载到指定节点
      app.config.globalProperties.$load = {
        show: () => vNode.component?.exposed?.show(),
        hide: () => vNode.component?.exposed?.hide(),
        }
      },
    }
    
    // 使用方法: 引入xx.ts, 然后app.use(xx)
    import { getCurrentInstance } from 'vue'
    const instance = getCurrentInstance()?.proxy?$load
    

vue3 中常使用的 UI 框架

  1. element plus 使用setup组合式 (常用)
  2. ant design Vue 使用setup函数模式
  3. iview 使用的options Api
  4. Vant 移动端 -- hooks和业务组件

样式穿透

  • scoped 的渲染规则:

    1. 给 HTML 的 DOM 节点加一个不重复 data 属性(形如:data-v-123)来表示他的唯一性
    2. 在每句 css 选择器的末尾(编译后的生成的 css 语句)加一个当前组件的 data 属性选择器(如[data-v-123])来私有化样式
    3. 如果组件内部包含有其他组件,只会给其他组件的最外层标签加上当前组件的 data 属性
  • 由于 style 中使用 scoped 属性,所以修改样式穿透要使用:deep(.el-input__inner)

插槽选择器: 修改插槽内容的样式 :slotted(.a)

全局选择器: :global(div)

动态 css color: v-bind(ts中定义好的变量) 可以通过 js 控制 css

module css

  ```vue
  <style module> </style> // module='zz'
  <div :class="$style.xx"></div> // class='zz.xx', 多种样式可以使用数组, 在style中定义相关的类样式
  ```

import {useCssModule} from 'vue' // 使用hooks const css = useCssModule('名称 zs') 打印或者使用 jsx 语法使用 css 中定义的类演示

tailwindcss

--- 类似 bootstrap 的 css 库

 npm install -D tailwindcss@latest postcss@latest autoprefixer@latest // 安装包
 npx tailwindcss init -p // 配置文件
 module.exports = {
   content: ['./index.html', './src/**/*.{vue,js,ts,jsx,tsx}'],
   theme: {
     extend: {},
   },
   plugins: [],
 }

eventLoop 和 nextTick

eventloop

1.宏任务
script(整体代码)、setTimeoutsetIntervalUI交互事件、postMessage、Ajax

2.微任务
Promise.then catch finallyMutaionObserver、process.nextTick(Node.js 环境)

运行机制:
所有的同步任务都是在主进程执行的形成一个执行栈,主线程之外,还存在一个"任务队列",异步任务执行队列中先执行宏任务,然后清空当次宏任务中的所有微任务,然后进行下一个tick如此形成循环。

nextTick

  • vue 更新数据是同步的,更新 dom 是异步的。当我们操作 dom 的时候发现数据是上次的,就需要使用 nextTick
  • 两种使用方式:
    1. 回调函数 nextTick(() => {})
    2. async + await, await 后面的代码都是异步的 await nextTick()

Vue 开发移动端

  • 主要是为了适配各种手机 npm install postcss-px-to-viewport -D -- 目前已经过期,使用 postcss8 版本的

移动端屏幕适配方案

  1. `npm install postcss-px-to-viewport-8-plugin -D`
  2.  新建postcss.config.js中,注册插件信息
    ```js
     export default {
      plugins: {
        'postcss-px-to-viewport-8-plugin': {
          unitToConvert: 'px', // 需要转换的单位,默认为"px"
          viewportWidth: 750, // 设计稿的视口宽度
          unitPrecision: 5, // 单位转换后保留的精度
          propList: ['*', '!font-size'], // 能转化为vw的属性列表,!font-size表示font-size后面的单位不会被转换
          viewportUnit: 'vw', // 希望使用的视口单位
          fontViewportUnit: 'vw', // 字体使用的视口单位
          // 需要忽略的CSS选择器,不会转为视口单位,使用原有的px等单位。
          // 下面配置表示类名中含有'keep-px'都不会被转换
          selectorBlackList: ['keep-px'],
          minPixelValue: 1, // 设置最小的转换数值,如果为1的话,只有大于1的值会被转换
          mediaQuery: false, // 媒体查询里的单位是否需要转换单位
          replace: true, //  是否直接更换属性值,而不添加备用属性
          exclude: [/node_modules/], // 忽略某些文件夹下的文件或特定文件,例如 'node_modules' 下的文件
          include: [/src/], // 如果设置了include,那将只有匹配到的文件才会被转换
          landscape: false, // 是否添加根据 landscapeWidth 生成的媒体查询条件 @media (orientation: landscape)
          landscapeUnit: 'vw', // 横屏时使用的单位
          landscapeWidth: 1338, // 横屏时使用的视口宽度
        },
      },
    }
    ```

函数式编程 h 函数

- 直接跳过编译过程

```ts
const Btn = (props: Props, ctx: any) => {
  return h(
    'div',
    {
      onClick: () => {
        ctx.emit('on-click', 123)
      },
    },
    ctx.slots.header()
  )
}
```

- h 函数接受三个参数:

  1. type 元素的类型
  2. propsOrChildren 数据对象, 这里主要表示(props, attrs, dom props, class 和 style)
  3. 子节点

- 使用场景: 展示成功和失败按钮,分别显示不同颜色

Vue 响应语法糖 (实践测试案例,暂时不适用生产环境)

import { $ref, $$, $ } from 'vue/macros ref定义变量可以直接修改 ref 对象的值,不需要.value,$$将对象返回 ref 类型的值, 解构保留 ref 属性

环境变量(配置开发和生产环境)

- dist 中 index.html 打开暂时没有反应,需要开启一个服务使用
  `npm install http-server -g`
  `http-server -p 9002`

新建两个环境配置文件, 配置使用到的常量

- .env.development
- .env.production

在 vue 组件中使用 `import.meta.env`
在 vite.config.ts 中使用通过 loadEnv 使用

```ts
import {loadEnv} from 'vue'
export default ({mode}:any) => {
  console.log(loadEnv(mode, process.cwd())) // 具有两个参数
  return ...
}

```

webpack 从 0 到 1 配置

  • externals 配置 cdn 加载方式
  • plugins 扩充 webapck 的功能,如 htmlPluginWebpack, 清除打包内容,解析 vue 模板
  • rules 配置文件的解析方式,加载相关的 loader

vue3 的性能优化

谷歌浏览器自带的性能分析工具 lightHouse (灯塔)

vite 是使用 rollup 打包的,可使用相关的可视化工具npm install rollup-plugin-visualizer

import { visualizer } from 'rollup-plugin-visualizer'
plugins: [
  vue(),
  vueJsx(),
  visualizer({
    open: true,
  }),
]
// npm run build打包,可以看到每块打包后的体积

// vite 配置优化

build:{
      chunkSizeWarningLimit:2000, // 体积限制警告
      cssCodeSplit:true, //css 拆分
      sourcemap:false, //不生成sourcemap
      minify:false, //是否禁用最小化混淆,esbuild打包速度最快,terser打包体积最小。
      assetsInlineLimit:5000 //小于该值 图片将打包成Base64
}

PWA 离线缓存技术 npm install vite-plugin-pwa -D

import { VitePWA } from 'vite-plugin-pwa'
plugins: [
  VitePWA({
    workbox: {
      cacheId: 'XIaoman', //缓存名称
      runtimeCaching: [
        {
          urlPattern: /.*\.js.*/, //缓存文件
          handler: 'StaleWhileRevalidate', //重新验证时失效
          options: {
            cacheName: 'XiaoMan-js', //缓存js,名称
            expiration: {
              maxEntries: 30, //缓存文件数量 LRU算法
              maxAgeSeconds: 30 * 24 * 60 * 60, //缓存有效期
            },
          },
        },
      ],
    },
  }),
]
// npm run build 会生成sw.js, 开启http-server即可看到在application中有workServer相关信息
  • 图片懒加载 npm install lazyPlugin -S
  • 虚拟列表
  • 多线程,使用 new Worker 创建, worker 脚本与主进程的脚本必须遵守同源限制。他们所在的路径协议、域名、端口号三者需要相同
  • vueUse 库已经集成了 webWorker, 防抖节流等功能

web components

  • 就是提供自定义标签的能力,并且提供了标签内完整的生命周期 。(利用 h 函数和 shadow 影子 dom,支持模板模式)
  • 在 vue 中使用的 defineCustomElement

跨域

  • jsonP
  • proxy (协议、域名、端口不同,注意是本地开发运行地址,和请求 api 的服务器地址) --- vite.config.js 中配置的是开发环境下的,生产环境要使用 nginx 中 proxy-pass 代理转发

pinia

npm install pinia -S 创建 store import { defineStore } from 'pinia'

defineStore(name, { state, getters, actions })

修改 state 不需要使用 vuex 中的 mutations actions 可以执行同步和异步操作 没有 modules,每个 store 都是独立的 具体使用:利用 hooks 调用使用

    1. 修改 state 的方法
    • 组件中直接修改 xxx.current++
    • 批量改xxx.$patch({current: xx, xx:de})
    • 常用函数写法xxx.$patch((state) => {})
    • 少用覆盖 state, 参数不能少xx.$state = {}
    • action 修改 xx.setCurrent(param)
    1. 解构 store
    • 普通解构后的 store 不具有响应式, 需要使用 storeToRefs 转换,其实转为 ref,要使用.value 修改值
    1. actions 和 getters
      • actions 可以执行同步和异步,成员直接可以相互调用, 不需要返回
      • getters 中具有缓存效果,成员可以相互调用, 类似 vue 中的 computed,必须 return 一个值---实际上是一个经过计算后的值
  • API 使用

    • $reset --- 重置 state
    • $subscribe --- 监听 state 变化
    • $onAction -- 监听 actions 变化
    Test.$subscribe(
      (args, state) => {
        console.log(args, state)
      },
      {
        detached: true,
        deep: true,
        flush: 'post',
      }
    )
    Test.$onAction((args) => {
      args.after(() => {
        console.log('after')
      })
      console.log('----eeee4e')
    }, true)
    
  • 持久化插件 PiniaPluginContext

    使用场景: state 值浏览器刷新数据会 reset,没有缓存

    import { createPinia, PiniaPluginContext } from 'pinia'
    const setStorage = (key: string, value: any) => {
      localStorage.setItem(key, JSON.stringify(value))
    }
    
    const getStorage = (key: string) => {
      return localStorage.getItem(key)
        ? JSON.parse(localStorage.getItem(key) as string)
        : {}
    }
    
    // 持久化插件
    
    type Options = {
      key: string
    }
    
    const __piniaKey__: string = 'ww'
    
    const piniaPlugin = (options: Options) => {
      return (context: PiniaPluginContext) => {
        const { store } = context
        const data = getStorage(`${options?.key ?? __piniaKey__}-${store.$id}`) // ?? 表示undefined就使用默认值
        store.$subscribe(() => {
          setStorage(
            `${options?.key ?? __piniaKey__}-${store.$id}`,
            toRaw(store.$state)
          )
        })
    
        return {
          ...data,
        }
      }
    }
    const store = createPinia()
    app.use(store)
    store.use(
      piniaPlugin({
        key: 'pinia',
      })
    )
    

router

  • vue3 使用 vue-router@4, vue2 使用 vue-router@3 npm install vue-router -S

  • router-view, router-link(to 属性)

  • 两种路由模式分析

    hash 使用#, 利用锚点在页面内进行导航,改变 url 中的 hash 部分不会引起页面刷新, 监听 hashchange 事件

    history 不使用#, 提供 pushState 和 replaceState 方法(执行的时候不会触发 popstate 事件,通过 js 调用 history 的 back, go, forward 方法),改变 url 不会引起页面刷新,监听 popstate 事件

  • 命名导航(name, path)和编程式导航 (使用 hooks --> useRouter)

  • 历史记录

    • router-link 中使用 replace 属性 ---浏览器没有历史记录,不可点击前进和后退
    • 编程式导航使用router.replace(xxx)
    • 自定义前进(几个)后退 router.go(1) router.back()
  • 路由传参 (通过 useRoute 获得当前路由信息)

      1. query 和 path 结合使用,query 接收一个对象,参数在浏览器地址栏中可以直接看到;
      1. params 和 name 结合使用,此时页面刷新数据消失,可以使用地址动态传参
  • 嵌套路由( 父子路由)

    • 注意: 通过编程式导航或者的时候,要带上父路由的 path 才可以
  • 命名视图

    • 使用场景: 一个路由,多个视图,为 router-view 设置 components 属性,注意路由的 name 属性要去掉
  • 重定向(别名)

    • redirect 和 alias
    • redirct 可以为字符串;接受一个 to 参数的回调函数,传递参数;返回相对位置的路径字符串
  • 导航守卫

    • 前置钩子
    // 设置白名单
    const whiteList = ['/']
    router.beforeEach((to, from, next) => {
      if (whiteList.includes(to.path) || localStorage.getItem('token')) {
        next()
      } else {
        next('/')
      }
    })
    
    • 后置钩子 (afterEach)

    • 实例: 加载进度条

    // LoadingBar.vue  wrapper fixed , 修改bar的width, 从0-100
    const startLoading = () => {
      let dom = bar.value as HTMLElement
      speed.value = 1
      // 递归调用,requestAnimationFrame优于settimeInterval
      timer.value = window.requestAnimationFrame(function fn() {
        if (speed.value < 90) {
          speed.value += 1
          dom.style.width = speed.value + '%'
          timer.value = window.requestAnimationFrame(fn)
        } else {
          speed.value = 1
          window.cancelAnimationFrame(timer.value)
        }
      })
    }
    
    const endLoading = () => {
      let dom = bar.value as HTMLElement
      setTimeout(() => {
        window.requestAnimationFrame(() => {
          speed.value = 100
          dom.style.width = speed.value + '%'
        })
      }, 1000)
    }
    
    defineExpose({
      startLoading,
      endLoading,
    })
    
    // 在钩子函数中使用 , 需要挂载虚拟dom,render到body中,才可以使用
    const vNode = createVnode(LoadingBar)
    render(vNode, document.body)
    
    router.beforeEach((to, from, next) => {
      vNode.component?.exposed?.startLoading()
    })
    router.afterEach((to, from, next) => {
      vNode.component?.exposed?.endLoading()
    })
    
  • 路由元信息: meta 中可以存放一些信息,如 tilte, hasAuth, icon, transition 等

    • ts 需要对 meta 做个类型声明
    declare module 'vue-router' {
      interface RouteMeta {
        title: string
        transition: string
      }
    }
    
  • 路由过渡动效

    <router-view #default="{ route, Component }">
      <transition :enter-active-class="`animate__animated ${route.meta.transition}`">
        <component :is="Component"></component>
      </transition>
    </router-view>
    
  • 滚动行为

    • 定义 router 的时候,有一个滚动行为和 history,routes 同级别,可以保存当前滚动的位置,切换路由的时候可以返回原来的滚动位置
    const router = createRouter({
      scrollBehavior: (to, from, savePosition) => {
        if (savePosition) {
          return savePosition
        } else {
          return { top: 0 }
        }
      },
    })
    
  • 动态路由: 常用于权限管理,如 vip 用户可以看到三个菜单,普通用户只能看到一个菜单。后台返回路由表,前端处理

    • 方法: 使用router.addRoute(), router.getRoute()
    router.addRoute({
      path: route.path,
      name: route.name,
      component: () => import(`../views/${route.component}`), // 拼接不可以使用@别名
    })
    // 注意: get请求需要通过{params:{ax: xx, b: xx}}, post可以通过{a:xx,b:xx}