Задача сравнения фреймворков очень неблагодарное занятие, предпочтения у разработчиков разные, технологии меняются очень быстро. Слишком быстро. Эта статья, устареет еще до того момента как я нажму кнопочку “опубликовать“.


Попытки сравнить были, так, порядка пяти лет назад, ребята (Colin Eberhardt и Chris Price) воодушевили ряд разработчиков сделать приложение для поиска недвижимости по четко составленному ТЗ. Идея классная, мы даже участвовали и сделали версию этого приложения на DevExtreme. Но в плане поддержки такой проект это ад и сейчас проект Property Cross, представляет некоторый исторический пласт, который вызывает ностальгию и теплые чувства, но вряд ли несет практическую пользу.

Если брать только js мир, то есть довольно живой проект todomvc, который сравнивает только js часть, без упаковки в мобильное, десктопное или какое бы то ни было приложение. Проект живой и поддерживается. Скорее всего, есть еще очень классные примеры, которые мы не заметили в выдаче гугла когда готовили статью, но не будем огорчаться из-за этого.

Наш эксперимент менее амбициозен и показывает только текущий срез технологий, даже его малую часть. Вот ссылочка на первую и вторую статьи эксперимента.

Дальнейшее чтиво это третья статья, о том как сделать приложение на React Native по ТЗ. Оно очень похоже на сотни, а может быть и тысячи, других пересказов документации о том как сделать приложение на React Native. Дорогой читатель, я тебя предупредил, совесть моя чиста.

Вспоминаем ТЗ


Вообще, его можно полностью посмотреть в первой статье. Но я добавлю картинку того, что должно получится в итоге. Картинок мало не бывает.
image

Щепотка матчасти. Что такое 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 — библиотека для импорта анимаций из Adobe After Effects, или кросс-платформенные карты.

JS код в приложении исполняется на движке JavaScriptCore. Коммуникация между нативным кодом и JS осуществляется с помощью асинхронного моста (bridge), который позволяет передавать свойства (props), вызывать события (events) и выполнять коллбеки.

Картинка взята из отличной переработки документации React Made Native Easy. (Настоятельно рекомендую к прочтению.)

В процессе сборки для преобразования JS кода используется новомодный babel, это позволяет использовать новый синтаксис ES6, а также некоторые фичи ES8 (например async-await). Если вы, мой дорогой читатель, js разработчик, то понимаете как хорошо, когда есть спред оператор и как плохо, когда его нет.

Для верстки страниц используется технология flexbox, реализованная кроссплатформенным движком Yoga. Она имеет отличия от браузерного flexbox, но они незначительны и, в основном, касаются дефолтов. Конечно, есть нюансы, но вам обязательно повезет, и все будет только согласно документации.

Подготовка и развертывание стека. Ламповый терминал


Для работы с RN нам потребуются Node.js и менеджер пакетов npm, который идет в комплекте. Не обязательно, но очень желательно установить на свой девайс приложение Expo. Оно позволит запустить наш проект на телефоне, а также собрать и запустить приложение для iOS, когда у вас под рукой нет macOS.

Создадим новое приложение. Для этого используем пакет create-react-native-app.

В терминале выполняем:

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” приложение.

И целого “Hello World”-a мало. Анализируем ТЗ


Согласно ТЗ, структура данных приложения будет такой

Note: {
	userName: string,
	avatar: string,
	editTime: string,
	text: string
}
Project: { name: string, notes: Array<Note>  }
Projects: Array<Project>

Для работы с такими данными я бы взял какое-нибудь очень модное решение на основе CQRS. Это позволило бы сохранить целостность данных, обеспечить высокую скорость чтения с возможностью перестраивания проекций, а также быстрый деплой в облако одной командой. Как Resolve, который разрабатывают наши коллеги.

Но не возьму, у нас же простой эксперимент, без бекенда. И для простоты буду использовать архитектуру flux, в частности ее реализацию — redux. Данные из состояния приложения приходят в компоненты в качестве props. Компоненты могут вызвать actions, чтобы обновить данные.

Приложение будет иметь 3 экрана, все согласно ТЗ:
  • список проектов — Projects,
  • детальная страница проекта со списком заметок — Project,
  • детальная страница заметки — Note


Для навигации между экранами буду использовать стандартную библиотеку react-navigation. Циферки около графика на странице библиотеки, показывают сколько раз ее скачивают в неделю. Сейчас там порядка 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
   }
 })

