Vue3+TS教程(二)

Typescript的支持
全局接口的抽取

1.src下定义types文件夹命名xx.d.ts
2.建立Person接口person.d.ts

interface personInterface{
    name:string
    age:number
}

3.组件中直接使用

<script setup lang="ts">
const props=defineProps<{person:personInterface[]}>()
</script>

4.如果不是在src下或src文件下的xx.d.ts文件则需要在tsconfig.json中配置

{
  {
 ...
  },
  "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"], //配置全局目录
  "references": [{ "path": "./tsconfig.node.json" }]
}

类型增强

1.使用环境:全局定义的数据,函数在vue组件中直接访问报错
2.index.html中定义数据

<!DOCTYPE html>
<html lang="en">
  <head>
  ...
  </head>
  <script>
    const  global=1
  </script>
  <body>
    ...
  </body>
</html>

3.定义类型增强

// common.d.ts
declare const global:string;

4.组件中直接读取

<script setup lang="ts">
console.log(global)
</script>
第三方库类型声明

1.安装一个库
2.安装库的ts类型声明@types/xxxx

props组件通讯TS

父组件

<script setup lang="ts">
import HelloWorld from './components/HelloWorld.vue'
</script>

<template>
  <HelloWorld msg="1"/>
</template>

子组件

<script setup lang="ts">
interface msgIterface{
  msg:string
}
const props = withDefaults(defineProps<msgIterface>(),{
     msg:'默认值'
    })
console.log(props.msg)
</script>
emit组件通讯TS

父组件

<script setup lang="ts">
import HelloWorld from './components/HelloWorld.vue'
const getuser=(a:number)=>{
  console.log(a)
}
</script>

<template>
  <HelloWorld @getuser="getuser"/>
</template>

<style scoped>
</style>

子组件

<script setup lang="ts">
 const emit = defineEmits<{(e: 'getuser', id: number): void}>()
  // (e: 事件名, 键名:类型): void
function buttonClick() {
  emit('getuser',1)
}
  </script>

<template>
  <button @click="buttonClick">传输</button>
</template>

<style scoped>

</style>

依赖注入类型推断

父组件(祖先组件)

<template>
  <div class="App">
    <button>我是App</button>
    <A></A>
  </div>
</template>

<script setup lang="ts">
import { provide, ref } from 'vue'
import A from './components/Acom.vue'
let flag = ref<number>(1)
provide('flag', flag)
</script>

子组件(后代组件)

<template>
  <div>
    我是B
    <div>{{ flag }}</div>
    <button @click="flag++">+1</button>
  </div>
</template>

<script setup lang="ts">
import { inject, ref , type Ref} from 'vue'
//  注入值,默认值(让其可以进行类型推断)
const flag<Ref<number>> = inject('flag', ref(1))
</script>

定义全局函数和全局函数的类型支持
import { createApp } from 'vue'
...
const app = createApp(App)
type Fileter = {
  format: <T>(str: T) => string
}
declare module '@vue/runtime-core' {
  export interface ComponentCustomProperties {
    $filters: Fileter
    $env: string
  }
}

// 全局函数
app.config.globalProperties.$filters = {
  format<T>(str: T): string {
    return `真${str}`
  }
}

// 全局变量
app.config.globalProperties.$env = '全局变量'
...
脚手架Vite
1.基本使用

1.创建vue3的项目yarn create vite || npm init vite@latest
2.安装插件Volar

配置项目路径

1.tsconfig.json中添加

// 让ts可以识别这个路径
{
  "compilerOptions": {
   ...
    "baseUrl": "./",
    "paths": {
      "@/*":[
        "src/*"
      ]
    }
  },
  ...
}

2.vite.config.ts中添加


// https://vitejs.dev/config/
export default defineConfig({
  plugins: [vue()],
  resolve:{
    alias:{
      "@":join(__dirname,'src')
    }
  }
})

eslint和prettierrc的配置

1..prettierrc.json

