FireEdit: Разработка редактора реального времени с JavaScript и Firebase

Разработка /
Разработка: FireEdit: Разработка редактора реального времени с JavaScript и Firebase
Я всегда восхищался веб-приложениями реального времени. Несколько лет назад я пробовал имитировать такое поведение в своём приложении многочисленными повторяющимися запросами к серверу. Это работало, но было очень неэффективно. Потом я узнал о веб-сокетах.

Я слышал, разработчики поговаривали о чем-то, называемом Firebase. А не так давно Эмме — это мой друг, коллега и просто хороший человек — понадобилась помощь с Firebase и мы решили освоить её вместе.

Мы не считаем себя экспертами, но на текущий момент отлично разбираемся в теме и можем вам показать как создать приложение с Firebase и JavaScript — а точнее, как создать текстовый редактор реального времени, который будет работать в вашем браузере.

Ссылка на конечный результат: FireEdit — пользуйтесь им, ставьте звёзды, форкайте и наслаждайтесь!

FireEdit

Совместная работа очень важна. К примеру, мы с Эммой очень часто обмениваемся кусочками кода (особенно часто при поиске багов). Однако, в чатах это делать неудобно — нет подсветки кода, неподходящие шрифты, нет возможности отредактировать код и т.п.

Для удобства я придумал редактор на базе Firebase, пока мы обучались работе с Firebase. Мы назвали наше приложение FireEdit.

Разработка: FireEdit: Разработка редактора реального времени с JavaScript и Firebase
Да, я знаю, что таких редакторов много, но этот мы сделали сами как первый опыт работы с Firebase, у него открытый исходный код и мы сейчас покажем, как мы его сделали!

Firebase

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

Также важно знать, что данные в базе данных Firebase хранятся в структурированном формате JSON.

Здесь вы можете почитать о том, как быстро начать работу с Firebase. Однако, здесь мы также рассмотрим основные моменты при работе с ней.

Одно из главных достоинств Firebase — это множество инструментов, упрощающих работу с аутентификацией пользователей (регистрация с email и паролем, а также через сторонних провайдеров), хранением файлов и синхронизацией в реальном времени.

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

Ace Editor


А теперь представьте текстовый редактор (типа Sublime Text), работающий в вашем браузере без установки дополнительных компонент и ПО.

Это и есть Ace Editor — текстовый редактор для браузера с подсветкой синтаксиса, темами оформления и даже сочетаниями клавиш от VIM/Emacs! Так же посмотрите демонстрационные версии Ace Editor.

В этой статье Wikipedia есть список всех подобных редакторов.

Создаём приложение

Во-первых, это будет веб-приложение. Поэтому нам нужен будет HTML, CSS и, конечно, JavaScript! Создадим файлы по такой иерархии:

├── css
│   └── style.css
├── index.html
└── js
    └── index.js

index.html это файл, который загрузит файлы css/style.css и js/index.js при его открытии в браузере.

Начнём с минимального шаблона HTML в index.html:

<!DOCTYPE html>
<html>
    <head>
        <meta http-equiv="content-type" content="text/html; charset=utf-8">
        <title>FireEdit</title>
        <link rel="stylesheet" href="css/style.css" type="text/css" media="screen" charset="utf-8">
    </head>
    <body>
        <h2>Welcome to FireEdit!</h2>
        <div id="editor"></div>
        
        <!-- Firebase -->
        <script src="https://www.gstatic.com/firebasejs/3.6.4/firebase.js"></script>
        
        <!-- jQuery -->
        <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script>
        
        <!-- Ace Editor—keep reading, and you'll see how we're going to use this -->
        <script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.2.6/ace.js" type="text/javascript" charset="utf-8"></script>
        
        <!-- Load the main JavaScript file from our app. -->
        <script type="text/javascript" src="js/index.js"></script>
    </body>
</html>

Сейчас мы не будем рассматривать работу с CSS (о стилях поговорим позже), а сфокусируемся на функционале (за это отвечает JavaScript). И в первую очередь создадим приложение Firebase.

