В первой части этого руководства мы настраиваем аутентификацию для нашего приложения. Мы сделали это без зависимостей и только с двумя файлами - index.html и main.js. Во второй части руководства мы создадим приложение, которое представляет собой простое приложение для создания заметок с содержимым, хранящимся в IPFS. Поскольку мы используем API-интерфейсы SimpleID, хранение содержимого IPFS осуществляется с помощью фантастической службы, предоставляемой Pinata.

Я думаю, что лучше всего начать с html. У нас уже есть готовый раздел для содержания нашего приложения. Найдите раздел, который выглядит так:

<div style="display: none" id="root-app">
  <button onclick="signOut()">Sign Out</button>
  <h1>This is the app</h1>
</div>

Кнопка выхода может остаться, но нам нужно удалить This is the app текст и заменить его тем, что будет фактическим содержимым нашего приложения. Я уже думаю о нескольких вещах:

  • Как переключаться между ведением заметок и просмотром всех существующих заметок?
  • Как будут отображаться все существующие заметки?

Давайте настроим наш html-контент для поддержки того, что, надеюсь, будет достойными ответами на эти вопросы:

<div style="display: none" id="root-app">
  <button onclick="signOut()">Sign Out</button>
  <div id="notes-collection">
    <ul id="notes-collection-items"></ul>
  </div>
  <div style="display: none;" id="single-note">
  </div>
</div>

Когда мы отображаем содержимое приложения, мы не хотим отображать одновременно заметки и коллекцию заметок. Мы могли бы, но я думаю, было бы проще показать коллекцию заметок пользователя, а затем при нажатии кнопки или что-то в этом роде будет отображаться раздел создания заметок. Итак, мы начинаем с notes-collection визуализированных и single-note скрытых.

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

Теперь, когда у нас есть скелет содержимого нашего приложения, давайте вернемся к нашему файлу JavaScript и начнем с получения файла индекса коллекции заметок. Этот файл будет содержать массив идентификаторов, заголовков и дат для всех наших заметок. В main.js нам сначала нужно добавить новую глобальную переменную в начало файла. Под переменной loading добавьте let notesCollection = [];. Теперь добавьте новую функцию с именем fetchCollection():

async function fetchCollection() {
  const url = "https://api.simpleid.xyz/fetchContent";
  const username = JSON.parse(localStorage.getItem('user-session')).username;
  const notesId = "notesIndex";
  const data = `username=${username}&devId=${config.devId}&devSuppliedIdentifier=${notesId}&development=true`;
  const pinnedContent = await postToApi(data, url);
  if(!pinnedContent.includes("ERROR")) {
    notesCollection = JSON.parse(pinnedContent);
  } else {
    notesCollection = [];
  }
}

Если вы помните, мы поступили умно и создали функцию многократного использования для публикации в SimpleID API, которая всегда возвращает обещание. Здесь мы просто указываем URL для публикации и данные, которые нужно включить. Отправляемые нами данные должны включать идентификатор, который будет использоваться для поиска нужного файла в сети IPFS. Это notesId. Вы можете спросить, как этот идентификатор можно использовать с несколькими пользователями, при этом возвращая правильный контент для каждого пользователя. Именно здесь на помощь приходит переменная username. В data, который мы публикуем, username должно быть вашим именем пользователя, выполнившего вход. Помните, что это хранится в localStorage, поэтому мы можем легко получить его и включить.

Когда мы делаем наш запрос к API, мы должны учитывать любую ошибку, которая может возникнуть, в том числе отсутствие ранее сохраненного контента. Итак, мы проверяем наличие ошибки и, если ее нет, устанавливаем наш массив notesCollection с ответом. В противном случае мы устанавливаем notesCollection в пустой массив.

Но этого недостаточно. Нам нужно пройти через этот массив и добавить элемент списка для каждой заметки между нашим notes-collection-items ul. Мы можем это сделать, если не будет ошибок. Итак, в этом блоке if добавьте это ниже notesCollection = JSON.parse(pinnedContent), давайте добавим вызов функции с именем renderCollection(). Затем мы можем создать функцию renderCollection следующим образом:

