「 Vue 」在 Vue 中实现可复用性

610 阅读2分钟

这是我参与11月更文挑战的9天,活动详情查看:2021最后一次更文挑战

Mixin

选项合并

Mixin 即混入,当组件使用 mixins 选项时, 所有 mixin 对象可以被混合进该组件本身的选项。

对于data中的数据,如果 mixin 对象与自身组件的数据 property 有冲突时,会以组件自身为主。

const myMixin = {
    data() {
        return { number: 2 }
    },
}

const app = Vue.createApp({
    data() {
        return { number: 1 }
    },
    mixins: [myMixin],
    template:/*html*/ `<div>{{number}}</div>`
})
<div>1</div>

生命周期函数也可以被混入到组件中,mixin 对象的钩子会在组件自身的钩子之前被调用。

methodscomputedcomponents等这些值为对象的选项将会与组件合并为同一个对象。对象中的键名冲突时,取组件自身的键值对。

全局 mixin

上述提到的是局部 mixin,除此之外,我们还可以创建一个全局 mixin。

const app = Vue.createApp({
    template:/*html*/ `
    <div>{{number}}</div>
    <my-component />
    `
})

app.component('my-component', {
    template: `<div>{{count}}</div>`
})

app.mixin({
    data() {
        return {
            number: 1,
            count: 2
        }
    }
})

使用全局 mixin 可以在 Vue 实例的所有组件中使用 mixin 中的选项。

自定义选项合并

在 Vue 中,我们可以定义一个自定义选项,使用 this.$options 来访问它。

const app = Vue.createApp({
    number: 1,
    template:/*html*/ `
    <div>{{this.$options.number}}</div>
    `
})

自定义选项名冲突时,组件的选项优先级高于 mixin 对象。

const myMixin = {
    number: 2
}

const app = Vue.createApp({
    number: 1,
    mixins: [myMixin],
    template:/*html*/ `
    <div>{{this.$options.number}}</div>
    `
})
<div>1</div>

我们可以在app.config.optionMergeStrategies中自己设置自定义选项的优先级:

app.config.optionMergeStrategies.number = (mixinValue, appValue) => {
    return mixinValue || appValue;
}
<div>2</div>

自定义指令

在 Vue 中,对 DOM 元素的操作需要我们使用 $refs 来实现。这样的代码无法被复用。

const app = Vue.createApp({
    mounted() {
        this.$refs.input.focus()
    },
    template:/*html*/ `
    <input ref='input'/>
    `
})

我们可以使用 directive 来定义全局自定义指令并在相应的 DOM 调用:

const app = Vue.createApp({
    template:/*html*/ `
    <input v-focus/>
    `
})

app.directive('focus', {
    mounted(el) {
        el.focus();
    },
})

我们还可以定义一个局部自定义指令,首先声明一个指令定义对象,在组件中使用directive选项:

const directives = {
    focus: {
        mounted(el) {
            el.focus();
        }
    }
}

const app = Vue.createApp({
    directives: directives,
    template:/*html*/ `
    <input v-focus/>
    `
})

钩子函数

一个指令定义对象提供以下几个周期函数:createdbeforeMountmountedbeforeUpdateupdatedbeforeUnmountunmounted

动态指令参数

自定义组件可以传参,参数可以是动态的。例如 v-pos:[direction]='100' 中, direction 是动态参数。

const app = Vue.createApp({
    data() {
        return { position: 'left' }
    },
    template:/*html*/ `
    <input v-pos:[position]='50'/>
    `
})

app.directive('pos', {
    mounted(el, binding) {
        el.style.position = 'fixed';
        const s = binding.arg || top;
        el.style[s] = binding.value + 'px';
    },
    updated(el, binding) {
        el.style.position = 'fixed';
        const s = binding.arg || top;
        el.style[s] = binding.value + 'px';
    },
})

指令定义对象中的钩子函数接收两个参数,第一个参数是该 DOM 元素的引用,第二个参数是传递的参数,上述例子中为bindingbinding.arg 是传递给指令的参数,binding.value是传递给指令的值。

函数简写

mountedupdated 中若是相同的行为,可以简写为入下形式:

app.directive('pos', (el, binding) => {
    el.style.position = 'fixed';
    const s = binding.arg || top;
    el.style[s] = binding.value + 'px';
})

对象字面量

如果想传递多个值,指令可以接收 JS 对象的字面量,方法是使用 JS 的表达式来作为参数值:

<div v-demo="{ color: 'white', text: 'hello!' }"></div>
app.directive('demo', (el, binding) => {
  console.log(binding.value.color) // => "white"
  console.log(binding.value.text) // => "hello!"
})

在组件中使用

