Я хочу потратить немного больше времени, чем обычно, на объяснение, для кого это руководство. Целевая аудитория - это читатели, которые были примерно на том уровне знаний, который был у меня две недели назад:

  • Вы уже писали проекты на React.
  • Вы понимаете, почему проверка типов, введенная Flow, может оказаться полезной.
  • Но масса проблем с настройкой среды разработки и написанием кода без того, чтобы вас беспокоить Flow, продолжает появляться.
  • Иногда вы пытаетесь скопировать и вставить код с официальных сайтов, но это даже не работает.

Да, новая эра JavaScript иногда может напоминать Дикий Запад. Если вы просто хотите увидеть исходный код проекта, который работает с последними версиями всего по состоянию на 16 мая 2018 года, вот, пожалуйста.

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

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

Сначала добавьте Nuclide в вашу Atom IDE, если вы еще этого не сделали. Автоматический сервер Flow будет очень полезен, и вы всегда можете отключить пакет позже.

Откройте свой проект в Nuclide после его создания с помощью:

create-react-app panda_factory

Затем следуйте инструкциям на сайте Flow, чтобы добавить его в проект. Я использовала варианты Пряжа + Вавилон.

Хорошо, вот где мы немного сбились с пути.

  1. Установите флажок «Использовать двоичный файл Flow, включенный в потоковую корзину каждого проекта» в разделе: Настройки - ›Пакеты -› Nuclide - ›Nuclide-flow (без этого сервер Flow может не знать, что обслуживать)
  2. В .babelrc добавьте «response» в массив предустановок (без этого команда babel yarn run babel src/ -- -d lib/ не будет знать, как обрабатывать и Flow, и React)
  3. В корне проекта выполните команду yarn run flow init (без этого вы не получите файл .flowconfig, который был необходим для запуска Flow в моем случае)

Добавьте в начало файла index.js:

// @flow

Ничего не произошло? Мне нужно было запустить мой сервер Flow, запустив

yarn run flow

Еще ничего? Попробуйте перезапустить Nuclide и написать ужасно неправильный код с проверкой типов, например:

let x: number = “actually a string”

Хорошо, надеюсь, теперь все работает. И вы должны увидеть, что Flow злится на код по умолчанию в index.js.

Да, Flow достаточно строг, чтобы начать делать снимки по шаблону в index.js. Вам нужно изменить тело файла с:

