专栏首页与前端沾边[day-ui] Alert 组件学习
原创

[day-ui] Alert 组件学习

从样式功能来看,整体不是很复杂,alert 组件主要包括了主题色,title,关闭按钮,关闭事件,居中,加粗等

源码

  • template<template> <!-- 显示隐藏有动画效果 --> <!-- 开发没用过,不是很理解为什么使用v-show判断显示 --> <transition name="d-alert-fade"> <div class="d-alert" :class="[typeClass, center ? 'is-center' : '', 'is-' + effect]" v-show="visible" role="alert" > <!-- 左侧图标 --> <i class="d-alert__icon" :class="[iconClass, isBigIcon]" v-if="showIcon" ></i> <!-- title 和 描述 --> <div class="d-alert__content"> <span class="d-alert__title" :class="[isBoldTitle]" v-if="title || $slots.title" > <slot name="title">{{ title }}</slot> </span> <p v-if="$slots.default && !description" class="d-alert__description"> <slot></slot> </p> <p v-if="description && !$slots.default" class="d-alert__description"> {{ description }} </p> <i class="d-alert__closebtn" :class="{ 'is-customed': closeText !== '', 'd-icon-close': closeText === '' }" v-show="closable" @click="close" >{{ closeText }}</i > </div> </div> </transition> </template>

使用 role 属性告诉辅助设备(如屏幕阅读器)这个元素所扮演的角色。本质上是增强语义性,当现有的 HTML标签不能充分表达语义性的时候,就可以借助 role 来说明。

这里不是很理解为什么 titledescription 使用了属性和 slot 判断,有清楚的朋友可以帮忙解答

  • props 属性比较常规,这里就不介绍了哈setup(props, { emit, slots }) { // 接受的属性转为响应式 const { description, type } = toRefs(props) // 使用 v-show 显示隐藏 const visible = ref(true) // 关闭事件 const close = () => { visible.value = false emit('close') } const typeClass = computed(() => { return `d-alert--${type.value}` }) const iconClass = computed(() => { return TYPE_CLASSES_MAP[type.value] || 'd-icon-info' }) const isBigIcon = computed(() => { return description.value || slots.default ? 'is-big' : '' }) const isBoldTitle = computed(() => { return description.value || slots.default ? 'is-bold' : '' }) return { close, visible, typeClass, iconClass, isBigIcon, isBoldTitle } }

组件介绍到这里就结束了,比较简单。为了凑字呢,这里在介绍下 transition 组件

transition

大部分朋友都了解这是设置组件动画的内置动画组件。通常有三种使用方式:

  1. CSS 过渡
  2. CSS 动画
  3. Javascript 钩子

CSS 过渡

我们通常使用的方法,css 配置 enterleave

<template>
  <div class="app">
    <button @click="show = !show">
      Toggle render
    </button>
    <transition name="fade">
      <p v-if="show">我是测试</p>
    </transition>
  </div>
</template>
<script>
  export default {
    data() {
      return {
        show: true
      }
    }
  }
</script>
<style>
  .fade-enter-active,
  .fade-leave-active {
    transition: opacity 0.5s ease;
  }
  .fade-enter-from,
  .fade-leave-to {
    opacity: 0;
  }
</style>

CSS 动画

<template>
  <div class="app">
    <button @click="show = !show">Toggle show</button>
    <transition name="bounce">
      <p v-if="show">我是测试</p>
    </transition>
  </div>
</template>
<script>
  export default {
    data() {
      return {
        show: true
      }
    }
  }
</script>
<style>
  .bounce-enter-active {
    animation: bounce-in 0.5s;
  }
  .bounce-leave-active {
    // reverse 很关键
    animation: bounce-in 0.5s reverse;
  }
  @keyframes bounce-in {
    0% {
      transform: scale(0);
    }
    50% {
      transform: scale(1.5);
    }
    100% {
      transform: scale(1);
    }
  }
</style>

js 钩子

监听 transition 组件的内置方法,js 控制动画

<template>
  <div class="app">
    <button @click="show = !show">
      Toggle render
    </button>
    <transition
      @before-enter="beforeEnter"
      @enter="enter"
      @before-leave="beforeLeave"
      @leave="leave"
      css="false"
    >
      <p v-if="show">hello</p>
    </transition>
  </div>
</template>
<script>
  export default {
    data() {
      return {
        show: true
      }
    },
    methods: {
      beforeEnter(el) {
        el.style.opacity = 0
        el.style.transition = 'opacity 0.5s ease'
      },
      enter(el) {
        this.$el.offsetHeight
        el.style.opacity = 1
      },
      beforeLeave(el) {
        el.style.opacity = 1
      },
      leave(el) {
        el.style.transition = 'opacity 0.5s ease'
        el.style.opacity = 0
      }
    }
  }
</script>

如果形参不指定 done ,则表明用户不手动控制动画的结束,而转由节点的 transition 或者 animationEnd 来标识动画结束,开始回调 afterEnter

钩子函数的形参的个数大于1,表示形参中有 done, 也就是说用户必须手动控制动画何时结束。所以一旦你配置了 done 形参,则转由你告诉框架,动画何时结束。需要在合适的时机调用 done,否则 afterEnter 接口就没法被调用了。

动画触发条件

  • 条件渲染(v-if)
  • 条件展示(v-show)
  • 动态组件
  • 组件根节点

执行原理

实例

<template>
  <div class="app">
    <button @click="show = !show">
      Toggle render
    </button>
    <transition name="fade">
      <p v-if="show">hello</p>
    </transition>
  </div>
</template>

编译生成的 render 函数(不是使用的模板组件)

import {
  createVNode as _createVNode,
  openBlock as _openBlock,
  createBlock as _createBlock,
  createCommentVNode as _createCommentVNode,
  Transition as _Transition,
  withCtx as _withCtx,
} from "vue";
export function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (
    // 收集动态节点 如 v-if v-for
    _openBlock(),
    _createBlock("template", null, [
      _createVNode("div", { class: "app" }, [
        _createVNode(
          "button",
          {
            onClick: ($event) => (_ctx.show = !_ctx.show),
          },
          " Toggle render ",
          8 /* PROPS */,
          ["onClick"]
        ),
        _createVNode(
          _Transition,
          { name: "fade" },
          {
            // transition 只有一个子节点,默认插槽。多个子节点报错
            default: _withCtx(() => [
              _ctx.show
                ? (_openBlock(), _createBlock("p", { key: 0 }, "hello"))
                : _createCommentVNode("v-if", true),
            ]),
            _: 1,
          }
        ),
      ]),
    ])
  );
}

