Содержание

В результате предыдущих статей у нас получилось приложение на Go, которое может обслуживать небольшой кусочек HTML. Эта статья расскажет о клиентской части, которая, увы, состоит в основном, из JavaScript, а не Go.


JavaScript в 2017 году


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


Виды JS


Наиболее распространенная в наши дни разновидность JS известна как ES2015 (он же ES6 или ECMAScript 6-е издание) и в основном поддерживается более-менее свежими браузерами. Последняя выпущенная спецификация JavaScript — ES7 (он же ES2016), но, поскольку браузеры все еще догоняют ES6, похоже, что ES7, как таковой, никогда не будет принят, потому что, скорее всего, в 2017 году выйдет следующий ES8, который заменит ES7 в плане ожидания готовности браузеров.


Это странно, но похоже, что нет простого способа создать среду, полностью соответствующую конкретной версии ECMAScript. Невозможно даже вернуться к старой полностью поддерживаемой версии ES5 или ES4, а следовательно нет возможности проверить ваш скрипт на соответствие. Максимум, что вы можете сделать, — это протестировать его во всех доступных браузерах и надеяться на лучшее.


Из-за постоянно изменяющейся и значительно различающейся поддержке языка среди платформ и браузеров, транспиляция возникла как общая идея решения этой проблемы. Транспиляция, в основном, сводится к преобразованию кода в такой, который соответствует конкретной версии ES или специфичной среде. Например, import Bar from 'foo'; может стать var Bar = require('foo');. Поэтому, если какая-то конкретная особенность не поддерживается, ее можно сделать доступной с помощью подходящего плагина или транспилятора. Я подозреваю, что феномен распространенности транспиляции привел к дополнительным проблемам, таким, что ввод, ожидаемый транспилятором, предполагающим существование более не поддерживаемой особенности, совпадает с выводом. Часто это может быть исправлено дополнительными плагинами, и бывает очень трудно в этом разобраться. Неоднократно я проводил много времени пытаясь заставить что-то заработать, чтобы позднее выяснить, что весь мой подход устарел из-за нового и лучшего решения, встроенного теперь в какой-то другой инструмент.


JS фреймворки


Также есть много разногласий по поводу того, какой фреймворк JS — самый лучший. Этот вопрос еще более запутывает тот факт, что один и тот же фреймворк может радикально отличаться от версии к версии. Удивительно, почему бы при этом просто не сменить название.


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


React и JSX


Если вы не знаете, что собой представляет React, то вот вам мое (технически неверное) объяснение: это HTML, встроенный в JavaScript. У всех нас промыты мозги насчет того, что JavaScript является встраиваемым в HTML и это и есть естественный порядок вещей, так что инвертирование этой взаимосвязи не приходит никому в голову. Из-за фундаментальной простоты этого революционного (!) принципа я считаю React гениальным.


"Hello World!" в React выглядит примерно так:


class Hello extends React.Component {
  render() {
    let who = "World";
    return (
      <h1> Hello {who}! </h1>
    );
  }
}

Обратите внимание — HTML-код начинается без всяких оберток или разделителей. Как ни странно, открывающая угловая скобка (”<”) работает достаточно надежно в качестве маркера, обозначающего начало HTML-кода. А внутри HTML открывающая фигурная скобка указывает, что мы временно возвращаемся в JavaScript, и таким образом значения переменных подставляются внутри HTML. Это почти все, что вам необходимо знать, чтобы “постичь” React.


Технически, вышеупомянутый формат файла известен как JSX, в то время как React — это библиотека, которая предоставляет классы для создания React-объектов, таких как React.Component выше. JSX транспилируется в обычный JavaScript с помощью инструмента, известного как Babel, и, фактически, JSX даже не требуется — React-компонент может быть написан на чистом JavaScript, и есть подход, в котором React используется без JSX. Лично я считаю, что способ "без JSX" несколько более шумный, и мне нравится, что Babel позволяет использовать более современный диалект JS (хотя, не иметь дела с транспиляторами — это, определенно, плюс).


