Vue3写一个面向对象插件系列2(使用篇2)

1,317 阅读5分钟

背景

创作背景纯粹是因为本人喜欢面向对象风格,并不是说其他风格不好,无意挑起风格流之争,不喜欢的道友求放过,我的目标是兼容现有的vue3的项目,也就是说可以在现有的项目中也可以使用。

使用

1. model

  1. 自定义双向数据绑定

    • 原生写法

      <!-- CustomInput.vue -->
      <script setup> 
          defineProps(['modelValue']) 
          defineEmits(['update:modelValue'])
      </script> 
      <template> 
          <input :value="modelValue" @input="$emit('update:modelValue', $event.target.value)" /> 
      </template>
      

      现在 v-model 可以在这个组件上正常工作了:

      <CustomInput v-model="searchText" />
      
    • 面向对象写法

      @Component({
        template: `
          <input :value="model" @input="modelChange" /> 
        `,
      })
      export default class OOPDemo {
        @Model() model!: string;
        
        modelChange = (e: InputEvent) => {
            this.model = e.target.value
        }
      }
      

      使用跟原生方式使用是一样的

      <CustomInput v-model="searchText" />
      
  2. v-model 的参数

    默认情况下,v-model 在组件上都是使用 modelValue 作为 prop,并以 update:modelValue 作为对应的事件。我们可以通过给 v-model 指定一个参数来更改这些名字:

      <MyComponent v-model:title="bookTitle" />
    
    • 原生写法

      <!-- MyComponent.vue -->
      <script setup>
          defineProps(['title'])
          defineEmits(['update:title'])
      </script>
      <template>
          <input type="text" :value="title" @input="$emit('update:title', $event.target.value)" />
      </template>
      
    • 面向对象写法

      @Component({
        template: `
          <input :value="model" @input="modelChange" /> 
        `,
      })
      export default class OOPDemo {
        @Model('title') model!: string;
        
        modelChange = (e: InputEvent) => {
            this.model = e.target.value
        }
      }
      
  3. 多个 v-model 绑定

    利用刚才在 v-model 参数小节中学到的指定参数与事件名的技巧,我们可以在单个组件实例上创建多个 v-model 双向绑定。

    组件上的每一个 v-model 都会同步不同的 prop,而无需额外的选项:

    <UserName v-model:first-name="first" v-model:last-name="last" />
    
    • 原生写法

      <script setup>
          defineProps({ firstName: String, lastName: String })
          defineEmits(['update:firstName', 'update:lastName'])
      </script>
      <template>
          <input type="text" :value="firstName" @input="$emit('update:firstName', $event.target.value)" /> 
          <input type="text" :value="lastName" @input="$emit('update:lastName', $event.target.value)" />
      </template>
      
    • 面向对象写法

      @Component({
        template: `
          <input :value="firstName" @input="firstName" /> 
          <input :value="lastName" @input="lastNameChange" /> 
        `,
      })
      export default class OOPDemo {
        @Model('firstName') firstName!: string;
        @Model('lastName') lastName!: string;
        
        firstNameChange = (e: InputEvent) => {
            this.firstName = e.target.value
        }
        
        lastNameChange = (e: InputEvent) => {
            this.lastName = e.target.value
        }
      }
      