那么如何在组建创建和销毁的时候执行事件呢?————创建钩子函数

transition 组件返回的是处理过的第一个子节点

  • 如果 Transition 组件内部嵌套的是 KeepAlive 组件,那么它会继续查找 KeepAlive 组件嵌套的第一个子元素节点,来作为渲染的元素节点。
  • 如果 Transition 组件内部没有嵌套任何子节点,那么它会渲染空的注释节点。

trantion 组件定义

const Transition = (props, { slots }) =>
  //esolveTransitionProps 函数主要作用是,在我们给 Transition 传递的 Props 基础上做一层封装,然后返回一个新的 Props 对象,由于它包含了所有的 Props 处理
  h(BaseTransition, resolveTransitionProps(props), slots);
const BaseTransition = {
  name: `BaseTransition`,
  props: {
    mode: String,
    appear: Boolean,
    persisted: Boolean,
    // enter
    onBeforeEnter: TransitionHookValidator,
    onEnter: TransitionHookValidator,
    onAfterEnter: TransitionHookValidator,
    onEnterCancelled: TransitionHookValidator,
    // leave
    onBeforeLeave: TransitionHookValidator,
    onLeave: TransitionHookValidator,
    onAfterLeave: TransitionHookValidator,
    onLeaveCancelled: TransitionHookValidator,
    // appear
    onBeforeAppear: TransitionHookValidator,
    onAppear: TransitionHookValidator,
    onAfterAppear: TransitionHookValidator,
    onAppearCancelled: TransitionHookValidator,
  },
  setup(props, { slots }) {
    const instance = getCurrentInstance();
    const state = useTransitionState();
    let prevTransitionKey;
    return () => {
      const children =
        slots.default && getTransitionRawChildren(slots.default(), true);
      if (!children || !children.length) {
        return;
      }
      // Transition 组件只允许一个子元素节点,多个报警告,提示使用 TransitionGroup 组件
      if (process.env.NODE_ENV !== "production" && children.length > 1) {
        warn(
          "<transition> can only be used on a single element or component. Use " +
            "<transition-group> for lists."
        );
      }
      // 不需要追踪响应式,所以改成原始值,提升性能
      const rawProps = toRaw(props);
      const { mode } = rawProps;
      // 检查 mode 是否合法
      if (
        process.env.NODE_ENV !== "production" &&
        mode &&
        !["in-out", "out-in", "default"].includes(mode)
      ) {
        warn(`invalid <transition> mode: ${mode}`);
      }
      // 获取第一个子元素节点
      const child = children[0];
      if (state.isLeaving) {
        return emptyPlaceholder(child);
      }
      // 处理 <transition><keep-alive/></transition> 的情况
      const innerChild = getKeepAliveChild(child);
      if (!innerChild) {
        return emptyPlaceholder(child);
      }
      const enterHooks = resolveTransitionHooks(
        innerChild,
        rawProps,
        state,
        instance
      );
      setTransitionHooks(innerChild, enterHooks);
      const oldChild = instance.subTree;
      const oldInnerChild = oldChild && getKeepAliveChild(oldChild);
      let transitionKeyChanged = false;
      const { getTransitionKey } = innerChild.type;
      if (getTransitionKey) {
        const key = getTransitionKey();
        if (prevTransitionKey === undefined) {
          prevTransitionKey = key;
        } else if (key !== prevTransitionKey) {
          prevTransitionKey = key;
          transitionKeyChanged = true;
        }
      }
      if (
        oldInnerChild &&
        oldInnerChild.type !== Comment &&
        (!isSameVNodeType(innerChild, oldInnerChild) || transitionKeyChanged)
      ) {
        const leavingHooks = resolveTransitionHooks(
          oldInnerChild,
          rawProps,
          state,
          instance
        );
        // 更新旧树的钩子函数
        setTransitionHooks(oldInnerChild, leavingHooks);
        // 在两个视图之间切换
        if (mode === "out-in") {
          state.isLeaving = true;
          // 返回空的占位符节点,当离开过渡结束后,重新渲染组件
          leavingHooks.afterLeave = () => {
            state.isLeaving = false;
            instance.update();
          };
          return emptyPlaceholder(child);
        } else if (mode === "in-out") {
          leavingHooks.delayLeave = (el, earlyRemove, delayedLeave) => {
            const leavingVNodesCache = getLeavingNodesForType(
              state,
              oldInnerChild
            );
            leavingVNodesCache[String(oldInnerChild.key)] = oldInnerChild;
            // early removal callback
            el._leaveCb = () => {
              earlyRemove();
              el._leaveCb = undefined;
              delete enterHooks.delayedLeave;
            };
            enterHooks.delayedLeave = delayedLeave;
          };
        }
      }
      return child;
    };
  },
};

