跨路由组件 | 看了某位大佬的视频后,我仿照着做了一个跨路由组件

2,164 阅读5分钟

“我报名参加金石计划1期挑战——瓜分10万奖池,这是我的第1篇文章,点击查看活动详情

i.gif

大佬指的是AnthonyFu,之前在逛b站的时候无意间看到了大佬发布的视频录播,视频的内容是用vue3实现的一个跨路由组件,当两个路由页面中有同样内容的时候,则可以为该元素添加跨路由的动画过渡效果,使其看起来像是跨路由一样,具体可以看gif图(注意页面路由)。

附上大佬的demo链接以及github地址

1.gif

当看到这个效果的时候我觉得还挺有意思的,刚好自己又在学react,于是就想着看能不能自己用react实现一下,大致的思路并不难,但是真的实现起来坑还是挺多的。

首先先说一下自己的一个大概思路吧,先假设页面a有一张图片a,页面b也有一张图片b(图片为同一张图片但是大小以及位置不同),当a页面向b页面进行路由跳转的时候便可以让图片a过渡到图片b的样式,我的想法是将该图片写入到我定义的一个自定义组件Trans中,然后在Trans内使用hook来捕捉图片a的卸载以及图片b的添加。

export default function Trans(props) {
  useLayoutEffect(() => {
    let target = props.children.ref.current
    let nodeStyle = props.children.props.style
    Promise.resolve().then(() => transitMoveNode(target, props.children.key, nodeStyle))
    return () => {
      transitCloneNode(target, props.children.key)
    }
  },[])
  return (
    <>{props.children}</>
  )
}

在这里使用的hook是useLayoutEffect,并不是useEffect,使用useEffect会出现无法正确获取target元素在页面中显示的位置,useLayoutEffect可以解决这个问题。

当a页面向b页面跳转时会先执行transitCloneNode方法,该方法的参数target此时就是图片a元素,执行该方法后会将a元素移动到html标签内部同时为该元素添加特定的css样式,使其在页面中显示的位置相对于原位置来说不变,稍后页面b进行渲染,此时便会执行transitMoveNode方法,Trans组件会发现元素a与元素b的key值相等,这时会把元素b的opacity的值设为0,便会使元素a过渡到元素b的样式,过渡完毕后会触发元素a身上的ontransitionend事件,触发的事件会将元素a从页面上移除,同时将元素b的opacity的值设为1,从而达到一个跨路由的过渡效果。

在这里有点坑的地方就是将要卸载的元素a只能将其移动的方式移动到html标签内,我所使用的方法是appendChild,这里不能对元素a先进行克隆后再插入,这么做会使得页面出现闪烁。

import React, { useLayoutEffect } from 'react'

export default function Trans(props) {
  useLayoutEffect(() => {
    let target = props.children.ref.current
    let nodeStyle = props.children.props.style
    //不放在微任务内会使得元素的某些属性会无法正确的获取到(元素的位置信息均为0)
    Promise.resolve().then(() => transitMoveNode(target, props.children.key, nodeStyle))
    return () => {
      transitCloneNode(target, props.children.key)
    }
  },[])

  return (
    <>{props.children}</>
  )
}

//用来存储被移动到html标签下方的元素
const startMap = new Map()
const root = document.querySelector('html')

//该函数的作用是获取到将要被移除的节点,并将该元素移动到html标签下方
//并添加对应的css样式使得该元素在页面中实际所显示的位置不变
//被移动了的节点会存储到startMap内,键值为给定的key值
function transitCloneNode(node, key) {
  if(node==null){
    return 0
  }
  let {top,left} = node.getBoundingClientRect()
  //这里添加一个微任务是因为--如果要移动多个元素,那么移动一个后该元素后面的元素位置可能会受到影响,所以放到一个微任务里面去进行,并且在进行此步骤之前先获取该元素在页面中显示的位置
  Promise.resolve().then(() => {
    let start = node
    start.classList.add('transit-0000')
    start.style.cssText += `top:${top}px !important;left:${left}px !important;`
    startMap.set(key, start)
    root.appendChild(start)
    clearMap()
  })
}

//该函数的作用是获取到将要新增的节点,在startMap内查找有没有对应的被移动到html标签下方的节点
//如果没有则直接退出
//如果有,则将新增的节点的opacity属性设为0,  并获取新增节点在页面中显示的位置
//将与之对应的之前被移动到html标签下方的节点(旧节点)的显示位置过渡到新节点的显示位置
//然后移除新节点的opacity属性同时将旧节点从dom中移除
function transitMoveNode(node, key, style) {
  if (startMap.get(key) == undefined) {
    return 0
  }
  node.style.opacity = 0
  let start = startMap.get(key)
  startMap.delete(key)

  let startPos = start.getBoundingClientRect()
  let nodePos = node.getBoundingClientRect()
  let moveLeft = nodePos.left - startPos.left
  let moveTop = nodePos.top - startPos.top
  //此处多加0.0001是避免新旧节点所显示的位置一样,位置一样可能会使得旧节点不会进行过渡动画
  //也就不会触发ontransitionend事件,便会一直显示在页面上
  start.style.cssText += `transform: translateX(${moveLeft + 0.0001}px) translateY(${moveTop + 0.0001}px) !important;`
  start.classList = node.classList
  start.classList.add('transit-0000')

  for (let h in style) {
    if (h != 'top' && h != 'left') {
      start.style[h] = style[h]
    }
  }

  start.ontransitionend = () => {
    node.style.opacity = ''
    start.ontransitionend = () => { }
    root.removeChild(start)
  }
}

let x = true
//该函数的作用是清除没有与之对应的元素,比如在a页面有个被Trans包裹的a元素
//但是b页面并没有与a元素对应key值的元素,此时就需要该函数来删除a元素
function clearMap() {
  if (x) {
    x = false
    Promise.resolve().then(() => {
      for (let [key, val] of startMap) {
        root.removeChild(val)
        startMap.delete(key)
      }
      x = true
    })
  }
}

//新增一个样式类
(function createStyle(){
  let text=document.createElement('style')
  text.setAttribute('type','text/css')
  text.innerHTML=`
  .transit-0000{
    transition: all ease .4s !important;
    position:absolute !important;
    top:0px;
    left:0px;
  }
  .transit-0000 *{
    transition: all ease .4s !important;
  }
  `
  document.querySelector('head').appendChild(text)
}())

github地址 最后贴一个实际效果图(注意路由):

localhost_5173 和另外 4 个页面 - 个人 - Microsoft_ Edge 2022-09-02 18-23-44.gif