vue3挑战-困难篇

1,018 阅读5分钟

我正在参加「掘金·启航计划」

前言

Vue挑战是一个非常适合Vue3入门的项目,里面的题目基本涵盖了Vue3的所有基础用法(特别是Vue3新引入的组合式写法),并且还在持续更新中,关于这个项目的具体介绍和原理,可以看一下这篇文章。并且在看这篇文章之前,你最好自己先去做一遍,这个文章里的写法只是我自己的方式(当然基础用法大家应该都大同小异)。

这篇文章是目前这个挑战里难度最高的几个问题,一般来说,可能你在日常的开发中不太能遇到,但是如果能够掌握这部分的内容,vue3你就已经炉火纯青了。

自定义元素

点击跳转到题目

你听说过 Web Components 吗 ? Vue 能很好地解析和创建 Web Components 。 在这个挑战中,我们将尝试了解它,让我们开始吧 👇:

之前提到过,代码复用我们采用了组件、函数式组件以及自定义指令等方式,但这些方式有一个共同点:它们都只在vue中生效,如果我们的项目采用了多种框架(React、Angular或者直接用原生),那以上三种方式的代码复用都不能满足需求了,这时候Web Components就登场了。

Web Components是一个跨框架的解决方案,在其中可以自定义元素以达到在不同框架中复用代码的目的,关于Web Components的更多内容可以参考这篇文章(深度好文!)

Vue3.2版本中提供了一个Web Components的解决方案,引入了defineCustomElement这一API,参数为Vue的选项式写法(props、data、$emit、生命周期钩子均可以使用):

const VueJs = defineCustomElement({
  props: {
    message: String
  },
  render() {
    return h("span" ,{
      style: {
        whiteSpace: "pre"
      }
    } ,`state:${this.state}\nmessage:${this.message}`)
  },
  data() {
    return {
      state: null
    }
  },
  mounted() {
    this.state = "ok"
    this.$emit("ok", this.state)
  }
})

之后使用浏览器的原生APIcustomElements.define将自定义元素注册到全局:

customElements.define('vue-js', VueJs)

这样,在vue框架之外,就可以直接使用<vue-js>标签了,不过,如果是仍要在vue中使用这个标签的话,就需要在vite或vue-cli的配置文件中加入规则避免解析这个标签,否则他就会被当作组件来处理。

// vite.config.js
import vue from '@vitejs/plugin-vue'

export default {
  plugins: [
    vue({
      template: {
        compilerOptions: {
          // 将所有带短横线的标签名都视为自定义元素
          isCustomElement: (tag) => tag.includes('-')
        }
      }
    })
  ]
}
// vue.config.js
module.exports = {
  chainWebpack: config => {
    config.module
      .rule('vue')
      .use('vue-loader')
      .tap(options => ({
        ...options,
        compilerOptions: {
          // 将所有带 ion- 的标签名都视为自定义元素
          isCustomElement: tag => tag.startsWith('ion-')
        }
      }))
  }
}

更多详情可以查看vue文档的Vue 与 Web Components

<script setup lang='ts'>

import { onMounted, defineCustomElement,h } from "vue"

/**
 * Implement the code to create a custom element.
 * Make the output of page show "Hello Vue.js".
*/
const VueJs = defineCustomElement({
  props: {
    message: String
  },
  render() {
    return h("span" ,null ,this.message)
  }
})

customElements.define('vue-js', VueJs)

onMounted(() => {
  document.getElementById("app")!.innerHTML = "<vue-js message=\"Hello Vue.js\"></vue-js>"
})

</script>

<template>
  <div id="app"></div>
</template>

自定义ref

点击跳转到题目

防抖函数在输入框操作场景中非常有用。 一个 防抖的refVue.js更加灵活,让我们开始吧 👇:

Vue提供了一个名为customRef的API,用来生成自定义的ref,这样,我们在响应式对象的取值和赋值时就可以提前进行一些特殊的处理,customRef接收一个工厂函数,返回值是带有gettersetter的对象,而函数的两个参数tracktrigger是两个方法,track用于标记响应性对象的取值,一般在getter中调用,而trigger则用于响应式对象的值改变后触发更新使用,一般在setter中调用,实际上,也就是说通过这些步骤,我们单独对响应式对象的取值和赋值重新进行了定义。

function customRef<T>(factory: CustomRefFactory<T>): Ref<T>

type CustomRefFactory<T> = (
  track: () => void,
  trigger: () => void
) => {
  get: () => T
  set: (value: T) => void
}

更多详情可以查看vue文档的Vue 与 Web Components

<script setup>
import { watch, customRef } from "vue"

/**
 * Implement the function
*/