Экраны навигатора это компоненты — контейнеры. Они получают данные из стейта приложения.

Список проектов — Projects


На экране со списком проектов будет список и кнопка добавления проекта — в хедере окна справа. Новый проект будем создавать на экране 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)


Детальная страница проекта со списком заметок — Project


Аналогично создаем детальную страницу. Отличия только в наличии заголовка в навигаторе и дополнительного инпута. В навигаторе зададим заголовок — название проекта. Если 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
})



Детальная страница заметки — Note


Страница заметки также довольно примитивна. Но, в отличие от предыдущих, мы используем 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>
   )
 }
}



Итого мы получили приложение как в ТЗ. Эксперимент завершен. Код приложения можно посмотреть в общем репозитории

Итого, плюсы и минусы React Native


Плюсы:


React Native привычен и понятен разработчикам, знакомым с React и инфраструктурой Node.js и npm. Есть возможность использовать все подходы и библиотеки, что и для обычного React.

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

Большое комьюнити. Как индивидуальные разработчики так и крупные компании использовали RN для разработки, и продолжают использовать.

Много готовых наборов UI компонентов, таких как NativeBase, React Native Elements, библиотеки от крупных компаний типа Facebook, Airbnb, Wix.com.

Понятный инструментарий, обеспечивающий удобную разработку приложения от Hello World до Instagram.

Минусы:


Приложение стартует медленнее нативного и есть некоторые сложности дебага. JS код в дебаггере и без него работает на разных движках. Об этой проблеме очень хорошо написали Airbnb в серии статей, почему они отказались от React Native в разработке.

Так как инструментарий состоит из множества пакетов, которые разрабатываются отдельно, существует вероятность конфликта версий и разлома.

Не все можно сделать без нативного кода. И когда вносишь изменения в нативный код, то теряешь возможность использовать Expo и вынуждаешь себя собирать приложение стандартными средствами нативной разработки.

