vite2 + Vue3 + ts 入门实操 – 大大的案例

前言

学习一下 vite2 + Vue3 + ts 的案例和知识点,从零到一实操练习,记录所遇的坑儿。

初始化

命令行创建 vite2 项目

npm init @vitejs/app
复制代码

启动项目时!

image-20210617152554421.png

解决:将 node 版本 >= 12.0.0

我个人解决办法:nvm use 12

跟着步骤创建完项目后,webStorm 不支持 Vue3 的语法,频繁报错,于是安装 VS Code 及其插件。

VS Code: 安装 Volar,禁用 Vetur,会发生冲突。快速调用命令行窗口 control +~

在导入文件时,因 ts 的原因,需要 shims-vue.d.ts 文件默认声明 ts 导出变量才能 import 进来。

第一个 Demo

代码

<template>
	<h1>{{msg}}</h1>
  <div>默认的 count: {{state.count}} </div>
  <div>默认的 count: {{state.double}} </div>
  <button @click="increment">增加</button>
</template>

<script setup lang="ts">
  import {reactive, computed, defineProps} from 'vue'
  
  defineProps({
    msg: String
  })
  type DState = {
    count: number;
    double: number;
  }

  const state: DState = reactive({
    count: 0,
    double: computed(() => state.count * 2)
  })
  function increment() {
    state.count++
  }
</script>
复制代码

image-20210618123534273.png

总结

  • 了解 setup 语法糖
  • 了解 defineProps
  • 了解 reactive
  • 了解 computed hook 的钩子函数
  • 了解 ts 的声明

Ref 和 Reactice

代码

<template>
  <h1>{{msg}}</h1>
  ---
  <div>ref: {{count}}</div>
  <div>reactive: {{state.count}} || {{obj.name}}</div>
  <div>结构 reactive: {{name}} || {{age}}</div>
</template>

<script setup lang="ts">
  import {reactive, computed, defineProps, ref, toRefs} from 'vue'
  defineProps({
    msg: String
  })
  // ref 语法糖
  ref: count = 0
  // 响应式数据,若不用语法糖则需要 count.value++
  count++
  

  // reactive 包裹单独的 ref
 const state = reactive({
   count
 })
 // 单独声明响应式对象数据
 const obj = reactive({
   name: "大大",
   age: 21
 })

 // 把 reactice 响应式对象解构出来
 const {name, age} = {...toRefs(obj)}
  
</script>
复制代码

image-20210618125717444.png

总结

  • 设置响应式数据:refreactive
    • ref:声明原始数据类型
    • reactive:声明复杂数据类型
  • ref 的语法糖
    • ref: count = 0 === const count = ref(0)
    • 若更改响应式数据,有语法糖的差异
  • reactive 的声明
    • 单独声明一个响应式对象数据
    • 该 reactive 对包裹着一个个 ref,也就是将其 proxy 代理
  • reactive 的解构:...toRefs(obj)

templateref

代码

<template>
  <h1>{{msg}}</h1>
  ---
  <input type="text" ref="root" value="ref使用例子" />
</template>

<script setup lang="ts">
  import {defineProps, onMounted, ref} from 'vue'
  defineProps({
    msg: String
  })
  // ref 会根据传入的数据来指向对应的数据类型
  // 若传入一个 null,则会指向 DOM
  const root = ref(null)
  
  // 首次渲染挂载完成时,才能获取到真实的 DOM
  onMounted(() => {
    console.log("ref.null:", root.value);
    
  })
</script>
复制代码

image-20210618130324175.png

总结

  • 了解 templateRef:其实就是 ref 指向了真实的 DOM
  • ref 类型:会根据传入的数据来指向对应的数据类型
    • 传入 0:Ref(number)
    • 传入 “”:Ref(string)
    • 传入 null:Ref(null),指向的是 DOM 对象
  • onMounted 生命钩子函数调用
    • 在首次渲染挂载完成时,调用 Ref(null).value 来获取真实的 DOM 对象,这个就是 templateRef

computed

代码

<template>
  <h1>{{msg}}</h1>
  ---
  <div>ref: {{count}}</div>
  <div>computed: {{plusOne}}</div>
  <div>computed(get/set): {{plusTwo}}</div>
</template>

