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

解决 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 控制动画的弹窗组件。

相关推荐
三门1 分钟前
docker安装mysql8.0.20过程
前端
BillKu38 分钟前
Vue3 + Vite 中使用 Lodash-es 的防抖 debounce 详解
前端·javascript·vue.js
一只小风华~1 小时前
HTML前端开发:JavaScript的条分支语句if,Switch
前端·javascript·html5
橙子家1 小时前
Select 组件实现【全选】(基于 Element)
前端
超级土豆粉1 小时前
HTML 语义化
前端·html
bingbingyihao1 小时前
UI框架-通知组件
前端·javascript·vue
wordbaby1 小时前
React Router 预渲染的工作原理和价值(Pre-rendering)
前端·react.js
依旧天真无邪1 小时前
Chrome 优质插件计划
前端·chrome
逝缘~2 小时前
小白学Pinia状态管理
前端·javascript·vue.js·vscode·es6·pinia
光影少年2 小时前
vite原理
前端·javascript·vue.js