Как слямзить Хабр по-быстрому

  • Tutorial

Это произошло в понедельник 13 февраля, мир больше не будет прежним. Открыта вакансия:



Для работы над нашими проектами мы ищем сильного front-end разработчика. Вам предстоит амбициозная задача: внедрить компонентный подход в разработку всех текущих и новых проектов компании.

Отлично, работаем дальше! Выполняю "тестовое задание".


Процесс переноса нашего любимого сайта в react-компоненты черезвычайно прост. Нужно кликнуть кнопку [квадратик со стрелочкой курсора] в Chrome DevTools, скопировать выбранный HTML-блок в файл temp.html, добавить аттрибуты вида data-component="MyReactComponent" к будущим компонентам, и запустить в консоли:


$ html2react ./temp.html

Результат в ./components/ — готовые JSX-скрипты.


В чем прелесть компонентов? Декомпозиция задач более очевидна, оценивать трудозатраты можно точнее, в итоге проще управление процессом разработки. Постепенно сформируется понимание, как разделять блоки верстки на компоненты: просто много кода на один компонент, какой-то интерактив, дополнительный функционал. Главное пока не увлекаться, первая задача — Minimum Viable Product (MVP).


Ох, сколько же бывалых веб-разработчиков оттолкнула каша из HTML-тегов внутри JavaScript-кода. У меня аллергия прошла через три года, а тут ещё ES6 подоспел — кошмар ретрограда. Но всё к лучшему, за это время экосистема React-а выросла и окрепла. Как раньше люди жили без Redux-а, наверно мучились. :)


Отличная штука create-react-app, избавляет от небходимости настройки node-приложения на текущем начальном этапе, при том что не навязывает ничего лишнего.




И вот появилась первая страничка Хабра на localhost:3000 почти неотличимая от оригинала — это щастье!


Встречаются и ошибки в верстке, и legacy-блоки; код требует вдумчивого просмотра и редактуры. Заменил ссылки по шаблону "https://habrahabr.ru/" на "/#/", благо они почти везде абсолютные. Подключил @font-face, восстановил работоспособность плашки "#scroll_to_top".


// Переписал вызовы 

<a ... onclick="if (typeof ga === 'function') { ga('send', 'event', 'footer', 'links', 'ios_app'); }" >

// на новый лад 

<a ... onClick={ga('footer', 'links', 'ios_app')} >

// и добавил функцию

export const ga = (eventCategory, eventAction, eventLabel) => () => {
  if (typeof window.ga === 'function') {
    window.ga('send', 'event', eventCategory, eventAction, eventLabel)
  }
}

Задействую Redux, пока наполнил фейковые данные в initialState. Уже можно добавить новую статью из формы в Store. Сделал некоторые статические страницы по ссылкам из колонки "Инфо" в подвале, роутинг работает сносно, но требуется реализовать крошки в разделе "Помощь". Применяя "react-helmet", разблюдовал подключение css-файлов (т.к. страница 404 оформлена иначе). Использую "reselect" (пример в components/PostTeaserList.js — фильтр по потокам), "redux-act" (пример в reducers/editPost.js). Для обработки сайд-эффектов подключил "redux-thunk". Инлайновые стили добавляю с помощью "styled-components" (пример в components/InfoHelpPost.js).


Общее правило — не зацикливаться на деталях. Если задача не решается за 15 минут, или просто отвлекает, то откладываю с пометкой TODO. Есть мнение, что 85% времени занимают мелкие доработки. Важно быстро вырастить скелет, а наращивать мясо — последующие этапы через итеративную непрерывную интеграцию.


PS