function renderCollection() {
  let list = document.getElementById('notes-collection-items');
  list.innerHTML = "";
  for(const note of notesCollection) {
    let item = document.createElement('li');
    item.appendChild(document.createTextNode(note.title));
    item.appendChild(document.createTextNode(note.date));
    item.setAttribute("id", note.id);
    list.appendChild(item);
  }
}

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

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

//in signin function
...
if(!userData.includes('ERROR')) {
  console.log(userData);
  let userSession = JSON.parse(userData);
  userSession.username = username;
  localStorage.setItem('user-session', JSON.stringify(userSession));
  loading = false;
  loggedIn = true;
  pageLoad();
  fetchCollection(); //call it here for sign in
} else {
  loading = false;
  loggedIn = false;
  pageLoad();
  console.log("Error");
}
//in sign up function
...
const keyData = `username=${username}&password=${password}&profile=${uriEncodedProfile}&url=https%3A%2F%2Fthisisnew.com&development=true&devId=imanewdeveloper`;
const userData = await postToApi(keyData, urlAppKeys);
if(!userData.includes('ERROR')) {
  console.log(userData);
  let userSession = JSON.parse(userData);
  userSession.username = username;
  localStorage.setItem('user-session', JSON.stringify(userSession));
  loading = false;
  loggedIn = true;
  pageLoad();
  fetchCollection(); //call it here
} else {
  loading = false;
  loggedIn = false;
  pageLoad();
  console.log("Error");
}

Нам также необходимо вызывать fetchCollection при каждой загрузке страницы, потому что пользователь может обновить экран. Мы должны делать это только в том случае, если пользователь вошел в систему. Итак, прямо под вызовом pageLoad() вверху main.js добавьте следующее:

pageLoad();
if(localStorage.getItem('user-session')) {
  fetchCollection();
}

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

<div style="display: none" id="root-app">
  <button onclick="signOut()">Sign Out</button>
  <div id="notes-collection">
    <div>
      <h3 id="notes-total"></h3>
      <button onclick="newNote()" id="create-note">New Note</button>
    </div>
    <ul id="notes-collection-items"></ul>
  </div>
  <div style="display: none;" id="single-note">
    <button onclick="closeNote()">Close</button>
    <ul id="toolbar">
      <li>Bold</li>
      <li>Italics</li>
      <li>Underline</li>
      <li class="right">Save</li>
    </ul>
    <div>
      <input type="text" id="title" placeholder="note title" />
    </div>
    <div id="note" contenteditable="true"></div>
  </div>
</div>

Здесь мы кое-что сделали. Мы добавили кнопку, позволяющую создавать новые заметки, и связали с ней обработчик событий, который вызывает newNote(). Мы настроим эту функцию через минуту. Мы добавили кнопку, чтобы закрыть экран заметки, когда он откроется. Он вызывает функцию, которая ... ну ... закроет экран заметок. Мы также добавили панель инструментов в наш раздел с единственными заметками. Это будет простое приложение для создания заметок с простыми функциями - полужирным шрифтом, курсивом, подчеркиванием. У нас также есть кнопка сохранения на нашей панели инструментов, которую мы подключим позже. Нам нужно дать нашим заметкам заголовок, поэтому мы также добавили поле для ввода заголовка. Наконец, мы добавили contenteditable div для хранения нашей заметки и дали ему id, на который мы можем ссылаться позже.

Что теперь? Что ж, я думаю, мы должны сделать так, чтобы кнопка New Note отображала нашу новую заметку, верно? Давайте сделаем это в нашем main.js файле. Откройте его и добавьте функцию с именем newNote():

function newNote() {
  document.getElementById('notes-collection').style.display = "none";
  document.getElementById('single-note').style.display = "block";
  document.getElementById('note-title').value = "";
  document.getElementById('note').innerHTML = "";
}

