标签 vue3 下的文章

上一篇文章的自定义指令非常简陋,使用场景仅限于静态的、不再改变的图片,如果用在列表中,会因为没有处理 update 事件,图片不会更新。

在原来的代码中,依赖原图片的 onload 事件获取原图像的尺寸,并最终用新生成的图片替换了原图片,导致自定义指令的 updated 钩子传入的参数 el 在 DOM 里已经是不存在了,也就没办法找到并更新这个图片了。

由于上述的缺点,本篇文章将换一种思路,不再替换原图像,而是替换原图像的 src,也不再从原图像的 onload 事件中获取其尺寸,而是获取原图像的 src,新创建一个 img 元素,在这个新 img 的 onload 事件中获取尺寸。

在 Vue 的自定义指令中,如果只需要关注 mounted 和 updated 钩子,那么可以使用简写形式,即直接返回一个函数。本篇文章就使用了简写的形式,新的自定义指令代码如下:

export default (el, binding, vnode) => {
    const _img = document.createElement('img');
    _img.crossOrigin = 'anonymous';
    _img.onload = () => {
        const canvas = document.createElement('canvas');
        const ctx = canvas.getContext('2d');
        const thumbWidth = binding.value ?? 50;
        const thumbHeight = _img.naturalHeight * thumbWidth / _img.naturalWidth;
        canvas.width = thumbWidth;
        canvas.height = thumbHeight;

        try {
            ctx.drawImage(_img, 0, 0, thumbWidth, thumbHeight);
        } catch (error) {
            console.error(error);
            return;
        }

        el.src = canvas.toDataURL();
    };
    _img.src = vnode.props.src;
}

代码比原来简单得多。在使用上也比原来简单,不需要添加 crossorigin 属性了:

<img v-thumb :src="xxxx" >

End

如果你平时老是需要编写各种列表,例如商品列表、图片列表,不妨自己写一个通用的列表组件,只需要传入几个参数,就能帮我们自动分列、自动调整间距。下面就是一个实现起来非常简单,但真的很好用的自定义列表组件。

定义

直接上完整的代码:

<script setup>
const props = defineProps({
    data: {
        type: Array,
        default: []
    },
    columnCount: {
        type: Number,
        default: 2
    },
    gap: {
        type: Number,
        default: 10
    }
});

const emit = defineEmits(['select']);
</script>

<template>
    <div class="list" :style="{ gap: `${gap}px` }">
        <template v-for="i in data">
            <div class="item" :style="{ 'flex-basis': `calc((100% - ${columnCount - 1} * ${gap}px) / ${columnCount})` }" @click="emit('select', i)">
                <slot :row="i"></slot>
            </div>
        </template>
    </div>
</template>

<style lang="scss" scoped>
.list {
    display: flex;
    flex-wrap: wrap;

    >.item {
        flex-shrink: 0;
        flex-grow: 0;
        cursor: pointer;
    }
}
</style>

在 script 部分,这个组件定义了 3 个属性,data是需要传入的数组形式的数据,就像 el-table 的 data 属性。columnCount是列数,可以是任意整数列,默认值为 2,这也是最常用的列数。gap是每一项与其他项的边距。如果你需要实现其他特定,可以非常容易地自己增加其他参数。

模板部分,列表使用了 flex 布局,根据参数自动计算每一列的宽度。还提供了一个默认插槽,用来自定义每一项里面的内容。

使用

使用起来非常简单,假设我们将上面的代码保存到一个叫 CommonMultiColumnList.vue 的文件中,首先引入这个组件:

import CommonMultiColumnList from 'CommonMultiColumnList.vue';

然后在模板中使用:

<CommonMultiColumnList :data="someData" :column-count="1" :gap="15">
    <template #default="{row}">
        <!-- 这里是自定义的每一项的结构 -->
    </template>
</CommonMultiColumnList>

现在只需要编写每一项的结构和样式,列表就会展示出来了。
如果你希望更加方便,也可以在组件中写好每一项的样式,而不是提供插槽,并多建立几个每一项样式不同的组件。


End

本文以封装一个旋转的 Loading 图标为例,讲解如何自定义 Message 动态(这里的“动态”的意思是"会动的")图标。

Element-Plus Message 提供了一个icon属性,用来自定义图标。icon属性接受Component或者String类型的参数。这个时候,就会想到可以引入 Element-Plus 自带的图标组件:

...
import { Loading } from '@element-plus/icons-vue'

ElMessage({
    icon:  Loading,
    ...
});
...

可惜上面这个图标不会动。

Element-Plus 提供了is-loadingCSS Class,只要添加在el-icon组件上,就能够这个组件旋转起来。
那么我们自己封装旋转的el-icon成一个自定义组件,就能够让 Message 的图标动起来了:

//新建组件loading.vue
<script setup>
import { Loading } from '@element-plus/icons-vue';
</script>

<template>
    <el-icon class="is-loading"><Loading /></el-icon>
</template>

使用上面的组件:

//javascript
import MyLoading from 'loading.vue';

ElMessage({
    icon:  MyLoading,
    ...
});

这样就实现了目标,不足就是需要建立一个组件文件。

如果不希望为了实现旋转效果而封装一个自定义组件,还有其他办法可以实现目标。
查看 Message 的源代码(位置在你的项目\node_modules\element-plus\es\utils\vue\icon.mjs),可以发现,Message 的icon属性可以接受 3 种类型:

const iconPropType = definePropType([
  String,
  Object,
  Function
]);

String 接受一个不能有特殊字符的字符串,经过 Vue 的 CreateNode 函数处理,会在页面中生成一个以这个字符串命名的标签,例如:


ElMessage({
    icon:  'MyTag',
    ...
});

会在图标应该出现的位置生成一个自定义 HTML 标签:

<MyTag></MyTag>

但暂时没有找到为它添加属性的方法。如果有明白的同学,欢迎通过站内联系方式告诉我,感谢!

最后,还可以传递 Function 类型。Element Message 文档中提到的icon属性可以是Component,也就是组件。组件可以是一个符合 Vue 组件格式的 JavaScript 对象,也可以是一个函数。前面的文章(戳这里)讲到了函数式组件,这种组件是一个函数。

所以,可以直接创建一个函数,该函数使用渲染函数h返回一个组件,最终代码如下:

import { Loading } from '@element-plus/icons-vue';

export const loading = ElMessage({
    ...
    icon: () => {
        return h(
            'div',
            //给任意 HTML 元素增加下面的两个 Class,都会应用旋转效果
            //这两个 Class 是 Element-Plus 提供的
            //这里这样用仅仅为了方便,也可以自己写动画
            { class: 'el-icon is-loading' },
            [h(Loading)]
        );
    },
    //不自动关闭,通过 Message 实例的 close 方法手动关闭
    duration: 0,
    ...
});

这样,一个会显示加载动画的自定义 Message 就实现了。下面是完整的代码,还顺便封装成了一个更方便的工具:

//新建 msg.js
import { h } from 'vue';
import { ElMessage } from "element-plus";
import { Loading } from "@element-plus/icons-vue";

//可以在全局样式中修改 Message 的样式
const customClass = 'el-message-custom';

const duration = 4000;
function msg(message, type = 'message') {
    if (type === 'loading') {
        return ElMessage({
            icon: () => {
                return h(
                    'div',
                    { class: 'el-icon is-loading' },
                    [h(Loading)]
                );
            },
            message,
            customClass,
            duration: 0,
        });
    }

    ElMessage({ message, type, customClass, duration });
}

export default {
    message: message => {
        msg(message);
    },
    success: message => {
        msg(message, 'success');
    },
    warning: message => {
        msg(message, 'warning');
    },
    error: message => {
        msg(message, 'error');
    },
    loading: message => {
        return msg(message, 'loading');
    }
};

使用:

import msg from 'msg.js'

const loading = msg.loading('提交中,请稍候');
setTimeout(()=>{
    loading.close();
    msg.success('提交成功');
}, 3000)

End

在 Element plus 中,除了对整个表单进行验证,还可以对单独的表单项进行验证。
查看 Form 文档,在 FormItem Exposes 节,可以看到 FormItem 提供了validate方法。该方法的使用方式与 Formrefvalidate方法基本相同,不同的就是需要获取 FormItemref

//javascript
const mobileRef = ref(null);

//template
...
<el-form-item prop="mobile" ref="mobileRef">...</el-form-item> 
...

接着,就能够调用该 ref 的 validate方法了:

//javascript
const submit = async () => {
    const result = await mobileRef.value.validate();
}