Если вам нужно быстро реинкарнировать старое веб-приложение, тогда примерьте модный компонентный подход (привет, Delphi'95 & Rapid Application Development).


Леонид Аркадьевич, пользуясь случаем, ещё передаю приветы Mail.ru и Fast Line Ventures; упрощается "адаптация" успешных забугорных проектов. Кстати, для инди-разработчиков это способ поднять свой маленький свечной заводик: экономия на ТЗ, на дизайне, на верстке. Помните, что идеи ничего не стоят? Реализация тоже подвержена удешевлению.


Описанный метод переноса верстки в react-компоненты отлично применим и для текущих проектов на React, если ваша команда разделена на верстальщиков и разработчиков.


Когда хочется освоить новую технологию (например React), то лучший метод обучения — это копирование существующего проекта с использованием требуемой технологии. Как минимум, для портфолио на GitHub. Исходники проекта обновляются тут.


Демо: yobr.ru


Вводные видео-уроки

Share post

Comments 66

    +5
    Ох, сколько же бывалых веб-разработчиков оттолкнула каша из HTML-тегов внутри JavaScript-кода. У меня аллергия прошла через три года, а тут ещё ES6 подоспел — кошмар ретрограда. Но всё к лучшему, за это время экосистема React-а выросла и окрепла. Как раньше люди жили без Redux-а, наверно мучились. :)

    В том то и дело, что каша из кода и тегов — это далеко не серебрянная пуля, как бы React не был хорош. Да, модно, да, решает задачу, но всегда найдется альтернативный подход, а у него — сторонники.
      +2

      Беда в том, что нет единственно правильного ответа. Пока React победил в моих изысканиях. Предыдущая ставка на Meteor — потерянное время.

        +5

        С нетерпением ждём следующей серии в изысканиях :-)

          0

          Вообще-то я очарован Clojure. Но такую корову не продать.

          0
          Можете пару слов о Метеоре, пожалуйста? Начинаю на нём сейчас серьезный проект, интересны чужие грабли.
            +1

            Слишком интимный вопрос, многих ранила моя статья эту тему. Отвечу в личку.

              +1
                0
                Спасибо, очень полезно. Как-то упустил статью.
                +1

                Вот это самая лучшая статья по React+Redux из тех, что я перелопатил. Три раза перечитывал. И постиг Дзен.

                  0

                  Ещё хочу дать самые лестные рекомендации проекту Monster Lessons: JavaScript, React, Redux. Превосходная подача материала!

                    +2

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


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


                    Из того, что мне очень нравится в метеоре: он дает высокую скорость при создании скелета приложения, дает возможность писать минимум серверного кода (и, как правило, синхронного кода) и дает данные в реальном времени на клиенте. Из специфичного, мне нравится blaze, и вообще, то как реализована реактивность на фронтэнде. Из главных минусов, по моему мнению: это то что все завязано на монге и ddp, и, как следствие, узкая область применимости, но это и есть суть фреймворка. Я не считаю, что клиентская часть приложения может быть проблемой в принципе, так как есть полная свобода в выборе инструментов, но, конечно, есть свои особенности, вытекающие из протокола. Также я не могу судить о возможных проблемах, связанных с высокой нагрузкой, но судя по различным публикациям в интернете — дела обстоят более-менее, и это уже индивидуальный вопрос для каждого проекта.


                    Сам бы я решился начать новый проект на метеоре, если нужны данные в реальном времени на клиенте, если не нужна сложная бизнес-логика и если монга подходит в качестве хранилища, со всеми вытекающими критериями. Наверное, такими условиями отбрасываются 95% проектов, если не больше, и, конечно, есть множество других вопросов, которыми стоит задаться при выборе. Но в общем случае, я считаю, что метеор хорошо подходит для приложений, где важна коллаборация в реальном времени, например: форумы, хелпдески, системы управления задачами и т. д.


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

                      0
                      Спасибо за развернутый комментарий!
                      Но в общем случае, я считаю, что метеор хорошо подходит для приложений, где важна коллаборация в реальном времени, например: форумы, хелпдески, системы управления задачами и т. д.
                      Разделяю это мнение, правда кругозор в JS-фреймворках у меня не особо большой.
                        0

                        Ваш коммент больше, чем моя статья. Спасибо за аргументы.

                  +2

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


                  Добавление поста не работает, да и выглядит паршиво. Не удалось реализовать за 15 минут?


                  Я так и не понял, зачем вам тут потребовался реакт, чтобы нарисовать несколько статичных html-шаблонов, практически без динамики.

                    –1
                    При переходе между страницами сначала появляется контент, а только чрез пол секунды — к нему подцепляются стили — всё очень неприятно скачет. И это на одностраничном сайте.

                    Подправил костылем, как заставить Helmet подключать стили правильно — может кто подскажет?

                      0
                      Добавление поста не работает

                      F12, после отправки формы нужно смотреть сюда:



                      Я говорил про добавление в Store.

                        0

                        А что толку от добавления в стор без обновления интерфейса?


                        А helmet правильно стили добавляет, но делает это динамически, во время рендеринга. Очевидно, для продакшена такой подход не годится.

                          0
                          А helmet правильно стили добавляет, но делает это динамически, во время рендеринга. Очевидно, для продакшена такой подход не годится.

                          пробовал бороться с ним вот так, локально помогало, а в продакшене опять беда:


                            componentWillMount() {
                              document.body.style.display = 'none'
                            }
                          
                            componentDidMount() {
                              // HACK стили подключаются в Helmet после рендеринга страницы
                              setTimeout(
                                () => document.body.style.display = 'block'
                              , 100)
                            }
                            0

                            react-helmet, по-моему, слабо заточен на работу с ресурсами, требующими загрузки. Для них нужно маунтить основной компонент, когда ресурс уже загружен (чего может вообще никогда не произойти, например из-за 404). Он хорошо работает, когда нужно не внешний стиль/скрипт подключить, а прямо в тегах style/script писать css/js. А чем вам что-то вроде


                            import React from 'react';
                            
                            import styles from './styles.css';
                            
                            export default class HabrPage extends ReactComponent {...};

                            не нравится?

                              0

                              Правильная постановка вопроса — половина решения. Нужно отловить момент загрузки скриптов в Helmet!


                              А чем вам что-то вроде не нравится?

                              Всё дело в том, что для 404 на Хабре подключаются другие стили (смотри /components/NotFound.js). Т.е. нужно было как-то разблюдовать. Импорт же подключает стили в проект намертво. Есть мысли подгружать саму страницу динамически, используя require.

                                +1

                                А как-то так:


                                import React, { Component } from 'react'
                                import Helmet from 'react-helmet'
                                
                                import styles from './../../public/styles/access_deny.css'
                                
                                class NotFound extends Component {
                                  render() {
                                    return (
                                      <div id="layout">
                                        <Helmet
                                          defaultTitle="Хабрахабр"
                                        />
                                        <div className={styles.main}>
                                          <div className={styles.logo}>
                                            <a href="/#/" title="На главную страницу"><img alt="" src="https://habrahabr.ru/images/logo.svg"/></a>
                                          </div>
                                
                                          <h1>Страница не найдена</h1>
                                          <p>Страница устарела, была удалена или не существовала вовсе</p>
                                
                                          <div className={styles.buttons}>
                                            <a href="/#/" className={styles.button}>Вернуться на главную</a>
                                          </div>
                                        </div>
                                      </div>
                                    )
                                  }
                                }
                                

                                Вообще говоря, компонентный подход предполагает, что и js-код, и разметка, и стили хранятся если не в одном файле (есть любители CSS в JS непосредственно писать, но меня они в удобстве подхода не убедили), то очень близко друг от друга, как-то так:
                                src/components/NotFound/index.js
                                src/components/NotFound/styles.css


                                При переделке существующего приложения на реакт, я просто в каталог каждого компонента копировал все стили, импортировал их в JS, а уж потом в каждом отдельном компоненте вычищал лишнее.

                                  0

                                  Спасибо, до меня дошло! За лесом не видно деревьев. Так можно тупо переопределить стили для каждого HTML-тега в компоненте NotFound.

                                    0

                                    Так можно определить стили вообще для каждого компонента и забыть про них в других местах :)

                                      0

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

                                        0

                                        Я ручками всё делал :)

                                          0

                                          В режиме хакатона — нереально.

                                0

                                Победил подключение стилей в Helmet после его рендеринга!


                                код
                                class NotFound extends React.Component {
                                  constructor(props) {
                                    super(props)
                                    this.state = {
                                      isRenderedHelmet: false,
                                    }
                                  }
                                
                                  render() {
                                    return (
                                      <div className="not-found">
                                        <Helmet
                                          title="MyApp - 404"
                                          onChangeClientState={() => {
                                            if (!this.state.isRenderedHelmet)
                                              this.setState({
                                                isRenderedHelmet: true,
                                              })
                                          }}
                                        >
                                          <style type="text/css">{`
                                            html, body {
                                              height: 100%;
                                            }
                                            body {
                                              margin: 0;
                                              padding: 0;
                                              font-family: 'Roboto', sans-serif;
                                              overflow: hidden;
                                            }
                                            div#root {
                                              padding: 0 56px;
                                              display: flex;
                                              min-height: 100%;
                                              overflow: auto;
                                            }
                                            a {
                                              text-decoration: none;
                                            }
                                            .not-found {
                                              height: 50vh;
                                              margin: auto;
                                            }
                                            .logo img {
                                              width: 100px;
                                              height: 100px;
                                            }
                                            h1 {
                                              font-weight: normal;
                                            }
                                            .back-button {
                                              border: 2px solid gray;
                                              border-radius: 4px;
                                              padding: 8px;
                                              background: #eee;
                                              display: inline-block;
                                            }
                                            .back-text {
                                              color: black;
                                            }
                                          `}</style>
                                        </Helmet>
                                        {this.state.isRenderedHelmet &&
                                          <div>
                                            <div className="logo">
                                              <Link to="/" title="Go to the main page"><img alt="logo" src={logo} /></Link>
                                            </div>
                                            <h1>Page not found</h1>
                                            <p>The page is deprecated, deleted, or does not exist at all</p>
                                            <div className="back-button">
                                              <Link to="/"><span className="back-text">Go back to the main page</span></Link>
                                            </div>
                                          </div>}
                                      </div>
                                    )
                                  }
                                }
                                +2

                                Судя по комментариям в этом треде, напрашивается мысль "I had 99 problems. Then I used React. Now I have 101 problem."


                                В чем суть статьи? Показать, как худо-бедно сделать драфт? Или как использовать все вышеперечисленные библиотеки (react-helmet, redux-act, redux-thunk)? Но ведь ничего из этого нету в статье...

                                  –2

                                  Пока не хватает примера только для redux-thunk. Остальное все есть в исходниках, в статье указано, куда смотреть.

                                    0

                                    Ну так почему бы не не уместить это все в статью?

                                      0

                                      Первые пять минут фильма "Быстрее пули" со Скалой — шедевр. Потом всё скатилось, как обычно.


                                      Краткость — сестра сами знаете чего.

                                        +4

                                        По-моему вы не поняли, что я пытаюсь донести. Высказывание "краткость — сестра таланта" хорошо работает для Твиттера. На блогах вроде Хабра или Медиума, как мне кажется, пользователи хотят видеть статьи, раскрывающие какую-либо тему. Ссылку на репозиторий гитхаба можно уместить и в 140 символов.

                                          –1

                                          Все дело в том, что суть статьи осталась за горизонтом. :)

                            0
                            Добавление поста не работает, да и выглядит паршиво. Не удалось реализовать за 15 минут?

                            На это ушло три недели, и ещё есть над чем работать. Тынц.

                            0
                            восстановил работоспособность плашки "#scroll_to_top"

                            Самое главное на сделано! xD
                              0

                              Не понял, я что-то упустил? :)

                                0
                                Как раз наоборот! Уже не знаю сколько долго оно на хабре в хроме не работает, но я уже даже и не пытаюсь пользоваться этим =)
                                  0

                                  Тестировал исключительно в Хроме — вроде всё пучком. Хочется немного подробнее, ведь интересно же!

                              +12
                              А о чем статья?
                                0

                                О вискасе!

                                +1
                                Я правильно понял, что это все только фронтэнд без серверной части и сохранения?
                                  0

                                  Именно, и в этом вся прелесть. Есть конечно SSR, благодаря которому сможешь работать, как в привычном PHP. Но в данном примере все выполняется на клиенте. Если собрать под Electron-ом, то получим кросс-платформенное десктоп-приложение. Если заменить React на ReactNative, то получим мобильное приложение. Благодаря Redux-у, определена архитектура круговорота данных в приложении. Дальше нужно подключиться к внешнему REST-API или Websocket-серверу. И можно купить backend, как услугу, засматриваюсь на scorocode.ru

                                  +1
                                  Ох, я уже и забыл как выглядит «нативный» хабр :)
                                  StyleBot уже давно скрывает все мусорные блоки, объявления, кросс-ссылки.
                                  Жаль родительский сайт для сохранения скриптов лежит, кто-нибудь знает достойные альтернативы StyleBot?
                                    0

                                    Спасибо за лестный эпитет. Но до нативного Хабра ещё ой как далеко.

                                      –1

                                      Понял, что затупил. Но все равно спасибо, "ваш отзыв очень важен для нас". :)

                                      0
                                        0
                                        Внезапно, а ведь ещё недавно «всё было». Впрочем, не первый раз.
                                        0
                                        небольшая ошибочка, не щастье, а счастье
                                          0

                                          В данном случае именно щастье — это высшая форма банального счастья :)

                                          +1
                                          Мне кажется, что «слямзить», это значит — «украсть».
                                            0

                                            Только если оставить в кавычках.

                                            +1
                                            // в этом месте:
                                            
                                            export const ga = (eventCategory, eventAction, eventLabel) => {
                                              if (typeof window.ga === 'function') {
                                                window.ga('send', 'event', eventCategory, eventAction, eventLabel)
                                              }
                                            }
                                            
                                            // добавил замыкание:
                                            
                                            export const ga = (eventCategory, eventAction, eventLabel) => () => {
                                              if (typeof window.ga === 'function') {
                                                window.ga('send', 'event', eventCategory, eventAction, eventLabel)
                                              }
                                            }
                                            
                                            // теперь вместо похабного:
                                            
                                            <a ... onClick={ga.bind(void 0, 'footer', 'links', 'ios_app')} >
                                            
                                            // несравненная красота:
                                            
                                            <a ... onClick={ga('footer', 'links', 'ios_app')} >
                                            
                                              0

                                              Но! Этот способ плох для передачи props в PureComponent, onClick={ga(...)} при каждом рендеринге будет возвращать новую функцию. Для понимания вопроса — видео.

                                                +1

                                                Мемоизация?

                                                  0

                                                  Переведи! :)

                                                    0

                                                    Внутренний кеш для уже возвращенных ранее сочетаний параметров. Есть готовые решения типа https://github.com/medikoo/memoizee

                                                      0

                                                      Только стоит учитывать, что без отслеживания зависимостей этот кеш будет храниться до скончания веков, так что не стоит мемоизировать ответы сервра.

                                                        0

                                                        вот она, сила функционально программирования на практике

                                                          0

                                                          заменил на lodash.memoize

                                                            +1

                                                            отдельное спасибо за memoizee, lodash.memoize работает "иначе":


                                                            import memoizee from 'memoizee'
                                                            import memoize from 'lodash.memoize'
                                                            
                                                            const test1 = memoizee((a, b, c) => (d) => {
                                                              console.log('test1', a, b, c, d)
                                                            })
                                                            const a1 = test1(1,2,3)
                                                            const b1 = test1(1,2,3)
                                                            const c1 = test1(1,2,4)
                                                            console.log('a1 === b1', a1 === b1) // a1 === b1 true
                                                            console.log('a1 === c1', a1 === c1) // a1 === c1 false
                                                            a1(1) // test1 1 2 3 1
                                                            b1(2) // test1 1 2 3 2
                                                            c1(3) // test1 1 2 4 3
                                                            
                                                            const test2 = memoize((a, b, c) => (d) => {
                                                              console.log('test2', a, b, c, d)
                                                            })
                                                            const a2 = test2(1,2,3)
                                                            const b2 = test2(1,2,3)
                                                            const c2 = test2(1,2,4)
                                                            console.log('a2 === b2', a2 === b2) // a2 === b2 true
                                                            console.log('a2 === c2', a2 === c2) // a2 === c2 true - как-так-то?
                                                            a2(1) // test2 1 2 3 1
                                                            b2(2) // test2 1 2 3 2
                                                            c2(3) // test2 1 2 3 3 - как-так-то?
                                                        0

                                                        А будет ли оно работать в таком виде?


                                                        import memoize from 'lodash.memoize'
                                                        
                                                        export const ga = (eventCategory, eventAction, eventLabel) => memoize(() => {
                                                          if (typeof window.ga === 'function') {
                                                            window.ga('send', 'event', eventCategory, eventAction, eventLabel)
                                                          }
                                                        })
                                                          0

                                                          неправильно! оборачивать нужно снаружи и применять memoizee (почему — смотри коммент выше)

                                                    +1

                                                    Изменил структуру папок: вместо раздельных actions и reducers, объединил попарно файлы в папке ducks. Мы согласились на HTML внутри JS, зачем же бегать за экшенами и редюсерами по разным файлам?


                                                    ducks/editPost.js


                                                    import { createAction, createReducer } from 'redux-act'
                                                    
                                                    export const actions = {
                                                      inputTitle: createAction('@@edit_post/INPUT_TITLE', title => ({ title })),
                                                      submit: createAction('@@edit_post/SUBMIT', post => post),
                                                    }
                                                    
                                                    const initialState = {
                                                      flow: '',
                                                      title: '',
                                                      content: ''
                                                    }
                                                    
                                                    const reducer = createReducer({
                                                      [actions.inputTitle]: (state, { title }) => ({...state, title}),
                                                      [actions.submit]: (state, post) => ({...state, ...post})
                                                    }, initialState)
                                                    
                                                    export default reducer

                                                    На мой вкус — замечательно!

                                                      0

                                                      Косяк на git-pages, видимо придется переезжать на firebase.com, там тоже можно подцепить свой домен.


                                                      Изменил везде в коде < a > на < Link >, и вернул browserHistory вместо hashHistory. Теперь роутинг работает нормально без хешей.

                                                        +1

                                                        А что такое git-pages, расскажите. То github pages знаю (он же gh pages), а что такое git pages — не ведаю.

                                                      Only users with full accounts can post comments. Log in, please.