在渲染的过程中,Transition 组件还会通过 resolveTransitionHooks 去定义组件创建和删除阶段的钩子函数对象,然后再通过 setTransitionHooks 函数去把这个钩子函数对象设置到 vnode.transition 上。

hooks定义

const hooks = {
    mode,
    persisted,
    beforeEnter(el) {
      let hook = onBeforeEnter;
      if (!state.isMounted) {
        if (appear) {
          hook = onBeforeAppear || onBeforeEnter;
        } else {
          return;
        }
      }
      if (el._leaveCb) {
        el._leaveCb(true /* cancelled */);
      }
      const leavingVNode = leavingVNodesCache[key];
      if (
        leavingVNode &&
        isSameVNodeType(vnode, leavingVNode) &&
        leavingVNode.el._leaveCb
      ) {
        leavingVNode.el._leaveCb();
      }
      callHook(hook, [el]);
    },
    enter(el) {
      let hook = onEnter;
      let afterHook = onAfterEnter;
      let cancelHook = onEnterCancelled;
      if (!state.isMounted) {
        if (appear) {
          hook = onAppear || onEnter;
          afterHook = onAfterAppear || onAfterEnter;
          cancelHook = onAppearCancelled || onEnterCancelled;
        } else {
          return;
        }
      }
      let called = false;
      const done = (el._enterCb = (cancelled) => {
        if (called) return;
        called = true;
        if (cancelled) {
          callHook(cancelHook, [el]);
        } else {
          callHook(afterHook, [el]);
        }
        if (hooks.delayedLeave) {
          hooks.delayedLeave();
        }
        el._enterCb = undefined;
      });
      if (hook) {
        hook(el, done);
        if (hook.length <= 1) {
          done();
        }
      } else {
        done();
      }
    },
    leave(el, remove) {
      const key = String(vnode.key);
      if (el._enterCb) {
        el._enterCb(true /* cancelled */);
      }
      if (state.isUnmounting) {
        return remove();
      }
      callHook(onBeforeLeave, [el]);
      let called = false;
      const done = (el._leaveCb = (cancelled) => {
        if (called) return;
        called = true;
        remove();
        if (cancelled) {
          callHook(onLeaveCancelled, [el]);
        } else {
          callHook(onAfterLeave, [el]);
        }
        el._leaveCb = undefined;
        if (leavingVNodesCache[key] === vnode) {
          delete leavingVNodesCache[key];
        }
      });
      leavingVNodesCache[key] = vnode;
      if (onLeave) {
        onLeave(el, done);
        if (onLeave.length <= 1) {
          done();
        }
      } else {
        done();
      }
    },
    clone(vnode) {
      return resolveTransitionHooks(vnode, props, state, instance);
    },
  };

