- PVSM.RU - https://www.pvsm.ru -

Задача сравнения фреймворков очень неблагодарное занятие, предпочтения у разработчиков разные, технологии меняются очень быстро. Слишком быстро. Эта статья, устареет еще до того момента как я нажму кнопочку “опубликовать“.
Попытки сравнить были, так, порядка пяти лет назад, ребята (Colin Eberhardt и Chris Price) воодушевили ряд разработчиков сделать приложение для поиска недвижимости по четко составленному ТЗ. Идея классная, мы даже участвовали и сделали версию этого приложения на DevExtreme [1]. Но в плане поддержки такой проект это ад и сейчас проект Property Cross [2], представляет некоторый исторический пласт, который вызывает ностальгию и теплые чувства, но вряд ли несет практическую пользу.
Если брать только js мир, то есть довольно живой проект todomvc [3], который сравнивает только js часть, без упаковки в мобильное, десктопное или какое бы то ни было приложение. Проект живой и поддерживается. Скорее всего, есть еще очень классные примеры, которые мы не заметили в выдаче гугла когда готовили статью, но не будем огорчаться из-за этого.
Наш эксперимент менее амбициозен и показывает только текущий срез технологий, даже его малую часть. Вот ссылочка на первую [4] и вторую [5] статьи эксперимента.
Дальнейшее чтиво это третья статья, о том как сделать приложение на React Native по ТЗ. Оно очень похоже на сотни, а может быть и тысячи, других пересказов документации о том как сделать приложение на React Native. Дорогой читатель, я тебя предупредил, совесть моя чиста.
Вообще, его можно полностью посмотреть в первой статье. Но я добавлю картинку того, что должно получится в итоге. Картинок мало не бывает.

React Native — фреймворк для создания кроссплатформенных мобильных приложений от Facebook. Как и в «обычном» React для веб, UI приложения собирается из кирпичиков — компонентов, которые реагируют на изменение своего состояния (state) и свойств им переданных (props), но, в отличие от веб, рендерятся в нативные контролы.
В идеале, используются принципы иммутабельности и чистые фунции, что обеспечивает простоту и изолированность тестирования. И тут стоит заметить, что сам по себе React очень простой, и эта простота переходит и в мобильную часть.
Дополнительные надстройки в нативном и JS коде сглаживают различие между платформами, когда это возможно. Фактически React Native обеспечивает некоторую унификацию свойств для компонента в каждой операционной системе.
Например, ScrollView, и HorizontalScrollView это 2 разных компонента в Android. А в iOS UIScrollView, который поддерживает как горизонтальный так и вертикальный скролл. А в React Native мы будем использовать следующий кроссплатформенный код:
<ScrollView horizontal={true}/>
При грамотном подходе на выходе получаем «честное» нативное приложение, работающее на iOS и Android.
В идеальном мире, разрабатывая на React Native, вам не придется писать на Java или Objective-C. Но такая возможность есть, когда необходимо реализовать компонент, который выходит за рамки возможностей React Native.
С этим много играли разработчики из Airbnb, и мы можем посмотреть много достойных реализаций в реакт комьюнити, которые раньше находились в их репозитории. Например Lottie [6] — библиотека для импорта анимаций из Adobe After Effects, или кросс-платформенные карты [7].
JS код в приложении исполняется на движке JavaScriptCore [8]. Коммуникация между нативным кодом и JS осуществляется с помощью асинхронного моста (bridge), который позволяет передавать свойства (props), вызывать события (events) и выполнять коллбеки.

Картинка взята из отличной переработки документации React Made Native Easy [9]. (Настоятельно рекомендую к прочтению.)
В процессе сборки для преобразования JS кода используется новомодный babel, это позволяет использовать новый синтаксис ES6, а также некоторые фичи ES8 (например async-await). Если вы, мой дорогой читатель, js разработчик, то понимаете как хорошо, когда есть спред оператор и как плохо, когда его нет.
Для верстки страниц используется технология flexbox, реализованная кроссплатформенным движком Yoga. Она имеет отличия от браузерного flexbox, но они незначительны и, в основном, касаются дефолтов. Конечно, есть нюансы, но вам обязательно повезет, и все будет только согласно документации.
Для работы с RN нам потребуются Node.js [10] и менеджер пакетов npm, который идет в комплекте. Не обязательно, но очень желательно установить на свой девайс приложение Expo [11]. Оно позволит запустить наш проект на телефоне, а также собрать и запустить приложение для iOS, когда у вас под рукой нет macOS.
Создадим новое приложение. Для этого используем пакет create-react-native-app [12].
В терминале выполняем:
npm install -g create-react-native-app
create-react-native-app notes
cd notes
npm run start
Сканируем QR-код с помощью Expo или вводим ссылку из терминала, или даже отсылаем ссылку себе на телефон, прямо из терминала.
У меня вообще есть подозрение, что в разработчики cli для react native затесался седоволосый старец, который застал roguelike игрушки без ui, когда есть только терминал, и вместо топовой видеокарты только твоя фантазия.