В функции newNote() мы скрываем коллекцию заметок, потому что нам не нужно это видеть при написании новой заметки. Затем мы показываем экран создания заметок. Мы также устанавливаем заголовок заметки и содержимое заметки в пустую строку, потому что каждый раз, когда нажимается New Note, это должна быть новая заметка без предварительно заполненного содержимого.

Пока мы здесь, давайте настроим и эту closeNote() функцию:

function closeNote() {
  document.getElementById('notes-collection').style.display = "block";
  document.getElementById('single-note').style.display = "none";
}

Это довольно просто. Мы скрываем экран создания заметок и снова показываем экран сбора заметок. Следует отметить (посмотрите, что я там сделал?): Заметки сохраняются только тогда, когда пользователь нажимает кнопку «Сохранить заметку». Мы еще не настроили это, но будем. Это означает, что когда пользователь нажимает кнопку закрытия, ничего не сохраняется. Вы можете подключить его по-другому.

Я думаю, что следующие две вещи, которые мы должны сделать, - это обработать изменения содержимого в нашем разделе заметок (когда пользователь вводит текст, мы хотим отслеживать это) и обработать кнопки панели инструментов. Начнем с отслеживания изменений содержания. Для этого мы собираемся добавить прослушиватель событий в наш main.js файл. Вы можете сделать это над первой функцией, но под вызовом pageLoad() следующим образом:

let editable = document.getElementById('note');
editable.addEventListener('input', function() {
  console.log('You are typing');
});

Сохраните это, а затем, войдя в свое приложение, создайте новую заметку и введите что-нибудь. Откройте консоль разработчика, и вы должны увидеть распечатанный You are typing. Это классно! Но это бесполезно. Нам нужно отслеживать фактический контент, чтобы, когда придет время сохранять, у нас была переменная, которую можно было бы использовать. Для этого давайте создадим новую глобальную переменную вверху вашего main.js файла прямо под переменной notesCollection:

let noteContent = "";

Теперь в прослушивателе событий мы можем удалить console.log и заменить его внутренним HTML-кодом contenteditable div, который представляет нашу заметку и установить равным переменной noteContent. Должно получиться так:

let editable = document.getElementById('note');
editable.addEventListener('input', function() {
  noteContent = editable.innerHTML;
  console.log(noteContent);
});

Я добавил console.log ниже, чтобы убедиться, что все работает. Сделайте это, если хотите, и протестируйте его, создав новую заметку, набрав и открыв консоль разработчика. Если вы это сделаете, вы должны увидеть html-представление того, что вы вводите, напечатанное в консоли.

Теперь давайте отформатируем этот текст! Для нашей панели инструментов нам нужно добавить обработчик событий onclick для каждого элемента. При нажатии каждой кнопки панели инструментов должно применяться указанное форматирование. К счастью, для этого есть встроенный JavaScript. Давайте попробуем:

<ul id="toolbar">
  <li onmousedown="event.preventDefault()" onclick="document.execCommand('bold', false, null)">Bold</li>
  <li onmousedown="event.preventDefault()" onclick="document.execCommand('italic', false, null)">Italics</li>
  <li onmousedown="event.preventDefault()" onclick="document.execCommand('underline', false, null)">Underline</li>
  <li class="right">Save Note</li>
</ul>

Мы используем встроенную функцию execCommand в JavaScript, чтобы создать очень простой редактор WYSIWYG для нашего приложения для создания заметок. Довольно круто, да? Вы заметите обработчик событий onmousedown. Это происходит потому, что когда вы нажимаете кнопки на панели инструментов, фокус снимается с текста, который необходимо отформатировать, поэтому создается впечатление, что кнопки не работают. Мы предотвращаем это с помощью этого обработчика событий.