Минимально работающий пример


Для начала, нам понадобятся три внешние библиотеки JavaScript. Это (1) React и ReactDOM, (2) браузерный транспилятор Babel и (3) небольшая библиотека под названием Axios, которая пригодится для выполнения JSON HTTP запросов. Я возьму их из Cloudflare CDN, но можно воспользоваться и каким-нибудь другим способом. Для подключения библиотек нужно расширить нашу переменную indexHTML до вот такого вида:


const (
    cdnReact           = "https://cdnjs.cloudflare.com/ajax/libs/react/15.5.4/react.min.js"
    cdnReactDom        = "https://cdnjs.cloudflare.com/ajax/libs/react/15.5.4/react-dom.min.js"
    cdnBabelStandalone = "https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/6.24.0/babel.min.js"
    cdnAxios           = "https://cdnjs.cloudflare.com/ajax/libs/axios/0.16.1/axios.min.js"
)

const indexHTML = `
<!DOCTYPE HTML>
<html>
  <head>
    <meta charset="utf-8">
    <title>Simple Go Web App</title>
  </head>
  <body>
    <div id='root'></div>
    <script src="` + cdnReact + `"></script>
    <script src="` + cdnReactDom + `"></script>
    <script src="` + cdnBabelStandalone + `"></script>
    <script src="` + cdnAxios + `"></script>
    <script src="/js/app.jsx" type="text/babel"></script>
  </body>
</html>
`

В самом конце теперь загружается "/js/app.jsx", который нам еще предстоит создать. В предыдущей части с помощью http.Dir() мы заполнили поле в структуре настройки UI под названием cfg.Assets. Теперь нам нужно обернуть его в обработчик, который обслуживает файлы, и Go легко нам это обеспечит:


    http.Handle("/js/", http.FileServer(cfg.Assets))

Таким образом, все файлы в каталоге "assets/js" будут доступны по пути "/js/".


Теперь создадим сам файл assets/js/app.jsx:


class Hello extends React.Component {
  render() {
    let who = "World";
    return (
      <h1> Hello {who}! </h1>
    );
  }
}

ReactDOM.render( <Hello/>, document.querySelector("#root"));

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


Если мы сейчас зайдем на главную страницу браузером (JS-совместимым), то мы увидим “Hello World”.


Как это работает: браузер загрузил “app.jsx”, как ему было сказано, но, поскольку “jsx” — это незнакомый тип файла, браузер просто проигнорировал его. Когда Babel получил свой шанс поработать, он просмотрел наш документ на предмет наличия тэгов script с типом “text/babel” и повторно запросил эти страницы (поэтому они дважды появляются в инструментах разработчика, но второй запрос должен быть обслужен полностью из кэша браузера). Затем Babel трансформировал этот скрипт в корректный JavaScript и выполнил его, что, в свою очередь, заставило React фактически отобразить “Hello World”.


Список людей


Сначала нам нужно вернуться к серверной части и создать URI, который будет выводить список людей. Для этого нам нужен http-обработчик, который может выглядеть так:


func peopleHandler(m *model.Model) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        people, err := m.People()
        if err != nil {
            http.Error(w, "This is an error", http.StatusBadRequest)
            return
        }

        js, err := json.Marshal(people)
        if err != nil {
            http.Error(w, "This is an error", http.StatusBadRequest)
            return
        }

        fmt.Fprintf(w, string(js))
    })
}

И нам надо зарегистрировать его:


    http.Handle("/people", peopleHandler(m))

Теперь, если мы зайдем на "/people", мы должны получить в ответ "[]". Если мы добавим запись в нашу таблицу людей с помощью:


INSERT INTO people (first, last) VALUES('John', 'Doe');

Ответ должен измениться на [{"Id":1,"First":"John","Last":"Doe"}].


Наконец, нам нужно подключить наш React/JSX код, чтобы он все отобразил.


Для этого мы создадим компонент PersonItem и еще один, под названием PeopleList, который будет использовать PersonItem.