Но мы, тем временем, только что создали и запустили “Hello World” приложение.
Согласно ТЗ, структура данных приложения будет такой
Note: {
userName: string,
avatar: string,
editTime: string,
text: string
}
Project: { name: string, notes: Array<Note> }
Projects: Array<Project>
Для работы с такими данными я бы взял какое-нибудь очень модное решение на основе CQRS. Это позволило бы сохранить целостность данных, обеспечить высокую скорость чтения с возможностью перестраивания проекций, а также быстрый деплой в облако одной командой. Как Resolve [13], который разрабатывают наши коллеги.
Но не возьму, у нас же простой эксперимент, без бекенда. И для простоты буду использовать архитектуру flux [14], в частности ее реализацию — redux [15]. Данные из состояния приложения приходят в компоненты в качестве props. Компоненты могут вызвать actions, чтобы обновить данные.
Приложение будет иметь 3 экрана, все согласно ТЗ:
Для навигации между экранами буду использовать стандартную библиотеку react-navigation [16]. Циферки около графика на странице библиотеки, показывают сколько раз ее скачивают в неделю. Сейчас там порядка 100 тысяч, в неделю. Хорошо, что я не один выбрал такую библиотеку для навигации. И да, можно посмотреть циферки у других npm пакетов, которые я указал в этой статье, чтобы примерно понимать количество пользователей данной технологии на данный момент времени.
Для React Native компонент App из файла App.js это точка входа в приложение.
export default class App extends Component {
render() {
return (
<Provider store={store}>
<Navigator />
</Provider>
)
}
}
Store с данными и состоянием приложения подключается компонентом Provider из библиотеки react-redux. Это обеспечивает проброс данных для вложенных компонентов.
Создадим навигатор для переходов между экранами в приложении. Он четко отражает структуру приложения, заявленную в эксперименте, и отрисовывает анимированные переходы между экранами для каждой из платформ.
const Navigator = createStackNavigator({
Projects: {
screen: Projects
},
Project: {
screen: Project
},
Note: {
screen: Note
}
})
Экраны навигатора это компоненты — контейнеры. Они получают данные из стейта приложения.
На экране со списком проектов будет список и кнопка добавления проекта — в хедере окна справа. Новый проект будем создавать на экране Project.
Для навигации используем объект navigation, который передал в props родительский компонент — навигатор.
export class Projects extends PureComponent {
static navigationOptions = ({ navigation }) => ({
headerRight: (
<AddButton onPress={() => navigation.navigate('Project')} />
)
})
navigateProject = project => {
this.props.navigation.navigate('Project', {
projectId: project.id,
name: project.name
})
}
render() {
return (
<ProjectList
projects={this.props.projects}
onPressProject={this.navigateProject}
/>
)
}
}
Для вывода списка проектов будем использовать FlatList — кросс-платформенный список с виртуализацией:
export class ProjectList extends PureComponent {
static propTypes = {
projects: ProjectsType,
onPressProject: PropTypes.func
}
renderItem = ({ item }) => (
<ProjectListItem
project={item}
onPressProject={this.props.onPressProject}
/>
)
render() {
return (
<FlatList
data={this.props.projects}
keyExtractor={item => item.id}
renderItem={this.renderItem}
/>
)
}
}
Для каждого элемента задаем уникальный ключ — у нас это id элемента. Это нужно для того, чтобы реакт мог различать элементы в списке и обновлять только те, которые изменились.
Добавим компонент для элемента списка.
export class ProjectListItem extends PureComponent {
static propTypes = {
project: ProjectType,
onPressProject: PropTypes.func
}
onPressProject = () => {
const { project, onPressProject } = this.props
onPressProject(project)
}
render() {
return (
<TouchableOpacity onPress={this.onPressProject}>
<View style={styles.project}>
<Text style={styles.name}>{this.props.project.name}</Text>
</View>
</TouchableOpacity>
)
}
}
TouchableOpactity — обертка, реагирующая на нажатия. При нажатии вложенный компонент становится прозрачнее.
View — аналог div для веб — базовый компонент разметки.
Text — контейнер для текста.
Добавим стили:
const styles = StyleSheet.create({
project: {
paddingVertical: 30,
paddingHorizontal: 15,
backgroundColor: 'white',
borderBottomWidth: StyleSheet.hairlineWidth,
borderColor: 'gray'
},
name: {
fontSize: 16
}
})
Синтаксис стилей напоминает css, главное отличие — стилизовать можно только сам компонент (например нельзя задать размер шрифта для всего приложения, только для конкретного компонента Text)

