Немногое ToDo: компоненты JavaScript без фреймворков
Вам просто нужна пара небольших компонентов и вам не нужны накладные расходы на фреймворк? Вам нужен фундамент для построения собственного фреймворка? Вы хотите мощь JSX без JSX?
Здесь мало что нужно сделать… используя мощь строковых литералов и деструктурирования, реструктуризации аргументов или значений по умолчанию плюс всего 25 строк кода поддержки, вы можете получить полностью реактивные автономные компоненты. Из этого туториала Вы узнаете, как это сделать.
Обратите внимание, что иллюстративный код подвержен HTML-инъекции, и что-то вроде little-cleaner
и / или DOMPurify
следует использовать для производственного кода.
После написания статьи читатель заметил, что remove
- это встроенная функция DOM, и наш компонент ToDo затеняет ее. Для обновления списка дел требуется функция, но, возможно, ее следует назвать по-другому. Использование виртуальных DOM во многих фреймворках помогает избежать проблем этого типа.
Если вам нравится этот стиль программирования и вы хотите протестировать мирко-фреймворк, обратите внимание на Компоненты HyperApp.
Компоненты ToDo
Давайте возьмем классический вариант использования ToDo в качестве примера, то есть напишем код для управления простым списком задач. Ниже представлен код компонента без комментариев, чтобы вы могли сначала сосредоточиться на коде. В следующем разделе Анализ кода объясняется мощное, но, возможно, неясное использование деструктуризации и реструктуризации. Посмотрите, сможете ли вы найти 6 используемых строк кода поддержки. Это те же 3 строки в каждом компоненте, ToDoView
и ToDoList
:
const ToDoView = function({title="",listid="",done=false}, el = document.createElement("todo")){ const attributes = {title,listid,done,...arguments[0]}; Object.assign(el,Mixins); const keys = Object.keys(attributes); keys.forEach(key => el.setAttribute(key,attributes[key],true)); el.id || (el.id = el.genId()); window[el.id] = el; el.render = (listid) => el.innerHTML = `<li id="${el.id}">${el.title} <input type="checkbox" ${(el.done ? "checked" : "")} onclick="(()=>${listid}.remove('${el.id}'))()"> </li>`; return el; } const ToDoList = function({title="",todos=[]}, el = document.createElement("todolist")){ const attributes = {title,todos,...arguments[0]}; Object.assign(el,Mixins); const keys = Object.keys(attributes); keys.forEach(key => el.setAttribute(key,attributes[key],true)); el.id || (el.id = el.genId()); window[el.id] = el; el.add = () => { // Add a task const title = prompt("ToDo Name"); if(title) { el.todos.push({title,id:el.genId()}); el.render(); } } el.remove = (id) => { // Remove a task const i = todos.findIndex((item) => item.id===id); if(i>=0) { el.todos.splice(i,1); el.render(); } } el.claim = () => { // Claim list el.setAttribute("title","Your List"); } el.render = () => el.innerHTML = `<p>${el.title}</p> <button onclick="${el.id}.add()">Add Task</button> <button onclick="${el.id}.claim()">Claim</button> <ul> ${todos.reduce((accum,todo) => accum += ToDoView(todo).render(el.id),"")} </ul>`; return el; } // Now use the components const list = ToDoList({title:"My List"}, document.getElementById("todos")); list.render();
Приведенный ниже текст и кнопки будут отображаться при наличии кода поддержки:
Вы нашли 6 строк кода?
Вот они, одинаковые по 3 в каждом компоненте:
// Adds stuff stored in the Mixins object to the component. Object.assign(el,Mixins); // Note the extra argument to setAttribute, defined in Mixins. // Enhances built-in setAttribute. keys.forEach(key => el.setAttribute(key,attributes[key],true)); // genId is defined in Mixins. Generates random ids. window.id = (el.id || (el.id = el.genId()));
Код поддержки
Код поддержки содержится в Mixin
object:
const Mixins = { // return attribute value getAttribute(name) { // if nothing on object itself, use built-in getAttribute if(typeof(this[name])==="undefined") { return HTMLElement.prototype.getAttribute.call(this,name); } return this[name]; }, // set attribute on object // extra argument lazy=true if change should not be reactive setAttribute(name,value,lazy) { // auxiliary test function const equal = (a,b) => { const typea = typeof(a), typeb = typeof(b); // same type and one of return typea === typeb && // identical (a===b || // same length, size, order (Array.isArray(a) && Array.isArray(b) && a.length===b.length && a.every((item,i) => b[i]===item)) || // all keys the same (a && b && typea==="object" && Object.keys(a).every(key => equal(a[key],b[key])) && Object.keys(b).every(key => equal(a[key],b[key])))); }; let type = typeof(value), oldvalue = this.getAttribute(name); const neq = !equal(oldvalue,value); // only make changes if new and old value are not equal // or change is non-reactive if(neq || lazy) { // remove object property and attribute if value is null if(value==null) { delete this[name]; this.removeAttribute(name); } // add to object if type is object if(type==="object") this[name] = value; // otherwise, use built-in setAttribute else HTMLElement.prototype.setAttribute.call(this,name,value); } // render if !lazy (i.e. is reactive), renderable, value changed if(!lazy && this.render && neq) this.render(); }, // not a great id generator, but good enough for demo genId() { return "id" + (Math.random()+"").substring(2); } }
Вышеупомянутое, очевидно, превышает 25 строк, но это с комментариями и настройками форматирования для руководства.
Анализируем код
Определения компонентов - это просто функции, которые принимают два аргумента: объект со свойствами, которые предоставляют значения атрибутов, и целевой HTMLElement, в котором будет отображаться компонент.
const ToDoList = function(attributes,el){...
А теперь первая часть магии разрушения! Вы можете предоставить значения по умолчанию для свойств объекта, разрушив спецификацию аргумента в определении функции. В нашем случае по умолчанию title
будет пустой строкой, а задачи todo todos
- пустым массивом.
const ToDoList = function({title="",todos=[]},el){...
По причинам, которые станут очевидными позже, также следует предоставить целевой элемент по умолчанию. Для этой демонстрации вы можете использовать любое имя тега. Если вы собираетесь расширить возможности Mixins
, вы можете использовать специальное имя тега.
const ToDoList = function({title="",todos=[]}, el = document.createElement("todos")){...
Первые шесть строк компонента обеспечивают большую часть остальных возможностей основного компонента.
Сначала создается объект атрибута с использованием значений по умолчанию и анонимно деструктурированной копии первого аргумента, предоставленного во время выполнения, …arguments[0]
. Это будет объект со свойствами, представляющими значения атрибутов, которые вы действительно хотите использовать. Путем «реструктуризации» этого как второй части attributes
object значения по умолчанию будут отменены и могут быть добавлены дополнительные атрибуты.
const ToDoList = function({title="",todos=[]}, el = document.createElement("todolist")){ const attributes = {title,todos,...arguments[0]};
Во-вторых, функции, определенные в Mixins
, копируются в HTMLElement для улучшения функциональности. В нашем случае это улучшает setAttribute
и getAttribute
при добавлении genId()
. На самом деле с этим можно было бы сделать гораздо больше. Подробнее об этом после шагов три и четыре.
const ToDoList = function({title="",todos=[]}, el = document.createElement("todolist")){ const attributes = {title,todos,...arguments[0]}; Object.assign(el,Mixins);
В-третьих, атрибуты назначаются HTMLElement.
const ToDoList = function({title="",todos=[]}, el = document.createElement("todolist")){ const attributes = {title,todos,...arguments[0]}; Object.assign(el,Mixins); const keys = Object.keys(attributes); keys.forEach(key => el.setAttribute(key,attributes[key],true));
Четвертый шаг гарантирует, что все экземпляры компонентов имеют уникальные идентификаторы и доступны из глобальной области видимости. Доступ к глобальной области требуется для поддержки обработчиков событий пользовательского интерфейса.
const ToDoList = function({title="",todos=[]}, el = document.createElement("todolist")){ const attributes = {title,todos,...arguments[0]}; Object.assign(el,Mixins); const keys = Object.keys(attributes); keys.forEach(key => el.setAttribute(key,attributes[key],true)); el.id || (el.id = el.genId()); window[el.id] = el;
Поскольку шаги третий и четвертый являются общими для всех компонентов, мы могли бы добавить init
функцию к Mixins
, которая будет принимать attributes
в качестве аргумента и выполнять оба шага. В этом руководстве код оставлен в каждом компоненте для обеспечения прозрачности.
За исключением последней render
функции, остальной код компонента фактически реализует функциональность компонента и будет сильно различаться по своему характеру. В этом примере обратите внимание на вызовы render
для отображения управления после изменений, не связанных с setAttribute
. Более сложная реализация могла бы улучшить начальную установку атрибутов на третьем шаге, чтобы было открыто Proxies
, которое автоматически повторно визуализирует компонент.
el.add = () => { // Add a task const title = prompt("ToDo Name"); if(title) { el.todos.push({title,id:el.genId()}); el.render(); } } el.remove = (id) => { // Remove a task const i = todos.findIndex((item) => item.id===id); if(i>=0) { el.todos.splice(i,1); el.render(); } } el.claim = () => { // Claim list el.setAttribute("title","Your List"); }
По соглашению, последней функцией является render,
, которая обычно является литералом шаблона, предназначенным для форматирования значений атрибутов в HTML. Функция render
предназначена для возврата HMTL с использованием того же соглашения, что и React и ряд других фреймворков. Однако, в отличие от некоторых других фреймворков, внутри компонент фактически является узлом DOM, и его можно передавать, добавлять, удалять или манипулировать с помощью всех стандартных функций DOM.
Обратите внимание на следующие render
функции:
- Обработчики событий прикрепляются с помощью
${el.id}.
- Внутри шаблонных литералов встроенные строки, такие как
reduce
, могут использоваться для генерации вложенного HTML для субкомпонентов путем объединения результатовrender
субкомпонента, который возвращает HTML в виде строки. Он почти такой же мощный, как JSX. ToDoView
создается вызовом функцииreduce
внутри функцииToDoList's
render
и предоставляется сid
содержащего элемента, чтобы обработчики событий наToDoViews
могли взаимодействовать с содержащим списком.
ToDoList визуализации:
el.render = () => el.innerHTML = ` <p>${el.title}</p> <button onclick="${el.id}.add()">Add Task</button> <button onclick="${el.id}.claim()">Claim</button> <ul> ${todos.reduce((accum,todo) => accum += ToDoView(todo).render(el.id),"")} </ul>`;
ToDoView рендеринг:
el.render = (listid) => el.innerHTML = `<li id="${el.id}">${el.title} <input type="checkbox" ${(el.done ? "checked" : "")} onclick="(()=>${listid}.remove('${el.id}'))()"> </li>`;
Имея приведенный выше код для ваших компонентов, вы можете:
- Установите их в существующие элементы HTML, например
ToDoList({title:"My ToDos"},document.getElementById("mytodos").
- Или создайте их напрямую и добавьте к другим элементам, например
const myapp = document.getElementById("myapp"); myapp.appendChild(ToDoList({title:"My ToDos"}));
Вы можете запустить полный пример на JSFiddle.
Это руководство основано на пограничной версии лицензированной MIT микропрограммы tlx, которая предоставляет гораздо более существенные Mixins
для сворачивания шаблонного кода, поддержки механизма полностраничных шаблонов и расширенной функциональности компонентов. Не стесняйтесь получить копию и оставить отзыв. Но тогда, возможно, вам не нужен фреймворк! Просто возьмите код из JSFiddle и улучшите его. В любом случае, если вы узнали что-то новое в этом уроке, хлопните ему в ладоши.