Vue3丨TS丨7 идей для инкапсуляции гибкого модального диалога

Vue.js
Vue3丨TS丨7 идей для инкапсуляции гибкого модального диалога

Давай сначала поговорим

Гибкий и достаточно абстрактный компонент может позволить нам повысить эффективность кодирования, стандартизировать код и унифицировать стиль пользовательского интерфейса... В Vue3 мы берем общий модальный диалоговый компонент в качестве примера для обсуждения нескольких идей и реализаций.

Мышление - 7 идей

  1. ✅ диалогу нужны основные элементы "заголовок, содержание, кнопки ОК/Отмена". Контент должен быть гибким, это может быть строка или фрагмент html-кода (например, слот).
  2. ✅ Диалоги должны «выпрыгивать», избегать «привязки» к родительским компонентам, использовать Vue3TeleportПакет встроенных компонентов.
  3. ✅ Вызов диалога должен быть введен в каждом родительском компонентеimport Modal from '@/Modal', сложнее. Также рассмотрите вариант использования API, как в Vue2:this.$modal.show({ /* 选项 */ }).
  4. ✅ Вызывается в виде API, контент может быть строковым, гибкимhфункция илиjsxсинтаксис для рендеринга.
  5. ✅ Стиль диалога или поведение... можно настроить глобально, а локальную конфигурацию можно переопределить.
  6. ✅ Международная, гибкая сvue-i18nсмешивание, то есть: если не введеноvue-i18nКитайская версия отображается по умолчанию, иначе будет использоватьсяvue-i18nизtспособ переключения языков.
  7. ✅ Объедините с ts, чтобы сделать вызовы на основе API более удобными.

Если у нас есть идеи, давайте будем гигантами в действии~

упражняться

Модальная структура каталогов компонентов

├── plugins
│   └── modal
│       ├── Content.tsx // 维护 Modal 的内容,用于 h 函数和 jsx 语法
│       ├── Modal.vue // 基础组件
│       ├── config.ts // 全局默认配置
│       ├── index.ts // 入口
│       ├── locale // 国际化相关
│       │   ├── index.ts
│       │   └── lang
│       │       ├── en-US.ts
│       │       ├── zh-CN.ts
│       │       └── zh-TW.ts
│       └── modal.type.ts // ts类型声明相关

Описание: Поскольку Modal будетapp.use(Modal)звонить какплагин, поэтому мы помещаем его в каталог плагинов.

Базовый пакет Modal.vue (показан только шаблон)

<template>
  <Teleport to="body"
            :disabled="!isTeleport">>
    <div v-if="modelValue"
         class="modal">

      <div class="mask"
           :style="style"
           @click="handleCancel"></div>

      <div class="modal__main">
        <div class="modal__title">
          <span>{{title||'系统提示'}}</span>
          <span v-if="close"
                title="关闭"
                class="close"
                @click="handleCancel">✕</span>
        </div>

        <div class="modal__content">
          <Content v-if="typeof content==='function'"
                   :render="content" />
          <slot v-else>
            {{content}}
          </slot>
        </div>

        <div class="modal__btns">
          <button :disabled="loading"
                  @click="handleConfirm">
            <span class="loading"
                  v-if="loading"> ❍ </span>确定
          </button>
          <button @click="handleCancel">取消</button>
        </div>

      </div>
    </div>
  </Teleport>
</template>

Описание: Из шаблона мы видим, что структура dom Modal состоит из нескольких частей: слой маски, заголовок, содержимое и нижняя кнопка. В этих блоках мы можем определять и получать соответствующие реквизиты для разных стилей или конфигураций поведения.

Теперь давайте сосредоточимся на блоке контента:

<div class="modal__content">
  <Content v-if="typeof content==='function'"
            :render="content" />
  <slot v-else>
    {{content}}
  </slot>
</div>

<Content />является функциональным компонентом:

// Content.tsx
import { h } from 'vue';
const Content = (props: { render: (h: any) => void }) => props.render(h);
Content.props = ['render'];
export default Content;

