Главная » Статьи » Как получить больше от асинхронных компонентов Vue

Как получить больше от асинхронных компонентов Vue

Как получить больше от асинхронных компонентов Vue

От автора: в основном мы используем асинхронные компоненты Vue, чтобы разделить их на собственные пакеты и уменьшить первоначальный размер пакета. В этой статье я рассмотрю, как получить от этих компонентов больше.

Параметры асинхронного компонента

Если вы используете Nuxt, вам обязательно понравится функция asyncData, поскольку она позволяет извлекать произвольные данные и вставлять их в данные компонента страницы. Но это работает только для компонентов страницы, когда речь идет об асинхронных данных на уровне компонентов, у нас нет реального ответа, по крайней мере, на данный момент.

Рассмотрим компонент с динамическим макетом, который изменяется в зависимости от конфигурации пользователя, например, приложение с динамическим макетом. Простая реализация выглядит так:

<template> <div> <div :class="`is-${layout[0]}`"> Section 1 </div> <div :class="`is-${layout[1]}`"> Section 2 </div> <div :class="`is-${layout[2]}`"> Section 3 </div> </div>
</template> <script> export default { props: { layout: { type: Array, default: () => [6, 3, 3] } } };
</script>

Естественно, вы должны получить информацию о макете в родительском компоненте этого компонента и передать ее через свойство layout. Это хорошо, но было бы намного лучше, если бы мы могли сделать этот компонент самодостаточным и отделить эту логику от его родителя. И если он будет использоваться часто, будет проблематично продолжать загружать конфигурацию снова и снова.

Я предполагаю, что мы можем просто получить информацию из API, и вместо использования макета мы могли бы использовать локальное состояние:

<template> <div> <div :class="`is-${layout[0]}`"> Profile Section </div> <div :class="`is-${layout[1]}`"> Sidebar </div> <div :class="`is-${layout[2]}`"> Ads </div> </div>
</template> <script> export default { name: 'Layout', data: () => ({ layout: [6, 3, 3] }), async mounted() { this.layout = await api.getUserLayout(); } };
</script>

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

Vue.js предлагает возможность создавать отложенные компоненты, мы в основном используем эту функцию для разделения кода на несколько частей, что полезно для больших компонентов, таких как компонент маршрутизации vue-router. Асинхронный компонент обычно составляется так:

Vue.component('async-component', () => import('./async-component'));

Но с помощью этой идеи мы могли бы сделать гораздо больше, ведь функция должна возвращать promise, который разрешается в объекте параметров компонента. Таким образом, мы можем выполнять любые произвольные операции между ними, например, мы можем получить данные из удаленного источника.

Vue.component('Layout', async () => { const layout = await api.getUserLayout(); return { data: () => ({ layout }), template: ` <div> <div :class="`is-${layout[0]}`"> Profile Section </div> <div :class="`is-${layout[1]}`"> Sidebar </div> <div :class="`is-${layout[2]}`"> Ads </div> </div> ` };
});

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

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

Помните, что мы можем поместить любую произвольную логику в распознаватель асинхронных компонентов, поэтому, например, мы можем загрузить компонент самостоятельно и вставить в него данные. Простой хак будет выглядеть так:

const Layout = async () => { // Our SFC const component = await import('./components/Layout'); const layout = await api.getUserLayout(); // Good ol' JS monkey patching. const originalData = component.data; component.data = () => { return { ...originalData(), layout }; }; return component;
};

Это исправляет определение нашего компонента с извлеченными удаленными данными, это также разделяет наш компонент в отдельный фрагмент, и поэтому мы получаем лучшее из обоих миров. Теперь это повторно используемо, верно?

Нам нужно внедрить данные в произвольный компонент с promise результата. Мы могли бы сделать это с помощью компонента высшего порядка (HOC):

const withData = (component, callback) => async () => { const asyncData = await callback(); // Handle both splitted components and directly imported ones. component = component.then ? await component : component; const originalData = component.data || (() => ({})); component.data = () => { return { ...originalData(), ...asyncData }; }; return component;
};

Теперь мы можем использовать это с любым компонентом:

Vue.component( 'Layout', withData(() => import('./components/Layout.vue'), api.getUserLayout)
);

Это не идеальное решение, поскольку ваш компонент будет оценивать функцию распознавателя только один раз, то есть он не будет обновляться при каждом посещении этого компонента. Но мы исследовали идею внедрения параметров компонента во время выполнения, это полезно, если вы делаете это один раз в режиме полного рендеринга, например, при загрузке хедера или футера.

Вот этот пример в действии:

Асинхронные функции