Хорошо, все работает. Теперь нам нужно подключить нашу кнопку «Сохранить заметку». Давайте подумаем, что для этого нужно:

  • Создайте объект, который содержит заголовок заметки, содержимое заметки и идентификатор заметки (который мы можем довольно легко сгенерировать)
  • Добавьте новую заметку в существующий массив notesCollection
  • Сохраните индексный файл, содержащий весь массив notesCollection
  • Сохраните сам файл заметки, который включает в себя полное содержание

Начнем с добавления обработчика событий к нашей кнопке Сохранить заметку на панели инструментов:

<ul id="toolbar">
  <li onmousedown="event.preventDefault()" onclick="document.execCommand('bold', false, null)">Bold</li>
  <li onmousedown="event.preventDefault()" onclick="document.execCommand('italic', false, null)">Italics</li>
  <li onmousedown="event.preventDefault()" onclick="document.execCommand('underline', false, null)">Underline</li>
  <li onclick="saveNote()" class="right">Save Note</li>
</ul>

Хорошо, теперь давайте создадим эту функцию в нашем main.js файле:

async function saveNote() {
  let note = {
    id: Date.now(),
    title: document.getElementById('note-title').value === "" ? "Untitled" : document.getElementById('note-title').value
  }
  console.log(note);
  notesCollection.push(note);
  console.log(notesCollection);
}

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

Следующее, что нам нужно сделать, это сохранить индексный файл в IPFS. Правильно, вы все это время ждали. Давайте сохраним контент!

Чтобы проверить это, давайте обновим нашу saveNote() функцию:

async function saveNote() {
  let note = {
    id: Date.now(),
    title: document.getElementById('note-title').value === "" ? "Untitled" : document.getElementById('note-title').value
  }
  notesCollection.push(note);
  const pinURL = "https://api.simpleid.xyz/pinContent";
  const username = JSON.parse(localStorage.getItem('user-session')).username;
  const identifier = "notesIndex";
  const content = encodeURIComponent(JSON.stringify(notesCollection));
  const data = `username=${username}&devId=${config.devId}&devSuppliedIdentifier=${identifier}&contentToPin=${content}&development=true`;
  const postedContent = await postToApi(data, pinURL);
  if(!postedContent.includes("ERROR")) {
    console.log(postedContent);
  } else {
    console.log("Error pinning content");
    console.log(postedContent);
  }
}

Здесь мы указываем конечную точку API, получаем имя пользователя вошедшего в систему, кодируем контент URIE, а затем создаем строку данных, совместимую с from-data. Все это отправляется функции postToApi(). Мы проверяем ответ от этого вызова API и ищем ошибку. Если ошибок нет, мы готовы двигаться дальше. Если есть, мы записываем его в консоль.