Конфигурация Firebase

1. Создайте приложение Firebase

2. Загрузите сценарий Firebase в ваше приложение:
<script src="https://www.gstatic.com/firebasejs/3.6.4/firebase.js"></script>

В вышеуказанном файле HTML мы уже указали этот сценарий. При появлении новых версий Firebase вы должны заменить версию в строке адреса ( сейчас это 3.6.4 ).

3. Настройте Firebase (кликните по кнопке «Add Firebase to your web app» в секции Overview):
// Initialize Firebase
var config = {
    apiKey: "AI...kBY",
    authDomain: "... .firebaseapp.com",
    databaseURL: "https://... .firebaseio.com",
    storageBucket: "... .appspot.com",
    messagingSenderId: "9...6"
};
firebase.initializeApp(config);


Небольшое введение в использование Firebase с JavaScript

Представьте себе Firebase как большой объект, который хранит ваши данные. Мы спроектируем съему подобную этой (это просто пример организации данных):

{
    "root": {
        "editor_values": {
            "johnny+emma+article": {
                "content": "Hello world. This is how we started this very article. :D",
                "lang": "markdown",
                "queue": {...}
            }
        }
    }
}

Итак, как мы сможем работать с такой структурой из JavaScript? Сперва нам нужно вызвать корневой узел root, и метод firebase.database() как раз вернёт его:

// Get the database object
var db = firebase.database();

После того, как у нас появился корень, мы можем получать другие данные. Предположим, нам нужно содержимое редактора с id «johnny+emma+article»:

// We know what's the editor id
var editorId = "johnny+emma+article";

// Get the reference to the editor values
var editorValues = db.ref("editor_values");

// Get the entire editor object
editorValues.child(editorId).once("value", function (snapshot) {
    console.log(snapshot.value());
    /* {
        "content": "Hello world. This is how we started this very article. :D",
        "lang": "markdown",
        "queue": {...}
    } */
});

// Get the value of the `content` field only:
editorValues.child(editorId).child("content").once("value", function (snapshot) {
   console.log(snapshot.value());
   // "Hello world. This is how we started this very article. :D"
});

Методами set или update мы можем записывать данные в Firebase. Например, так:

// This is going to set the content in the editor to "hello world"
editorValues.child(editorId).update({
    content: "hello world"
});


Синхронизация данных с другими клиентами

При любом обновлении данных, это обновление автоматически распространяется на все клиенты, прослушивающие его. К примеру, когда я редактирую текст у себя, я хочу, чтобы Эмма увидела эти изменения.

Мы начали с простого элемента для лучшего понимания концепции.
В псевдокоде это будет выглядеть так:

[textarea]
 при изменении-> сохранить значение в Firebase

Firebase:
 - при изменении: обновить значение в textarea

Такое будет работать, но мы быстро поймём, что это неудобно — при каждом обновлении данных курсор будет улетать в конец textarea. Нам нужен настоящий редактор. Поэтому мы выбрали ACE editor. Инициализируем его:

var editor;
...
// Initialize the editor
editor = ace.edit("editor");

// Set the editor theme
// The getTheme() is returning a string
// which is the user's selected theme
editor.setTheme(getTheme());

После этого необходимо переопределить метод onchage. Вот что произойдёт при изменении текста:

// Get the reference to the editor id
var currentEditorValue = editorValues.child(editorId);

// Get the `queue` child (which looks like an array where we push update events)
var queueRef = currentEditorValue.child("queue");

// This boolean is going to be true only when the value is being set programmatically
// We don't want to end with an infinite cycle since ACE editor triggers the
// `change` event on programmatic changes (which, in fact, is a good thing)
var applyingDeltas = false;