function useDebouncedRef(value, delay = 200) {
  let timer = null
  return customRef((track, trigger) => ({
    get() {
      track()
      return value
    },
    set(newValue) {
      if (timer) clearTimeout(timer)
      timer = setTimeout(() => {
        value = newValue
        trigger()
      }, delay)
    }
  }))
}
const text = useDebouncedRef("hello")

/**
 * Make sure the callback only gets triggered once when entered multiple times in a certain timeout
*/
watch(text, (value) => {
  console.log(value)
})
</script>

<template>
  <input v-model="text" />
</template>

激活的样式-指令

点击跳转到题目

在这个挑战中,我们将实现一个"激活的样式"指令,让我们开始吧 👇:

一个关于自定义指令的练习,主要还是从bindings.value将需要修改的样式和判断函数取出来,此外,因为activeTab并不是指令对应组件的依赖,因此activeTab更新时不会直接进入自定义指令的updated周期中,因此需要使用watch进行辅助:

<script setup lang='ts'>

import { ref, watch } from "vue"

/**
 * Implement the custom directive
 * Make sure the list item text color changes to red when the `toggleTab` is toggled
 *
*/
const VActiveStyle = {
  mounted(el, binding, vnode, prevVnode) {
    watch(activeTab, () => {
      const [ style, isClick ] = binding.value
      if (isClick()) {
        for(let key in style) {
          el.style[key] = style[key]
        }
      }
      else {
        for(let key in style) {
          el.style[key] = ""
        }
      }
    }, {
      immediate: true
    })
  }
}

const list = [1, 2, 3, 4, 5, 6, 7, 8]
const activeTab = ref(0)
function toggleTab(index: number) {
  activeTab.value = index
}

</script>

<template>
  <ul>
    <li
      v-for="(item,index) in list"
      :key="index"
      v-active-style="[{'color':'red'},() => activeTab === index]"
      @click="toggleTab(index)"
    >
      {{ item }}
    </li>
  </ul>
</template>

实现简易版v-model指令

点击跳转到题目

同样是自定义指令的练习,v-model的双向绑定包含两个方面的内容:

  1. ref变化时,<input/>文本框中的内容发生变化
  2. <input/>文本框中的内容进行修改时,ref同时变化

要在自定义指令中实现1,我们需要: 初始化时从ref取值填入文本框中:

mounted(el,bindings) {
    el.value = bindings.value
}

ref更新时将新的值填入文本框中:

updated(el, bindings) {
    el.value = bindings.value
},

要在自定义指令中实现2,我们需要在初始化时监听input事件:

const inputHandle = e => {
  value.value = e.target.value
}
mounted(el,bindings) {
    el.addEventListener("input", inputHandle)
},

并记得在指令绑定组件卸载时取消监听:

unmounted(el) {
    el.removeEventListener("input", inputHandle)
}

总体来说就是:

<script setup lang='ts'>

import { ref } from "vue"

/**
 * Implement a custom directive
 * Create a two-way binding on a form input element
 *
*/

const inputHandle = e => {
  value.value = e.target.value
}

const VOhModel = {
  mounted(el,bindings) {
    el.value = bindings.value
    el.addEventListener("input", inputHandle)
  },
  updated(el, bindings) {
    el.value = bindings.value
  },
  unmounted(el) {
    el.removeEventListener("input", inputHandle)
  }
}

const value = ref("Hello Vue.js")

setTimeout(() => {
  value.value = "Hello World!!!"
},2000)

</script>

<template>
  <input v-oh-model="value" type="text" />
  <p>{{ value }}</p>
</template>

树组件

点击跳转到题目

在这个挑战中,你需要实现一个树组件,让我们开始吧。

树组件最核心的思想就是递归,递归一个组件的方式有很多,我这里采用了函数式组件,在h()函数中做递归,生成树组件:

<script setup lang="ts">
import { h } from 'vue'
interface TreeData {
  key: string
  title: string
  children: TreeData[]
}
defineProps<{data: TreeData[]}>()
const TreeNode = ({data = {}}) => {
  return h('div', null, data.children && data.children.length ? 
  [
    h('li', null, data.title),
    h('ul', null, data.children.map(item => {
      return h(TreeNode, {
        data: item
      })
    }))
  ]
  : [
    h('li', null, data.title)
  ])
  
}
TreeNode.props = ["data"]
</script>

<template>
  <ul>
    <tree-node v-for="node in data" :data="node" />
  </ul>
</template>

结尾

越往后,题目难度越大,解决问题的方式也就越来越多,尤其是在这个困难篇里基本上都不止一种实现方法了,可以试着把简单篇和中等篇学习到的各种API结合起来,找到更多不一样解决方案。