validate方法返回一个Promise,如果验证通过,上面代码中的result将会等于true。如果验证不通过,相应的表单将会显示在el-form上设置的rules中提供的报错信息。

End

0、前言

渲染函数h()取自 hyperscript 的第一个字母,意思是“能够生成 HTML 的 JavaScript”,用来创建 vnodes。

JSX是 React 最早使用的,类似 XML 的 JavaScript 扩展,能够用这样的方式写代码:


const vnode = <h1>Hi!</h1>

渲染函数和 JSX 都比模板语法灵活得多,所以可以用来创建高自由度的组件,实现复杂逻辑。

函数式组件实际上就是一种轻量化的组件,去掉了大多数组件的属性,仅支持有限的属性,用来创建功能简单的组件:接收 props,返回 vnodes。由于轻量化,所以性能比正常组件高。

下面介绍如何创建这些组件。

1、使用渲染函数创建组件

由于渲染函数是使用纯 JavaScript 创建组件,所以将其放在一个 .js.ts文件中。
创建Component1.js,并添加如下代码:

import { h } from 'vue';

export default {
    setup() {
        return () => h('div', 'Hi!');
    }
}

这样就完成了一个最简单的组件的创建。h函数的参数,以及 props 的接收、数据的处理请参见文档。

2、使用 JSX 创建组件

JSX 组件的文件后缀是 .jsx。如果你使用 TypeScript的话,后缀则为.tsx

创建Component2.jsx并添加如下代码:


export default {
    render() {
        return <h1>Hi!</h1>;
    }
}

JSX 组件创建完成。同样地,具体细节请参考文档。

3、在Component1中使用Component2

修改 Component1.js 的代码如下:

import { h } from 'vue';
import Component2 from './Component2.jsx';

export default {
    setup() {
        return () => h('div', h(Component2));
    }
}

如果提示React is not defined,可能是 Vite 的版本过低,没有自带@vitejs/plugin-vue-jsx,请更新 Vite,或者安装 @vitejs/plugin-vue-jsx,不过,由于版本原因,安装过程中会报错,请根据提示强制安装,或者更新 Vite。

4、创建函数式组件

函数式组件实际上就是一个函数,其接收一些参数,这些参数和setup()完全相同。
在 Component1.js 中创建一个函数式组件:

const Component3 = props => {
    return h('div', props.msg);
}

5、在 Component1 中使用函数式组件

修改 Component1.js:

//...
setup() {
    return () => h('div', h(Component3, { msg: 'Hi!' }));
}
//...

6、总结

注意, JSX 组件和函数式组件都需要经过渲染函数h()来渲染。

熟练创建文中各种类型的组件,需要了解渲染函数的参数,渲染函数以及 JSX 的语法,setup()函数的参数。掌握这些知识,对于提高程序灵活性、提高程序性能会有很大的帮助。

End

对于reactive类型的响应式数据,只需要按照文档中介绍的方法建立侦听器:

const x = reactive({ a: 1 });

watch(() => x.a, val => alert(1));

对于ref类型的数据,则需要多加一个value来获取到正确的侦听对象:

const x = ref({ a: 1 });

watch(() => x.value.a, val => console.log(val););

使用时必须注意,否则无法正确侦听。

对于数组、对象类型的数据,使用reactive比较方便,因为不需要使用.value获取此类数据的值。

element-plus 的图标其实是一个个的组件:

JavaScript:

import { IconName, IconName2 } from '@element-plus/icons-vue';

Template:

<el-icon>
    <IconName/>
</el-icon>

使用 :is 切换组件:

<component :is="bool ? IconName : IconName2"></component>

然而 :is 的值必须是引入的组件,不能是字符串,否则就会渲染成这个样子:

<iconname></iconname>

如果菜单结构单独放在一个文件中,比如 menu.js

export default [
    {
        name: 'Menu 1',
        icon: 'IconName'
    }
]

这里的 icon的值就不能是字符串,需要是一个组件对象的引用。这就需要先在 menu.js 中引入图标组件:

import { IconName } from '@element-plus/icons-vue';

然后将组件传给 icon

export default [
    {
        name: 'Menu 1',
        icon: IconName
    }
]

这样,就能够在组件中正确显示图标了。当然,这时候组件中就不需要再引入图标了。


End