Теперь это только половина того, что нам нужно сделать. Также нам нужно сохранить отдельную заметку с ее содержимым. Так что давай сделаем это. Давайте добавим внутрь блока if(!postedContent.indludes("ERROR"):

note.content = noteContent;
const noteIdentifier = JSON.stringify(note.id);
const saveNoteContent = encodeURIComponent(JSON.stringify(note));
const noteData = `username=${username}&devId=${config.devId}&devSuppliedIdentifier=${noteIdentifier}&contentToPin=${saveNoteContent}&development=true`;
const postedNote = await postToApi(noteData, pinURL);
console.log(postedNote);
if(!postedNote.includes("ERROR")) {
  document.getElementById('notes-collection').style.display = "block";
  document.getElementById('single-note').style.display = "none";
  renderCollection();
} else {
  console.log("Error posting note content");
  console.log(postedNote);
}

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

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

function renderCollection() {
  let list = document.getElementById('notes-collection-items');
  list.innerHTML = "";
  for(const note of notesCollection) {
    let item = document.createElement('li');
    item.appendChild(document.createTextNode(note.title));
    item.setAttribute("id", note.id);
    item.onclick = () => loadNote(note.id);
    item.style.cursor = "pointer";
    list.appendChild(item);
  }
}

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

Давайте сейчас уберем эту loadNote функцию:

async function loadNote(id) {
  console.log(id);
}

Давайте проверим, прежде чем двигаться дальше. Если вы сохраните и обновите свой браузер, вы должны увидеть (если вы сохранили какие-либо заметки) список сохраненных заметок появится в виде неупорядоченного списка. Щелкните заголовок одной из заметок и проверьте консоль разработчика. Вы должны увидеть распечатанный идентификатор заметки.

Мы почти закончили! Все, что нам нужно сделать сейчас, это загрузить заметку с ее фактическим содержанием, чтобы пользователь мог просматривать или вносить изменения по мере необходимости. Давайте закончим, сделав это в нашей функции loadNote():

async function loadNote(id) {
  const url = "https://api.simpleid.xyz/fetchContent";
  const username = JSON.parse(localStorage.getItem('user-session')).username;
  const noteId = JSON.stringify(id);
  const data = `username=${username}&devId=${config.devId}&devSuppliedIdentifier=${noteId}&development=true`;
  document.getElementById('notes-collection').style.display = "none";
  document.getElementById('single-note').style.display = "block";
  const pinnedContent = await postToApi(data, url);
  console.log(pinnedContent);
  if(!pinnedContent.includes("ERROR")) {
    noteContent = JSON.parse(pinnedContent).content;
    document.getElementById('note-title').value = JSON.parse(pinnedContent).title;
    document.getElementById('note').innerHTML = noteContent;
  } else {
    console.log("Couldn't load note")
  }
}

Идите и проверьте это сейчас. Создайте заметку, сохраните ее, откройте. Вроде все работает, но мы забыли об одном. Если бы мы отредактировали существующую заметку, она не обновила бы указанную заметку, а создала бы новую заметку. Давай исправим это.

Во-первых, нам нужно убедиться, что мы установили наш индивидуальный идентификатор заметки в глобальную переменную, чтобы мы могли использовать ее снова при попытке сохранить обновленную заметку. Итак, в верхней части вашего main.js файла добавьте эту глобальную переменную ниже noteContent = "":

let singleNoteId = null;

Затем в своей функции loadNote добавьте это в начало:

singleNoteId = id;

Теперь перейдите к своей функции saveNote() и обновите верхнюю часть этой функции следующим образом:

let note = {
  id: singleNoteId ? singleNoteId : Date.now(),
  title: document.getElementById('note-title').value === "" ? "Untitled" : document.getElementById('note-title').value
}
let index = await notesCollection.map((x) => {return x.id }).indexOf(note.id);
if(index < 0) {
  //This is a new note
  notesCollection.push(note);
} else if(index > -1) {
  //The note exists and needs to be updated
  notesCollection[index] = note;
} else {
  console.log("Error with note index")
}

Теперь это должно позволить нам создавать новые заметки И обновлять существующие. Давай попробуем и убедимся. Откройте существующую заметку, отредактируйте ее, затем нажмите кнопку «Сохранить».

Ты сделал это! Вы только что создали приложение с нулевой зависимостью, которое позволяет вам делать следующее:

  • зарегистрироваться
  • войти
  • выход
  • создавать заметки
  • форматировать заметки
  • сохранить в IPFS
  • получить из IPFS

В дополнение к тому, что это приложение с нулевой зависимостью, полный размер приложения, разделенного, не минимизированного и разархивированного, составляет чуть менее 20 КБ. Это здорово.

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

С помощью этого приложения вы можете сделать гораздо больше, и ваша домашняя работа, если вы решите это сделать, - это попытаться самостоятельно добавить следующее:

  • Дополнительные параметры форматирования (новичок)
  • Удалить заметки (промежуточный)
  • Ссылка для общего доступа, защищенная паролем (дополнительно)

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

Спасибо, что вы внимательно изучили обе части этого урока. Если вам это понравилось и вы хотите использовать простые инструменты аутентификации и простые инструменты IPFS, обратите внимание на SimpleID. Нам бы очень хотелось увидеть, что вы можете построить.