Всем привет! В последнее время все чаще использую Firebase в своих проектах: очень удобно обходится без фактического написания серверной части. Хочу поделиться небольшим опытом работы на стороне фронтенда. В данном случае это Angular, поэтому используется официальная библиотека AngularFire. Наперед отмечу, что в Android лучше обстоят дела с библиотеками и реализацией возможностей Firebase, как мне показалось.


Больше касаемо Firestore


Первое, это Offline Data. Она по умолчанию включена в Android и iOS, но для веба отключена, и это оказалось не удивительным. Мне абсолютно не понравилась эта возможность на сегодняшний день, так как данные не обновлялись (а должны были бы) даже после перезагрузки страницы, и нет никаких явных рычагов управления (по крайней мере, в документациях) для их очистки или обновления. Они хранятся в IndexedDB, их можно вручную очистить, однако firebase постоянно подключен к бд. Т.е. почти единственная возможность для пользователя удалить (== очистить в данном случае) только при начале загрузки страницы, когда firebase еще не инициализирован. Поэтому я решил проблему переходом по ссылке window.location.href += '?clear';, затем после перезагрузки страницы в главном html файле выполнением такого первоочередного скрипта:


<body>
<script>
if (window.location.href.indexOf('?clear') !== -1) {
    window.history.replaceState(null, null, window.location.pathname);
    var request = window.indexedDB.deleteDatabase("firestore/[DEFAULT]/your-project/main");
    request.onsuccess = function() {
        console.log("Cleared cache successfully");
    };
    request.onerror = function() {
        console.log("Couldn't clear cache");
    };
    request.onblocked = function() {
        console.log("Couldn't clear cache due to the operation being blocked");
    };
} else {
    console.log("Missing clear cache");
}
</script>
<!-- root это корневой компонент приложения -->
<root></root>
</body>

Работает, но не скажу, что решает главные проблемы, поскольку просто ужасно реализована синхронизация.


Во-вторых, не используйте valueChanges у collection и document, поскольку в 99% случаев необходимы метаданные (оказывается). Хардкодно запоминать uid документа в каком-либо его поле; оптимальный способ будет использовать snapshotChanges с map-ом для получения непосредственного uid, как здесь для Angular:


this.store.collection<any>("yourCollection", ref => {
        return ref.orderBy("yourFiled");
    }).snapshotChanges()
    .map(actions => {
        return actions.map(action => {
            return {
                _uid: action.payload.doc.id,
                ...action.payload.doc.data()
            };
        });
    })

Еще хотел бы отметить, что коллекции и документы независимы друг от друга, т.е. вы не получите при выборки документа его subcollections: это отдельные данные, и сделано это просто для удобства представления данных (одно из главных отличий от Realtime Database).
Здесь же при проектировании бд учитывайте ограничение на размер документа в 1 Мб. И если данных может быть много (какого-либо array), то надо выносить их в subcollection.


Больше касаемо Angular


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


this.store.collection(collection)
    .doc(document).ref
    .set(object, {
        merge: true
    })
    .then(() => this.showSnackbar("Изменения сохранены"))
    .catch((error) => {
        console.log(error);
        this.showSnackbar("Не удалось сохранить изменения");
    });
// замечу, что showSnackbar должен быть реализован как функциональный литерал
showSnackbar = (message) => {
    this.snackbar.open(message, null, {
        duration: 1000
    });
};

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


clearDocuments() {
    this._documents.complete();
    this._documents = null;
    this._documents = new ReplaySubject();
    this._refresh.next(true);
}

Здесь _refresh это BehaviorSubject, с ним отлично сочетается switchMap:


this.service._refresh.asObservable()
    .switchMap(() => {
        this.items.data = [];
        return this.service._documents.asObservable();
    })
    .subscribe(document => {

    });

Небольшое личное впечатление


Я имел опыт работы с Realtime Database и скажу, что Firestore более продвинутое хранилище, хотя и возможно со своими недостатками. Даже в плане работы в консоли (бывало по ошибке удалял корневой узел Realtime Database)
В Firestore нет онлайн эмулятора правил (собственно тесты никто не отменял), поэтому для тестирования я использую библиотеку firestore-security-tests. Она работает замечательно, единственно, вот такого типа выборки get(/databases/$(database)/documents/info/app).data.version не поддерживаются.
Надеюсь, с релизом Firestore все изменится к лучшему


Дополнение


Никак только не получается подключить Firebase с правами админа для внутреннего сайта (а из-за этого приходится хардкодить с правилами, единственно хорошо, что в консоли можно указать подпись сертификата приложения и тп). Есть решение для Node.js, но как-то не удалось еще внедрить его. Может кто имеет такой опыт?

Комментарии (10)


  1. sl1m_dogg
    06.02.2018 15:32

    мда уж. мне тоже нужно будет на основе этого кода целую статью подымать
    github.com/artemzakholodilo/angular-5-todolist


  1. Hazrat
    06.02.2018 16:59

    Скажите, а вот вы запросы на чтение и запись с фронта отправляете? То есть на фронте есть данные (токен или secret key) приложения и любой может не хитрым способом просто переписать или удалить некоторые значения в хранилище?
    Вообще интересно, сам столкнулся с такой задачей, на клиенте токен, с помощью которого в хранилище может записывать и удалять данные, как можно защититься?
    csrf защита не подходит


    1. androidovshchik Автор
      06.02.2018 17:20

      Может попробовать ограничить http-запросы от определенного сайта в developers консоли? console.developers.google.com/apis/credentials/key/112
      Т.е. настроить Browser key


      1. Hazrat
        06.02.2018 17:41

        Вы имеете ввиду firebase конкретно? А как быть если злоумышленник выполнит запрос из консоли? в этом случае origin будет текущий домен?


        1. androidovshchik Автор
          06.02.2018 17:59

          Из консоли google? Это фантастика больше, так как авторизация на уровне гугл аккаунта (думаю, защита должна быть на высоте)


          1. Hazrat
            06.02.2018 18:09

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


    1. iKest
      07.02.2018 08:47

      В правилах можно прописать авторизацию и права юзера. Так что доступ к бд будет только по паролю....


      1. androidovshchik Автор
        07.02.2018 11:06

        А как насчет того, что тот же самый пароль в таком случае будет сохранен в документе
        Например, request.resource.data.password, его же проверять?


        1. iKest
          08.02.2018 00:32

          Firebase Auth это отдельный модуль и сами правила доступа тоже хранятся не в самой базе, хотя и схожи с ней по структуре.


  1. androidovshchik Автор
    06.02.2018 18:42

    Наверное, больше ничего не смогу подсказать, так как в этом небольшой специалист