Прогрессивный Petite-vue

от автора

Привет ?, это статья про progressive enchancement с помощью petite-vue. Тут я расскажу про его прикольные фичи (как отдельного инструмента, так и в составе Vue). Конечно, было бы прикольно, если бы ты прочитал(а) предыдущую статью по Petite-vue, там много чего расказано про либу в целом, есть какие-то базовые примеры, но «it’s okay» не читать её. Если соображаешь что-то во Vue — тут не так уж и много отличий (о которых, помимо прочего, тут и пойдёт речь).

Ну, я надеюсь ты «ready to action», так что давай сразу запрыгнем в код.

Простая реализация progressive enchancementа

<title> Petite Vue Progressive Enchancement </title> <style>   [v-cloak] {display:none}   body { background: #fff!important } </style>  <script src="https://unpkg.com/petite-vue" defer init></script> <script>   const SCRIPTS = [     "https://ahfarmer.github.io/calculator/static/js/main.b319222a.js",   ]   const CSS = [     "https://ahfarmer.github.io/calculator/static/css/main.b51b4a8b.css",   ]    function embedScript(mount, src) {     const tag = document.createElement("script")     tag.src = src     return mount.appendChild(tag)   }   function embedCSS(mount, src) {     const tag = document.createElement("link")     tag.rel = "stylesheet"     tag.href = src     return mount.appendChild(tag)   }   function tryToLoadSPA() {     setTimeout(loadSPA, 1000)   }   function loadSPA() {     const mount = document.getElementsByTagName("head")[0]     SCRIPTS.map(src => embedScript(mount, src))     CSS.map(src => embedCSS(mount, src))   } </script>  <body>   <div id="root" v-scope="{num: 0}" v-cloak @mounted="tryToLoadSPA">     {{ num }} <button @click="num++">+</button>   </div> </body>

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

Что происходит по сути: пока не загрузился petite-vue — ничего не показываем, после загрузки petite-vue — показываем приложение-counter и ставим на загрузку стили и скрипты мощного React приложения через N (=1) секунд, после загрузки React приложения — petite-vue пропадает.

В качестве подопытного React приложения я взял (первая ссылка из документации :)) вот этот калькулятор

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

<style>   [v-cloak] {display:none}   body { background: #fff!important } </style>

Атрибут v-cloak позволяет скрывать часть дерева до момента, когда petite-vue её распарсил. По сути можно задать там любой стиль, но самое логичное — скрывать элемент, который не будет работать как ожидает пользователь (эта директива есть и в обычном Vue). Ещё я задал белый бэкграунд, так как стили калькулятора его меняют, а я хочу смотреть, остаётся ли страница responsive пока я подгружаю скрипты и стили (просто на чёрном не видно чёрный текст моего счётчика (короче забей)).

После стилей видно данный джаваскрипт:

const SCRIPTS = [   "https://ahfarmer.github.io/calculator/static/js/main.b319222a.js", ] const CSS = [   "https://ahfarmer.github.io/calculator/static/css/main.b51b4a8b.css", ]  function embedScript(mount, src) {   const tag = document.createElement("script")   tag.src = src   return mount.appendChild(tag) } function embedCSS(mount, src) {   const tag = document.createElement("link")   tag.rel = "stylesheet"   tag.href = src   return mount.appendChild(tag) } function tryToLoadSPA() {   setTimeout(loadSPA, 1000) } function loadSPA() {   const mount = document.getElementsByTagName("head")[0]   SCRIPTS.map(src => embedScript(mount, src))   CSS.map(src => embedCSS(mount, src)) }

По сути тут создаётся глобальная функция tryToLoadSPA, которая будет загружать большое SPA приложение опираясь на какую-то логику (у меня стоит таймаут в демонстрационных целях). Туда можно поставить данные performance, чтобы грузить SPA в зависимости от FCP или TTI или ещё чего угодно… Можно на этом моменте делать всплывающее окно, в котором спрашивать пользователя или он хочет загрузить расширенную версию сайта. В общем суть понятна. Костыли для асинхронной загрузки jsа и cssок я взял с stackoverflow.

Ну и заключающий кусок — уже бородатый counter:

<div id="root" v-scope="{num: 0}" v-cloak @mounted="tryToLoadSPA">   {{ num }} <button @click="num++">+</button> </div>

Если вдруг впервые видишь v-scope — это фича исключительно petite-vue. По сути ты задаёшь $data поле, одновременно инициализируя дочернюю часть DOM дерева как Vue компонент (подробнее в статье x).

Тут же очередная эксклюзивная штука — @mounted. Это директива, которая исполнит JS код каждый раз, когда компонент будет замаунтен (в нашем случае только 1 раз). В обычном Vue это поле mounted структурки компонента. По аналогии с другими названиями директив может показаться, что есть event onmounted, но это не так — только petite-vue может обработать/послать это событие. Точно также есть событие @unmounted, которое срабатывает когда элемент удаляется из дерева.

Заметим, что данный элемент имеет id="root". Это не случайно — когда придёт React приложение, оно перетрёт всё что мы пишем на petite-vue и не возникнет никаких артефактов.

Давай посмотрим на получившийся график производительности

v-effect

Конечно, стоит согласиться, что в предыдущем примере мы получили очень мощный фундамент для progressive приложения, но я всё-таки не соглашусь. Первый вопрос, который покажет несостоятельность системы — я использую колбек @mounted, а что если он уже будет занят???

Давай попробуем придумать решение с помощью ещё одного эксклюзива Petite-vue v-effect. Это очень мощная директива, которая позволяет исполнять реактивные скрипты.

Если ты знаешь про useEffect из Reactа, то v-effect это такой useEffect на минималках, который сам определяет массив зависимостей и прогоняет инлайн скрипт при изменении какой-то зависимости. Давай посмотрим пример из документации:

<div v-scope="{ count: 0 }">   <div v-effect="$el.textContent = count"></div>   <button @click="count++">++</button> </div>

Тут массив зависимостей (входные/внешние переменные) определяется как [ $data.count ] и при каждом обновлении этой переменной скриптик $el.textContent = count будет снова и снова исполняться.

Давай теперь применим этот инструмент для нашего прогрессивного примера:

<div   id="root"   v-scope="{num: 0}"   v-cloak   v-effect="tryToLoadSPA()"   @mounted="console.log('hi')" >   {{ num }} <button @click="num++">+</button> </div>

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

Можно удостоверится что этот пример рабочий на странице.

Кастомные директивы

Ну а что если я хочу использовать и v-effect и @mounted??? Неужели всё-таки придётся писать костыльно? А что если я хочу автоматически собирать приложение, а не прописывать каждый раз entrypointы вручную прямо в Джаваскрипте?

В таком случае можно сделать кастомную директиву. В обычном Vue это тоже возможно, но там, очевидно, другой набор возможностей :). В общем давай попробуем сделать хоть что-то — потом разберёмся.