// Listen for the `change` event
editor.on("change", function(e) {

    // In case the change is emitted by us, don't do anything
    // (see below, this boolean becomes `true` when we receive data from Firebase)
    if (applyingDeltas) {
        return;
    }

    // Set the content in the editor object
    // This is being used for new users, not for already-joined users.
    currentEditorValue.update({
        content: editor.getValue()
    });

    // Generate an id for the event in this format:
    //  <timestamp>:<random>
    // We use a random thingy just in case somebody is saving something EXACTLY
    // in the same moment
    queueRef.child(Date.now().toString() + ":" + Math.random().toString().slice(2)).set({
        // Store the data we get from ACE editor
        event: e,
        // Store the pseudo-user id
        by: uid
    }).catch(function(e) {
        // In case of errors, we want to see them in the console
        console.error(e)
    });
});

Теперь мы знаем, что сохранение в базе данных работает, мы можем добавить слежение за обновлением:

// Listen for updates in the queue
queueRef.on("child_added", function (ref) {

    // Get the timestamp
    var timestamp = ref.key.split(":")[0];

    // Do not apply changes from the past
    if (openPageTimestamp > timestamp) {
        return;
    }

    // Get the snapshot value
    var value = ref.val();
    
    // In case it's me who changed the value, I am
    // not interested to see twice what I'm writing.
    // So, if the update is made by me, it doesn't
    // make sense to apply the update
    if (value.by === uid) { return; }

    // We're going to apply the changes by somebody else in our editor
    //  1. We turn applyingDeltas on
    applyingDeltas = true;
    //  2. Update the editor value with the event data
    doc.applyDeltas([value.event]);
    //  3. Turn off the applyingDeltas
    applyingDeltas = false;
});

Помните, что событие change также содержит внутренние метаданные о том, что конкретно изменилось. И оно отлично работает с applyDeltas, который получает массив событийных объектов и применяет их к данным в редакторе без участия пользователя.

Это изображение лучше пояснит, что происходит на самом деле:

Разработка: FireEdit: Разработка редактора реального времени с JavaScript и Firebase
Настройка языка в редакторе

Установка языка довольно проста:

// Select the desired programming language you want to code in 
var $selectLang = $("#select-lang").change(function () {
    // Set the language in the Firebase object
    // This is a preference per editor
    currentEditorValue.update({
        lang: this.value
    });
    // Set the editor language
    editor.getSession().setMode("ace/mode/" + this.value);
});

...

// Somebody changed the lang. Hey, we have to update it in our editor too!
currentEditorValue.child("lang").on("value", function ® {
    var value = r.val();
    // Set the language
    var cLang = $selectLang.val();
    if (cLang !== value) {
        $selectLang.val(value).change();
    }
});

Настройка темы

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

// This function will return the user theme or the Monokai theme (which
// is the default)
function getTheme() {
    return localStorage.getItem(LS_THEME_KEY) || "ace/theme/monokai";
}

// Select the desired theme of the editor
$("#select-theme").change(function () {
    // Set the theme in the editor
    editor.setTheme(this.value);
    
    // Update the theme in the localStorage
    // We wrap this operation in a try-catch because some browsers don't
    // support localStorage (e.g. Safari in private mode)
    try {not focus
        localStorage.setItem(LS_THEME_KEY, this.value);
    } catch (e) {}
}).val(getTheme());

Мы не будем переписывать здесь весь файл, целиком он лежит тут.

Хостинг

Как же мы можем выложить наш редактор в интернет? Так как весь код хранится на Github, самый просто способ выложить — воспользоваться GitHub Pages.

1. Перейдите в настройки репозитория (https://github.com//<repo-name>/settings)
2. Пролистайте до раздела «GitHub Pages»
3. Выберите ветку master
4. Нажмите Save и проект будет доступен по адресу https://.github.io/<repo-name>.

В нашем случае, Эмма — владелец, а репозиторий называется FireEdit. Поэтому доступ к приложению будет таким:

coltaemanuela.github.io/FireEdit/

Идеи

У нас есть ещё несколько идей, которые вы можете захотеть реализовать:

  • Аутентификация
  • Консольная утилита, подключающаяся к нужному редактору, и позволяющая редактировать текст прямо в консоли

  • История правок
  • Улучшение безопасности
  • Вывод списка активных пользователей


По материалам: «FireEdit: Build a Real-time Editor with JavaScript & Firebase»
0 комментариев
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.