Сценарий 1. На основе вызова API, когда контент является методом, вызывается компонент Content, например:

  • использоватьhфункция:
$modal.show({
  title: '演示 h 函数',
  content(h) {
    return h(
      'div',
      {
        style: 'color:red;',
        onClick: ($event: Event) => console.log('clicked', $event.target)
      },
      'hello world ~'
    );
  }
});
  • Используйте удобный синтаксис jsx
$modal.show({
  title: '演示 jsx 语法',
  content() {
    return (
      <div
        onClick={($event: Event) => console.log('clicked', $event.target)}
      >
        hello world ~
      </div>
    );
  }
});

Сценарий 2: Традиционный способ вызова компонентов, когда контент не является методом (в ветке v-else), например:

  • default slot
<Modal v-model="show"
          title="演示 slot">
  <div>hello world~</div>
</Modal>
  • Передать свойство содержимого напрямую
<Modal v-model="show"
          title="演示 content"
          content="hello world~" />

Как указано выше, содержимое модального окна может помочь нам использовать4 способанаписать.

APIизация

В Vue2 мы хотим API компонента сVue.extendспособ получить экземпляр компонента, а затем динамически добавить его в тело, например:

import Modal from './Modal.vue';
const ComponentClass = Vue.extend(Modal);
const instance = new ComponentClass({ el: document.createElement("div") });
document.body.appendChild(instance.$el);

Удалено в Vue3Vue.extendметод, но мы можем сделать

import Modal from './Modal.vue';
const container = document.createElement('div');
const vnode = createVNode(Modal);
render(vnode, container);
const instance = vnode.component;
document.body.appendChild(container);

Преобразуйте модальный компонент в виртуальный дом и визуализируйте его в div с помощью функции рендеринга (когда компонент контролируется для отображения). Затем динамически добавьте к телу.

Давайте посмотрим на конкретный код (некоторые части опущены, подробности смотрите в комментариях):

// index.ts
import { App, createVNode, render } from 'vue';
import Modal from './Modal.vue';
import config from './config';

// 新增 Modal 的 install 方法,为了可以被 `app.use(Modal)`(Vue使用插件的的规则)
Modal.install = (app: App, options) => {
  // 可覆盖默认的全局配置
  Object.assign(config.props, options.props || {});

  // 注册全局组件 Modal
  app.component(Modal.name, Modal);
  
  // 注册全局 API
  app.config.globalProperties.$modal = {
    show({
      title = '',
      content = '',
      close = config.props!.close
    }) {
      const container = document.createElement('div');
      const vnode = createVNode(Modal);
      render(vnode, container);
      const instance = vnode.component;
      document.body.appendChild(container);
        
      // 获取实例的 props ,进行传递 props
      const { props } = instance;

      Object.assign(props, {
        isTeleport: false,
        // 在父组件上我们用 v-model 来控制显示,语法糖对应的 prop 为 modelValue
        modelValue: true,
        title,
        content,
        close
      });
    }
  };
};
export default Modal;

Осторожные друзья спросят, как API-вызов Modal обрабатывает событие клика? Давайте посмотрим вниз с вопросами.

Обработка событий API

Когда мы инкапсулируем Modal.vue, мы уже прописали соответствующие события «ОК» и «Отмена»:

// Modal.vue
setup(props, ctx) {
  let instance = getCurrentInstance();
  onBeforeMount(() => {
    instance._hub = {
      'on-cancel': () => {},
      'on-confirm': () => {}
    };
  });

  const handleConfirm = () => {
    ctx.emit('on-confirm');
    instance._hub['on-confirm']();
  };
  const handleCancel = () => {
    ctx.emit('on-cancel');
    ctx.emit('update:modelValue', false);
    instance._hub['on-cancel']();
  };

  return {
    handleConfirm,
    handleCancel
  };
}

здесьctx.emitПросто давайте использовать при вызове компонента в родительском компоненте@on-confirmформу для мониторинга. Итак, как мы можем слушать в API? Другими словами, как мы можем$modal.showметод «слушай».