2. 模板引用

  1. 访问DOM元素

    • 原生写法 为了通过组合式 API 获得该模板引用,我们需要声明一个同名的 ref:

      <script setup>
          import { ref, onMounted } from 'vue' 
          // 声明一个 ref 来存放该元素的引用 
          // 必须和模板里的 ref 同名
          const input = ref(null)
          onMounted(() => { input.value.focus() })
      </script>
      <template> 
          <input ref="input" /> 
      </template>
      
    • 面向对象写法

      @Component({
        template: `
          <input ref="input" /> 
        `,
      })
      export default class OOPDemo implements LifecycleHook {
        @ViewChild() input?: HTMLInputElement;
      
        onMounted(): void {
            console.log(this.input)
        }
      }
      
  2. 组件上的ref

    模板引用也可以被用在一个子组件上。这种情况下引用中获得的值是组件实例:

    • 原生写法

      <script setup>
          import { ref, onMounted } from 'vue' 
          const child = ref(null)
          onMounted(() => { 
             // child.value 是 <Child /> 组件的实例
          })
      </script>
      <template> 
          <Child ref="child" /> 
      </template>
      
    • 面向对象写法

      @Component({
         template: `
           <Child ref="child" />
         `,
      })
      export default class OOPDemo implements LifecycleHook {
         @ViewChild() child?: Child;
      
         onMounted(): void {
             console.log(this.child)
         }
      }
      
  3. v-for 中的模板引用

    当在 v-for 中使用模板引用时,对应的 ref 中包含的值是一个数组,它将在元素被挂载后包含对应整个列表的所有元素:

    • 原生写法

      <script setup> 
          import { ref, onMounted } from 'vue'
          const list = ref([ /* ... */ ])
          const itemRefs = ref([])
          onMounted(() => console.log(itemRefs.value))
      </script>
      <template>
          <ul> 
              <li v-for="item in list" ref="itemRefs"> {{ item }} </li> 
          </ul> 
      </template>
      
    • 面向对象写法

      @Component({
         template: `
           <ul> 
             <li v-for="item in list" ref="itemRefs"> {{ item }} </li> 
           </ul> 
         `,
      })
      export default class OOPDemo implements LifecycleHook {
         @ViewChildren() childs: HTMLInputElement[];
      
         onMounted(): void {
             console.log(this.childs)
         }
      }
      

3. watch

  1. 基本使用

    计算属性允许我们声明性地计算衍生值。然而在有些情况下,我们需要在状态变化时执行一些“副作用”:例如更改 DOM,或是根据异步操作的结果去修改另一处的状态。

    在组合式 API 中,我们可以使用watch在每次响应式状态发生变化时触发回调函数:

    • 原生写法

      <script setup>
      import { ref, watch } from 'vue'
      
      const question = ref('')
      const answer = ref('Questions usually contain a question mark. ;-)')
      
      // 可以直接侦听一个 ref
      watch(question, async (newQuestion, oldQuestion) => {
        if (newQuestion.indexOf('?') > -1) {
          answer.value = 'Thinking...'
          try {
            const res = await fetch('https://yesno.wtf/api')
            answer.value = (await res.json()).answer
          } catch (error) {
            answer.value = 'Error! Could not reach the API. ' + error
          }
        }
      })
      </script>
      
      <template>
        <p>
          Ask a yes/no question:
          <input v-model="question" />
        </p>
        <p>{{ answer }}</p>
      </template>
      
    • 面向对象写法

      @Component({
         template: `
           <p>
             Ask a yes/no question:
             <input v-model="question" />
           </p>
           <p>{{ answer }}</p>
         `,
      })
      export default class OOPDemo implements LifecycleHook {
         question = ref('')
         answer = ref('Questions usually contain a question mark. ;-)')
         
         @Watch()
         async questionChange(newQuestion, oldQuestion) {
            if (newQuestion.indexOf('?') > -1) {
             this.answer.value = 'Thinking...'
             try {
               const res = await fetch('https://yesno.wtf/api')
               this.answer.value = (await res.json()).answer
             } catch (error) {
               this.answer.value = 'Error! Could not reach the API. ' + error
             }
           }
         }
      }
      
  2. 监听数据源类型

    watch 的第一个参数可以是不同形式的“数据源”:它可以是一个 ref (包括计算属性)、一个响应式对象、一个 getter 函数、或多个数据源组成的数组:

    • 原生写法

      const x = ref(0)
      const y = ref(0)
      
      // 单个 ref
      watch(x, (newX) => {
        console.log(`x is ${newX}`)
      })
      
      // getter 函数
      watch(
        () => x.value + y.value,
        (sum) => {
          console.log(`sum of x + y is: ${sum}`)
        }
      )
      
      // 多个来源组成的数组
      watch([x, () => y.value], ([newX, newY]) => {
        console.log(`x is ${newX} and y is ${newY}`)
      })
      
    • 面向对象写法

      @Component()
      export default class OOPDemo implements LifecycleHook {
         x = ref(0)
         y = ref(0)
         
         // getter 函数 数据源
         @Watch(context => context.x.value)
         callbackGetter(newX, oldX) {
            console.log(`x is ${newX}`)
         }
         
         // 以Change结尾命名的方法名 会自动解析到x属性 也就是这个监听监听的是x的变化
         @Watch()
         xChange(newX, oldX) {
            console.log(`x is ${newX}`)
         }
         
         // 指定具体属性 会自动解析到x属性 也就是这个监听监听的是x的变化
         @Watch('x')
         xxxCallback(newX, oldX) {
            console.log(`x is ${newX}`)
         }
         
         // 多个来源组成的数组
         @Watch(['x', (context) => context.y.value])
         xxxCallback([newX, newY], [oldX, oldY]) {
            console.log(`x is ${newX} and y is ${newY}`)
         }
      }
      
  3. 即时回调监听器

    watch 默认是懒执行的:仅当数据源变化时,才会执行回调。但在某些场景中,我们希望在创建侦听器时,立即执行一遍回调。举例来说,我们想请求一些初始数据,然后在相关状态更改时重新请求数据。

    我们可以通过传入 immediate: true 选项来强制侦听器的回调立即执行:

    • 原生写法

      watch(source, (newValue, oldValue) => {
        // 立即执行,且当 `source` 改变时再次执行
      }, { immediate: true })
      
    • 面向对象写法

      @Component()
      export default class OOPDemo implements LifecycleHook {
         x = ref(0)
         
         @Watch({immediate: true})
         xChange(newX, oldX) {
            console.log(`x is ${newX}`)
         }
      }
      
  4. 回调触发时机

    当你更改了响应式状态,它可能会同时触发 Vue 组件更新和侦听器回调。

    默认情况下,用户创建的侦听器回调,都会在 Vue 组件更新之前被调用。这意味着你在侦听器回调中访问的 DOM 将是被 Vue 更新之前的状态。

    如果想在侦听器回调中能访问被 Vue 更新之后的 DOM,你需要指明 flush: 'post' 选项:

    • 原生写法

      watch(source, callback, {
        flush: 'post'
      })
      
      
    • 面向对象写法

      @Component()
      export default class OOPDemo implements LifecycleHook {
         x = ref(0)
         
         @Watch({flush: 'post'})
         xChange(newX, oldX) {
            console.log(`x is ${newX}`)
         }
      }
      

