Давай сначала поговорим
Гибкий и достаточно абстрактный компонент может позволить нам повысить эффективность кодирования, стандартизировать код и унифицировать стиль пользовательского интерфейса... В Vue3 мы берем общий модальный диалоговый компонент в качестве примера для обсуждения нескольких идей и реализаций.
Мышление - 7 идей
- ✅ диалогу нужны основные элементы "заголовок, содержание, кнопки ОК/Отмена". Контент должен быть гибким, это может быть строка или фрагмент html-кода (например, слот).
- ✅ Диалоги должны «выпрыгивать», избегать «привязки» к родительским компонентам, использовать Vue3
TeleportПакет встроенных компонентов. - ✅ Вызов диалога должен быть введен в каждом родительском компоненте
import Modal from '@/Modal', сложнее. Также рассмотрите вариант использования API, как в Vue2:this.$modal.show({ /* 选项 */ }). - ✅ Вызывается в виде API, контент может быть строковым, гибким
hфункция илиjsxсинтаксис для рендеринга. - ✅ Стиль диалога или поведение... можно настроить глобально, а локальную конфигурацию можно переопределить.
- ✅ Международная, гибкая с
vue-i18nсмешивание, то есть: если не введеноvue-i18nКитайская версия отображается по умолчанию, иначе будет использоватьсяvue-i18nизtспособ переключения языков. - ✅ Объедините с 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~'
});
У этого способа вызова лично я считаю, что есть два недостатка:
- Углубленный доступ к каждому вызову страницы
globalProperties - Производный тип TS
globalPropertiesЭто свойство «ошибочно», что означает, что нам нужно настроить интерфейс для расширения
Создаем новую папку в проекте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, чтобы получить исходный код этой статьи~