{
    "arrowParens": "always",
    "bracketSameLine": true,
    "bracketSpacing": true,
    "embeddedLanguageFormatting": "auto",
    "htmlWhitespaceSensitivity": "css",
    "insertPragma": false,
    "jsxSingleQuote": false,
    "printWidth": 120,
    "proseWrap": "never",
    "quoteProps": "as-needed",
    "requirePragma": false,
    "semi": false,
    "singleQuote": true,
    "tabWidth": 2,
    "trailingComma": "all",
    "useTabs": false,
    "vueIndentScriptAndStyle": false,
    "singleAttributePerLine": false
  }
  

2..eslintrc.cjs

/* eslint-env node */
require('@rushstack/eslint-patch/modern-module-resolution')

module.exports = {
  root: true,
  extends: [
    'plugin:vue/vue3-essential',
    'eslint:recommended',
    '@vue/eslint-config-typescript',
    '@vue/eslint-config-prettier'
  ],
  rules: {
    'vue/multi-word-component-names': 'off', // 关闭命名
    semi: 0 // 结尾无分号
  },
  parserOptions: {
    ecmaVersion: 'latest'
  }
}

vite环境变量的配置

1.vite的环境在import中

<script setup lang="ts">
console.log(import.meta.env)
</script>

2.创建.env.development .env.production
3.package.json中配置运行生产环境,会自动注入

{
  ...
  "scripts": {
    "dev": "vite --mode development",
    ...
  },
  
  
}

4.vite.config.ts中读取环境变量

import { fileURLToPath, URL } from 'node:url'
import { defineConfig, loadEnv } from 'vite'
import unocss from 'unocss/vite'
import vue from '@vitejs/plugin-vue'
import { presetIcons, presetAttributify, presetUno } from 'unocss'

// https://vitejs.dev/config/
export default ({ mode }: any) => {
  // 读取环境变量
  console.log(loadEnv(mode, process.cwd()))
  return defineConfig({
    plugins: [vue()],
    resolve: {
      alias: {
        '@': fileURLToPath(new URL('./src', import.meta.url))
      }
    }
  })
}

5.找不到模块“./App.vue”或其相应的类型声明

declare module '*.vue' {
  import type { DefineComponent } from 'vue'
  // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types
  const component: DefineComponent<{}, {}, any>
  export default component
}

6.类型“ImportMeta”上不存在属性“env”

// tsconfig.json
{
  ...
  "compilerOptions": {
     ...
    "types": [ "vite/client" ],

  },
  ...
}

自定义指令

全局自定义指令
// mian.ts
import { createApp } from 'vue'
import './style.css'
import App from './App.vue'

const app=createApp(App)
app.directive('focus',{
    mounted(el){
     el.focus()
    }
   })
app.mount('#app')

使用自定义指令

<template>
 <input type="text"  v-model="value" v-focus>
</template>

局部自定义指令
<script setup>
// 在模板中启用 v-focus
const vFocus = {
  mounted: (el) => el.focus()
}
</script>

<template>
  <input v-focus />
</template>

自定义拖拽指令

<script setup lang="ts">
import type { Directive } from 'vue'

const vMove: Directive = (el: HTMLElement) => {
  const move = (e: MouseEvent) => {
    console.log(e)
    el.style.left = e.clientX + 'px'
    el.style.top = e.clientY + 'px'
  }

  // 鼠标按下
  el.addEventListener('mousedown', () => {
    // 鼠标按下拖拽
    document.addEventListener('mousemove', move)
    // 鼠标松开
    document.addEventListener('mouseup', () => {
      // 清除事件
      document.removeEventListener('mousemove', move)
    })
  })
}
</script>

<template>
  <!-- 自定义指令,参数,修饰符 -->
  <div
    v-move
    style="
      background-color: red;
      width: 200px;
      height: 200px;
      position: fixed;
      left: 50%;
      top: 50%;
      transform: translate(-50%, -50%);
    "
  >
    <div style="background-color: black; width: 200px; color: white">
      自定义指令
    </div>
  </div>
</template>

内置组件

Teleport组件

可以将一个组件内部的一部分模板“传送”到该组件的 DOM 结构外层的位置去
父组件:

<!-- 遮罩层组件传送到body下 -->
<script setup lang="ts">
import Acom from './components/Acom.vue'
</script>

<template>
<div class="app"></div>
<Acom/>
</template>

<style scoped >
.app{
  width: 200px;
  height: 200px;
  background-color: pink;
}
</style>

子组件