Иногда вы сталкиваетесь с таким сценарием: ваш удивительный компонент выполняет задание по умолчанию, затем, основываясь на свойстве или каком-либо условии во время выполнения, он переносит код на тяжелый путь выполнения.
Давайте проиллюстрируем это на примере, с которым я столкнулся. У меня есть компонент AppContent, который добавляет стили и предварительную обработку к произвольному тексту. Например, он конвертирует :emoji: в графику смайликов, такую как Slack или Discord.

Это не так сложно, но у нас также есть другой сценарий, в котором мы можем столкнуться с тремя ` для отображения некоторого фрагмента кода, который потребует подсветки.

Поэтому нам также нужно загрузить выбранный выделитель (Prism или Highlight.js), затем загрузить выбранную тему и применить ее к фрагменту кода.

Теперь вы поймете, что этот компонент, даже если он загружен отложено, будет упаковывать кучу килобайт для поддержки функций, которые могут отсутствовать во всем написанном контенте, и если нам потребуется добавить больше функций, он будет увеличиваться очень быстро. Не было бы лучше просто загрузить абсолютный минимум, а затем постепенно подгружать функции при необходимости?

Вы можете решить эту проблему несколькими способами, предполагая, что узлы контента плоские (без дочерних элементов), что позволит нам исключить рекурсивные компоненты, что к лучшему, потому что мы хотим сохранить простоту.

Отличным решением было бы сопоставить узлы с компонентами, и загружать эти компоненты отложено с помощью import().

<template> <div class="AppContent"> <Component v-for="node in mappedNodes" :is="node.component" v-bind="node.props" > {{ node.body }} </Component> </div>
</template> <script>
export default { components: { SnippetNode: () => import('./SnippetNode.vue'), EmojiNode: () => import('./EmojiNode.vue') }, props: { nodes: { type: Array, required: true } }, computed: { mappedNodes () { return this.nodes.map(node => { if (node.startsWith(':') && node.endsWith(':')) { // Emoji Node return { component: 'EmojiNode', props: { id: node.replace(/:/g, '') } }; } if (node.startsWith('```') && node.endsWith('```')) { // Snippet node return { component: 'SnippetNode', props: { language: node.match(/```(w+)/)[1] }, body: node.replace(/```(w+)?/g, '') }; } // just a paragraph return { component: 'p', body: node }; }); } }
};
</script>

Вот это в действии:

На первый взгляд это выглядит прекрасно, но вы заметите, что код загружает оба компонента в любом случае, даже если вы удалить некоторые узлы из файла nodes.json.

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

Давайте определим нашу цель: нам нужно динамически регистрировать компоненты, если они необходимы, в противном случае мы их вообще не регистрируем.

Полезной особенностью Dynamic component является то, что свойство is может принимать параметры компонентов, поэтому вместо регистрации отложенных компонентов мы можем отложено загружать их при вычислении списка узлов. Немного улучшенный пример выглядит так:

// Create a lazy loader.
const loadComponent = function (component) { return () => import(`./${component}.vue`);
} export default { props: { nodes: { type: Array, required: true } }, computed: { mappedNodes () { return this.nodes.map(node => { if (node.startsWith(':') && node.endsWith(':')) { // Emoji Node return { component: loadComponent('EmojiNode'), props: { id: node.replace(/:/g, '') } }; } if (node.startsWith('```') && node.endsWith('```')) { // Snippet node return { component: loadComponent('SnippetNode'), props: { language: node.match(/```(w+)/)[1] }, body: node.replace(/```(w+)?/g, '') }; } // just a paragraph return { component: 'p', body: node }; }); } }
};

Вот и весь трюк! Код загружает компонент только тогда, когда это необходимо. Мы сумели решить проблему, но можем ли мы избежать неприятностей с чем-то более простым?

Так как мы только преобразуем текст, мы можем отображать спаны с привязкой HTML с помощью v-html. И мы можем отложено загружать любой код JavaScript, используя import() как асинхронные компоненты. Это работает не только для компонентов Vue.

<template> <div class="AppContent"> <p v-for="node in mappedNodes" v-html="node"></p> </div>
</template> <script>
function transform(node) { function loadTransformer(transformer) { return import(`../transformers/${transformer}.js`).then(({ transform }) => { return transform; }); } if (node.startsWith(":") && node.endsWith(":")) { return loadTransformer("emoji").then(t => t(node)); } if (node.startsWith("```") && node.endsWith("```")) { return loadTransformer("code").then(t => t(node)); } return node;
} export default { props: { nodes: { type: Array, required: true } }, data: () => ({ mappedNodes: [] }), async mounted() { for (const node of this.nodes) { this.mappedNodes.push(await transform(node)); } }
};
</script>

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

Заключение

Мы изучили пару интересных идей, которые можем сделать с асинхронными компонентами, я уверен, что вы можете придумать несколько безумных идей.

Источник: https://logaretm.com

Редакция: Команда webformyself.