Аналогично создаем детальную страницу. Отличия только в наличии заголовка в навигаторе и дополнительного инпута. В навигаторе зададим заголовок — название проекта. Если id проекта не задан — предложим ввести название проекта и создадим новый.
export class Project extends PureComponent {
static navigationOptions = ({ navigation }) => {
const projectId = navigation.getParam('projectId')
return {
title: navigation.getParam('name', ''),
headerRight: (
<AddButton
onPress={() => navigation.navigate('Note', { projectId })}
/>
)
}
}
removeNote = noteId => {
const { projectId, removeNote } = this.props
removeNote(projectId, noteId)
}
navigateNote = noteId => {
const { projectId, navigation } = this.props
navigation.navigate('Note', { noteId, projectId })
}
createProject = name => {
const newProjectId = shortid.generate()
this.props.navigation.setParams({ projectId: newProjectId, name })
this.props.addProject(newProjectId, name)
}
render() {
const { projectId, project } = this.props
if (!projectId) {
return (
<ProjectNameInput
onSubmitEditing={this.createProject}
/>
)
}
return (
<NoteList
notes={project.notes}
onNavigateNote={this.navigateNote}
onRemoveNote={this.removeNote}
/>
)
}
}
Страница проекта представляет собой список заметок. По ТЗ для каждой заметки есть контекстное меню с редактированием и удалением. Также удалить заметку можно свайпом. В React Native существует отдельный список, с возможностью свайпа — SwipeableFlatList.
<SwipeableFlatList
data={this.props.notes}
bounceFirstRowOnMount={false}
keyExtractor={item => item.id}
maxSwipeDistance={MAX_SWIPE_DISTANCE}
renderQuickActions={this.renderQuickActions}
renderItem={this.renderItem}
/>
При удалении заметки мы запросим подтверждение, для этого вызовем стандартный системный Alert
onRemoveNote = noteId => {
Alert.alert(
'Remove Note',
'Do you want to remove note ?',
[
{ text: 'Cancel', onPress: () => {}},
{ text: 'Remove', onPress: () => this.props.onRemoveNote(noteId) }
]
)
}

Есть интересный момент для контекстного меню. В отличие от алерта, его реализация в RN для Android и iOS различается.
Для андроид используем попап меню
showPopupMenu = () => {
const button = findNodeHandle(this._buttonRef)
UIManager.showPopupMenu(
button,
[ 'Edit', 'Delete' ],
e => console.error(e),
(e, i) => this.onPressMenu(i)
)
}
Для iOS — actionSheet
showActionSheet = () => {
ActionSheetIOS.showActionSheetWithOptions({
options: [ 'Edit', 'Delete', 'Cancel' ],
destructiveButtonIndex: 1,
cancelButtonIndex: 2
},
this.onPressMenu
)
}
Есть несколько способов разделить платформо-зависимый код. Мы воспользуемся объектом Platform.
onOpenMenu = Platform.select({
android: this.showPopupMenu,
ios: this.showActionSheet
})