<script setup lang="ts">
  import { ref } from 'vue'
  
  const open = ref(false)
  </script>
  
  <template>
    <button @click="open=true">显示遮罩层</button>
    <!-- 传送到body -->
  <Teleport to="body">
    <div class="cover" v-show="open">
      <span @click="open=false"> X</span>
   </div>
  </Teleport>
  
  </template>
  
  <style scoped>
  .cover {
   position: absolute;
   z-index:2;
   top: 0;
   left: 0;
   bottom: 0;
   right: 0;
   background-color: rgba(0,0,0,0.5);
  }
  </style>
  

Transition组件

1.非命名动画

<script setup lang="ts">
import { ref } from 'vue';

const show=ref(true)
</script>

<template>
<button @click="show=!show">显示/隐藏</button>
<Transition>
<div class="div" v-if="show"></div>
</Transition>

</template>

<style scoped>
.div{
  background-color: pink;
  width: 200px;
  height: 200px;
  margin: auto;
}
.v-enter-active,
.v-leave-active {
  transition: opacity 0.5s ease;
}

.v-enter-from,
.v-leave-to {
  opacity: 0;
}
</style>

2.命名动画

<script setup lang="ts">
import { ref } from 'vue';

const show=ref(true)
</script>

<template>
<button @click="show=!show">显示/隐藏</button>
<Transition name="fade">
<div class="div" v-if="show"></div>
</Transition>

</template>

<style scoped>
.div{
  background-color: pink;
  width: 200px;
  height: 200px;
  margin: auto;
}
.fade-enter-active {
  transition: all 0.3s ease-out;
}

.fade-leave-active {
  transition: all 0.8s cubic-bezier(1, 0.5, 0.8, 1);
}

.fade-enter-from,
.fade-leave-to {
  transform: translateX(20px);
  opacity: 0;
}
</style>

3.过度动画

<Transition mode="out-in">
  ...
</Transition>

4.结合第三方库Animate.css

<!--  yarn add animate.css -->
<script setup lang="ts">
import { ref } from 'vue'
import 'animate.css'
import Acom from './components/Acom.vue'
const show = ref(true)
</script>

<template>
  <transition
    leave-active-class="animate__animated animate__fadeOut"
    enter-active-class="animate__animated animate__fadeIn"
  >
    <Acom v-if="show"></Acom>
  </transition>

  <button @click="show = !show">显示/隐藏</button>
</template>

<style scoped lang="less"></style>

4.transition 生命周期

<script setup lang="ts">
import { ref } from 'vue'
import 'animate.css'
import Acom from './components/Acom.vue'
const show = ref(true)
const beforeEnter = () => {
  console.log('进入之前')
}
const enter = (_, done: Function) => {
  console.log('过度曲线')
  setTimeout(() => {
    done()
  }, 3000)
}
const afterEnter = () => {
  console.log('过度完成')
}
const enterCancelled = () => {
  console.log('进入效果被打断')
}
const beforeLeave = () => {
  console.log('离开之前')
}
const leave = (_, done: Function) => {
  setTimeout(() => {
    done()
  }, 3000)
  console.log('过度曲线')
}
const afterLeave = () => {
  console.log('离开之后')
}
const leaveCancelled = () => {
  console.log('离开效果被打断')
}
</script>

<template>
  <transition
    leave-active-class="animate__animated animate__fadeOut"
    enter-active-class="animate__animated animate__fadeIn"
    @before-enter="beforeEnter"
    @enter="enter"
    @after-enter="afterEnter"
    @enter-cancelled="enterCancelled"
    @before-leave="beforeLeave"
    @leave="leave"
    @after-leave="afterLeave"
    @leave-cancelled="leaveCancelled"
  >
    <Acom v-if="show"></Acom>
  </transition>

  <button @click="show = !show">显示/隐藏</button>
</template>


5.生命周期结合第三方库gsap.js

<!-- yarn add  gsap -->
<script setup lang="ts">
import { ref } from 'vue'
import Acom from './components/Acom.vue'
import gsap from 'gsap'
const show = ref(true)