// index.ts
app.config.globalProperties.$modal = {
   show({}) {
     /* 监听 确定、取消 事件 */
   }
}

Мы можем видеть вышеsetupВнутри метода получаем экземпляр текущего компонента.Перед монтированием компонента добавляем свойство без авторизации_hub(Назовем его центром обработки событий~) и добавим два метода пустых операторов.on-cancel,on-confirmи вызываются соответственно в событии клика.

Здесь мы добавляем себе некоторую "сложность". Мы хотим реализовать нажатие ОК. Если событие ОК является асинхронной операцией, то нам нужно отобразить значок загрузки на кнопке ОК и отключить кнопку для ожидания асинхронного завершения.

Посмотрите прямо на код:

// index.ts
app.config.globalProperties.$modal = {
  show({
    /* 其他选项 */
    onConfirm,
    onCancel
  }) {
    /* ... */

    const { props, _hub } = instance;
    
    const _closeModal = () => {
      props.modelValue = false;
      container.parentNode!.removeChild(container);
    };
    // 往 _hub 新增事件的具体实现
    Object.assign(_hub, {
      async 'on-confirm'() {
        if (onConfirm) {
          const fn = onConfirm();
          // 当方法返回为 Promise
          if (fn && fn.then) {
            try {
              props.loading = true;
              await fn;
              props.loading = false;
              _closeModal();
            } catch (err) {
              // 发生错误时,不关闭弹框
              console.error(err);
              props.loading = false;
            }
          } else {
            _closeModal();
          }
        } else {
          _closeModal();
        }
      },
      'on-cancel'() {
        onCancel && onCancel();
        _closeModal();
      }
    });

    /* ... */

  }
};

i18n

Компонент поставляется с

Учитывая, что наши компоненты тоже могут делать i18n, поэтому сохраняем его здесь. По умолчанию стоит китайская конфигурация i18n, обратитесь к вышеБазовая оболочка для Modal.vueКак видите, нам нужно настроить 4 константы, например:

<span>{{title||'系统提示'}}</span>
title="关闭"
<button @click="handleConfirm">确定</button>
<button @click="handleCancel">取消</button>

заменить на

<span>{{title||t('r.title')}}</span>
:title="t('r.close')"
<button @click="handleConfirm">{{t('r.confirm')}}</button>
<button @click="handleCancel">{{t('r.cancel')}}</button>

Нам также нужно инкапсулировать методt:

// locale/index.ts
import { getCurrentInstance } from 'vue';
import defaultLang from './lang/zh-CN';

export const t = (...args: any[]): string => {
  const instance = getCurrentInstance();
  // 当存在 vue-i18n 的 t 方法时,就直接使用它
  const _t = instance._hub.t;
  if (_t) return _t(...args);

  const [path] = args;
  const arr = path.split('.');
  let current: any = defaultLang,
    value: string = '',
    key: string;

  for (let i = 0, len = arr.length; i < len; i++) {
    key = arr[i];
    value = current[key];
    if (i === len - 1) return value;
    if (!value) return '';
    current = value;
  }
  return '';
};

использовать этотtМетод, нам нужно сделать это только в MODAL.VUE:

// Modal.vue
import { t } from './locale';
/* ... */
setup(props, ctx) {
  /* ... */
  return { t };
}

Интеграция с vue-i18n

Мы видим, что выше есть строка кодаconst _t = instance._hub.t;,это.tВот как:

  • В Modal.vue получите глобальное монтированиеvue-i18nиз$tметод
setup(props, ctx) {
  let instance = getCurrentInstance();
  onBeforeMount(() => {
    instance._hub = {
      t: instance.appContext.config.globalProperties.$t,
      /* ... */
    };
  });
}
  • При глобальной регистрации собственности используйте напрямуюapp.useПараметры приложения метода обратного вызова
Modal.install = (app: App, options) => {
  app.config.globalProperties.$modal = {
    show() {
      /* ... */
      const { props, _hub } = instance;
      Object.assign(_hub, {
        t: app.config.globalProperties.$t
      });
      /* ... */
    }
  };
};

