vue3.0 初体验

193 阅读7分钟

创建一个项目

环境配置

node版本

根据官网可知,vue3需要v10以上的版本,可以通过nvm进行版本控制。

node -v

@vue/cli版本

@vue/cli是官方脚手架,通过 vue --version查看版本号,需要保证版本在v4.5.0+,可以通过以下命令,更新至最新版本:

npm install -g @vue/cli

初始化项目

1、通过 vue create vue3-basic 命令,回车,会出现如图选项:

image.png

  • Default([Vue 2] babel, eslint) : vue2模板
  • Default(Vue 3)([Vue 2] babel, eslint) : vue3模板
  • Manualy select features:手动选择特性

2、因为我们添加typescript支持,所以选择第三项,回车:

image.png

3、选中上述内容,回车:

image.png

4、选中 3.x版本,回车

image.png

5、不选择 class-style component syntax,输入N,回车

image.png

6、是否选择 Babel 结合 typescriptBabel会自动添加 polyfills转换 JXS 功能,因为不需要,所以输入 N ,回车

image.png

7、eslint配置选择默认第一项,回车

image.png

8、lint默认选择第一项,回车

image.png

9、选择配置文件是在单独的文件还是package.json文件里,选择默认在单独文件里,回车

image.png

10、是否保存以上配置在以后的安装,可自行选择,一般保存

image.png

到这儿,项目初始化完成。

执行npm run serve命令会报错,重新选择一下 eslint配置 ,安装 vue add @vue/eslint,选择 Standard模式,项目就可以启动成功。

image.png

插件文文档

推荐插件安装

1、 eslint

image.png

可检测 eslint 是否生效,如果没有生效,可在项目根目录下添加 .vscode/setting.json 文件

{
  "eslint.validate": ["typescript"]
}

2、vetur (vue2使用) image.png

3、Volar (vue3使用) image.png

不要同时开启veturVolar

vue3 新特性

响应性-ref与reactive

响应性原理直通车

ref的妙用

ref 的响应式变量

假设点击一个按钮,给count递增, vue2的写法

<template>
  <img alt="Vue logo" src="./assets/logo.png">
  <h1>{{count}}</h1>
  <button @click="increase">👍+1</button>
</template>

<script lang="ts">
import { defineComponent } from 'vue'

export default defineComponent({
  name: 'App',
  data () {
    return {
      count: 0
    }
  },
  methods: {
    increase () {
      this.count++
    }
  }
})
</script>

vue3 引用 ref 可以不使用 datamethods也能实现同样的效果:

<template>
  <img alt="Vue logo" src="./assets/logo.png">
  <h1>{{count}}</h1>
  <button @click="increase">👍+1</button>
</template>

<script lang="ts">
import { defineComponent, ref } from 'vue'

export default defineComponent({
  name: 'App',
  setup () {
    const count = ref(0)
    console.log(count)
    const increase = () => {
      count.value++
    }
    return {
      count,
      increase
    }
  }
})
</script>

创建computed属性

<template>
  <img alt="Vue logo" src="./assets/logo.png">
  <h1>{{count}}</h1>
  <h2>{{double}}</h2>
  <button @click="increase">👍+1</button>
</template>

<script lang="ts">
// 引入ref
import { defineComponent, ref, computed } from 'vue'

export default defineComponent({
  name: 'App',
  setup () {
    const count = ref(0)
    console.log(count)
    // 使用computed方法
    const double = computed(() => {
      return count.value * 2
    })
    const increase = () => {
      count.value++
    }
    return {
      count,
      increase,
      double
    }
  }
})
</script>

ref 解包

<template>
  <img alt="Vue logo" src="./assets/logo.png">
  <h1>{{count}}</h1>
  <button @click="increase">👍+1</button>
</template>

<script lang="ts">
import { defineComponent, ref } from 'vue'

export default defineComponent({
  name: 'App',
  setup () {
    const count = ref(0)
    console.log(count)
    const increase = () => {
      count.value++
    }
    return {
      count,
      increase,
      nested: {
        count,
        increase
      }
    }
  }
})
</script>

如果你不想要访问实际的对象实例,可将其用 reactive 包裹:

// 引入 reactive
import { reactive } from 'vue'


// 返回值用reactive包裹
nested: reactive({
    count,
    increase
    })

reactive

除了 Ref可以实现响应式功能外,reactive也可以实现