// 进入之前
const beforeEnter = (el: Element) => {
  gsap.set(el, {
    width: 0,
    height: 0
  })
}
// 进入过度动画
const enter = (el: Element, done: gsap.Callback) => {
  gsap.to(el, {
    width: 200,
    height: 200,
    onComplete: done
  })
}
// 离开之前
const beforeLeave = (el: Element) => {
  gsap.set(el, {
    width: 200,
    height: 200
  })
}
// 进入过度动画
const leave = (el: Element, done: gsap.Callback) => {
  gsap.to(el, {
    width: 0,
    height: 0,
    onComplete: done
  })
}
</script>

<template>
  <transition
    @before-enter="beforeEnter"
    @enter="enter"
    @before-leave="beforeLeave"
    @leave="leave"
  >
    <Acom v-if="show"></Acom>
  </transition>

  <button @click="show = !show">显示/隐藏</button>
</template>


6.初始化动画

<script setup lang="ts">
import { ref } from 'vue'
import Acom from './components/Acom.vue'

const show = ref(true)
</script>

<template>
  <transition
    appear-from-class="from"
    appear-active-class="active"
    appear-to-class="to"
    appear
  >
    <Acom v-if="show"></Acom>
  </transition>

  <button @click="show = !show">显示/隐藏</button>
</template>

<style scoped>
.from {
  /* 初始化之前 */
  width: 0;
  height: 0;
}
.active {
  /* 过度动画 */
  transition: all 2s ease;
}
.to {
  /* 初始化完成 */
  width: 200px;
  height: 200px;
}
</style>

7.初始化动画结合Animate.css

<script setup lang="ts">
import { ref } from 'vue'
import Acom from './components/Acom.vue'
import 'animate.css'
const show = ref(true)
</script>

<template>
  <transition appear-active-class="animate__animated animate__heartBeat" appear>
    <Acom v-if="show"></Acom>
  </transition>

  <button @click="show = !show">显示/隐藏</button>
</template>

<style scoped></style>

transition-group过度列表

1.Transition组件无法对v-for的列表进行渲染
2.transition-group的tag属性

<!-- tag属性可以让transition-group多加一层节点元素 -->
<template>
  <div class="wraps">
    <transition-group tag="session">
      <!-- 使用transition-group渲染的组件要有key-->
      <div class="item" v-for="item in 5" :key="item">{{ item }}</div>
    </transition-group>
  </div>
</template>

3.添加列表时的动画效果

<script setup lang="ts">
import { ref } from 'vue'
import 'animate.css'
const num = ref(5)
</script>

<template>
  <div class="wraps">
    <transition-group
      leave-active-class="animate__animated animate__fadeOut"
      enter-active-class="animate__animated animate__fadeIn"
    >
      <!-- 使用transition-group渲染的组件要有key-->
      <div class="item" v-for="item in num" :key="item">{{ item }}</div>
    </transition-group>
  </div>
  <button @click="num++">添加</button>
  <button @click="num--">删除</button>
</template>

<style scoped lang="less">
.wraps {
  display: flex;
  flex-wrap: wrap;
  word-break: break-all;
  border: 1px solid #ccc;
  .item {
    margin: 10px;
  }
}
</style>

4.平移动画move-class

<script setup lang="ts">
import { ref } from 'vue'
import _ from 'lodash'
// 建立9x9数组
let list = ref(
  Array.apply(null, { length: 81 } as number[]).map((_, index) => {
    return {
      id: index,
      number: (index % 9) + 1
    }
  })
)
// 打乱数组
const random = () => {
  list.value = _.shuffle(list.value)
}
console.log(list)
</script>

<template>
  <div>
    <button @click="random">打乱</button>
    <transition-group tag="div" class="wraps" move-class="move">
      <div v-for="item in list" :key="item.id" class="item">
        {{ item.number }}
      </div>
    </transition-group>
  </div>
</template>

<style scoped lang="less">
.wraps {
  display: flex;
  flex-wrap: wrap; // 换行
  width: calc(25px * 10 + 9px);
  .item {
    width: 25px;
    height: 25px;
    border: 1px solid #ccc;
    text-align: center;
  }
}
.move {
  transition: all 1s;
}
</style>

5.状态过度(数字过度颜色过度)

<script setup lang="ts">
import { reactive, watch } from 'vue'
import gsap from 'gsap'