<script setup lang="ts">
  import {defineProps, computed} from 'vue'
  defineProps({
    msg: String
  })
  
  // ref 语法糖
  ref: count = 1
  ref: count2 = 0
  
  // 计算属性
  const plusOne = computed(() => count += 1)
  // 查看计算属性生成对象
  console.log("coumputed:", plusOne);
  
  // get/set 重新设置计算属性
  const plusTwo = computed({
    get: () => count2 + 2,
    set: (val) => count2 = val - 1
  })
  plusTwo.value = Math.random()
</script>
复制代码

image-20210618132928810.png

总结

  • computed 计算属性方法包裹响应式数据
  • computed 生成对象
    • 只读
    • 生成新的 ref
    • dirty 值为 false
  • computed 利用 get/set 进行遮罩

注意:在书写案例期间,遇到一个有意思的地方,若将 const plusOne = computed(() => count += 1) 中的 count += 1 换成 count ++ 也就是自加,结果为 ref: 2computed: 1

image-20210618133227206.png

watch

代码

<template>
  <h1>{{msg}}</h1>
  ---
  <div>state: {{state.count}}</div>
  <div>state2: {{state2.count.count}}</div>
  <div>ref1: {{ref1}}</div>
  <div>ref2: {{ref2}}</div>
  <button @click="add">添加</button>
</template>

<script setup lang="ts">
  import {defineProps, reactive, watch} from 'vue'
  defineProps({
    msg: String
  })
  
  const state = reactive({
    count: 0
  })
  // 复杂的响应式对象
  const state2 = reactive({
    count: {count: 1}
  })

  // 多个 ref
  ref: ref1 = 2
  ref: ref2 = 3

  /**
   * 参数:1.数据源, 2.回调, 3.参数配置
   */
  watch(() => state.count, (newValue, oldValue) => {
    console.log("state 监听的值:", newValue, oldValue);
  }, {
    immediate: true,  // 立即监听
  })

  // 监听深拷贝
  watch(() => state2.count, (newValue, oldValue) => {
    console.log("state2 监听的值:", newValue.count, oldValue.count);
  }, {
    deep: true
  })

  // 监听多个 ref
  watch(() => [ref1, ref2], ([newRef1, oldRef1], [newRef2, oldRef2]) => {
    console.log('ref1:', newRef1, oldRef1);
    console.log('ref1:', newRef2, oldRef2);
  })

  const add = () => {
    state.count++
    state2.count.count++
    ref1++
    ref2++
  }
</script>
复制代码

image-20210618223717220.png

总结

官方内容

watch API 完全等同于组件侦听器 property。watch 需要侦听特定的数据源,并在回调函数中执行副作用。默认情况下,它也是惰性的,即只有当被侦听的源发生变化时才执行回调。

watchEffect 比较,watch 允许我们:

  • 懒执行副作用;

  • 更具体地说明什么状态应该触发侦听器重新运行;

  • 访问侦听状态变化前后的值。

注意:当使用 watch 监听多个 ref 时,若没有使用回调函数监听数组,则会提示警告监听数据非响应式数据

// 监听多个 ref
watch([ref1, ref2], ([newRef1, oldRef1], [newRef2, oldRef2]) => {
console.log('ref1:', newRef1, oldRef1);
console.log('ref1:', newRef2, oldRef2);
})
复制代码

image-20210618223918532.png

改正:

// 监听多个 ref
watch(() => [ref1, ref2], ([newRef1, oldRef1], [newRef2, oldRef2]) => {
console.log('ref1:', newRef1, oldRef1);
console.log('ref1:', newRef2, oldRef2);
})
复制代码

Watch 和 Watcheffect

代码

<template>
  <h1>{{msg}}</h1>
  ---
  <div>watchEffect: {{num}}</div>
  <button @click="add">增加</button>
</template>

<script setup lang="ts">
  import {defineProps, ref, watchEffect, onMounted} from 'vue'
  defineProps({
    msg: String
  })
  ref: num = 0;
  onMounted(() => {
    console.log("onMounted");
  })

  /**
   * 1. 不需要手动传入依赖
   * 2. 不是 lazy,初始化时就执行分析依赖
   * 3. 无法获取原始值
   * 4. 适合操作异步操作
   * 5. 第一个参数处理副作用
   */
  const stop = watchEffect((onInvalidate) => {
    console.log("watcherEffed 在 onMounted 之前调用", num);
    // 清除副作用
    onInvalidate(() => {
      console.log("监听后的副作用");
    })
  }, {
    // flush: "sync",  // 调用的时机
    onTrigger(e) {    // 将在依赖项变更导致副作用被触发时被调用。
      console.log('onTrigger', e);
    },
    onTrack(e) {   // 将在响应式 property 或 ref 作为依赖项被追踪时被调用。
      console.log('onTrack', e);
    }
  })
  const add = () => {
    num++
  }

  // 暂停监听
  stop()

