Немногое 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()));

Код поддержки

Код поддержки содержится в Mixinobject:

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]. Это будет объект со свойствами, представляющими значения атрибутов, которые вы действительно хотите использовать. Путем «реструктуризации» этого как второй части attributesobject значения по умолчанию будут отменены и могут быть добавлены дополнительные атрибуты.

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 функции:

  1. Обработчики событий прикрепляются с помощью ${el.id}.
  2. Внутри шаблонных литералов встроенные строки, такие как reduce, могут использоваться для генерации вложенного HTML для субкомпонентов путем объединения результатов render субкомпонента, который возвращает HTML в виде строки. Он почти такой же мощный, как JSX.
  3. 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>`;

Имея приведенный выше код для ваших компонентов, вы можете:

  1. Установите их в существующие элементы HTML, например ToDoList({title:"My ToDos"},document.getElementById("mytodos").
  2. Или создайте их напрямую и добавьте к другим элементам, например const myapp = document.getElementById("myapp"); myapp.appendChild(ToDoList({title:"My ToDos"}));

Вы можете запустить полный пример на JSFiddle.

Это руководство основано на пограничной версии лицензированной MIT микропрограммы tlx, которая предоставляет гораздо более существенные Mixins для сворачивания шаблонного кода, поддержки механизма полностраничных шаблонов и расширенной функциональности компонентов. Не стесняйтесь получить копию и оставить отзыв. Но тогда, возможно, вам не нужен фреймворк! Просто возьмите код из JSFiddle и улучшите его. В любом случае, если вы узнали что-то новое в этом уроке, хлопните ему в ладоши.