const num = reactive({
  current: 0,
  tweenedNumber: 0
})

watch(
  () => num.current,
  newVal => {
    gsap.to(num, {
      duration: 1, // 过度时间
      tweenedNumber: newVal
    })
  }
)
</script>

<template>
  <div>
    <input type="text" v-model="num.current" step="20" />
    <div>
      <!-- 去掉小数点 -->
      {{ num.tweenedNumber.toFixed(0) }}
    </div>
  </div>
</template>

<style scoped lang="less"></style>

keep-alive组件

1.开启keep-alive 生命周期的变化

初次进入时: onMounted-> onActivated
退出后触发:  deactivated

2.缓存数据

<script setup lang="ts">
import { ref } from 'vue'
import Acom from './components/Acom.vue'

const show = ref(true)
</script>

<template>
  <keep-alive>
    <Acom v-if="show"></Acom>
  </keep-alive>
  <button @click="show = !show">显示/隐藏</button>
</template>

3.include属性和exclude属性

<!-- 注意组件一定要命名才可以使用include -->
<script setup lang="ts">
import { ref } from 'vue'
import Acom from './components/Acom.vue'
import Bcom from './components/Bcom.vue'
const show = ref(true)
</script>

<template>
  <keep-alive :include="['Acom']" :exclude="['Bcom']">
    <Acom v-if="show"></Acom>
    <Bcom v-else></Bcom>
  </keep-alive>
  <button @click="show = !show">显示/隐藏</button>
</template>

<style scoped lang="less"></style>

普通组件

1.配置全局组件

import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import Acom from './components/Acom.vue'
import './assets/main.css'

const app = createApp(App)

app.use(createPinia())

app.component('Acom', Acom)

app.mount('#app')

2.使用组件

<template>
  <div>
    <Acom></Acom>
  </div>
</template>
异步组件

1.子组件中发送了请求变成异步

<script setup lang="ts">
interface ResItf {
  code: number
  data: { a: number; b: number }[]
  message: string
}

let p: Promise<ResItf> = new Promise(resolve => {
  setTimeout(() => {}, 3000)
  resolve({
    code: 0,
    data: [
      { a: 1, b: 2 },
      { a: 11, b: 22 }
    ],
    message: ''
  })
})
const a = await p
console.log(a)
</script>

<template>
  <div>异步组件</div>
  <div>异步组件</div>
  <div>异步组件</div>
</template>

2.父组件异步调用组件

<script setup lang="ts">
// 异步组件不能这样引入
// import Acom from './components/Acom.vue'
import { defineAsyncComponent } from 'vue'
const Acom = defineAsyncComponent(() => import('./components/Acom.vue'))
</script>

<template>
  <div>
    <Suspense>
      <template #default>
        <Acom></Acom>
      </template>

      <template #fallback> 加载中。。。 </template>
    </Suspense>
  </div>
</template>

<style scoped lang="less"></style>

常用的CSS的功能

1.样式穿透

<style scoped lang="less">
:deep(input) {
  color: red;
}
</style>

2.插槽选择器

<template>
  <div>
    <slot name="nums" :nums="['1', '2', '3']"> </slot>
  </div>
</template>

<style scoped lang="less">
:slotted(.li) {
  color: red;
}
</style>

3.全局选择器

<script setup lang="ts"></script>

<template>
  <div>
    <slot name="nums" :nums="['1', '2', '3']"> </slot>
  </div>
</template>

<style scoped lang="less">
:global(.li) {
  color: red;
}
</style>

4.动态CSS

<script setup lang="ts">
import { reactive } from 'vue'
const style = reactive({
  color: 'red'
})
setTimeout(() => {
  style.color = 'blue'
}, 3000)
</script>

<template>
  <div class="div">动态css</div>
</template>

<style scoped lang="less">
.div {
  color: v-bind('style.color');
}
</style>


移动端适配

第一种适配方案

1.安装依赖yarn add amfe-flexible postcss postcss-pxtorem@5.1.1
2.main.ts引入amfe-flexibleimport "amfe-flexible"
3.根目录下创建postcss.config.js文件并配置

module.exports = {
  plugins: {
    'postcss-pxtorem': {
      // 能够把所有元素的px单位转成Rem
      // rootValue: 转换px的基准值。
      // 编码时, 一个元素宽是75px,则换成rem之后就是2rem
      rootValue: 37.5,
      propList: ['*']
    }
  }
}

第二种适配方案

1.安装依赖yarn add postcss-px-to-viewport -D
2.vite.config.ts内置postcss.config.js中修改配置

import { fileURLToPath, URL } from 'node:url'
import pxtoViewPort from 'postcss-px-to-viewport'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [vue()],
  css: {
    postcss: {
      plugins: [
     // postcss-px-to-viewport的配置
        pxtoViewPort({
          unitToConvert: 'px', // 要转化的单位
          viewportWidth: 750, // UI设计稿的宽度
          unitPrecision: 6, // 转换后的精度,即小数点位数
          propList: ['*'], // 指定转换的css属性的单位,*代表全部css属性的单位都进行转换
          viewportUnit: 'vw', // 指定需要转换成的视窗单位,默认vw
          fontViewportUnit: 'vw', // 指定字体需要转换成的视窗单位,默认vw
          selectorBlackList: ['ignore-'], // 指定不转换为视窗单位的类名,
          minPixelValue: 1, // 默认值1,小于或等于1px则不进行转换
          mediaQuery: true, // 是否在媒体查询的css代码中也进行转换,默认false
          replace: true, // 是否转换后直接更换属性值
          landscape: false // 是否处理横屏情况
        })


      ]
    }
  },
  resolve: {
    alias: {
      '@': fileURLToPath(new URL('./src', import.meta.url))
    }
  }
})

3.创建postcss-px-to-viewport.d.ts的声明文件

declare module 'postcss-px-to-viewport' {
  type Options = {
    unitToConvert: 'px' | 'rem' | 'cm' | 'em'
    viewportWidth: number
    viewportHeight: number // not now used; TODO: need for different units and math for different properties
    unitPrecision: number
    viewportUnit: string
    fontViewportUnit: string // vmin is more suitable.
    selectorBlackList: string[]
    propList: string[]
    minPixelValue: number
    mediaQuery: boolean
    replace: boolean
    landscape: boolean
    landscapeUnit: string
    landscapeWidth: number
  }

  export default function (options: Partial<Options>): any
}

4.在tsconfig.json中引入声明文件

{
  "extends": "@vue/tsconfig/tsconfig.web.json",
  "include": ["env.d.ts", "src/**/*", "src/**/*.vue", "postcss-px-to-viewport.d.ts"],
  "compilerOptions": {
    "baseUrl": ".",
    "types": ["element-plus/global"],
    "paths": {
      "@/*": ["./src/*"]
    }
  },

  "references": [
    {
      "path": "./tsconfig.config.json"
    }
  ]
}

5.注意:如果外面用到了postcss.config.js,在postcss.config.js中添加配置文件

// 要禁用vite.config.ts内置postcss.config.js
module.exports = {
  plugins: {
    tailwindcss: {},
    autoprefixer: {},
    'postcss-px-to-viewport': {
      unitToConvert: 'px', // 要转化的单位
      viewportWidth: 320 // UI设计稿的宽度
      // unitPrecision: 6, // 转换后的精度,即小数点位数
      // propList: ['*'], // 指定转换的css属性的单位,*代表全部css属性的单位都进行转换
      // viewportUnit: 'vw', // 指定需要转换成的视窗单位,默认vw
      // fontViewportUnit: 'vw', // 指定字体需要转换成的视窗单位,默认vw
      // selectorBlackList: ['wrap'], // 指定不转换为视窗单位的类名,
      // minPixelValue: 1, // 默认值1,小于或等于1px则不进行转换
      // mediaQuery: true, // 是否在媒体查询的css代码中也进行转换,默认false
      // replace: true, // 是否转换后直接更换属性值
      // exclude: [/node_modules/], // 设置忽略文件,用正则做目录名匹配
      // landscape: false // 是否处理横屏情况
    }
  }
}

函数式编程

1.h函数

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

2.h函数的多种组合

// 除类型之外的所有参数都是可选的
h('div')
h('div', { id: 'foo' })
 
//属性和属性都可以在道具中使用
//Vue会自动选择正确的分配方式
h('div', { class: 'bar', innerHTML: 'hello' })
 