Запомнить, если вы хотите объединиться с vue-i18n, вам нужно сделать еще один шаг, который заключается в объединении языкового пакета Modal с языковым пакетом проекта проекта.

const messages = {
  'zh-CN': { ...zhCN, ...modal_zhCN },
  'zh-TW': { ...zhTW, ...modal_zhTW },
  'en-US': { ...enUS, ...modal_enUS }
};

с ТС

Мы начнем эту тему с «Как вызывать модальные компоненты в виде API в Vue3». В настройке Vue3 этого понятия нет, необходимо вызывать API, который монтируется глобально, например:

const {
  appContext: {
    config: { globalProperties }
  }
} = getCurrentInstance()!;

// 调用 $modal 
globalProperties.$modal.show({
  title: '基于 API 的调用',
  content: 'hello world~'
});

У этого способа вызова лично я считаю, что есть два недостатка:

  1. Углубленный доступ к каждому вызову страницыglobalProperties
  2. Производный тип TSglobalPropertiesЭто свойство «ошибочно», что означает, что нам нужно настроить интерфейс для расширения

Создаем новую папку в проектеhooks

// hooks/useGlobal.ts
import { getCurrentInstance } from 'vue';
export default function useGlobal() {
  const {
    appContext: {
      config: { globalProperties }
    }
  } = (getCurrentInstance() as unknown) as ICurrentInstance;
  return globalProperties;
}

Вам также нужно создать новый глобальный файл объявления ts global.d.ts, а затем написать его так:ICurrentInstanceинтерфейс:

// global.d.ts
import { ComponentInternalInstance } from 'vue';
import { IModal } from '@/plugins/modal/modal.type';

declare global {
  interface IGlobalAPI {
    $modal: IModal;
    // 一些其他
    $request: any;
    $xxx: any;
  }
  // 继承 ComponentInternalInstance 接口
  interface ICurrentInstance extends ComponentInternalInstance {
    appContext: {
      config: { globalProperties: IGlobalAPI };
    };
  }
}
export {};

Как и выше, мы наследуем оригиналComponentInternalInstanceИнтерфейс может компенсировать эту «недостаток».

Таким образом, правильный способ использования API-вызовов модального компонента находится на уровне страницы в:

// Home.vue
setup() {
  const { $modal } = useGlobal();
  const handleShowModal = () => {
    $modal.show({
      title: '演示',
      close: true,
      content: 'hello world~',
      onConfirm() {
        console.log('点击确定');
      },
      onCancel() {
        console.log('点击取消');
      }
    });
  };
 
  return {
    handleShowModal
  };
}

фактическиuseGlobalМетод должен ссылаться на метод useContext Vue3:

// Vue3 源码部分
export function useContext() {
    const i = getCurrentInstance();
    if ((process.env.NODE_ENV !== 'production') && !i) {
        warn(`useContext() called without active instance.`);
    }
    return i.setupContext || (i.setupContext = createSetupContext(i));
}

некоторые демо

глубоко

Друзья, которым нравятся упаковочные компоненты, также могут попробовать следующее:

  • Кнопки «ОК» и «Отмена» настраиваются
  • Показывать ли кнопку «Отмена» или отображать все кнопки внизу
  • Если содержимое превышает определенную длину, отображается полоса прокрутки.
  • Простое центрирование текста строкового содержимого слева/по центру/справа
  • можно перетащить
  • ...

Суммировать

Форма вызова API может быть относительно фиксированной, и его целью является простой и частый вызов компонентов.Если задействована сложная сцена, используйте общий способ вызова компонентов. Эта статья предназначена для предоставления идей инкапсуляции для того, как инкапсулировать гибкий компонент. Когда у нас есть идеи и реализации, мы можем делать выводы из одного случая в другой~

😬 В 2021 году внимание привлекает «Front-end Refinement».

Официальный аккаунт обращает внимание на «внешний штраф», ответьте 1, чтобы получить исходный код этой статьи~