</script>
复制代码

image-20210621211501121.png

总结

  • watchEffect:立即执行传入的一个函数,同时响应式追踪其依赖,并在其依赖变更时重新运行该函数。
  • 优点
    • 不需要手动传入依赖
    • 不是 lazy,初始化时就执行分析依赖
    • 无法获取原始值
    • 适合操作异步操作
    • 副作用刷新时机:在核心的具体实现中,组件的 update 函数也是一个被侦听的副作用。当一个用户定义的副作用函数进入队列时,默认情况下,会在所有的组件 update 执。
      • 如果需要在组件更新重新运行侦听器副作用,我们可以传递带有 flush 选项的附加 options 对象
        • pre:update 前(默认)
        • post:update 后
        • sync:强制效果始终同步触发。然而,这是低效的,不推荐使用
  • onInvalidate:侦听副作用传入的函数可以接收一个 onInvalidate 函数作入参,用来注册清理失效时的回调。
  • 侦听器调试:在开发模式下 onTrackonTrigger ,可用于调试侦听器的行为。
    • onTrack 只在响应式 property 或 ref 作为依赖项被追踪时被调用。
    • onTrigger 将在依赖项变更导致副作用被触发时被调用。
  • 显式调用返回值以停止侦听 stop()

Vue3 生命周期

代码

<template>
  <h1>{{msg}}</h1>
  ---
  <div>Vue3 生命周期</div>
  <div>{{count}}</div>
</template>

<script setup lang="ts">
  import {defineProps, onBeforeMount, onMounted, onBeforeUpdate, onUpdated, onBeforeUnmount, onUnmounted, onErrorCaptured} from 'vue'
  defineProps({
    msg: String
  })
  ref: count = 1
  onBeforeMount(() => {
    console.log('onBeforeMount');
  })
  onMounted(() => {
    console.log('onMounted');
  })
  onBeforeUpdate(() => {
    console.log('onBeforeUpdate');
  })
  onUpdated(() => {
    console.log('onUpdated');
  })
  onBeforeUnmount(() => {
    console.log('onBeforeUnmount');
  })
  onUnmounted(() => { 
    console.log('onUnmounted');
  })
  onErrorCaptured(() => {
    console.log('onErrorCaptured');
  })
  setTimeout(() => {
    count++
  }, 100)

</script>
复制代码

image-20210621213633457.png

总结

  • Vue3 与 Vue2 生命周期对比

    Vue2--------------vue3
    beforeCreate  -> setup()
    created       -> setup()
    beforeMount   -> onBeforeMount
    mounted       -> onMounted
    beforeUpdate  -> onBeforeUpdate
    updated       -> onUpdated
    beforeDestroy -> onBeforeUnmount
    destroyed     -> onUnmounted
    activated     -> onActivated
    deactivated   -> onDeactivated
    errorCaptured -> onErrorCaptured
    复制代码
  • 使用场景

    • setup() :开始创建组件之前,在beforeCreate和created之前执行。
    • onBeforeMount() : 组件挂载到节点上之前执行的函数。
    • onMounted() : 组件挂载完成后执行的函数。
    • onBeforeUpdate(): 组件更新之前执行的函数。
    • onUpdated(): 组件更新完成之后执行的函数。
    • onBeforeUnmount(): 组件卸载之前执行的函数。
    • onUnmounted(): 组件卸载完成后执行的函数。
    • onActivated(): 被包含在中的组件,会多出两个生命周期钩子函数。被激活时执行。
    • onDeactivated(): 比如从 A 组件,切换到 B 组件,A 组件消失时执行。
    • onErrorCaptured(): 当捕获一个来自子孙组件的异常时激活钩子函数。

provide&inject

代码

// Parent.vue
<template>
  <h1>{{msg}}</h1>
  ---
  <div>provide&inject</div>
  <Child />
  <input @click="change" type="button" value="注入">
</template>

<script setup lang="ts">
  import {defineProps, ref, provide} from 'vue'
  import Child from './Child.vue'

  defineProps({
    msg: String
  })
  const themeRef = ref("dark")
  provide("test", themeRef)

  const change = () => {
    themeRef.value = Math.random().toString()
  }