Компонент PersonItem должен только знать, как отобразить себя в виде строки таблицы:


class PersonItem extends React.Component {
  render() {
    return (
      <tr>
        <td> {this.props.id}    </td>
        <td> {this.props.first} </td>
        <td> {this.props.last}  </td>
      </tr>
    );
  }
}

PeopleList немного сложнее:


class PeopleList extends React.Component {
  constructor(props) {
    super(props);
    this.state = { people: [] };
  }

  componentDidMount() {
    this.serverRequest =
      axios
        .get("/people")
        .then((result) => {
           this.setState({ people: result.data });
        });
  }

  render() {
    const people = this.state.people.map((person, i) => {
      return (
        <PersonItem key={i} id={person.Id} first={person.First} last={person.Last} />
      );
    });

    return (
      <div>
        <table><tbody>
          <tr><th>Id</th><th>First</th><th>Last</th></tr>
          {people}
        </tbody></table>

      </div>
    );
  }
}

У него есть конструктор, который инициализирует переменную this.state. Также в нем объявлен метод componentDidMount(), который будет вызван React'ом в тот момент, когда компонент должен приготовиться к отрисовке, т.е. это правильное место (одно из), чтобы получить данные с сервера. Метод получает данные через вызов Axios и сохраняет результат в this.state.people. Наконец, render() перебирает содержимое this.state.people, создавая для каждого элемента экземпляр PersonItem.


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


Заключение


По сути, это все, что вам нужно знать, чтобы создать полнофункциональное веб-приложение на Go. У этого приложения, конечно, есть ряд недостатков, которые, я, по возможности, рассмотрю позже. Например, транспиляция в браузере не идеальна, и хотя такой вариант подойдет для не очень ценного приложения, для которого не важно время загрузки страницы, вероятно, мы захотим найти способ предварительной транспиляции. Кроме того, наш JSX ограничен одним файлом, такой подход затруднит управление любым приложением серьезного размера с большим количеством компонентов. Приложение не имеет навигации. Нет стиля. Есть вещи, о которых я забываю…


Наслаждайтесь!


P.S. Весь код полностью находится тут.


Продолжение

Поделиться с друзьями
-->

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


  1. JPEG
    28.05.2017 00:04
    +1

    > babel-standalone/6.24.0/babel.min.js

    А webpack в 2017 году еще не завезли?


    1. kilgur
      28.05.2017 00:22
      +1

      Немного завезли :) В 4й части (скоро выложу) автор слегка упомянул webpack.


    1. comerc
      28.05.2017 10:05

      Для начинающих webpack не нужен. Достаточно Create React App.


      1. JPEG
        28.05.2017 11:31

        Это не по хардкору. Автору так будет слишком скучно.


      1. GreatKoshak
        28.05.2017 12:55

        Стоп-стоп-стоп, я думал, что в create-react-app используется webpack, просто он заботливо сконфигурирован для нас, разве нет?


        1. comerc
          28.05.2017 15:50

          просто он заботливо сконфигурирован для нас

          именно, а потому "скрипач не нужен".


  1. danforth
    28.05.2017 00:18
    +2

    Я все ваши посты не читал, но мне немного не ясна логика завязывать шаблон и JS скрипты прямо в бинарнике, не подскажете, для чего вы так делаете?
    Я, допустим, при инициализации сервера просто читаю index.html файл в слайс байт, и оттуда уже пишу его через

    w.Write(index)
    

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


    1. kilgur
      28.05.2017 00:21
      -2

      Это сильно упрощает развертывание приложения (deploy). В процессе разработки/отладки удобнее, разумеется, пользоваться внешней статикой. Можно разделить сборку на develop/production, а в коде это учитывать (если есть статика в бинарнике, используем ее, в противном случае пользуемся внешней).


      1. JPEG
        28.05.2017 11:36

        Ребята, мы про embedded devices говорим для дешевого холодильника, или про веб в 2017 году? Там у автора файловая система-то хоть есть? А ядро? Меня переполняют чувства :)