在组件中使用时,仅在单个根节点的组件中生效。自定义指令会传递到根节点上,并且不会通过v-bind='$attrs'传入另一个元素。

Teleport

Teleport 意为传送门。

首先我们实现如下代码,点击 button 时产生一个蒙层效果:

.area {
    position: absolute;
    left: 50%;
    top: 50%;
    transform: translate(-50%, -50%);
    width: 300px;
    height: 200px;
    background-color: orange;
}

.mask {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    opacity: 0.5;
    background-color: black;
}
const app = Vue.createApp({
    data() {
        return {
            show: false
        }
    },
    methods: {
        handleClick() {
            this.show = !this.show
        }
    },
    template:/*html*/ `
    <div class='area'>
        <button @click='handleClick'>mask</button>
        <div class='mask' v-show='show'></div>   
    </div>
    `
})

demo1.gif

我们想要的效果是覆盖整个屏幕的蒙层。如果我们要操作 DOM,需要将 mask 类的元素移到 <body> 标签中,操作麻烦。我们可以使用 <teleport> 来实现。

<teleport>to attribute 的值为要渲染到的标签。

<div class='area'>
    <button @click='handleClick'>mask</button>
    <teleport to='body'>
        <div class='mask' v-show='show'></div>   
    </teleport>
</div>

demo2 (1).gif

与组件一起使用

如果 <teleport> 包含 Vue 组件,则它仍将是 <teleport> 父组件的逻辑子组件。

渲染函数

下面的代码通过模板来创建 HTML,内容过于繁杂:

const app = Vue.createApp({
    template:/*html*/ `
    <my-title :level='1'>
        Hello    
    </my-title>
    `
})

app.component('my-title', {
    props: ['level'],
    template: `
    <h1 v-if="level === 1"><slot /></h1>
    <h2 v-else-if="level === 2"><slot /></h2>
    <h3 v-else-if="level === 3"><slot /></h3>
    <h4 v-else-if="level === 4"><slot /></h4>
    <h5 v-else-if="level === 5"><slot /></h5>
    <h6 v-else-if="level === 6"><slot /></h6>
    `
})

我们可以使用 render 渲染函数通过 JS 来实现 HTML 完成相同的功能:

app.component('my-title', {
    props: ['level'],
    render() {
        const { h } = Vue;
        return h(
            'h' + this.level, // 标签名
            {}, // prop 或 attribute
            this.$slots.default() // 包含其子节点的数组,这里使用的是默认插槽
        )
    }
})

template 被编译后会生成 render 函数,render 函数返回的内容是虚拟 DOM,然后产生真实 DOM。这样做使得 Vue 的性能更快,并且可以跨平台开发。

插件

插件把通用性的功能封装起来。

// 定义插件
const myPlugin = {
    install(app, options) {
        app.provide('name', options.name);
        app.directive('focus', {
            mounted(el) {
                el.focus();
            },
        });
        app.mixin({
            mounted() {
                console.log('Mixin');
            },
        });
        app.config.globalProperties.$sayHello = 'Hello World';
    }
}

const app = Vue.createApp({
    template:/*html*/ `
    <my-title />
    `
})

app.component('my-title', {
    inject: ['name'],
    mounted() {
        console.log(this.$sayHello)
    },
    template: `
    <div>{{name}}</div>
    <input v-focus />
    `
})

// 使用插件
app.use(myPlugin, { name: 'Evan' })

插件如果是对象,则会调用其中的install方法,它接收两个参数,一个是 Vue 创建的 app,另一个是用户传入的选项。

除此之外,插件还可以是函数,函数本身会被调用:

const myPlugin = (app, options) => {
    // ...
}

通过调用 Vue app 的 use 方法使用插件,它接收两个参数,一个是插件的名字,另一个是传入的选项。第二个参数可选。

下面实现了一个插件示例,该插件是一个校验器插件,对 data 中的 property 进行了校验:

const validatorPlugin = (app, options) => {
    app.mixin({
        created() {
            console.log('created')
            for (let key in this.$options.rules) {
                const item = this.$options.rules[key];
                this.$watch(key, (value) => {
                    const result = item.validate(value);
                    if (!result) {
                        console.log(item.message)
                    }
                })
            }
        }
    })
}

const app = Vue.createApp({
    data() {
        return {
            name: 'Jack',
            age: 20
        }
    },
    rules: {
        age: {
            validate: age => age >= 18,
            message: 'too young'
        },
        name: {
            validate: name => name.length >= 3,
            message: 'too short'
        }
    },
    template:/*html*/ `
    <div>name: {{name}}, age: {{age}}</div>
    `
})

app.use(validatorPlugin)

使用插件使代码具有更高的可读性和可扩展性。