<template>
  <img alt="Vue logo" src="./assets/logo.png">
  <h1>{{data.count}}</h1>
  <h2>{{data.double}}</h2>
  <button @click="data.increase">👍+1</button>
</template>

<script lang="ts">
// 引入reactive
import { computed, defineComponent, reactive } from 'vue'

interface DataProps {
  count: number;
  double: number;
  increase: () => void;
}

export default defineComponent({
  name: 'App',
  setup () {
    const data: DataProps = reactive({
      count: 0,
      increase: () => { data.count++ },
      double: computed(() => data.count * 2)
    })
    return {
      data
    }
  }
})
</script>

refreactive 响应式失效

因为refreactive返回的是响应式数据,如果使用解构的语法,就失去了响应的功能。(传送门

vue3.0 针对这个问题,引入了 toRefs api.

<template>
  <img alt="Vue logo" src="./assets/logo.png">
  <h1>{{count}}</h1>
  <h2>{{double}}</h2>
  <button @click="increase">👍+1</button>
</template>

<script lang="ts">
// 引入reactive
import { computed, defineComponent, reactive, toRefs } from 'vue'

interface DataProps {
  count: number;
  double: number;
  increase: () => void;
}

export default defineComponent({
  name: 'App',
  setup () {
    const data: DataProps = reactive({
      count: 0,
      increase: () => { data.count++ },
      double: computed(() => data.count * 2)
    })
    // 使用toRefs解决解构语法使响应式失效问题
    const { count, increase, double } = toRefs(data)
    return {
      count,
      increase,
      double
    }
  }
})
</script>

ref 和 reactive 区别

ref函数:

语法:const xxx = ref (initValue)

  • 创建一个包含响应式数据的引用对象(reference对象,简称ref对象)
  • JS中操作数据:xxx.value
  • 模板中读取数据:不需要.value,直接读取 xxx 接受的数据类型:基本类型,引用类型
    作用:把参数加工成一个响应式对象:reference对象
    核心原理:
  • 基本类型响应式: 依赖Object.defineProperty()的get()和set()完成
  • 是引用类型响应式:底层ref会借助reactive函数的proxy定义响应式

reactive函数:

语法:const xxx = reactive (源对象)
接受的数据类型:引用类型
作用:把参数加工成一个代理对象,全称为proxy对象
核心原理:基于ES6的Proxy实现,通过Reflect反射代理操作源对象,相比于reactive定义的浅层次响应式数据对象,reactive定义的是更深层次的响应式数据对象

响应式侦听 - watch

监听单个数据变化

<template>
  <h2>{{greetings}}</h2>
  <h2>title: {{title}}</h2>
  <button style="margin: 20px;padding:20px;" @click="updateGreeting">updateGreeting</button>
</template>

<script lang="ts">
// 引入reactive
import { defineComponent,watch, ref } from 'vue'

export default defineComponent({
  name: 'App',
  setup () {
    // watch监听数据变化
    const title = ref('')
    watch(greetings, (newValue, oldValue) => {
      console.log(newValue)// hello
      console.log(oldValue)// ''
      title.value = 'update ' + greetings.value
    })
    return {
      updateGreeting,
      title
    }
  }
})
</script>

监听多个数据变化

监听多个数据,第一个参数用数组表示,监听函数的参数,也是对应顺序的新旧值。

<template>
  <img alt="Vue logo" src="./assets/logo.png">
  <h1>{{count}}</h1>
  <h2>{{double}}</h2>
  <h2>{{greetings}}</h2>
  <h2>title: {{title}}</h2>
  <button @click="increase">👍+1</button><br>
  <button style="margin: 20px;padding:20px;" @click="updateGreeting">updateGreeting</button>
</template>

<script lang="ts">
// 引入reactive
import { computed, defineComponent, reactive, toRefs, watch, ref } from 'vue'

interface DataProps {
  count: number;
  double: number;
  increase: () => void;
}

export default defineComponent({
  name: 'App',
  setup () {
    const data: DataProps = reactive({
      count: 0,
      increase: () => { data.count++ },
      double: computed(() => data.count * 2)
    })
    // 使用toRefs解决解构语法使响应式失效问题
    const { count, increase, double } = toRefs(data)

    const greetings = ref('')
    const updateGreeting = () => {
      greetings.value += 'Hello! '
    }
    // watch监听数据变化
    const title = ref('')
    watch([greetings, data], (newValue, oldValue) => {
      console.log(newValue)// ['hello', Proxy]  data是一个reactive对象,所以是一个Proxy对象
      console.log(oldValue)// ['', Proxy]
      title.value = 'update ' + greetings.value + data.count
    })
    return {
      count,
      increase,
      double,
      greetings,
      updateGreeting,
      title
    }
  }
})
</script>

因为data是一个 reactive对象,我们知道,reactive对象底层是通过Proxy实现的,所以,监听的结果是一个Proxy对象,如图所示: image.png

想要正确监听到data对象下的某个属性的变化,可以使用以下方法

watch([greetings, () => data.count], (newValue, oldValue) => {
  console.log(newValue) // ['hello',2]
  console.log(oldValue)// ['', 0]
  title.value = 'update ' + greetings.value + data.count
})

image.png

模块化的妙用

基础用法

实现一个实时打印鼠标位置的需求:

1、无模块化实现

<template>
  <h2>X:{{x}}</h2>
  <h2>Y:{{y}}</h2>
</template>

<script lang="ts">
// 引入reactive
import { defineComponent, ref, onMounted, onUnmounted } from 'vue'

export default defineComponent({
  name: 'App',
  setup () {
    // 监听鼠标位置的变化
    const x = ref(0)
    const y = ref(0)
    const updateMouse = (e: MouseEvent) => {
      x.value = e.pageX
      y.value = e.pageY
    }
    onMounted(() => {
      document.addEventListener('click', updateMouse)
    })
    onUnmounted(() => {
      document.removeEventListener('click', updateMouse)
    })
    return {
      x,
      y
    }
  }
})
</script>

2、vue3支持模块化后的写法:

<template>
  <h2>X:{{x}}</h2>
  <h2>Y:{{y}}</h2>
</template>

<script lang="ts">

import { defineComponent } from 'vue'
// 引入hooks
import useMousePosition from './hooks/useMousePosition'

export default defineComponent({
  name: 'App',
  setup () {
    // 监听鼠标位置的变化
    const { x, y } = useMousePosition()
    return {
      x,
      y
    }
  }
})
</script>

src/hooks/useMousePostion文件

import { ref, onMounted, onUnmounted, Ref } from 'vue'

interface position {
  x: Ref<number>;
  y: Ref<number>;
}

function useMousePosition (): position {
  const x = ref(0)
  const y = ref(0)
  const updateMouse = (e: MouseEvent) => {
    x.value = e.pageX
    y.value = e.pageY
  }
  onMounted(() => {
    document.addEventListener('click', updateMouse)
  })
  onUnmounted(() => {
    document.removeEventListener('click', updateMouse)
  })
  return { x, y }
}

export default useMousePosition

如果未定义函数的返回值,会提示如下警告: image.png

解决方案有两种:

  • 1、定义函数的返回类型
// Ref类型需要从vue中引入
interface position {
  x: Ref<number>;
  y: Ref<number>;
}
  • 2、在.eslinttrc.js文件中关掉eslint的这个规则
modules.export = {
    rules: {
        // 每个函数都要显示声明返回值
       "@typescript-eslint/explicit-module-boundary-types": "off"
    }
}

模块化 + typescript泛型

src/hooks/useURLLoader文件

import axios from 'axios'
import { Ref, ref, UnwrapRef } from 'vue'
// result的类型由函数调用时决定
function useURLLoader<T> (url: string): {
  result: Ref<UnwrapRef<T> | null>;
  loading: Ref<boolean>;
  loaded: Ref<boolean>;
  error: Ref<null>;
} {
  const result = ref<T | null>(null)
  const loading = ref(true)
  const loaded = ref(false)
  const error = ref(null)

  axios.get(url).then(rawData => {
    loading.value = false
    loaded.value = true
    result.value = rawData.data
  }).catch(e => {
    error.value = e
    loading.value = false
  })
  return {
    result,
    loading,
    loaded,
    error
  }
}

export default useURLLoader

defineComponent

Vue3 推出defineComponent方法,没有实现任何的逻辑,只是把传入的Object对象直接返回,它的存在,是为了让传入的Object对象获得对应的类型,也就是说,它完全是为了服务typescript存在的。

组合式 API:setup

setup() 钩子是在组件中使用组合式 API 的入口,通常只在以下情况下使用:

  1. 需要在非单文件组件中使用组合式 API 时。
  2. 需要在基于选项式 API 的组件中集成基于组合式 API 的代码时。

其他情况下,都应优先使用 <script setup> 语法。

setup()参数:

  • props:该 props 对象将仅包含显性声明的 prop,并且是响应式对象
  • context:在setup中无法访问this对象,context提供了this的常用的三个属性:attrsslotsemit(对应vue2的$attrs、$slots、$emit)。

Teleport

遇到的问题

  • Dialoy被包裹在其他组件之中,容易被干扰
  • 样式也在其他组件中,容易变得非常混乱

Teleport 提供了一种干净的方法,允许我们控制在 DOM 中哪个父节点下渲染了 HTML,而不必求助于全局状态或将其拆分为两个组件。传送门

teleport特性:

  • 表现层跟其他子组件没有差异
  • 渲染层,可以像传送门一样,将组件传送到指定的父节点下渲染

teleport属性

  • to: 需要传送到哪个DOM节点下

modal.vue

<template>
  <teleport to='#modal'>
    <div id="center" v-if="isOpen">
      <h2>
        <slot>this is a modal</slot>
      </h2>
      <button @click="buttonClick">Close</button>
    </div>
  </teleport>
</template>

<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({
  name: 'Modal',
  props: {
    isOpen: Boolean
  },
  emits: {
    'close-modal': null
  },
  setup (props, context) {
    const buttonClick = () => {
      context.emit('close-modal')
    }
    return {
      buttonClick
    }
  }
})
</script>

<style scoped>
#center {
  width: 200px;
  height: 200px;
  border: 2px solid black;
  background: white;
  position: fixed;
  left: 50%;
  top: 50%;
  margin-left: -100px;
  margin-top: -100px;
}
</style>