Большое спасибо Mirimon и HeaTTheatR за приглашение поучаствовать в этом эксперименте. Было увлекательно. На последок добавлю голосовалку.

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


  1. Pilot72
    29.08.2018 11:46
    +1

    Синтаксис стилей напоминает css, главное отличие — стилизовать можно только сам компонент (например нельзя задать размер шрифта для всего приложения, только для конкретного компонента Text)

    В React Native есть возможность использовать массивы стилей.
    Можно создать стиль, в котором будет только настройка шрифта, и этот стиль подмешивать во все места, где требуется аналогичный шрифт. Таким образом получится сэмулировать глобальную настройку шрифта для всего приложения.


    1. SeOd Автор
      29.08.2018 12:55

      Да это возможно и даже будет работать. Но, для данного случая, реакт рекомендует использовать композицию, как это описано в документации.


    1. shadek
      29.08.2018 18:04

      Пардон, а в Kivy/Xamarin можно на все приложение наложить стиль? Можно примеры, так сказать для развития навыков?


      1. Mirimon
        29.08.2018 18:33

        В Xamarin.Forms некоторое время назад появились css: docs.microsoft.com/en-us/xamarin/xamarin-forms/user-interface/styles/css

        Но, лично я их пока еще не использовал, так что подробно про них ничего сказать не могу.


        1. shadek
          29.08.2018 18:51

          Спасибо.

          По моему, вы не правы в своем утверждении. Можно, например, зайти в styles.xml и вписать

          <style name="FontTheme" parent="Theme.AppCompat.Light.NoActionBar">
                  <item name="android:textSize">20sp</item>
                  <item name="android:textColor">@color/black</item>
           </style>
          


          Это установить параметры текста по-умолчанию для всего приложения. На счет iOS не вспомню, возможно там тоже можно установить эти параметры.


          1. Mirimon
            30.08.2018 09:57

            А где я что-то утверждал? Я просто показал, что появились css, и все.


  1. vba
    29.08.2018 12:32

    React native не является более перспективным направлением. По моему отказ Airbnb от этого фреймворка этому яркое подтверждение. Жаль что вместо RN автор не использовал NativeScript, который концептуально отличается от RN.


    1. SeOd Автор
      29.08.2018 12:47

      Я не готов спорить что является более перспективным, так как каждый разработчик верит в свой стек технологий и хвалит его. И это абсолютно нормально. Я полностью уважаю выбор других разработчиков.

      За NativeScript наблюдаю с его появления, все-таки телереки один из наших конкурентов, и тут стоит отдать должное, ребята проделали большую работу. И то что было пару лет назад вызывало только грусть и печаль, сейчас же вполне можно пользоваться.


      1. theWaR_13
        30.08.2018 13:08

        У меня как раз таки есть сомнения в том, что сегодня Nativescript является юзабельным. Расскажу почему. Есть приложение, написанно на Angular 5 и есть идея создать гибридное приложение с максимально сжатыми сроками и минимальными усилиями со стороны разработчиков (насколько это возможно).
        И вот при создании нового приложения на NativeScript (я использую Sidekick для создания шаблона и сборки проекта) можно выбрать темплейт на Angular + Typescript. Выглядит идеально! Ведь в таком случае часть кода, которая не работает с UI, можно будет полностью или частично переиспользовать (сервисы, например). И вот что я могу сказать, начав делать самую простую форму логина: я нашел много минусов, некоторые из них плохо поддаются пониманию (например, скомпилированные .js файлы лежат не в папке dist, а рядом с исходным кодом, из-за чего приходится делать лишние телодвижения и учить IDE скрывать их и это в фреймворке, который подается как ready to use), но документации хуже, чем у Telerik я еще наверное не встречал. Я еще глубоко не копал, но самое первое с чем столкнулся — отсутствие описания clearHistory параметра для SideDrawer. Если не использовать этот параметр, то при навигации между разными страницами будет появляться кнопка назад, которая конкретно в моем случае не нужна. И как эту кнопку убрать я нашел только благодаря issues на Github. Серьезно? А с чем еще я столкнусь дальше, чего не описано в документации? Это наверное одна из самых серьезных проблем, с которой я столкнулся, есть еще несколько более мелких проблем, но если взять это все вместе, Nativescript использовать не хочется от слова совсем.
        Не знаю, может у меня нет опыта и я делаю что-то не так, было бы здорово если найдется человек, который сможет меня поправить и рассказать, как делать правильно.


    1. Listrigon
      29.08.2018 15:07

      Как ни странно, но Airbnb никогда не являлся примером, потому что у меня даже на компе постоянно тормозит их сайт, а после того как нам пришлось выпилить их react date-range компонент из-за его размеров и общих проблем и подавно мнение испрртилось, но это субъективно.


      1. vba
        29.08.2018 15:17

        Я как то пытался запилить их компонент для работы с картами, в одно приложение и я узрел блеск и нищету RN и весь ад совместимостей между версией андроида и компонента. Субъективно конечно, но сразу как то расхотелось использовать эту горючую смесь.


  1. AnarchyMob
    29.08.2018 16:39

    Странно, а почему в эксперименте не участвует Qt с QML? Я сам пишу на Xamarin'e, но Qt, по моему, очень достойный кроссплатформенный фреймворк. Единственный минус Qt для Android это интероп с Java, все таки JNI это ад.


    1. SeOd Автор
      29.08.2018 16:44

      Вообще, ограничений нет. Можно продолжить эксперимент. Тем более ТЗ есть. (Mirimon и HeaTTheatR)


      1. aknew
        30.08.2018 16:25

        Если больше никто не откликнется, то я бы попробовал продолжить начинание на qt+qml, но сразу скажу — использую их только изредка (последний раз что-то более-менее объемное писал еще во времена 5.7 т.е. чуть меньше 2-х лет назад) и мой код может быть не лучшим представителем.


    1. Neikist
      29.08.2018 17:22

      Еще flutter не участвовал. Найти бы человека который уже немного его знает)


  1. raoffonom
    29.08.2018 17:26

    Не все можно сделать без нативного кода. И когда вносишь изменения в нативный код, то теряешь возможность использовать Expo и вынуждаешь себя собирать приложение стандартными средствами нативной разработки.

    Экспо еще та печаль, поэтому вынуждать себя не надо изначально, а пилить приложение сразу, как true девелоперы, на react-native cli.


  1. yushkevichv
    30.08.2018 00:17

    Смотрели аналоги под vuejs (https://weex.apache.org например)? Интересно было бы ваше мнение услышать.


    1. SeOd Автор
      30.08.2018 11:27

      Нет, не смотрел. Точнее как, название слышал, сайт окрывал, но не пробовал. Сейчас попробовал, но не судьба:

      бандлер который не смог