// props modifiers such as .prop and .attr can be added
// with '.' and `^' prefixes respectively
h('div', { '.name': 'some-name', '^width': '100' })
 
// class 和 style 可以是对象或者数组
h('div', { class: [foo, { bar }], style: { color: 'red' } })
 
// 定义事件需要加on 如 onXxx
h('div', { onClick: () => {} })
 
// 子集可以字符串
h('div', { id: 'foo' }, 'hello')
 
//如果没有props是可以省略props 的
h('div', 'hello')
h('div', [h('span', 'hello')])
 
// 子数组可以包含混合的VNode和字符串
h('div', ['hello', h('span', 'hello')])

3.使用props传递参数

<template>
    <Btn text="按钮"></Btn>
</template>
  
<script setup lang='ts'>
import { h, } from 'vue';
type Props = {
    text: string
}
const Btn = (props: Props, ctx: any) => {
    return h('div', {
        class: 'p-2.5 text-white bg-green-500 rounded shadow-lg w-20 text-center inline m-1',
 
    }, props.text)
}
</script>

4.接收emit

<template>
    <Btn @on-click="getNum" text="按钮"></Btn>
</template>
  
<script setup lang='ts'>
import { h, } from 'vue';
type Props = {
    text: string
}
const Btn = (props: Props, ctx: any) => {
    return h('div', {
        class: 'p-2.5 text-white bg-green-500 rounded shadow-lg w-20 text-center inline m-1',
        onClick: () => {
            ctx.emit('on-click', 123)
        }
    }, props.text)
}
 
const getNum = (num: number) => {
    console.log(num);
}
</script>

5.定义插槽

<template>
    <Btn @on-click="getNum">
        <template #default>
            按钮slots
        </template>
    </Btn>
</template>
  
<script setup lang='ts'>
import { h, } from 'vue';
type Props = {
    text?: string
}
const Btn = (props: Props, ctx: any) => {
    return h('div', {
        class: 'p-2.5 text-white bg-green-500 rounded shadow-lg w-20 text-center inline m-1',
        onClick: () => {
            ctx.emit('on-click', 123)
        }
    }, ctx.slots.default())
}
 
const getNum = (num: number) => {
    console.log(num);
}
</script>

vue性能优化

vite配置文件中vite的优化

import { fileURLToPath, URL } from "node:url";
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import vueJsx from "@vitejs/plugin-vue-jsx";

// https://vitejs.dev/config/
export default  defineConfig({
    ...
    build: {
      chunkSizeWarningLimit: 2000,
      cssCodeSplit: true, //css 拆分
      sourcemap: false, //不生成sourcemap
      minify: 'terser', //是否禁用最小化混淆,esbuild打包速度最快,terser打包体积最小。
      assetsInlineLimit: 5000 //小于该值 图片将打包成Base64
    }
  })

PWA离线存储技术
安装依赖yarn add vite-plugin-pwa -D
配置

import { fileURLToPath, URL } from "node:url";
import { VitePWA } from "vite-plugin-pwa";
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import vueJsx from "@vitejs/plugin-vue-jsx";

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [
    vue(),
    vueJsx(),
    VitePWA({
      workbox: {
        cacheId: "key", //缓存名称
        runtimeCaching: [
          {
            urlPattern: /.*\.js.*/, //缓存文件
            handler: "StaleWhileRevalidate", //重新验证时失效
            options: {
              cacheName: "XiaoMan-js", //缓存js,名称
              expiration: {
                maxEntries: 30, //缓存文件数量 LRU算法
                maxAgeSeconds: 30 * 24 * 60 * 60, //缓存有效期
              },
            },
          },
        ],
      },
    }),
  ],
  ....
});

图片懒加载

import { createApp } from 'vue'
import App from './app'
import lazyPlugin from 'vue3-lazy'

const app = createApp(App)
app.use(lazyPlugin, {
  loading: 'loading.png',
  error: 'error.png'
})
app.mount('#app')


<img v-lazy="user.avatar" >
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,686评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,668评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,160评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,736评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,847评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,043评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,129评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,872评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,318评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,645评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,777评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,470评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,126评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,861评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,095评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,589评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,687评论 2 351

推荐阅读更多精彩内容