</script>
复制代码
// Child.vue
<template>
  <div>inject: {{theme}}</div>
</template>

<script lang="ts">
  import {defineComponent, inject, Ref, watchEffect} from 'vue'
  export default defineComponent({
    name: "Child",
    setup() {
      const theme = inject("test", "")
      watchEffect(() => {
        console.log("主题修改", (theme as unknown as Ref<string>).value);
      })
      return {
        theme
      }
    }
  })
</script>

复制代码

image-20210621221120006.png

总结

  • provide&inject:父组件可以作为其所有子组件的依赖项提供程序,而不管组件层次结构有多深。这个特性有两个部分:父组件有一个 provide 选项来提供数据,子组件有一个 inject 选项来开始注入使用这个数据。

  • ref 语法糖对应 provide&inject 未响应支持

    • ref: theme = "dark"
      const change = () => {
          themeRef = Math.random().toString()
      }
      复制代码

expose

代码

// Parent.vue
<template>
  <h1>{{msg}}</h1>
  ---
  <div>expose</div>
  <Child  ref="child"/>
  <button @click="">点击变化</button>
</template>

<script setup lang="ts">
  import {defineProps, ref, onMounted} from 'vue'
  import Child from './Child.vue'

  defineProps({
    msg: String
  })

  ref: child = null
  
  onMounted(() => {
    (child as any)?.changeNum()
  })

</script>
复制代码
<template>
  <div>expose: {{num}}</div>
</template>

<script setup lang="ts">
import {useContext, ref} from 'vue'
const {expose} = useContext()
// let num = ref(0)
ref: num = 0

const changeNum = () => {
  num = Math.random()
}

expose({
  changeNum
})

</script>
复制代码

image-20210621223838409.png

总结

  • expose:个人理解就是 Vue2 中利用 ref 获取到子组件实例上的内容和操作
  • 在子组件通过 useContext 钩子函数来获取 expose 函数,抛出方法到子组件实例上。
    • useContext 代替了 setup(props, cox)
  • 在父组件通过 ref 获取,需要在 onMounted 生命周期中获取 DOM 实例
    • 注意获取时因利用 ref(null) 获取 DOM,故内容为 null 时,ts 会报错默认不存在 DOM 实例。
    • 故设置类型为 any(theme as unknown as Ref<() => {changeNum() {}}>).changeNum()

封装 ts 请求 hooks

代码

// src/hooks/useApi.ts
import {Ref, ref} from 'vue';

export type ApiRequest = () => Promise<void>

export interface UsableAPI<T> {
    response: Ref<T | undefined>;
    requst: ApiRequest;
}

function useApi<T>(url: RequestInfo, options?:RequestInit) {
    const response:Ref<T | undefined> = ref();
    const request = async () => {
        const res = await fetch(url, options);
        const data = await res.json();
        response.value = data
    };
    return {
        response,
        request
    }
}
export default useApi
复制代码
// src/hooks/index.ts
export {useApi} from './useApi'
复制代码

总结

泛型

学习:TypeScript 官网

概念

function identity(arg: any): any {
 return arg;
}
复制代码

若使用any类型会导致这个函数可以接收任何类型的arg参数,这样就丢失了一些信息:传入的类型与返回的类型应该是相同的。如果我们传入一个数字,我们只知道任何类型的值都有可能被返回。

因此,我们需要一种方法使返回值的类型与传入参数的类型是相同的。 这里,我们使用了 类型变量,它是一种特殊的变量,只用于表示类型而不是值。

function identity<T>(arg: T): T {
 return arg;
}
复制代码

我们给identity添加了类型变量TT帮助我们捕获用户传入的类型(比如:number),之后我们就可以使用这个类型。 之后我们再次使用了 T当做返回值类型。现在我们可以知道参数类型与返回值类型是相同的了。 这允许我们跟踪函数里使用的类型的信息。

我们把这个版本的identity函数叫做泛型,因为它可以适用于多个类型。 不同于使用 any,它不会丢失信息,可以保持准确性,传入数值类型并返回数值类型。

使用

我们定义了泛型函数后,可以用两种方法使用。

第一种是,传入所有的参数,包含类型参数:

let output = identity<string>("myString");  // type of output will be 'string'
复制代码

这里我们明确的指定了Tstring类型,并做为一个参数传给函数,使用了<>括起来而不是()