teleport可以将这个 modal 传送到 #modal元素下,而不是 #app元素的子元素

index.html

<div id="app"></div>
<!-- 添加modal -->
<div id="modal"></div>

Suspense组件

  • 解决异步请求的困境:比如,请求接口时,页面会有一个数据从无到有的过程
  • Suspense是vue3推出的一个内置的特殊组件
  • 如果使用Suspense,要返回一个promise

特性:

  • <suspense> 组件有两个插槽。它们都只接收一个直接子节点。
  • default 插槽里的节点会尽可能展示出来。
  • 如果default 插槽里的节点不能展示,则展示 fallback 插槽里的节点

asyncShow.vue

<template>
  <h1>{{result}}</h1>
</template>

<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({
  name: 'AsyncShow',
  setup () {
    return new Promise((resolve) => {
      setTimeout(() => {
        resolve({
          result: 42
        })
      }, 3000)
    })
  }
})
</script>

<style scoped>

</style>

App.vue

<Suspense>
    <template #default>
      <async-show></async-show>
    </template>
    <template #fallback>Loading</template>
  </Suspense>

捕获Suspense包裹的异步组件的错误,可以使用onErrorCaptured生命周期钩子函数

const error = ref(null)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
onErrorCaptured((e: any) => {
  error.value = e
  return true
})