ReactDOM.render(<App />, document.getElementById('root')
registerServiceWorker()

to:

const root = document.getElementById('root')
if (root)
  ReactDOM.render(<App />, root)
registerServiceWorker()

Flow был обеспокоен тем, что мы можем отрисовывать элемент HTML, прежде чем подтвердить его существование.

Хорошо, а теперь давайте немного структурируем наш проект. В разделе «src» создайте папки «components» и «imgs».

Пожалуйста, заполните папку imgs изображениями здесь (любезно предоставлено pixabay.com).

Что касается компонентов, пожалуйста, создайте файл Panda.js. Начните писать то, что станет классическим заголовком для вашего JavaScript:

// @flow
import React from 'react'

Хорошо, теперь мы можем заняться классными вещами, связанными с Flow. Уточним, какой реквизит возьмет на себя наша панда:

type Props = {
  id: number,
  name: string,
  age: number,
  hobby: string,
  img: string
}

И давайте создадим функциональный компонент React с этими реквизитами:

const Panda = (props: Props) => (
  <div>
    <p>Name: {props.name} | Age: {props.age} | Hobby: {props.hobby}</p>
    <img src={require("../imgs/panda"+props.img+".jpg")} alt="most likely a panda" />
  </div>
)

Компоненты Functional React обычно полезны, когда в компоненте нет состояния, и вы не хотите писать полноценный класс.

Но ждать! Flow раздражен тем, что в нашей require функции не используется «строковый литерал». Строковый литерал означает, что в строке нет изменчивости. "I like hamburgers" - строковый литерал. "I would like to eat " + x + " hamburgers" не является строковым литералом, потому что существует некоторая вариативность в том, какой окажется строка.

Flow хочет, чтобы мы использовали только строковые литералы, чтобы убедиться, что ресурс, который нам «требуется», действительно существует в определенном месте. В этом случае нам действительно нужно, чтобы изображение, которое использует компонент panda, было различным. Поэтому, чтобы вежливо попросить Flow игнорировать необходимость использования строковых литералов в вызовах require, в .flowconfig (в корне проекта) добавьте в [options] следующее:

module.ignore_non_literal_requires=true

Наконец, в конце нашего файла Panda.js давайте экспортируем нашу работу:

export type { Props as PandaType }
export default Panda

Мне потребовалось несколько попыток понять этот синтаксис типа экспорта, но теперь мы можем разрешить другим файлам импортировать «PandaType», а не «Props», что не имеет смысла вне контекста.

Давайте перейдем на уровень выше к нашему файлу App.js и действительно увидим на экране несколько панд. Чтобы запустить файл:

// @flow
import React, { Component } from 'react'
import logo from './imgs/logo.svg'
import './App.css'
import Panda, { type PandaType } from './components/Panda.js'

Обратите внимание на причудливый синтаксис для импорта типа, а также компонент Panda из только что созданного файла Panda.js. В этом случае слово type находится внутри фигурных скобок, а не снаружи, как это было при экспорте.

Теперь давайте настроим наши свойства и типы состояний:

type Props = {||}
type State = {
  pandas: Array<PandaType>,
  count: number
}

Да, если вы помните index.js, компонент App на самом деле не принимает никаких свойств. Итак, почему там вообще Props? Что ж, когда мы напишем класс компонента, мы увидим, что в нем есть места для свойств и состояния. После энергичных любительских экспериментов я обнаружил, что невозможно получить тип состояния без некоторого заполнителя для типа реквизита. Вот что я имею в виду:

class App extends Component<?Props, State> {
  constructor(props: ?Props) {
    super(props)
    this.state = {pandas: [], count: 0}
    this.getUpdatedCount = this.getUpdatedCount.bind(this)
    this.addNewPanda = this.addNewPanda.bind(this)
  }

Я попытался указать, что Props является заполнителем с ?, что на самом деле не требуется для запуска кода. {||} - это соглашение Facebook для пустого объекта. Трубки означают «точное значение» (т.е. ничего больше нельзя добавить, в отличие от других типов Flow).

Еще одно замечание, которого, похоже, не было в документации, заключается в том, что я получал ошибки, если не отмечал параметр props в конструкторе как тип Props:

constructor (props: Props) { ... }

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

Если мы хотим вызвать this.getUpdatedCount() из класса App, JavaScript по умолчанию не сможет найти эту функцию. Вы получите ужасный undefined. Это потому, что даже код внутри класса, вероятно, имеет this привязку к глобальному контексту, а не к контексту класса. Нашей getUpdatedCount функции не существует! Итак, мы должны привязать getUpdatedCount к this этого класса специально. Надеюсь, это не слишком запутало. Существуют пакеты с автоматическим связыванием, которые сделают это за вас, но я предпочитаю делать это вручную для небольшого количества функций класса.

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

getUpdatedCount = (): number => {
    const newCount = this.state.count + 1
    this.setState({count: newCount})
    return newCount
}
addNewPanda = (panda: PandaType) => {
    const newPandas = [...this.state.pandas, panda]
    this.setState({pandas: newPandas})
}

Почему мы используем стрелочные функции внутри класса, когда мы делали constructor обычным способом? На самом деле это обходной путь, позволяющий привязать функции к классу в конструкторе, чтобы Flow не рассердился.

Если вы помните, когда мы выполняли эту важную привязку, мы фактически устанавливали одно из свойств каждой функции с помощью оператора присваивания (=). Это большой запрет для Flow, который требует, чтобы функции были только для чтения. Это один из ключевых принципов, позволяющих работать Flow. Разработчикам Flow было бы намного сложнее, если бы им также пришлось учитывать тот факт, что люди могут изменять свойства функций.

Так что мы обходим эту проблему, используя стрелочную функцию вместо того, чтобы напрямую использовать функцию. Обратите внимание, что первое решение, упомянутое в связанной проблеме GitHub с (this:any) error’d для меня.

Хорошо, давайте закончим App.js:

render() {
    const pandaComponents = []
for (let i=0; i<this.state.pandas.length; i++)
      pandaComponents.push(<Panda key={i} id={this.state.pandas[i].id}
        name={this.state.pandas[i].name} age={this.state.pandas[i].age}
        hobby={this.state.pandas[i].hobby} img={this.state.pandas[i].img} />)
return (
      <div className="App">
        <header className="App-header">
          <img src={logo} className="App-logo" alt="logo" />
          <h1 className="App-title">Panda Factory</h1>
        </header>
        <br />
        <div className="App-intro">
          {pandaComponents}
        </div>
      </div>
    )
  }
}
export default App

Попробуйте заполнить начальное состояние массива pandas, объявленное в конструкторе, некоторыми фиктивными данными, соответствующими формату нашего PandaType. Это приведет к появлению на экране нескольких панд! Пример:

{id: 0, name: "Todd", age: 5, hobby: Dance Dance Revolution, img: "1"}

Также я немного подправил App.css:

.App {
  text-align: center;
}
.App-logo {
  animation: App-logo-spin infinite 20s linear;
  height: 200px;
}
.App-header {
  background-color: #222;
  height: 350px;
  padding: 20px;
  color: white;
}
.App-title {
  font-size: 2em;
}
.App-intro {
  font-size: large;
}
@keyframes App-logo-spin {
  from { transform: rotate(0deg); }
  to { transform: rotate(360deg); }
}

Попробуйте запустить yarn start или npm start.

Я живу этими моментами - когда весь JavaScript работает, а мой экран заполнен пандами.

Во второй части мы рассмотрим синтетические события, ссылки и дочерние элементы React с помощью Flow. Перевод: мы создадим небольшую форму, в которой пользователь сможет указать, каких панд он хочет создать, и сконструировать компонент-контейнер.

"Увидимся там!"