异步加载弹出层动画丢失问题

解决 antd-mobile 异步弹窗首次无动画问题的最佳实践

在移动端 React 项目中,弹窗(Popup/Modal)是非常常见的交互组件。为了优化首屏加载性能,我们常常会将弹窗组件异步加载(即点击时才加载对应 JS 文件)。antd-mobile 提供了 Popup 组件和 renderImperatively 函数式调用方式,非常适合业务场景。

但在实际开发中,我遇到了一个细节问题:异步加载的弹窗,首次弹出时没有动画,直接闪现出来,后续弹出才有动画。以下是我解决这个问题的记录与方案。


1. 问题复现

假设我们有如下代码:

javascript 复制代码
import React from "react";
import { Popup, Button } from "antd-mobile";

export function AsyncPopup({ visible, onClose }) {
  return (
    <Popup
      visible={visible}
      onMaskClick={onClose}
      onClose={onClose}
      bodyStyle={{
        minHeight: 200,
        display: "flex",
        alignItems: "center",
        justifyContent: "center",
      }}
    >
      <div style={{ textAlign: "center" }}>
        <h3>我是业务弹窗内容</h3>
        <Button color="primary" onClick={onClose}>
          关闭
        </Button>
      </div>
    </Popup>
  );
}

异步加载 + 函数式调用:

javascript 复制代码
import React, { Suspense } from "react";
import { renderImperatively } from "antd-mobile/es/utils/render-imperatively";

const AsyncPostTypeSelectPopup = React.lazy(() =>
  import("./AsyncPopup").then((mod) => ({
    default: mod.AsyncPopup,
  }))
);

export const AsyncPopupProxy = (props) => (
  <Suspense fallback={<div>加载中...</div>}>
    <AsyncPostTypeSelectPopup {...props} />
  </Suspense>
);

export function showAsyncPopup(props = {}) {
  const handler = renderImperatively(
    <AsyncPopupProxy onCloseIconClick={() => handler.close()} />
  );
  return handler;
}

调用:

ini 复制代码
<Button
  color="primary"
  onClick={async () => {
    await showAsyncPopup();
  }}
>
  打开异步弹窗(函数式)
</Button>

现象:

第一次点击按钮,弹窗直接"闪现"出来,没有动画。后续再点,动画正常。


2. 问题分析

2.1 为什么会这样?

  • 弹窗动画的原理:Popup 组件的 visible 属性从 false 变为 true 时,才会有动画。

  • 但异步加载时,流程如下:

    1. 点击按钮,showAsyncPopup 被调用。
    2. 由于 React.lazy + Suspense,先渲染 fallback(比如"加载中...")。
    3. 弹窗组件 JS 加载完毕后,AsyncPopup 组件第一次挂载,此时 visible 直接就是 true
    4. 由于没有经历 false -> true 的过程,动画不会触发,弹窗直接显示。

2.2 关键点

  • 动画的触发依赖于 visible 的变化,而不是初始值。
  • 异步加载时,组件一挂载 visible 就是 true,动画不会执行。

3. 解决思路

  • 目标: 让弹窗组件先以 visible=false 挂载,然后再变为 true,这样动画就能正常触发。
  • 方案: 不用 React.lazy + Suspense,而是自己用 import() 动态加载组件,加载完后先渲染 visible=false,再异步 setState 变为 true

4. 代码实现

4.1 新的 Proxy 组件

javascript 复制代码
import React, { useState, useEffect } from "react";
import { renderImperatively } from "antd-mobile/es/utils/render-imperatively";

export const AsyncPopupProxy = ({ imperativelyClose, ...props }) => {
  const [Comp, setComp] = useState(null);
  const [visible, setVisible] = useState(false);

  // 1. 异步加载弹窗组件
  useEffect(() => {
    let unmounted = false;
    import("./AsyncPopup").then((mod) => {
      if (!unmounted) {
        setComp(() => mod.AsyncPopup);
      }
    });
    return () => { unmounted = true; };
  }, []);

  // 2. 组件加载完后,下一帧再设置 visible=true,触发动画
  useEffect(() => {
    if (Comp) {
      setTimeout(() => setVisible(true), 0);
    }
  }, [Comp]);

  // 3. 关闭弹窗
  const handleClose = () => {
    setVisible(false);
    imperativelyClose && imperativelyClose();
  };

  if (!Comp) return <div>加载中...</div>;

  return (
    <Comp
      {...props}
      visible={visible}
      onClose={handleClose}
      onMaskClick={handleClose}
    />
  );
};

4.2 函数式调用

ini 复制代码
export function showAsyncPopup(props = {}) {
  let handler;
  handler = renderImperatively(
    <AsyncPopupProxy
      {...props}
      imperativelyClose={() => handler.close()}
    />
  );
  return handler;
}

4.3 使用方式

javascript 复制代码
function App() {
  return (
    <div style={{ padding: 40 }}>
      <Button
        color="primary"
        onClick={async () => {
          await showAsyncPopup();
        }}
      >
        打开异步弹窗(函数式)
      </Button>
    </div>
  );
}

这样就正常啦,这种方式适用于 antd-mobile、antd、mui 等所有依赖 visible 控制动画的弹窗组件。

相关推荐
河畔一角5 分钟前
一些感悟
前端
excel11 分钟前
理解 JavaScript 中的 for...in 与 for...of 的区别
前端
前端小巷子40 分钟前
Webpack 5模块联邦
前端·javascript·面试
玲小珑43 分钟前
Next.js 教程系列(十九)图像优化:next/image 与高级技巧
前端·next.js
晓得迷路了44 分钟前
栗子前端技术周刊第 91 期 - 新版 React Compiler 文档、2025 HTML 状态调查、Bun v1.2.19...
前端·javascript·react.js
江城开朗的豌豆1 小时前
Vue和React中的key:为什么列表渲染必须加这玩意儿?
前端·vue.js·面试
江城开朗的豌豆1 小时前
前端路由傻傻分不清?route和router的区别,看完这篇别再搞混了!
前端·javascript·vue.js
pengzhuofan1 小时前
Web开发系列-第0章 Web介绍
前端
小鱼人爱编程1 小时前
Java基石--反射让你直捣黄龙
前端·spring boot·后端
JosieBook3 小时前
【web应用】如何进行前后端调试Debug? + 前端JavaScript调试Debug?
前端·chrome·debug