钩子函数对象定义了 4 个钩子函数,分别是 beforeEnterenterleaveclone。在节点 patch 阶段的 mountElement 函数中,在插入节点前且存在过度会执行 vnode.transition 中的 beforeEnter 函数

//beforeEnter 钩子函数主要做的事情就是根据 appear 的值和 DOM 是否挂载,来执行 onBeforeEnter 函数或者是 onBeforeAppear 函数。appear 是否节点现实的时候执行动画
beforeEnter(el) {
  let hook = onBeforeEnter
  if (!state.isMounted) {
    if (appear) {
      hook = onBeforeAppear || onBeforeEnter
    }
    else {
      return
    }
  }
  if (el._leaveCb) {
    el._leaveCb(true /* cancelled */)
  }
  const leavingVNode = leavingVNodesCache[key]
  if (leavingVNode &&
    isSameVNodeType(vnode, leavingVNode) &&
    leavingVNode.el._leaveCb) {
    leavingVNode.el._leaveCb()
  }
  callHook(hook, [el])
}

resolveTransitionProps 函数

function resolveTransitionProps(rawProps) {
  let {
    name = "v",
    type,
    css = true,
    duration,
    enterFromClass = `${name}-enter-from`,
    enterActiveClass = `${name}-enter-active`,
    enterToClass = `${name}-enter-to`,
    appearFromClass = enterFromClass,
    appearActiveClass = enterActiveClass,
    appearToClass = enterToClass,
    leaveFromClass = `${name}-leave-from`,
    leaveActiveClass = `${name}-leave-active`,
    leaveToClass = `${name}-leave-to`,
  } = rawProps;
  const baseProps = {};
  for (const key in rawProps) {
    if (!(key in DOMTransitionPropsValidators)) {
      baseProps[key] = rawProps[key];
    }
  }
  if (!css) {
    return baseProps;
  }
  const durations = normalizeDuration(duration);
  const enterDuration = durations && durations[0];
  const leaveDuration = durations && durations[1];
  const {
    onBeforeEnter,
    onEnter,
    onEnterCancelled,
    onLeave,
    onLeaveCancelled,
    onBeforeAppear = onBeforeEnter,
    onAppear = onEnter,
    onAppearCancelled = onEnterCancelled,
  } = baseProps;
  const finishEnter = (el, isAppear, done) => {
    removeTransitionClass(el, isAppear ? appearToClass : enterToClass);
    removeTransitionClass(el, isAppear ? appearActiveClass : enterActiveClass);
    done && done();
  };
  const finishLeave = (el, done) => {
    removeTransitionClass(el, leaveToClass);
    removeTransitionClass(el, leaveActiveClass);
    done && done();
  };
  const makeEnterHook = (isAppear) => {
    return (el, done) => {
      const hook = isAppear ? onAppear : onEnter;
      const resolve = () => finishEnter(el, isAppear, done);
      hook && hook(el, resolve);
      nextFrame(() => {
        removeTransitionClass(el, isAppear ? appearFromClass : enterFromClass);
        addTransitionClass(el, isAppear ? appearToClass : enterToClass);
        if (!(hook && hook.length > 1)) {
          if (enterDuration) {
            setTimeout(resolve, enterDuration);
          } else {
            whenTransitionEnds(el, type, resolve);
          }
        }
      });
    };
  };
  return extend(baseProps, {
    onBeforeEnter(el) {
      onBeforeEnter && onBeforeEnter(el);
      addTransitionClass(el, enterActiveClass);
      addTransitionClass(el, enterFromClass);
    },
    onBeforeAppear(el) {
      onBeforeAppear && onBeforeAppear(el);
      addTransitionClass(el, appearActiveClass);
      addTransitionClass(el, appearFromClass);
    },
    onEnter: makeEnterHook(false),
    onAppear: makeEnterHook(true),
    onLeave(el, done) {
      const resolve = () => finishLeave(el, done);
      addTransitionClass(el, leaveActiveClass);
      addTransitionClass(el, leaveFromClass);
      nextFrame(() => {
        removeTransitionClass(el, leaveFromClass);
        addTransitionClass(el, leaveToClass);
        if (!(onLeave && onLeave.length > 1)) {
          if (leaveDuration) {
            setTimeout(resolve, leaveDuration);
          } else {
            whenTransitionEnds(el, type, resolve);
          }
        }
      });
      onLeave && onLeave(el, resolve);
    },
    onEnterCancelled(el) {
      finishEnter(el, false);
      onEnterCancelled && onEnterCancelled(el);
    },
    onAppearCancelled(el) {
      finishEnter(el, true);
      onAppearCancelled && onAppearCancelled(el);
    },
    onLeaveCancelled(el) {
      finishLeave(el);
      onLeaveCancelled && onLeaveCancelled(el);
    },
  });
}

