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