4. 组合式函数(hooks)

  1. 组合式函数

    在 Vue 应用的概念中,“组合式函数”(Composables) 是一个利用 Vue 的组合式 API 来封装和复用有状态逻辑的函数。

    当构建前端应用时,我们常常需要复用公共任务的逻辑。例如为了在不同地方格式化时间,我们可能会抽取一个可复用的日期格式化函数。这个函数封装了无状态的逻辑:它在接收一些输入后立刻返回所期望的输出。复用无状态逻辑的库有很多,比如你可能已经用过的 lodash 或是 date-fns

    相比之下,有状态逻辑负责管理会随时间而变化的状态。一个简单的例子是跟踪当前鼠标在页面中的位置。在实际应用中,也可能是像触摸手势或与数据库的连接状态这样的更复杂的逻辑。

  2. 鼠标跟踪示例

    我们可以把这个逻辑以一个组合式函数的形式提取到外部文件中:

    // mouse.js
    import { ref, onMounted, onUnmounted } from 'vue'
    
    // 按照惯例,组合式函数名以“use”开头
    export function useMouse() {
      // 被组合式函数封装和管理的状态
      const x = ref(0)
      const y = ref(0)
    
      // 组合式函数可以随时更改其状态。
      function update(event) {
        x.value = event.pageX
        y.value = event.pageY
      }
    
      // 一个组合式函数也可以挂靠在所属组件的生命周期上
      // 来启动和卸载副作用
      onMounted(() => window.addEventListener('mousemove', update))
      onUnmounted(() => window.removeEventListener('mousemove', update))
    
      // 通过返回值暴露所管理的状态
      return { x, y }
    }
    
    • 原生写法

      <script setup>
      import { useMouse } from './mouse.js'
      
      const { x, y } = useMouse()
      </script>
      
      <template>Mouse position is at: {{ x }}, {{ y }}</template>
      
    • 面向对象写法

      @Component({ template: `<template>Mouse position is at: {{ x }}, {{ y }}</template>`})
      export default class OOPDemo extends HookReturns<ReturnType<typeof useMouse>> implements LifecycleHook {
         onSetup() {
            const {x, y} = useMouse();
            
            return {x, y}
         }
      }
      

后续

后续会补充实现的过程 内容比较多 最后可能还会出适配这个插件的Webstrom插件的实现过程(视情况而定)