第二种方法更普遍。利用了类型推论 — 即编译器会根据传入的参数自动地帮助我们确定T的类型:

let output = identity("myString");  // type of output will be 'string'
复制代码

注意我们没必要使用尖括号(<>)来明确地传入类型;编译器可以查看myString的值,然后把T设置为它的类型。 类型推论帮助我们保持代码精简和高可读性。如果编译器不能够自动地推断出类型的话,只能像上面那样明确的传入T的类型,在一些复杂的情况下,这是可能出现的。

内容

  • 封装请求方法 useApi,类型为泛型 <T>
  • response 调用 vue 中的 ref,并且类型是 Ref<undefined>
    • 因为可能是泛型,所以 Ref<T | undefined>
  • 定义接口 interface:UsableAPI
    • requst 的 类型 type:ApiRequest
  • 最后返回 request 和 response

使用 ts 请求 hooks

代码

  • 封装 ts 接口 modules
// src/modules/products.ts
// https://ecomm-products.modus.workers.dev
import { useApi } from "@/hooks";
import {Ref, ref} from 'vue'
export interface Product {
    id:          string;
    title:       string;
    category:    string;
    description: string;
    images:      string[];
    variants:    Variant[];
    price:       string;
    tags:        Tag[];
}

export enum Tag {
    Awesome = "Awesome",
    Ergonomic = "Ergonomic",
    Fantastic = "Fantastic",
    Generic = "Generic",
    Gorgeous = "Gorgeous",
    Handcrafted = "Handcrafted",
    Handmade = "Handmade",
    Incredible = "Incredible",
    Intelligent = "Intelligent",
    Licensed = "Licensed",
    Practical = "Practical",
    Refined = "Refined",
    Rustic = "Rustic",
    Sleek = "Sleek",
    Small = "Small",
    Tasty = "Tasty",
    Unbranded = "Unbranded",
}

export interface Variant {
    id:       string;
    quantity: number;
    title:    string;
    sku:      string;
}

export type UsableProducts = Promise<{products: Ref<Product[] | undefined>}>

export default async function useProducts():UsableProducts {
    const {response: products, request} = useApi<Product[]>('https://ecomm-products.modus.workers.dev')
    const loaded = ref(false)
    if (loaded.value === false) {
        await request();
        loaded.value = true
    }
    return {products};
}
复制代码
  • 子组件调用
// HelloWorld.vue

<template>
  <h1>{{msg}}</h1>
  ---
  <ul>
    <li v-for="product in products" :key="product.id">
      {{product.title}}
    </li>
  </ul>
</template>

<script setup lang="ts">
  import {defineProps, ref, onMounted} from 'vue'
  defineProps({
    msg: String
  })
  import useProducts from '@/modules/products';
  const {products} = await useProducts()
</script>
复制代码
  • 父组件调用
// App.vue
<template>
	<img alt="Vue logo" src="./assets/logo.png"/>
	
	<div>
		<Suspense>
      <template #default>
        <HelloWorld msg="Hello Vue 3 + TypeScript + Vite"/>			
      </template>
      <template #fallback>
        <div>Loading...</div>
      </template>
    </Suspense>
	</div>
</template>

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

</script>

<style>
	#app {
		font-family: Avenir, Helvetica, Arial, sans-serif;
		-webkit-font-smoothing: antialiased;
		-moz-osx-font-smoothing: grayscale;
		text-align: center;
		color: #2c3e50;
		margin-top: 60px;
	}
</style>

复制代码

image-20210622163440771.png

总结

  • 根据请求 json 格式来快速转为指定类型的声明代码 – 「QuickType」
  • 请求接口方法的 ts 声明要好好学学,尤其是需要套上 Promise。
    • 如果需要封装个 ref 就要声明 Ref 类型
  • 在组件中使用请求接口时,可以直接 await,setup 语法糖支持。
  • 倘若在组件的 setup 中使用异步,则需要在调用的地方套上 <Suspense> 标签。否则内容无法加载出来(即便是在控制台中已经拿到了数据)

后言

Vue3 + vite2 + ts 的入门级教程,学习自【2021年5月更新】Vue3+TypeScript从入门到实战全系列教程 – 2021.06.21

代码都是手打的噢,也是一步步进行实操练习,原本是想自己做个案例,只是实习的需要,得去熟悉学习下 react 了。那就跟 Vue 暂时先错过一下吧。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享