Современный веб позволяет решать часть задач которые раньше были прерогативой нативных мобильных приложений. Мы с вами создадим веб-приложение (сайт) которое будет загружаться и сохранит полную функциональность даже в отсутствии интернета, а при его появлении автоматически синхронизируется с сервером. На мобильном устройстве для такого приложения достаточно создать ярлык и в плане автономности мы получим аналог нативного приложения.
Мы напишем подобие 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
.
Комментарии (6)
G-M-A-X
08.09.2016 22:30То есть это localStorage под капотом :)
И будь на месте реакта любимый мной vue.js он перерендерил бы весь dom.
js фреймворки настолько умные, что до такой степени тупые? :)
Imbolc
09.09.2016 07:02IndexedDB точнее. А какие ещё варианты? Так же как у серверных бд под капотом файловая система :)
js фреймворки настолько умные, что до такой степени тупые? :)
Там другой подход, не предполагается полностью перезаписывать модель постоянно.
G-M-A-X
09.09.2016 09:47IndexedDB точнее.
Точно. Вчера не добавлял loop-ы. Проверил: loop-ы пишутся в IndexedDB, какие-то рефы в localStorage.
А какие ещё варианты?
Мне было интересно, что там под капотом, чтобы не тянуть разный мусор, ну или понимать, с чем работаю.
comerc
Хотел бы удивиться реализации синхронизации клиентских данных.
http://youtu.be/1ddm7WCMclA
http://youtu.be/ZWNtxmrA4UY