Страница заметки также довольно примитивна. Но, в отличие от предыдущих, мы используем state для хранения промежуточных результатов ввода пользователя.
export class Note extends PureComponent {
static navigationOptions = ({ navigation }) => ({
headerRight: (
<SaveButton onPress={navigation.getParam('onSaveNote')} />
)
})
state = {
noteText: ''
}
componentDidMount() {
this.props.navigation.setParams({ onSaveNote: this.onSaveNote })
}
onSaveNote = () => {
Keyboard.dismiss()
const { projectId, noteId, note, navigation, addNote, editNote } = this.props
const { noteText } = this.state
if (!noteId) {
const newNoteId = shortId.generate()
navigation.setParams({ noteId: newNoteId })
addNote(projectId, newNoteId, noteText)
} else if (noteText && noteText !== note.text) {
editNote(projectId, noteId, noteText)
}
}
onChangeNote = noteText => {
this.setState({ noteText })
}
render() {
const initialTextValue = this.props.note ?
this.props.note.text : ''
const noteText = this.state.noteText || initialTextValue
return (
<NoteDetail
noteText={noteText}
onChangeNoteText={this.onChangeNote}
/>
)
}
}
Детальный экран заметки — классический “глупый” компонент — докладывает наверх об изменении текста и показывает текст, который ему передает родитель
export class NoteDetail extends PureComponent {
static propTypes = {
noteText: PropTypes.string,
onChangeNoteText: PropTypes.func
}
render() {
const { noteText, onChangeNoteText } = this.props
return (
<View style={styles.note}>
<TextInput
multiline
style={styles.noteText}
value={noteText}
placeholder="Type note text here ..."
underlineColorAndroid="transparent"
onChangeText={onChangeNoteText}
/>
</View>
)
}
}

Итого мы получили приложение как в ТЗ. Эксперимент завершен. Код приложения можно посмотреть в общем репозитории [17]
React Native привычен и понятен разработчикам, знакомым с React и инфраструктурой Node.js и npm. Есть возможность использовать все подходы и библиотеки, что и для обычного React.
Огромное количество js пакетов из npm. Скорее всего, большая часть стандартных задач уже решена и возможно под MIT лицензией.
Большое комьюнити. Как индивидуальные разработчики так и крупные компании использовали RN для разработки, и продолжают использовать.
Много готовых наборов UI компонентов, таких как NativeBase [18], React Native Elements [19], библиотеки от крупных компаний типа Facebook, Airbnb, Wix.com.
Понятный инструментарий, обеспечивающий удобную разработку приложения от Hello World до Instagram [20].
Приложение стартует медленнее нативного и есть некоторые сложности дебага. JS код в дебаггере и без него работает на разных движках. Об этой проблеме очень хорошо написали Airbnb в серии статей [21], почему они отказались от React Native в разработке.
Так как инструментарий состоит из множества пакетов, которые разрабатываются отдельно, существует вероятность конфликта версий и разлома.
Не все можно сделать без нативного кода. И когда вносишь изменения в нативный код, то теряешь возможность использовать Expo и вынуждаешь себя собирать приложение стандартными средствами нативной разработки.
Большое спасибо Mirimon [22] и HeaTTheatR [23] за приглашение поучаствовать в этом эксперименте. Было увлекательно. На последок добавлю голосовалку.
Автор: SeOd
Источник [24]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/javascript/290722
Ссылки в тексте:
[1] DevExtreme: https://js.devexpress.com/
[2] Property Cross: http://propertycross.com/
[3] todomvc: http://todomvc.com/
[4] первую: https://habr.com/post/420691/
[5] вторую: https://habr.com/company/devexpress/blog/420999/
[6] Lottie: https://github.com/react-community/lottie-react-native
[7] кросс-платформенные карты: https://github.com/react-community/react-native-maps
[8] JavaScriptCore: https://trac.webkit.org/wiki/JavaScriptCore
[9] React Made Native Easy: https://www.reactnative.guide/index.html
[10] Node.js: https://nodejs.org
[11] Expo: https://expo.io/
[12] create-react-native-app: https://github.com/react-community/create-react-native-app
[13] Resolve: https://github.com/reimagined/resolve
[14] flux: https://facebook.github.io/flux/
[15] redux: https://redux.js.org/
[16] react-navigation: https://www.npmjs.com/package/react-navigation
[17] репозитории: https://github.com/Mirimon/KivyXamarinReactComparison/tree/master/ReactNative
[18] NativeBase: https://github.com/GeekyAnts/NativeBase
[19] React Native Elements: https://github.com/react-native-training/react-native-elements
[20] Instagram: https://instagram-engineering.com/react-native-at-instagram-dd828a9a90c7
[21] серии статей: https://medium.com/airbnb-engineering/sunsetting-react-native-1868ba28e30a
[22] Mirimon: https://habr.com/users/mirimon/
[23] HeaTTheatR: https://habr.com/users/heattheatr/
[24] Источник: https://habr.com/post/421571/?utm_campaign=421571
Нажмите здесь для печати.