我们来看 onBeforeEnter 函数,它的内部执行了基础 props 传入的 onBeforeEnter 钩子函数,并且给 DOM 元素 el 添加了 enterActiveClassenterFromClass 样式。

其中,props 传入的 onBeforeEnter 函数就是我们写 Transition 组件时添加的 beforeEnter 钩子函数。enterActiveClass 默认值是 v-enter-activeenterFromClass 默认值是 v-enter-from,如果给 Transition 组件传入了 nameprop,比如 fade,那么 enterActiveClass 的值就是 fade-enter-activeenterFromClass 的值就是 fade-enter-from。(onBeforeAppearonBeforeEnter 的逻辑类似,就不赘述了,它是在我们给 Transition 组件传入 appearProp,且首次挂载的时候执行的。执行完 beforeEnter 钩子函数,接着插入元素到页面,然后会执行 vnode.transition 中的 enter 钩子函数,上面的 hooks 中)

enter 函数内部,首先执行基础 props 传入的 onEnter 钩子函数,然后在下一帧给 DOM 元素 el 移除了 enterFromClass,同时添加了 enterToClass 样式(动画也就是所谓的样式交替改变)

Transition 组件允许我们传入 enterDuration 这个 prop,它会指定进入过渡的动画时长,当然如果你不指定,Vue.js 内部会监听动画结束事件,然后在动画结束后,执行 finishEnter 函数

来看它的实现

const finishEnter = (el, isAppear, done) => {
  removeTransitionClass(el, isAppear ? appearToClass : enterToClass);
  removeTransitionClass(el, isAppear ? appearActiveClass : enterActiveClass);
  done && done();
};

其实就是给 DOM 元素移除 enterToClass 以及 enterActiveClass,同时执行 done 函数,进而执行 onAfterEnter 钩子函数

leave 钩子主要功能和 enter 相反。小伙伴们可自行查阅。

以上就是对 alert 组件的学习, 如有不对欢迎指正。