<title> Petite Vue Progressive Enchancement </title> <style>   [v-cloak] {display:none}   body { background: #fff!important } </style>  <script type="module">   import { createApp } from "https://unpkg.com/petite-vue?module"    const SCRIPTS = [     "https://ahfarmer.github.io/calculator/static/js/main.b319222a.js",   ]   const CSS = [     "https://ahfarmer.github.io/calculator/static/css/main.b51b4a8b.css",   ]    function embedScript(mount, src) {     const tag = document.createElement("script")     tag.src = src     return mount.appendChild(tag)   }   function embedCSS(mount, src) {     const tag = document.createElement("link")     tag.href = src     tag.rel = "stylesheet"     return mount.appendChild(tag)   }   function tryToLoadSPA() {     setTimeout(loadSPA, 1000)   }   function loadSPA() {     const mount = document.getElementsByTagName("head")[0]     SCRIPTS.map(src => embedScript(mount, src))     CSS.map(src => embedCSS(mount, src))   }    function App() {     return { num: 0 }   }    const progressiveDirective = (ctx) => {     tryToLoadSPA()   }    createApp({ App, tryToLoadSPA })     .directive('progressive', progressiveDirective)     .mount() </script>  <body>   <div id="root" v-scope="App()" v-cloak v-progressive>     {{ num }} <button @click="num++">+</button>   </div> </body>

Вот, мы заменили v-effect и @mounted на v-progressive. Это директива, которую мы добавили вот так:

const progressiveDirective = (ctx) => {   tryToLoadSPA() }  createApp({ App, tryToLoadSPA })   .directive('progressive', progressiveDirective)   .mount()

У нас очень простой случай — нам нужно исполнить что-то на mountе элемента, к которому привязана директива, поэтому мы не используем ctx контекст доступный директиве, а просто вызываем там необходимую функцию.

Но в ctx передаётся куча полезностей (из доки):

const myDirective = (ctx) => {   // элемент, на который привязана директива   ctx.el    // необработанное значение, передающееся в директиву   // для v-my-dir="x" это будет "x"   ctx.exp    // дополнительный аргумент через ":"   // v-my-dir:foo -> "foo"   ctx.arg    // массив модификаторов   // v-my-dir.mod -> { mod: true }   ctx.modifiers    // можно вычислить выражение ctx.exp   ctx.get()    // можно вычислить произвольное выражение   ctx.get(`${ctx.exp} + 10`)    ctx.effect(() => {     // это аналог v-effect, будет вызыватся при изменении значения ctx.get()     console.log(ctx.get())   })    return () => {     // колбек, который вызывается при unmountе элемента   } }  // добавляем директиву к глобальной области petite-vue createApp().directive('my-dir', myDirective).mount()

Можно усовершенствовать наш пример если не хардкодить константы SCRIPTS и CSS, а передавать внутрь директивы v-progressive массив entrypointов и автоматически всё парсить и подгружать (cssки отдельно от jsа). Но это очень много кода, который не хочется просто вставлять сюда — идея понятна :).

Кастомные ограничители

Вроде разобрались со всем что касается расширяемости, теперь наметилась ещё одна проблема: я использую mustache/handlebars/jinja/ещё что-то где уже есть ограничители {{ и }}.

В таком случае можно изменить ограничители petite-vue, передав…

createApp({   $delimiters: ['$<', '>$'] }).mount()

На месте $< и >$ естественно может быть что угодно. Лучше чтобы длина была покороче и в шаблоне такие символы встречались не слишком часто (нужно подумать о скорости поиска в строке 🙂 ).

Заключение.min.js

Не хочу что-то тут писать, потому что рассказал не про всё что есть в petite-vue. Однако рассказал обо всех уникальных (отличительных от Vue) особенностях, которые позволяют строиться прямо поверх DOMа быстрее и эффективнее. В общем нормально…

Мои примеры можно посмотреть тут.


ссылка на оригинал статьи https://habr.com/ru/articles/594837/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *