Petite-vue

от автора

Привет, эта статья — «перевод документации и часто используемых примеров» для petite-vue + ещё немного приколов и одна интересная практика (с которой не всё так очевидно, как могло бы казаться).

Зачем нужен petite-vue?

Petite-vue это 6-ти килобайтное подмножество Vue, основной задачей которого, по словам Эвана, является Progressive Enchancement (как на пикче).

Petite-vue сильно похож на обычный Vue, в нём позаимствованы часто используемые директивы и есть возможность создавать кастомные. Синтаксис тоже напоминает стандартный вьюшный, но в силу легковесности фреймворка, довольно урезанный.

Очевидно, что petite-vue отлично получится использовать в местах, где бэкенд сам отдаёт htmlник, сгенерированный в ответ на запрос (хороший первый шаг для миграции фронта с php на что-то джаваскриптовое-SPAшное). Понять это можно из того, что фреймворк надо подключать с CDNа, и он не поставляется как сбилженный npm пакет, взаимодействие планируется через инлайн-скрипты.

Простые примеры

Пока мы ещё не начали спорить что же лучше, производительнее и выгоднее для архитектуры (ну ты же уже вроде научился не спорить о языках программирования 🙂 ), давай посмотрим что тут можно поделать:

<script src="https://unpkg.com/petite-vue" defer init></script> <title>Petite Vue button</title>  <body v-scope="{num: 0}">   <button @click="num++">     +   </button>   {{ num }} </body>

Удивительно, да? Этого кода (8 sloc) вполне достаточно чтобы что-то работало.

Смотри, первая строчка

<script src="https://unpkg.com/petite-vue" defer init></script>

просто подгружает на страницу библиотеку (атрибут defer позволяет сделать это после полной загрузки htmlя, а init сообщает petite-vue, что он может начинать парсить DOM и аккуратно его менять).

Вторая строчка — просто тайтл, думаю можно скипать ;).

А вот дальше начинаются приколы: в тег body я зачем-то добавил v-scope и присвоил ему какое-то странное значение.

<body v-scope="{num: 0}">   ... </body>

V-scope это атририбут, который указывает Vue на этапе инициализации (да-да как раз тот init), что именно нужно инициализировать как новое приложение. Таких v-scopeов может быть много на странице и все они будут работать независимо друг от друга (ну или почти — расскажу через одну главу). Параметр, который мы передаём такой директивой — начальный стейт (состояние) приложения. Тут видно, что стейт содержит только поле num и по дефолту его значение равно 0. В стейт можно класть что угодно: функции, массивы и объекты произвольной вложенности — как обычно. Если v-scope не присвоить значение — это тоже самое, что написать:

<body v-scope="undefined">   ... </body>

Окей, с v-scopeом вроде разобрались, посмотрим, что там дальше:

<button @click="num++">   + </button>

Смотри, button это известно что, давай поймём что же это за @click. Если ты уже знаком с Vue, можешь смело скипать следующий абзац 🙂 .

Во Vue eventы добавляются к элементам вот так: @<название eventа>. Для кнопки нам тут очевидно нужен @click. Основной прикол этих директив — возможность тут же сделать допустим @click.prevent чтобы сделать preventDefault eventу или @keyup.enter, чтобы событие срабатывало только для клавиши «enter». В качестве значения передаётся тело функции, которая будет исполнена, когда произойдёт необходимое событие, или название функции-хендлера как в обычном джаваскрипте (тоже принимает Event).

Здесь мы в качестве тела функции указали "num++", благодаря вью, в тело функции пробросится наш стейт и она вызовется с with($data){} (так, кстати этот стейт называется внутри либы, и если ты когда-то писал на полноценном вью — ловишь сейчас флешбеки). В общем да, значение num увеличится на единицу 🙂 .

Теперь давай посмотрим на оставшуюся строку

{{ num }}

Эта строка подставляет в текущее место htmlя рассчитанное значение от того, что находится в скобках. Туда тоже пробрасывается $data, так что мы можем спокойно вывести num.

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

Todo приложение

Обычно, изучая джаваскрипт фреймворк ты создашь очередной туду-лист. Не хочу нарушать традиции 🙂 . Приложение такого минимального скейла позволяет понять, как писать код модульно так, чтобы его потом можно было поддерживать.

Закину сразу бойлерплейт на котором всё будет держаться:

<script src="https://unpkg.com/petite-vue" defer init></script> <title>Petite Vue todo</title>  <body v-scope="/* сейчас допишу */">   <input v-model="model.input"/>   <button @click="addTodo">Add Todo</button>   <template v-for="(todo, i) of todos" :key="i">     <div>       {{ todo }}     </div>   </template> </body>

Тут сразу же появилось очень много всего, хотя v-scope я пока по-честному так и не написал.

Из нового привлекает внимание строка

<input v-model="model.input"/>

Она рендерит input-элемент, который использует 2 way data binding с полем "model.input" в стейте. Простым языком, эта директива синхронизирует значение переменной $data.model.input со значением, написанным внутри inputа. (Тут я использовал "model.input", а не просто "input" только ради культуры кода, так как когда ты будешь делать сложную форму, моделей будет много и хорошо бы их хранить отдельным объектом).

Для кнопки я пока беру в долг завести функцию addTodo.

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

Ну а теперь давай я допишу какой же будет v-scope и ты поймёшь, почему есть другие способы его инициализировать:

<body v-scope="{todos: [], model: { input: '' }, addTodo(){ this.todos.push(this.model.input); this.model.input = ''}}">   ... </body>

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

Поэтому я провёл рефакторинг и теперь файл выглядит вот так:

<script src="https://unpkg.com/petite-vue" defer init></script> <title>Petite Vue todo</title> <script>   function generateConfig() {     return {       todos: [],       model: { input: '' },       addTodo() {         this.todos.push(this.model.input)         this.model.input = ''       },     }   } </script> <body v-scope="generateConfig()">   <input v-model="model.input"/>   <button @click="addTodo">Add Todo</button>   <template v-for="todo of todos">     <div>       {{ todo }}     </div>   </template> </body>

Мысль очень проста — делаем глобальную функцию по генерации конфига и вызываем её там, где от нас требуют предъявить стейт для инициализации.

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

<script src="https://unpkg.com/petite-vue" defer init></script> <title>Petite Vue todo</title>  <body>   <div v-scope="App()"></div>    <template id="TodoTemplate">       <input v-model="checked" type="checkbox"></input>       <span         :style="checked && `text-decoration: line-through`"       >{{ text }}</span>   </template>   <script>     function Todo(props) {       return {         $template: "#TodoTemplate",         text: props,         checked: false,       }     }   </script>   <style></style>    <template id="AppTemplate">     <input v-model="model.input"/>     <button @click="addTodo">Add Todo</button>     <template v-for="todo of todos">       <div v-scope="Todo(todo)"></div>     </template>   </template>   <script>     function App() {       return {         $template: "#AppTemplate",         todos: [],         model: { input: '' },         addTodo() {           this.todos.push(this.model.input)           this.model.input = ''         },       }     }   </script>   <style></style>  </body>

Да, тут много чего произошло…

Во-первых, теперь я инициализирую petite-vue не на body, а на «рутовом» divе с конфигом App.

Во-вторых добавились какие-то templateы и сама структура поменялась (если вдруг знаешь как выглядят .vue файлы — наверное обо всём догадался).

В-третьих, как и обещал, компоненты усложнились внутри. Теперь компонент App рисует в цикле несколько самостоятельных компонентов Todo, которые имеют свой стейт, свои обработчики событий и даже входные параметры (т.н. пропсы props).

К сожалению, для достижения такой абстракции понадобилось добавить templateы и ссылки на них из стейта в виде

$template: "#AppTemplate"

Petite-vue использует этот селектор для поиска подходящего темплейта и использует его в качестве контента элемента, на который монтируется App.

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

$template: "<div> <h1> Hi </h1>  </div>"

Но это будет негативно сказываться на производительности (а это один из основных goalов petite-vue), потому что парсинг строки займёт больше времени, чем копирование существующих нод уже распарсенного DOMа (ну это уже из области очевидного).

Такой способ организации кода (как в «большом» Vue), мне кажется наиболее натуральным, разве что можно опустить рутовый темплейт, из-за того что он обычно используется только один раз. Не столько с целью оптимизации, сколько для более прозрачной структуры. Ну нормально.

ES modules подход

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

Это продолжение всё того же проекта с тудушками:

<title>Petite Vue todo</title>  <body>   <script type="module">     import { createApp, nextTick, reactive } from "https://unpkg.com/petite-vue?module"      const store = reactive({       allTodos: [],     })      function Todo(props) {       return {         $template: "#TodoTemplate",         text: props.todo.text,         id: 'todo-' + props.i,         checked: false,         onCheck(e) {           const globalTodo = store.allTodos             .find(item => item.id === props.i)            globalTodo.checked = e.target.checked         },       }     }      function App() {       return {         $template: "#AppTemplate",         model: { input: '' },         addTodo() {           const newTodo = this.model.input           if (!newTodo) return           this.store.allTodos.push({             text: newTodo,             id: this.store.allTodos.length,             checked: 0,           })           this.model.input = ''         },       }     }      createApp({ Todo, App, store }).mount()   </script>    <template id="TodoTemplate">       <input         v-model="checked"         type="checkbox"         @input="onCheck"         :id="id"       ></input>       <label         :for="id"         :style="checked && `text-decoration: line-through`"       >{{ text }}</label>   </template>    <template id="AppTemplate">     <input v-model="model.input"/>     <button @click="addTodo">Add Todo</button>     <template v-for="(todo, i) of store.allTodos" :key="i">       <div v-scope="Todo({ todo, i })"></div>     </template>     <div v-if="store.allTodos.reduce((a, b) => a + b.checked, 0) === store.allTodos.length">       All Completed!     </div>    </template>    <div v-scope="App()"></div> </body>

Смотри, тут большинство концептов я уже объяснил — давай разберём только новое. Как видно, мы больше не подключаем petite-vue скрипт-тегом, теперь мы делаем это в стиле ES6 import (script type="module"). Так мы импортируем функции: createApp, nextTick и reactive. Так как мы не initим скрипт, теперь это нужно делать вручную. Это делает строка

createApp({ Todo, App, store }).mount()

Внутрь этой функции мы передаём то, что я бы назвал VueGlobalObject (в исходном коде оно называется initialData, так что тут кажется норм отойти от канона 😉 ). Так как ты знаешь, что снаружи script type="module" все Referenceы к переменным будут ReferenceErrorами, VueGlobalObject это объект, with с которым выполняется в аргументе v-scope. Это как $data, но влияет на весь petite-vue контекст, позволяя уйти от глобальных переменных.

Ещё, как ты мог заметить, вызывается метод .mount(). Ему можно передать параметр, чтобы ограничить контекст только на потомство какого-то элемента (без аргумента = без ограничений) например createApp().mount('#navigation').

Второе значимое изменение — store. Он создаётся как

const store = reactive({   allTodos: [], })

Если не слишком вдаваться — это просто глобальная переменная, обёрнутая в reactive(), которая нормально работает с petite-vue.

Если вдаваться: попробуй создать вместо этого обычную глобальную переменную и передать её также в VueGlobalObject. После этого попробуй её обновить. Её отображаемое значение не изменится. Другими словами, reactive заставляет petite-vue перерисовывать все компоненты в которых есть вхождение данной глобальной переменной. Плюс там под капотом десятки оптимизаций, которые позволяют достигать «скорость!!! я скорость!!!».

Итоги

Очевидно, что эта либа позволяет очень круто проектировать небольшие проекты/что-то с упором на progressive enchancement. Из явных конкурентов есть разве что preact, но не знаю насколько имеет смысл их сравнивать. Это как сравнивать старших братьев — Vue и React. Могу точно сказать, что petite-vue как первый фреймворк, как переезд с php на «норм фронт» и в целом как машина для адской оптимизации ttl — офигенно крутое, интересное, новое решение. Зная поддержку экосистемы Vue и особенность данного проекта, можно предсказать, что какое-то развитие там будет.

Для меня ещё остаётся открытым выбор между двумя разными подходами (ES6 modules vs original Vue), их плюсы и минусы довольно неочевидны в рамках одного index.html. Так что было бы круто узнать мнение кого-то покруче меня про это 😉

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


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


Комментарии

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

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