Provide/Inject

  • Provide:数据通过props属性由上向下(由父及子)进行传递,这种特性称为Prop Drilling image.png

有多个子组件时,就会经过多级传递。此时,Prop Drilling就显得很繁琐。Provide/Inject的推出就是为了解决这个问题,目的就是为共享那些被认为对于一个组件树而言是”全局“的数据。

image.png

<script setup>

<script setup> 是在单文件组件 (SFC) 中使用组合式 API 的编译时语法糖。

ts+vue3问题总结

init

image.png

样式规范

好用的样式库:

本项目使用 bootstrap,下载

npm install bootstrap

如果使用命令npm install bootstrap@next --save,则会安装最新的线上版本,包括beta版本。

typescript支持

使用PropType指明构造函数

如果要对一个构造函数进行断言,需要使用PropType指明构造函数。详见官方文档

<template>
  <div></div>
</template>

<script lang="ts">
import { defineComponent, PropType } from 'vue'

interface ColumnProps {
  id: number;
  title: string;
  avatar: string;
  description: string;
}

export default defineComponent({
  name: 'ColumnList',
  props: {
    list: {
      // 因为Array是一个构造函数,不能直接断言成一个类型,所以需要使用PropType泛型
      type: Array as PropType<ColumnProps[]>,
      require: true
    }
  },
  setup () {
    return {

    }
  }
})
</script>

vscode配置vue的Templete自动补全

vscodesetting.json文件配置

"vetur.experimental.templateInterpolationService": true,

获取html元素节点

vue3中获取html元素的节点vue2有点出入,具体操作如下:

  1. html上使用ref="xxx"标记 html节点
  2. setup 中生成一个 ref对象,并且该对象的名称跟 步骤1html节点名称一致
  3. setup步骤2ref对象导出

image.png

备注:验证某节点是否是一个节点的子节点,使用 contains()方法。contains方法传送门

通用正则

邮箱

const emailReg = /^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/

v-model迁移策略

当我们在定义组件上使用v-model时,vue3的语法进行了重构。

自定义组件上的 v-model 相当于传递了 modelValue prop 并接收抛出的 update:modelValue 事件。迁移策略详见文档

  1. v-model不带参数 对于所有不带参数的 v-model,请确保分别将 prop 和 event 命名更改为 modelValue 和 update:modelValue

image.png

image.png

  1. v-model带参数

若需要更改 model 的名称,现在我们可以为 v-model 传递一个参数,以作为组件内 model 选项的替代

<ChildComponent v-model:title="pageTitle" />

$attrs

传送门

一个非 prop 的 attribute 是指传向一个组件,但是该组件并没有相应 props 或 emits 定义的 attribute。常见的示例包括 classstyle 和 id attribute。可以通过 $attrs property 访问那些 attribute。

  1. 当组件返回单个根节点时,非 prop 的 attribute 将自动添加到根节点的 attribute 中。

image.png

  1. 如果你希望组件的根元素继承 attribute,可以在组件的选项中设置 inheritAttrs: false
    setup函数的第二个参数context中可以看到所有的非props的attribute

image.png

同时,可以使用v-bind绑定到任意的目标元素上。

image.png

image.png

事件监听器

在 vue2 中,Vue 实例可用于触发由事件触发器 API 通过指令式方式添加的处理函数 ($on$off 和 $once),但vue3已经移除。在 Vue 3 中,借助这些 API 从一个组件内部监听其自身触发的事件已经不再可能了。

我们可以使用外部的库 mitt实现相同效果。

父组件 ValidateForm.vue

import mitt from 'mitt'
export const emitter = mitt()


export default defineComponent({
  name: 'ValidateForm',
  emits: ['form-submit'],
  setup (props, context) {
    const callback = (val: string) => {
      console.log(val, '测试')
    }
    emitter.on('test', callback)
    onUnmounted(() => {
      emitter.off('test')
    })
    return {}
  }
})

子组件

import { emitter } from './ValidateForm.vue'

export default defineComponent({
  name: 'ValidateInput',
  props: {
    rules: {
      type: Array as PropType<RulesProp>
    },
    modelValue: {
      type: String
    }
  },
  inheritAttrs: false,
  setup (props, context) {
    
    onMounted(() => {
      emitter.emit('test', '12')
    })
    return {}
  }
})

由于mitt3.0版本,对于类型定义更严格,使用时,会出现如下错误提示:

image.png

解决方案:

定义一个事件类型,并且让事件类型跟callback的参数的类型一一对应

父组件

import mitt from 'mitt'
// 定义一个事件类型
type Events = {
  'test': string
}
// 实例化时,将类型作为泛型传递进去
export const emitter = mitt<Events>()

// 这里的参数类型 string 跟事件类型定义时的类型string对应
const callback = (val: string) => {
  console.log(val, '测试')
}

url组成解析

nodejs.org/api/url.htm…

image.png

ts中默认空对象报错

typescript中,当给一个变量设置为一个空对象时,会有如下错误出现

image.png 解决办法

const lowerObj: Record<string, unknown> = {};

vue简单工作原理

文档地址

Virtual DOM:一种虚拟的,保存在内存中的数据结构,用来代表 UI 的表现,和真实 DOM 节点保持同步。Virtual DOM是由一系列的 Vnode 组成的。

Render Pipeline

  • Compile, Vue 组件的 Template 会被编译成 render function,一个可以返回 Virtual DOM 树的函数。
  • Mount,执行 render function,遍历虚拟DOM 树,生成真正的 DOM 节点。
  • Patch,当组件中任何响应式对象(依赖)发生变化的时候,执行更新操作 。生成新的虚拟节点树,Vue 内部会遍历新的虚拟节点树,和旧的树做对比,然后执行必要的更新。

image.png

虚拟DOM 的优点

  • 可以使用一种更方便的方式,供开发者操控 UI 的状态和结构,不必和真实的DOM 节点打交道。
  • 更新效率更高,计算需要的最小化操作,并完成更新。
  • Template 比 render function 更接近 html,更好懂,更容易修改。
  • Template 更容易做静态优化,Vue 的 compiler 在编译过程中可以做很多自动的性能优化。