vant源码学习 Dialog弹窗

375 阅读2分钟

Dialog组件结构

  • 基于Popup的二次封装 Popup可以看这里
  • pick函数自动赋值给Popup组件
  • tsx写法的组件的结构和逻辑真的很清晰
// Dialog.tsx
...
return () => {
  const { width, title, theme, message, className } = props;
  return (
    <Popup
      ref={root}
      role="dialog"
      class={[bem([theme]), className]}
      style={{ width: addUnit(width) }}
      tabindex={0}
      aria-labelledby={title || message}
      onKeydown={onKeydown}
      onUpdate:show={updateShow}
      {...pick(props, popupInheritKeys)}
    >
      {renderTitle()}
      {renderContent()}
      {renderFooter()}
    </Popup>
  );
};
// pick函数实现
function pick<T, U extends keyof T>(obj:T,keys:ReadonlyArray<U>,ignoreUndefined?:boolean){
    return keys.reduce((ret, key) => {
      if(!ignoreUndefined || obj[key] !== undefined){
        ret[key] = obj[key]
      }
      return ret
    },{} as Writeable<Pick<T,U>>)
  }

withInstall注册全局组件

  • vue3在调用app.use的过程中会判断组件有没有install函数,会自动安装
  • withInstall给属性增加了一个函数,没有立刻加载组件,做到懒加载全局组件
  • 自动安装驼峰和原名组件
// index.ts
import _Dialog from './Dialog';
export const Dialog = withInstall(_Dialog);
export default Dialog;
export {
  showDialog,
  closeDialog,
  showConfirmDialog,
  setDialogDefaultOptions,
  resetDialogDefaultOptions,
} from './function-call';
// withinstall.ts
export function withInstall<T extends Component>(options: T) {
  (options as Record<string, unknown>).install = (app: App) => {
    const { name } = options;
    if (name) {
      app.component(name, options);
      app.component(camelize(`-${name}`), options); // 首字母大写
    }
  };

  return options as WithInstall<T>;
}

全局调用弹窗方法

  • createApp实例化一个单例的instance,通过mount挂载在document.body
  • usePopupState 提供弹窗开关响应式状态以及暴露的方法控制弹窗开关状态
  • showDialog 方法就可以直接调用usePopupState提供的open方法打开

Vant 中导出了以下 Dialog 相关的辅助函数:

方法名说明参数返回值
showDialog展示弹窗options: DialogOptionsPromise<void>
showConfirmDialog展示消息确认弹窗options: DialogOptionsPromise<void>
closeDialog关闭弹窗-void
setDialogDefaultOptions修改默认配置,影响所有的 showDialog 调用options: DialogOptionsvoid
resetDialogDefaultOptions重置默认配置,影响所有的 showDialog 调用-void
// function-call.tsx
let instance: ComponentInstance;

const DEFAULT_OPTIONS = {
  title: '',
  width: '',
  theme: null,
  message: '',
  overlay: true,
  callback: null,
  teleport: 'body',
  className: '',
  allowHtml: false,
  lockScroll: true,
  transition: undefined,
  beforeClose: null,
  overlayClass: '',
  overlayStyle: undefined,
  messageAlign: '',
  cancelButtonText: '',
  cancelButtonColor: null,
  cancelButtonDisabled: false,
  confirmButtonText: '',
  confirmButtonColor: null,
  confirmButtonDisabled: false,
  showConfirmButton: true,
  showCancelButton: false,
  closeOnPopstate: true,
  closeOnClickOverlay: false,
} as const;

let currentOptions = extend({}, DEFAULT_OPTIONS); // 避免污染DEFAULT_OPTIONS 常量

function initInstance() {
  const Wrapper = {
    setup() {
      const { state, toggle } = usePopupState();
      return () => <Dialog {...state} onUpdate:show={toggle} />;
    },
  };

  ({ instance } = mountComponent(Wrapper));
  // 实例化新组件挂载到新节点上 createApp mount
  // usePopupState hook 拓展了instance暴露的方法以及提供响应式状态
  // useExpose({ open, close, toggle });
}

export function showDialog(options: DialogOptions) {
  /* istanbul ignore if */
  if (!inBrowser) {
    return Promise.resolve();
  }

  return new Promise((resolve, reject) => {
    // 单个弹窗实例
    if (!instance) {
      initInstance();
    }
    // 暴露的open方法控制show的状态,返回promise
    instance.open(
      extend({}, currentOptions, options, {
        callback: (action: DialogAction) => {
          (action === 'confirm' ? resolve : reject)(action);
        },
      })
    );
  });
}

// 设置全局currentOptions状态
export const setDialogDefaultOptions = (options: DialogOptions) => {
  extend(currentOptions, options);
};

// 重置全局currentOptions状态
export const resetDialogDefaultOptions = () => {
  currentOptions = extend({}, DEFAULT_OPTIONS);
};

export const showConfirmDialog = (options: DialogOptions) =>
  showDialog(extend({ showCancelButton: true }, options));

export const closeDialog = () => {
  if (instance) {
    instance.toggle(false);
  }
};