Подписаться на блог

Фрирайтинг, заметки, шитпостинг и все подряд (18+)

Позднее Ctrl + ↑

Диплом, часть 2. Сборка и CI

Как уже было сказано в предыдущем посте, диплом я буду писать полностью на JavaScript. 2018 год, поэтому никакие Gulp’ы (или Makefile’ы) для сборки JS я использовать не буду. Обойдусь одним WebPack.

Изначально я планировал собирать только FrontEnd, но потом и BackEnd решил тоже включить в сборку, ибо это даст больше профита, т. к. нативно NodeJS не всё хорошо поддерживает. Да и это отличная возможность пощупать TypeScript везде, где возможно.

Т. к. собираются и клиент и сервер, то будет соответственно 2 конфига webpack’a. Ничего особенного в них нет, если не брать во внимание, что в каждом конфиге есть и babel, и TypeScript. Приложение я планирую писать всё-таки на JS, а всякую сложную логику на TypeScript. В результате такой “архитектуры” у меня и в конфигах некоторая путаница (на первый взгляд), и линтеров целых 2. Ну, опять же в первую очередь я хочу хоть чуть пощупать TypeScript, так что приемлемо.

Конфиги клиента так же имеют кучу всяких loader’ов для стилей, картинок и всего-всего, но в них углубляться не буду. “Для вязкости” были добавлены всякие плагины.

В production-сборке:

config.plugins = [
  new CaseSensitivePathsPlugin(),
  new HappyPack({
    id: 'JavaScript',
    threads: Math.min(os.cpus().length, 4),
    loaders: [{
      loader: 'babel-loader',
    }],
  }),
  new webpack.optimize.CommonsChunkPlugin({
    name: 'vendor',
    minChunks(module) {
      return module.context && module.context.indexOf('node_modules') !== -1;
    },
  }),
  new JavaScriptObfuscator({
    rotateUnicodeArray: true,
  }),
  new HtmlWebpackPlugin({
    title: 'Event-Listener',
    template: '../src/client/templates/production.html',
  }),
  new CompressionPlugin({
    algorithm: 'gzip',
  }),
];

В development-сборке:

config.plugins = [
  new HappyPack({
    id: 'JavaScript',
    threads: Math.min(os.cpus().length, 4),
    loaders: [{
      loader: 'babel-loader',
    }],
  }),
  new webpack.optimize.CommonsChunkPlugin({
    name: 'vendor',
    minChunks(module) {
      return module.context && module.context.indexOf('node_modules') !== -1;
    },
  }),
  new HtmlWebpackPlugin({
    title: 'Event-Listener',
    template: '../src/client/templates/development.html',
  }),
  new CaseSensitivePathsPlugin(),
];

Серверный конфиг оказался поинтереснее для меня, ибо их я никогда не писал, да и в результате поиска решений некоторых ошибок всплыли интересные особенности. Серверную часть, конечно, так же можно писать и на JS, и на TS. По итогу конфиг webpack’а для сборки сервера выглядит так:

const config = {
  entry: {
    server: path.resolve(__dirname, '../src/server/index.js'),
  },
  target: 'node',
  externals: fs.readdirSync(path.resolve(__dirname, '../node_modules'))
    .reduce((acc, mod) => {
      if (mod === '.bin') {
        return acc;
      }

      acc[mod] = 'commonjs ' + mod;
      return acc;
    }, {}),
  node: {
    console: false,
    global: true,
    process: true,
    Buffer: false,
    __filename: false,
    __dirname: false,
  },
  output: {
    path: path.resolve(__dirname, '../build/server'),
    filename: '[name].js',
    publicPath: '/',
  },
  resolve: {
    extensions: [
      '.ts',
      '.js',
      '.json',
    ],
  },
  module: {
    loaders: [
      {
        test: /\.ts$/,
        exclude: /node_modules/,
        loader: 'awesome-typescript-loader',
      },
      {
        test: /\.js$/,
        exclude: /node_modules/,
        loader: 'babel-loader',
      },
      {
        test: /\.json$/,
        loader: 'json',
      },
    ],
  },
};

CI/CD одна из тех вещей, которую я делал не потому что надо (в отличие от webpack-конфига), а потому что интересно было всё это настроить. В предыдущем посте я рассказал, что деплой на Heroku — просто. Не совсем так.

Я хотел, чтобы билд производился на GitLab CI, а уже собранное приложение заливалось на Heroku.
Первая проблема: dpl заливал “чистую” версию приложения, без билда, без ещё чего-то. Я долго не мог понять, почему так, нормальной документации у dpl нет и не было (либо я не нашел). Но, где-то в обсуждениях GitLab CI я нашел решение: параметр —skip-cleanup. Теперь “скрипт”, производящий деплой выглядит так:

- dpl --provider=heroku --app=el-dev --api-key=$HEROKU_API_KEY --skip-cleanup

Для stage’а “Deploy” в качестве зависимости указан stage “Build”:

dependencies:
  - "Build"

Stage “Build” производит сборку и сохраняет результат на 3 часа (не знаю, почему именно на 3, но просто сделал так):

"Build":
  image: node:8.9.2
  stage: Checks
  dependencies:
    - "Install"
  script:
    - npm run build
  artifacts:
    expire_in: 3 hrs
    paths:
      - assets
      - build
      - public

С деплоем собранного приложения разобрались, но так же на Heroku улетало множество конфигов, исходников и подобных бесполезных для production’а файлов. Нужно было удалять всё лишнее. В *nix довольно несложно решить эту проблему, однако большая часть способов построена на использовании глобальных параметров системы или изменении. Использовать эти способы CI не позволяет, поэтому решением стал небольшой sh-скрипт, удаляющий всё лишнее:

#!/bin/bash

find ./ \
     -type f \
     -maxdepth 1 \
     -mindepth 1 \
     -name "*" \
     ! -name "Procfile" \
     ! -name "app.json" \
     ! -name "package.json" \
     ! -name "package-lock.json" \
     -exec rm -r {} \;

find ./ \
     -type d \
     -maxdepth 1 \
     -mindepth 1 \
     -name "*" \
     ! -name "public" \
     ! -name "assets" \
     ! -name "build" \
     ! -name ".git" \
     -exec rm -r {} \;

После удаления можно заливать только необходимые файлы на Heroku.
Кстати, Heroku передаёт вообще все переменные проекта через process.env.VARIABLE, например, параметры MongoDB.

Схема CI в Gitlab

Схема CI в Gitlab

Теперь можно начать писать чисто код, изредка добавляя loader’ы в конфиг клиентского webpack и не о чем другом не думать. Особенно хорошо, что в TypeScript я могу погружаться совсем понемногу и использовать его только там, где нужно, хотя, вероятно, для подобного использования стоило бы просто втянуть Flow, но уже поздно (да и мне как-то лень)…

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

GitLab CI & Telegram

Последнее время стал замечать, что всё больше людей юзают GitLab, отмечая, что у него клёвый CI/CD. Не знаю, как правильно всё это оценивать, но попробовать я решил однозначно.

Во-первых, переместил статические сайтики с GitHub Pages на GitLab Pages. Во-вторых, сделал небольшое ExpressJS-приложение и разместил на GitLab, а так же настроил деплой на Heroku, что, оказалось, очень просто.

GitLab CI, конечно, крут, много полезных примеров и доков, из коробки поддерживается множество интеграций и фич, но мне была нужна интеграция с Telegram, т. к. он всегда включен.

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

Задача поставлена, сложностей никаких (на самом деле они возникали, но это я доки невнимательно читал). Делаем.

Чтобы взаимодействовать с Telegram создадим бота: пишем @BotFather, создаём обычного бота, загружаем аватарку и т. п. Всё это делается максимально просто. После создания бота в чатик прилетит token для работы с API.

Бот будет слать уведомления пользователю с нужным ID, но просто так Telegram не позволяет рассылать сообщения от бота любому пользователю, поэтому новосозданному боту нужно отправить хотя бы какое-то сообщение от пользователя, которому должны приходить уведомления.

Сохраняем нужные значения в GitLab:

  1. Settings нужного репозитория;
  2. Пункт CI/CD;
  3. Secret variables;
  4. Сохранить в переменную TELEGRAM_BOT_TOKEN token, присланный после создания бота через BotFather;
  5. В TELEGRAM_USER_ID сохраняем ID пользователя, которому будут отправляться уведомления.

Для удобной отправки уведомлений создадим скрипт в `ci-notify.sh`, в котором будем обращаться к API Telegram и отправлять сообщение нужному пользователю через созданного бота:

#!/bin/bash

TIME="10"
URL="https://api.telegram.org/bot$TELEGRAM_BOT_TOKEN/sendMessage"
TEXT="Deploy status: $1%0A%0AProject:+$CI_PROJECT_NAME%0AURL:+$CI_PROJECT_URL/pipelines/$CI_PIPELINE_ID/%0ABranch:+$CI_COMMIT_REF_SLUG"

curl -s --max-time $TIME -d "chat_id=$TELEGRAM_USER_ID&disable_web_page_preview=1&text=$TEXT" $URL > /dev/null

В качестве первого параметра для этого скрипта должен передаваться статус деплоя. В .gitlab-ci.yml описываем вызов этого скрипта в нужное время с нужными параметрами:

stages:
- deploy
- notify

deploy:
  image: node:4.2.2
  stage: deploy

  script:
  #deploy success
  - sh .ci-notify.sh ✅

  artifacts:
    paths:
    - public
  only:
  - master

notify_error:
  stage: notify
  script:
  - sh .ci-notify.sh ❌
  when: on_failure #deploy fail

Для обозначения статуса деплоя я использовал Emoji. В случае успешного выполнения деплоя просто вызывается скрипт с нужны параметром. В случае неудачного деплоя “управление переходит” в следующий stage, в котором происходит уведомление о неудачном деплое.

Таким образом, где-то за 10 минут можно настроить простые уведомления в Telegram из GitLab CI и хоть чуть-чуть почувствовать себя DevOps’ом. Если подзаморочиться, то можно расширить возможности всего этого, но мне как-то лень.

Диплом, часть 1. Главное начать

Итак, 4 курс. В конце этого учебного года надо защищать диплом.
За процессом написания диплома надо как-то наблюдать, ну а чтобы как-нибудь потом порефлексировать, буду сюда писать небольшие посты.

В любом деле самое сложное — начать. Благо, начать писать диплом мне помог Hacktoberfest. А как именно — я рассказал на последнем митапе WebPurple.

В целом, всё, что я получил от Open Source сообщества — заготовки общих концепций. Нужно переходить к конкретики.

Результатом моего диплом, в теории, должно быть готовое программное обеспечение, решающуе какую-либо проблему. Моя тема — “ПО для организации мероприятий”. Под мероприятиями подразумеваются митапы, конференции, etc. Тема несложная, что и как должно работать я понимаю, поэтому с продумыванием реализации проблем возникнуть не должно.

С технологиями я решил чуть-чуть подзагнаться (хотя мог бы взять PHP и нашлёпать за день), поэтому буду писать диплом с надеждой изучить что-то новое и закрепить знания в том, что знаю.

На клиенте у меня будет типичная связка React + Redux, к которым я прикручу TypeScript (ибо давно хотел его подробно пощупать), а так же SASS (чтобы стили просто писались).
На сервере будет NodeJS и Epress, а в качестве БД — MongoDB с Mongoose. Этот стек серверных технологий мне кажется наиболее подходящим для текущей задачи, да и он мне интересен сильнее других.

Так как относительно недавно я открыл в себе интерес к DevOps, то всё это должно быть максимально автоматизировано, тестироваться, деплоиться и выполнять трюки само. Для этого я планирую использовать GitLab CI (потому что он клёвый).

Именно с CI/CD я и начал. Попытался сделать конфиг, который бы деплоил приложение на Express на Heroku. Вышло что-то типа этого:

stages:
  - heroku

before_script:
  - apt-get update -y
  - apt-get install apt-transport-https -y

deploy:
  stage: heroku
  script:
    - apt-get update -yq
    - apt-get install rubygems ruby-dev -y
    - gem install dpl
    - dpl --provider=heroku --app=el-dev --api-key=$HEROKU_API_KEY

К моему удивлению всё это с первого раза задеплоилось и заработало, исключая SSL-сертификаты, но это уже проблемы моего конфига Cloudflare.

На этоп часть первая окончена, следующим шагом планирую написать все конфиги (babel, webpack, etc) для development и production.

Production-билд будет крутиться на diploma.ifedyukin.ru.

HEXO

Где-то через месяц у меня кончится студенческая подписка на Github, в результате чего все мои приватные репозитории перейдут в режим read-only. Так-то оно особо не волнует, исключая 2 репозитория: блог и сайт.

Сайт представлял (и представляет) собой статичную страницу, а блог — jekyll-приложение. Они хостились на github pages.

И вот, наконец, настал тот момент, когда я решил переехать на Gitlab. Во-первых, из-за бесплатных приватных репозиториев. Во-вторых, из-за клёвого CI из коробки. В-третьих, из-за нормальных правил использования.

Конечно, я решил чуть-чуть изменить структуру своих веб-ресурсов. В первую очередь я переехал на HEXO, вместо Jekyll. Не знаю зачем, просто он клёвый.

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

Да и тема у блога теперь именно та, что я хотел, взял ”Minos” и чуть-чуть подправил напильником. Надеюсь, в результате этих изменений буду почаще писать, ибо мысли надо где-то собирать, чтобы в будущем перечитывать и рефлексировать (что я очень люблю).

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

Каррирование в JS

Сегодня столкнулся с одной задачкой, которая по своему типу напоминает типичную вайтборд-задачу.
Есть функция:

const add = (a, b, c, d) => a + b + c + d;

Для неё нужно реализовать функцию-обёртку возвращающую соответствующие значения при таких вызовах:

const a = getWrap(add, 1)
console.log(a(2)(4));     // 8
console.log(a(1)(2)(3));  // 9

const b = getWrap(add);
console.log(b(5)(5, 5));  // 15
console.log(b(5, 5)(5));  // 15

Одним из условий является использование каррирования.

Карринг (currying) или каррирование — термин функционального программирования, который означает создание новой функции путём фиксирования аргументов существующей.

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

Промежуточное значение вычислений будет храниться в переменной count, а каррированная функция задаваться следующим образом:

const curried = val ? fn.bind(null, val) : fn;

Если требуется зафиксировать переменную, то каррируем переменную с помощью метода bind(), в который в качестве контекста вызова передаём null, так как в данном случае он нам не важен, а следующим аргументом — фиксируемый аргумент.
Если фиксировать переменную не требуется, то каррированная и каррируемая функции будут одинаковы.

Результатом выполнения каррированной функции будет функция, сохраняющая вычисления в переменную count и возвращающая саму себя, так же следует учесть количество аргументов, передаваемых в эту функцию, так как результатом вызова функции add с аргументами, хотя бы один из которых не задан (является undefined) будет NaN:

const func = function (...par) {
  const params = [...par];
  for (let i = 0; i < curried.length; i++) {
    params[i] = params[i] || 0;
  }
  count += curried(...params);
  return func;
};

Когда в функцию больше не передаётся параметров, нужно возвращать результат, это задачу решим с помощью метода объектов valueOf() (функция является объектом), а так же обнулять сохранённое значение результата, чтобы при следующем вызове вычисления были корректны:

func.valueOf = function () {
  const result = count;
  count = 0;
  return result;
};

В целом моё решение задачи имеет следующий вид:

function getWrap(fn, val = 0) {
  let count = 0;
  const curried = val ? fn.bind(null, val) : fn;

  const func = function (...par) {
    const params = [...par];
    for (let i = 0; i < curried.length; i++) {
      params[i] = params[i] || 0;
    }
    count += curried(...params);
    return func;
  };

  func.valueOf = function () {
    const result = count;
    count = 0;
    return result;
  };

  return func;
}

React Higher-Order Components

Несколько расстраивает, что сюда ничего не пишу, так что буду вкидывать какие-то заметки и кусочки кода.
Начнём с простого и понятного примера Higher-Order Components в React.

Enhance.jsx

import { Component } from "React";

export var Enhance = ComposedComponent => class extends Component {
  constructor() {
    this.state = { data: null };
  }
  componentDidMount() {
    this.setState({ data: 'Hello, World!!!' });
  }
  render() {
    return <ComposedComponent {...this.props} data={this.state.data} />;
  }
};

HigherOrderComponent.jsx

import { Enhance } from "./Enhance";

class MyComponent {
  render() {
    if (!this.data) return <div>React loading...</div>;
    return <div>{this.data}</div>;
  }
}

export default Enhance(MyComponent);

Запуск Karma в Chrome — Travis-CI

Если вы тестируете свой код и используете GitHub, то скорее всего вы уже используете Travis, однако, при тестировании JavaScript в Travis может возникнуть проблема — Travis не умеет запускать тесты в Chrome, но преодолеть её можно всего несколькими строчками config-файлов.

Для начала нам нужно настроить сам travis, в моём случае `.travis.yml` выглядит следующим образом:

language: node_js
node_js:
- '6.2'
before_install:
- export CHROME_BIN=chromium-browser
- export DISPLAY=:99.0
- sh -e /etc/init.d/xvfb start
- sudo apt-get update
- sudo apt-get install -y libappindicator1 fonts-liberation
- wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb
- sudo dpkg -i google-chrome*.deb
script:
- npm install
- npm test

Внимание здесь стоит обратить на блок `before_install`.

Для Karma тоже нужно несколько изменить файл настроек — создадим лаунчер, который будет использоваться только в окружении Travis:

module.exports = function (config) {
    var configuration = {
        // configs
        
        browsers: ['Chrome'],
        customLaunchers: {
            Chrome_travis_ci: {
                base: 'Chrome',
                flags: ['--no-sandbox']
            }
        },
      
        // configs
    };

    if (process.env.TRAVIS) {
        configuration.browsers = ['Chrome_travis_ci'];
    }

    config.set(configuration);
}

На этом процедура настройки закончена — пушим изменения на Github и можем наблюдать процесс тестирования в “консоли” Travis соответствующего репозитория.

Ранее Ctrl + ↓