我正在参加「掘金·启航计划」
前言
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
防抖函数在输入框操作场景中非常有用。 一个 防抖的
ref在Vue.js更加灵活,让我们开始吧 👇:
Vue提供了一个名为customRef的API,用来生成自定义的ref,这样,我们在响应式对象的取值和赋值时就可以提前进行一些特殊的处理,customRef接收一个工厂函数,返回值是带有getter和setter的对象,而函数的两个参数track和trigger是两个方法,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的双向绑定包含两个方面的内容:
ref变化时,<input/>文本框中的内容发生变化<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结合起来,找到更多不一样解决方案。