В процессе изучения Next и React мне очень понравился простой способ добавления макетов в систему маршрутизации.
Вместо того чтобы фреймворк автоматически заботился о макетах, Next позволяет читателю самому реализовать их.
Такая реализация выглядит примерно так, как показано ниже:
/**
* layouts/default.js
*/
export default function DefaultLayout({ children }) {
return (
<>
<header>
<h1>My Website</h1>
</header>
<main>{children}</main>
<footer>
Copywrite 2022 - My Website
</footer>
</>
)
}
/**
* pages/index.js
*/
export default function Page() {
return (
<div>This is my page</div>
)
}
Page.getLayout = (children) => <DefaultLayout>{children}</DefaultLayout>
/**
* pages/_app.js
*/
export default function MyApp({ Component, pageProps }) {
// Use the layout defined at the page level, if available
const getLayout = Component.getLayout || ((page) => page)
return getLayout(<Component {...pageProps} />)
}
Хотя в настоящее время существует RFC для улучшения ситуации с макетами в Next, описанный выше вариант подходит для большинства базовых сайтов, и проблемы возникают только по мере того, как вам нужно отслеживать все больше и больше состояний в ваших макетах.
Поэтому при использовании Vue у нас нет системы макетов, если только вы не используете что-то вроде Nuxt или Vite Plugin Vue Layouts, которые абстрагируют проблему с помощью некоторой магии. К сожалению, Nuxt пока не имеет фантастической поддержки JSX/TSX в Nuxt3, а Vite Plugin в настоящее время предназначен только для работы с однофайловыми компонентами (SFC), поэтому для пользователя JSX/TSX, такого как я, это неприемлемо.
Для решения этой проблемы мы можем взять предложенное решение из Next и сделать его совместимым с Vue, для этого нам нужно использовать слоты, доступные в компоненте <RouterView />
, чтобы мы могли проверить наличие метода getLayout
, определенного на странице.
Для целей этой статьи мы будем считать, что вы используете JSX с Vue, хотя это далеко не норма, а мое предпочтение. Если вы все еще используете SFC, не бойтесь, вы также можете воспользоваться кодом из этой статьи, который можно увидеть в репозитории примеров к этой статье.
Итак, зачем нам вообще нужны макеты?
Использование макетов при работе с такими библиотеками, как React или Vue, позволяет нам значительно сократить количество происходящего на одной странице. Мы можем извлекать простую логику и элементы в макет в дополнение к подготовке магазинов или других провайдеров для потребления дочерних компонентов.
Это также позволяет нам поддерживать согласованность набора страниц, которые мы посчитали связанными между собой, гарантируя, что если мы обновим общий контейнер для страниц, то все они получат обновление, а не станут потенциально непоследовательными.
Так почему бы просто не определить макет в функции рендеринга или шаблоне?
Хотя мы можем обернуть нашу функцию рендеринга или шаблон макетом, обычно это нежелательно, поскольку это показывает тесную связь между ними и создает дополнительную когнитивную нагрузку для редакторов, поскольку они должны отбрасывать первый элемент в данной функции рендеринга или шаблоне.
В связи с этим мы наблюдаем стандартизацию, при которой макеты определяются как свойство или метод компонента или маршрута.
Как же нам добавить эту систему компоновки?
Начнем с того, что в стране Vue мы используем Vue Router для маршрутизации. Это плагин от первого лица, который решает все ваши потребности в маршрутизации, обеспечивая как веб-историю, так и маршрутизацию на основе хэша. Кроме того, он поддерживает вложенные маршруты и представления маршрутизатора.
Традиционно мы просто добавляем компонент <RouterView />
в любом месте, где мы хотим отобразить страницу, и Vue Router находит соответствующий компонент и затем отображает его для нас.
Однако Vue Router также позволяет нам, как пользователям, рендерить собственный контент с помощью слотов, где он будет передавать Component
и route
как набор реквизитов для содержимого нашего слота.
Мы можем использовать этот вторичный метод рендеринга, чтобы вместо этого проверить, есть ли у компонента метод getLayout
, и затем рендерить его с компонентом страницы в качестве аргумента.
Это будет выглядеть следующим образом:
export const App = defineComponent({
name: 'App',
setup(_props, { attrs }) {
return () => (
<RouterView>
{{
default: ({ Component }) => {
if (!Component) {
return <div />;
}
// If the component comes with a layout then we should render that with the component
// as a child
if (Component.type?.getLayout && typeof Component.type.getLayout === 'function') {
return Component.type.getLayout(h(Component, { ...attrs }));
}
// Otherwise we default to the typical <RouterView /> behaviour
return h(Component, { ...attrs });
},
}}
</RouterView>
);
},
});
Подпись для getLayout
будет выглядеть следующим образом:
{
getLayout: (children: VNode) => VNode;
}
Чтобы сохранить порядок, мы рекомендуем извлечь логику в компоненте <App />
в компонент <RouterViewWithLayout />
или <AppView />
. Это также пригодится при работе с вложенными компонентами <RouterView />
, если вы решите использовать их в своем проекте.
И что теперь?
Теперь, когда у нас есть логика для рендеринга макета при получении его через getLayout
, мы можем использовать его на наших страницах. Вы можете увидеть это в действии на игровой площадке Stackblitz ниже.
Бонусный раунд: Макеты SFC
Для SFC мы используем свойство layout
, которое ссылается на компонент, а не метод getLayout
, который возвращает VNodes
. Это связано с ограничениями в том, где можно использовать синтаксис <template>
. Это означает, что хотя вышеприведенный вариант будет работать фантастически для большинства потребностей, он все же не будет таким гибким, как вариант JSX.
Вы можете увидеть SFC-версию в действии на альтернативной игровой площадке ниже.