原创声明,本文系作者授权云+社区发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • day 83 Vue学习三之vue组件

      我们在进行vue开发的时候,还记得我们自己创建的vm对象吗,这个vm对象我们称为一个大组件,根组件(页面上叫Root),在一个网页的开发中,根据网页上的功能...

    py3study
  • golang-fullcalendar拖曳drop、resize的坑

    版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/hotqin888/article/det...

    hotqin888
  • JS-结合html综合练习js的对象——班级成绩表制作

    xing.org1^
  • 前端少为人知的知识–前端冷知识集锦(javascript篇)

    利用Math.random和toString生成随机字符串。这里的技巧是利用了toString方法可以接收一个基数作为参数的原理,这个基数从2到36封顶。如果不...

    疯狂的技术宅
  • [day-ui] 组件库打包环境配置

    上一节我们书写了 button 和 icon 组件,单元测试和文档也都完成了,接下来我们把写好的库打包发布到 npm 上。之后我们建个小 vue3 的项目,安装...

    测不准
  • [day-ui]基于 vue3.0 从 0-1 搭建组件库 - 环境搭建

    其实之前使用 vue2 的时候就想写个开源组件库,学习交流使用。如果公司有自己的需求也可以快速上手。开始想的是能在网上找到好的教程,环境搭建 - 组件编写 - ...

    测不准
  • 前端|利用手机号登录获取手机验证码

    利用手机号直接登录账号在现有的app、微信小程序以及各大网址上都比较常见。利用手机号直接登录账号它省略的用户密码这一环节,直接采用验证码的形式进行用户身份验证,...

    算法与编程之美
  • Android UI新组件学习和使用

    今天来学习总结一下,Android 后添加的一些新的组件和UI效果,Material Dialog,SwipeRefreshLayout,ListPopupWi...

    砸漏
  • vue引入各类ui库 原

    本身集成并没有vue的部分,我们采用的方式是把ui-css部分拿来使用,js效果自己用jquery写到methods里

    晓歌
  • Web前端学习 第3章 JavaScript基础教程9 内置对象

    javascript为我们提供了很多内置对象,这些内置对象为我们提供了很多语言的基本功能。我们之前学过的数组就是JavaScript的内置对象,除了数组我们还应...

    学习猿地
  • 盘点用jQuery框架实现“for循环”的四种方式!

    jQuery框架系列文章已经和大家分享了很多了,从jQuery框架的基础入门,到案例分析、再到现在的高级进阶。其中不但我自己学习到了很多东西,同时也帮助了很多需...

    灰小猿
  • PHP全栈学习笔记4

    JavaScript是网景公司开发的,是一种基于对象和事件驱动并具有安全性能的解释型脚本语言。

    达达前端
  • Javascript入门学习

    编程之道,程序员不仅仅要精通一门语言,而是要多学习几门。 第一课 1:javascript的主要特点 解释型:不需要编译,浏览器直接解释执行 基于对象:我们可以...

    别先生
  • SpringBoot+Vue2.x登陆功能

    tanoak
  • JavaScript运算符和控制语句

    语法: switch(n)  {      case 1:      执行代码块 1      break;      case 2:      执行代码块 2...

    用户3159471
  • Vue2.0 + Element-UI + WebAPI实践:简易个人记账系统

    最近正在学习Vue2.0相关知识,正好近期饿了么桌面端组件Element-UI发布,便动手做了一款简易个人记账系统,以达到实践及巩固目的。

    guokun
  • wordpress实现 ajax 分页加载

    由于我们可以在后台使用wp query来输出文章列表,所以我们并不需要文章分页的入口,砍掉了分页入口也避免了搜索引擎抓取这些页面。我们只需要在AJAX 执行的过...

    李维亮
  • 前端学习(30)~js学习(七):流程控制语句-选择结构(if,switch)

    (1)首先,计算出表达式的值,和case依次比较,一旦有对应的值,就会执行相应的语句,在执行的过程中,遇到break就会结束。

    Vincent-yuan
  • Blazor WebAssembly 修仙之途 - 初尝

    Blazor 是一个可是使用 .NET/C# 来编写交互式客户端的 Web UI 框架,在官网有一句话概括 “Build client web apps wit...

    晓晨

扫码关注云+社区

领取腾讯云代金券