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


Мы напишем подобие todo-листа, с одним отличием: "выполненные" задачи будут не удаляться, а переноситься в конец списка и по мере решения остальных задач всплывать вверх. Такой список удобно использовать для повторяющихся вещей, таких как различные спортивные активности, развлечения, еда и т.п. Одна моя социально-реализованная знакомая использует его, чтобы равномерно поддерживать контакт с многочисленной популяцией своей френдзоны.


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


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


Часть 0: настройка окружения


Чтобы не засорять текст очередными настройками вебпака, я создал репу с готовыми настройками. Просто склонируйте её и переключитесь на часть 0: git checkout part0. Ключевые точки на которые стоит обратить внимание:


  • npm start — запускает hoodie-сервер и сборку приложения вебпаком в режиме watch (пересборки при изменении)
  • public/index.html — именно то, что вы подумали, тут мы создаём корневой div приложения и подключаем собранный вебпаком бандл
  • src/index.js — точка входа в приложение, тут мы рендерим корневую компоненту: ReactDOM.render(<App />
  • src/App.js — наша пока единственная компонента, тут мы и начнём писать код в следующей части

Попробуйте запустить сервер. Когда вебпак скажет, что всё готово, по адресу http://localhost:8000/ вы должны увидеть симпатичную шапку сайта.


Часть 1: браузерная бд


Для функционирования приложения в оффлайн-режиме, нам потребуется где-то хранить данные и как-то синхронизировать их между устройствами. Pouchdb подходит для этого идеально. Но мы и с ней будем работать не напрямую а через обёртку Hoodie, которая умеет авторизацию. Давайте подключим её в src/App.js:


import Hoodie from '@hoodie/client'
const hoodie = new Hoodie()

Добавление документа в базу


Теперь создадим компоненту для добавления loop-а (отдельного todo-листа):


AddLoop.js
import React from 'react'
import TextField from 'material-ui/TextField'
import RaisedButton from 'material-ui/RaisedButton'

export default class AddLoop extends React.Component {
  constructor(props) {
      super(props)
      this.state = {
        title: ''
      }
  }

  handleTitleChange = (e) => this.setState({title: e.target.value})

  handleSubmit = (e) => {
    e.preventDefault()
    this.props.store.add({
      type: 'loop',
      title: this.state.title
    })
    this.setState({title: ''})
  }

  render() {
    return (
      <form onSubmit={this.handleSubmit}>
        <TextField
          hintText="New loop"
          value={this.state.title}
          onChange={this.handleTitleChange}
          style={{marginRight: '10px'}}
        />
        <RaisedButton label="Add" type="submit" primary={true} />
      </form>
    );
  }
}

Суть здесь заключена в строчке this.props.store.add. Где store — это hoodie.store который нам нужно передать в свойствах компоненты при рендеренге её в App.js:


render() {
  ...
  <AddLoop store={hoodie.store} />

Чтение базы данных


Давайте теперь отобразим сохранённые в базу документы. В App.js мы добавим метод loadLoops:


  constructor(props) {
    super(props);
    this.state = {
      loops: []
    };
    this.loadLoops();
  } 

  loadLoops = () => {
    hoodie.store.findAll(doc => doc.type == 'loop')
      .then(loops => this.setState({loops}))
  }

Нам понадобился конструктор, в котором мы инициализируем state и инициируем загрузку документов. В hoodie.store.findAll мы передаём фильтр для нужных документов и получаем промис, в котором просто записываем полученные документы в state.


Давайте теперь отрисуем их, добавив в render():


          {this.state.loops.map(loop => (
            <Paper key={loop.id} zDepth={2} style={{maxWidth: 400, margin: '20px auto'}}>
              <h3 style={{padding: 15}}>{loop.title}</h3>
            </Paper>
          ))}

Теперь вы должны видеть список добавленных loop-ов. Однако при добавлении новых они появятся в списке только при перезагрузке страницы. Давайте исправим это.


Подписка на изменения бд


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


  componentDidMount() {
    hoodie.store.on('change', this.loadLoops);
  }

Теперь при добавлении loop-ов они будут автоматически появляться в списке. Причём независимо от того, каким образом изменилась база данных. Это важно — даже если изменения произошли в ходе синхронизации с другим устройством, наш коллбэк сработает.


Почему React


Из логики работы с бд становится понятно почему среди обилия браузерных шаблонизаторов был выбран реакт. В ответ на каждое изменение бд мы будем просто перезагружать всё состояние. И будь на месте реакта любимый мной vue.js он перерендерил бы весь dom. А это не просто медленно, представьте вы заполняете форму и в этот момент другое устройство инициирует изменение бд — форма перерисована, ваши изменения потеряны. Тогда как реакт с его виртуальным dom аккуратно перерисует изменившиеся детали и даже фокуса в вашей форме не собьёт.


Конец


Сегодня мы научились взаимодействовать с браузерной бд. В следующей части мы настроим серверную бд и прикрутим авторизацию. Буду рад любым вашим замечаниям / исправлениям / пожеланиям. Код этой части доступен тут: https://github.com/imbolc/action-loop под тегом part1.

Поделиться с друзьями
-->

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


  1. comerc
    07.09.2016 21:24

    Хотел бы удивиться реализации синхронизации клиентских данных.


    http://youtu.be/1ddm7WCMclA
    http://youtu.be/ZWNtxmrA4UY


  1. RubaXa
    08.09.2016 11:38
    +1

    Может добавите в статью пример работы с ServiceWorker, который у вас и отвечает за Offline, или это вторая часть?


    1. Imbolc
      08.09.2016 13:32

      Да, наверное, даже третья :)


  1. G-M-A-X
    08.09.2016 22:30

    То есть это localStorage под капотом :)

    И будь на месте реакта любимый мной vue.js он перерендерил бы весь dom.

    js фреймворки настолько умные, что до такой степени тупые? :)


    1. Imbolc
      09.09.2016 07:02

      IndexedDB точнее. А какие ещё варианты? Так же как у серверных бд под капотом файловая система :)


      js фреймворки настолько умные, что до такой степени тупые? :)

      Там другой подход, не предполагается полностью перезаписывать модель постоянно.


      1. G-M-A-X
        09.09.2016 09:47

        IndexedDB точнее.

        Точно. Вчера не добавлял loop-ы. Проверил: loop-ы пишутся в IndexedDB, какие-то рефы в localStorage.

        А какие ещё варианты?

        Мне было интересно, что там под капотом, чтобы не тянуть разный мусор, ну или понимать, с чем работаю.