Менеджер проекта в тисках. Почему так и что с этим делать

Заметка из двух частей: почему в тисках и что с этим делать.

Дано

  1. Начальство — наделяет полномочиями и спрашивает с него за результат, который, напомню, создал не он.
  2. ПМ — сам не выполняет задачи, организовывает работу других.
  3. Команда — выполняет задачи.

Как думаете, кому в этой цепочке некомфортней всего? Правильно, нашему герою-менеджеру, потому что его зажали с двух сторон.

Команда

Он ею управляет. Она даёт результат и обратную связь. Именно команда выполняет проект.

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

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

Начальство

Менеджер проекта им подчиняется. Они требуют выполнения проектов. Снова два варианта: карт-бланш и игнорирование.

С первым всё ясно — начальство доверяет, не суется в проект, ожидает результатов сообразно доверенным полномочиям.

Кстати, важнейший момент — ставя задачу управленцу, нужно понимать, есть ли у него власть это сделать? Условно, собрать маркетолога, HR-а и сделать что-то с HR-брендом. А эти двое, вообще, ему хоть как-то подчиняются? Например проектно. Что им будет, если не придут на встречу? Если ничего, то и HR бренд поручать ему не надо. Не его цирк, не его обезьяны.

Второй вариант — начальство рассказывает байки, что нужно поставить себя, просит доказать свою профпригодность, говорит

«Давай, у тебя все карты на руках — жги»

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

«Ну вы же ничего не показали пока, я чувствую надо вмешаться»

Такое начальство, обычно, не разбирается ни в менеджменте, ни в организации. Даже с 20-ю годами опыта управления. Молдавский бизнес — привет. Потому что бизнес устойчив тогда, когда работает система управления. Убрать даже собственника — ничего не рухнет. А здесь отправить этого горе-начальника на больничный, к примеру, рухнет проект, отдел, направление. При том, что и с ним некрепко то стояло.

Что же делать проджект-менеджеру?

Для начала, не попадать в компании со вторыми опциями — тщательно выбирать место работы. Особенно внимательно — начальство.

Коротко:

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

Если всё же, угораздило попасть в тиски, как меня однажды, то еще проще: осознать эти моменты, и уходить. Предварительно подготовив запасной аэродром.

Увы, способа решения таких вопросов без заявления по собственному желанию — я не вижу. Если ваш руководитель не понимает элементарных вещей — ему не объяснить, вы подчиненный и не вам его учить. Если команда токсична — опять вопрос кто это допустил? Будут ли её распускать ради нового ПМ-а? Не будут. Проще вам поискать нормальную.

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

P.S. Здесь коротко, а в следующей публикации опишу подробно свой опыт собеседований, найма и адаптации, а также капканы которые могут ждать на этом пути.

ссылка на оригинал статьи https://habr.com/ru/post/490170/

Создаем прогрессивный PWA интернет-магазин на Nuxt.js 2 пошаговое руководство Часть 1

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

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

В этой статье я хочу поделиться как создать примитивный интернет-магазин:

  • Который будет быстро загружаться у пользователя.
  • Который полюбит Google (или любой другой поисковик) с точки зрения SEO.

Для упрощения восприятия процесса в этой статье не будет разбираться создание backend api, так как эта тема довольно объёмная и тянет на отдельную статью.

Введение

Преимущество прогрессивных (PWA) фреймворков вроде Nuxt.js в том, что:

  • Вам не нужно заботиться об отдаче html с помощью пререндера как в случае работы с SPA для поисковых роботов.
  • Более быстрое взаимодействие с сайтом, в сравнении со статическими сайтами, за счет подгрузки только необходимых js chunks, css styles и api запросов (большую часть этого процесса автоматизирует webpack 4, который работает под капотом nuxt.js)
  • Высокие показатели Google Lighthouse при правильно настройке, о которой будет идти речь в статье (можно набрать 100/100 даже на слабом сервере).
  • CSS Modules, Babel из коробки

Структура интернет магазина:

  • Главная страница с каталогом товаров
  • Страница категорий со списком товаров
  • Карточка товара
  • Страница оформления заказа

Возможности приложения:

  • Модальные окна (нужные при нажатии на кнопку купить).
  • Хранение идентификатора пользователя в cookie (без авторизации для упрощения).
  • Ленивая подгрузка и гидрация компонентов.
  • Сжатие изображений, стилей, скриптов, html кода и всего что только можно.
  • 2 версии сайта: 1) для последних браузеров которым не нужен транспилированный код (через Babel) 2) Legacy версия транспилированная через Babel.
  • Умная загрузка шрифтов без FOUT, FOIT, FOFT (распространённая проблема при использовании своих шрифтов, пока они не загрузятся у пользователя, он не будет видеть текст).
  • Поддержка всех браузеров от IE 10.
  • Поддержка svg.
  • Работа со всеми SEO тегами.
  • Автоопределение устройства пользователя ещё до рендера сайта, для отдачи нужного html контента.
  • Работа с API с помощью XHR запросов.

Установка Nuxt

И так начнем. Я буду использовать Node v12.16.1 и Yarn v1.22.0.
Создаем папку, открываем её вставляем package.json такого содержания и прописываем в консоли yarn install

package.json

{   "name": "habr",   "version": "1.0.0",   "private": true,   "scripts": {     "analyze": "cross-env NODE_ENV=production  nuxt build --analyze",     "build": "cross-env NODE_ENV=production nuxt build",     "buildandstart": "cross-env NODE_ENV=production nuxt build && cross-env NODE_ENV=production nuxt start",     "dev": "cross-env NODE_ENV=development NUXT_PORT=3000 nuxt",     "start": "cross-env NODE_ENV=production nuxt start"   },   "dependencies": {     "@nuxtjs/axios": "5.9.5",     "@nuxtjs/style-resources": "1.0.0",     "@nuxtjs/svg": "0.1.6",     "cookie-universal-nuxt": "2.1.1",     "cross-env": "7.0.0",     "cssnano": "4.1.10",     "cssnano-preset-advanced": "4.0.7",     "imagemin-mozjpeg": "^8.0.0",     "imagemin-webpack-plugin": "^2.4.2",     "intersection-observer": "^0.7.0",     "node-sass": "^4.13.1",     "normalize.css": "8.0.1",     "nuxt": "2.11.0",     "nuxt-trailingslash-module": "1.1.0",     "nuxt-webfontloader": "^1.1.0",     "sass-loader": "^8.0.2",     "vue-js-modal": "1.3.31",     "vue-lazy-hydration": "^1.0.0-beta.12",     "vue-lazyload": "1.3.3",     "vue-svg-loader": "0.11.0",     "vuelidate": "^0.7.5"   },   "devDependencies": {     "@nuxtjs/eslint-config": "2.0.0",     "babel-eslint": "8",     "eslint": "^6.8.0",     "eslint-config-prettier": "6.10.0",     "eslint-config-standard": "14.1.0",     "eslint-friendly-formatter": "4.0.1",     "eslint-loader": "3.0.3",     "eslint-plugin-array-func": "3.1.3",     "eslint-plugin-import": "2.20.1",     "eslint-plugin-jest": "23.7.0",     "eslint-plugin-lodash": "6.0.0",     "eslint-plugin-no-loops": "0.3.0",     "eslint-plugin-no-use-extend-native": "0.4.1",     "eslint-plugin-node": "11.0.0",     "eslint-plugin-prettier": "3.1.2",     "eslint-plugin-promise": "4.2.1",     "eslint-plugin-security": "1.4.0",     "eslint-plugin-standard": "4.0.1",     "eslint-plugin-vue": "6.1.2",     "prettier": "1.19.1",     "prettier-eslint": "9.0.1"   } } 

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

Теперь по порядку о каждом пакете:

  • @nuxtjs/axios Удобная работа с XHR запросами.
  • @nuxtjs/style-resources Позволяет создать файл с глобальными переменными для sass, который будет включен на этапе сборке в каждый компонент.
  • cookie-universal-nuxt Универсальное api для работы с cookie как на сервере так и клиенте
  • cross-env Актуально если работаете из под Windows, позволяет прописывать переменные окружения (можно вместо него использовать dotenv)
  • cssnano Хороший модуль для сжатия css, устранение дубликатов и тд.
  • image-webpack-loader Пережимаем JPG, GIF, WEBP, PNG с потерей или без потери качества.
  • intersection-observer Полифил для Intersection Observer API (нужен для ленивой загрузки картинок и компонентов).
  • node-sass Webpack загрузчик для scss.
  • normalize.css Сбрасываем дефолтные стили браузеров.
  • nuxt-trailingslash-module Удаляем лишние слэши в конце адресной строки (для SEO).
  • nuxt-webfontloader Умная загрузка шрифтов.
  • vue-js-modal Модальные окна.
  • vue-lazy-hydration Уменьшаем Max Potential First Input Delay (очень узкое место при работе с любыми SSR фреймворками, так как после получения html и скриптов, они начинаются гидрацию, которая увеличивает этот показатель при наличии множества компонентов на странице).
  • vue-lazyload Ленивая подгрузка картинок.
  • vue-svg-loader SVG загрузчик.
  • vuelidate Удобная валидация форм.

Структура проекта

--assets    --scss   --img   --svg --components --pages   --category   --checkout   --product --layouts --plugins  --store  --middleware (функции которые будут вызываться при переходах между страницами) --serverMiddleware (то же самое но только server only)

  • assets Здесь хранятся все ассеты, которые мы позже будем импортировать вручную.
  • components Вручную задаем удобную для нас структуру папок со всеми компонентами Vue
  • pages Папка, в которой, мы складываем наши страницы, которые позже будет переданы как маршруты в Vue Router. Позже разберём это подробнее.
  • layouts Папка, в которой, мы складываем обвёртки вокруг страниц, в которых, мы задаём общие для страниц компоненты (вроде шапки и футера).
  • plugins В этой папке мы будем задавать внешние плагины в зависимости от разных условий. Об этом позже.

Создаём layout

Мы должны задать дефолтную страницу и layout, чтобы для начала хотя бы отрендерить корень сайта.

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

default.vue

<template>     <nuxt/> </template>  <script> export default {   computed: {     meta () {       return [         { charset: 'utf-8' },         {           name: 'viewport',           content: 'width=device-width, initial-scale=1, maximum-scale=1 shrink-to-fit=no'         },         { hid: 'description', name: 'description', content: 'Главная' }       ]     }   },    head () {     const canonical = `https://mysite.com${this.$route.path       .toLowerCase()       .replace(/\/$/, '')}`     return {       meta: [         ...this.meta        ],       script: [         // { src: 'https://markknol.github.io/console-log-viewer/console-log-viewer.js' }       ],       link: [{ rel: 'canonical', href: canonical }]     }   } } </script>

Пока что в теге template мы имеем слот в который Nuxt будет рендерить контент каждой страницы с layout по умолчанию. Позже мы вернёмся и добавим туда шапку и футер.

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

Далее свойство head(), которое вызывает Nuxt у всех страниц и шаблонов. В него мы передаём метатеги, а так же задаём канонически ссылки для сайта.

В данном случае не будем мудрить, и вернем просто объект. Который Nuxt отрендерит в это:

<meta data-n-head="ssr" charset="utf-8"> <meta data-n-head="ssr" name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1 shrink-to-fit=no"> <meta data-n-head="ssr" data-hid="description" name="description" content="Главная"> <link data-n-head="ssr" rel="canonical" href="https://mysite.com">

Где:

  • data-n-head="ssr" Служебный атрибут на который можно не обращать внимание.

  • data-hid="description" Ранее, в объекте, мы указали свойство hid: ‘description’. Оно не является обязательным, но говорит Nuxt присвоить этому конкретному тегу уникальный id, для того, чтобы позже мы могли на других страницах этот тег менять, обращаясь к нему по id. Это очень полезно так как description в layout задан стандартный и для каждого товара и категории товаров он будет своим уникальным (что нужно для SEO).

  • link: [{ rel: ‘canonical’, href: canonical }] Очень полезно задавать каноническую ссылку потому, что по умолчанию все страницы в Nuxt регистронезависимые и "слешнезависимые", а это создаёт ненужные нам дубликаты в индексе поиска.
    Этим тегом мы динамически задаём тег canonical для всех страниц использующих стандартный шаблон. Технически это не уберёт возможность обращаться к страницам по-разному.
    Например, site.com/product/myProduct, site.com/product/myProduct/, site.com/product/MyProduct/ будут разным адресами одной и той же страницы, но у всех их будет одинаковый canonical, что уберёт дубликаты из индекса поисковика.

    <link data-n-head="ssr" rel="canonical" href="https://site.com/product/myproduct">

    Для удобства можно вынести эту функцию в отдельный файл, но пока оставим это так для наглядности.

  • Так же вы могли заметить закомментированный тег в script. Эту вещь можно смело раскомментировать во время дебага. Зашли на сайт с телефона и автоматически открылась консоль браузера на телефоне. Очень удобно так как можно не прибегать к более сложным инструментам отладки и экономить себе время.

Про остальные теги

  • name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1 shrink-to-fit=no" запрещаем зум на мобильных устройствах (кроме Safary iOS).
  • charset="utf-8" задаём кодировку страницы

Создаём страницу

Мы создали шаблон, создадим и корневую страницу. В папке pages создаём index.vue

index.vue

<template>     <div> <h1>Привет Хабр!</h1>     </div> </template>  <script>     export default {      } </script>

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

Конфигурация Nuxt

Теперь дадим Nuxt инструкцию как готовить наш сайт. В корне проекта создаём файл nuxt.config.js

nuxt.config.js

const imageminMozjpeg = require('imagemin-mozjpeg') const ImageminPlugin = require('imagemin-webpack-plugin').default const isDev = process.env.NODE_ENV !== 'production'  module.exports = {   mode: 'universal',   ...(!isDev && {     modern: 'client'   }),    head: {     htmlAttrs: {       lang: 'ru'     },     title: 'Nuxt APP',     meta: [       { hid: 'description', name: 'description', content: 'Интернет-магазин' }     ],     link: [       { rel: 'shortcut icon', href: 'favicon.ico' }     ]   },   rootDir: __dirname,   serverMiddleware: [   ],   router: {     prefetchLinks: false   },   loading: { color: '#ddd' },   css: [     'normalize.css',     './assets/scss/global-styles.scss'   ],   plugins: [   ],   modules: [     // Doc: https://axios.nuxtjs.org/usage     '@nuxtjs/axios',     'nuxt-trailingslash-module',     'nuxt-webfontloader',     'cookie-universal-nuxt',     '@nuxtjs/style-resources'   ],    webfontloader: {     events: false,     google: {       families: ['Montserrat:400,500,600:cyrillic&display=swap']     },     timeout: 5000   },   styleResources: {     // your settings here     // scss: ['./assets/scss/global-variables.scss'], // alternative: scss     less: [],     stylus: []   },   /*   ** Axios module configuration   */   axios: {     // See https://github.com/nuxt-community/axios-module#options   },   render: {     // http2: {     //     push: true,     //     pushAssets: (req, res, publicPath, preloadFiles) => preloadFiles     //     .map(f => `<${publicPath}${f.file}>; rel=preload; as=${f.asType}`)     //   },     // compressor: false,     resourceHints: false,     etag: false,     static: {       etag: false     }   },   /*   ** Build configuration   */   build: {     optimizeCss: false,     filenames: {       app: ({ isDev }) => isDev ? '[name].js' : 'js/[contenthash].js',       chunk: ({ isDev }) => isDev ? '[name].js' : 'js/[contenthash].js',       css: ({ isDev }) => isDev ? '[name].css' : 'css/[contenthash].css',       img: ({ isDev }) => isDev ? '[path][name].[ext]' : 'img/[contenthash:7].[ext]',       font: ({ isDev }) => isDev ? '[path][name].[ext]' : 'fonts/[contenthash:7].[ext]',       video: ({ isDev }) => isDev ? '[path][name].[ext]' : 'videos/[contenthash:7].[ext]'     },     ...(!isDev && {       html: {         minify: {           collapseBooleanAttributes: true,           decodeEntities: true,           minifyCSS: true,           minifyJS: true,           processConditionalComments: true,           removeEmptyAttributes: true,           removeRedundantAttributes: true,           trimCustomFragments: true,           useShortDoctype: true         }       }     }),     splitChunks: {       layouts: true,       pages: true,       commons: true     },     optimization: {       minimize: !isDev     },     ...(!isDev && {       extractCSS: {         ignoreOrder: true       }     }),     transpile: ['vue-lazy-hydration', 'intersection-observer'],     postcss: {       plugins: {         ...(!isDev && {           cssnano: {             preset: ['advanced', {               autoprefixer: false,               cssDeclarationSorter: false,               zindex: false,               discardComments: {                 removeAll: true               }             }]           }         })       },       ...(!isDev && {         preset: {           browsers: 'cover 99.5%',           autoprefixer: true         }       }),        order: 'cssnanoLast'     },     extend (config, ctx) {       const ORIGINAL_TEST = '/\\.(png|jpe?g|gif|svg|webp)$/i'       const vueSvgLoader = [         {           loader: 'vue-svg-loader',           options: {             svgo: false           }         }       ]       const imageMinPlugin = new ImageminPlugin({         pngquant: {           quality: '5-30',           speed: 7,           strip: true         },         jpegtran: {           progressive: true          },         gifsicle: {           interlaced: true         },         plugins: [           imageminMozjpeg({             quality: 70,             progressive: true           })          ]       })       if (!ctx.isDev) config.plugins.push(imageMinPlugin)        config.module.rules.forEach(rule => {         if (rule.test.toString() === ORIGINAL_TEST) {           rule.test = /\.(png|jpe?g|gif|webp)$/i           rule.use = [             {               loader: 'url-loader',               options: {                 limit: 1000,                 name: ctx.isDev ? '[path][name].[ext]' : 'img/[contenthash:7].[ext]'               }             }           ]         }       })       //  Create the custom SVG rule       const svgRule = {         test: /\.svg$/,         oneOf: [           {             resourceQuery: /inline/,             use: vueSvgLoader           },           {             resourceQuery: /data/,             loader: 'url-loader'           },           {             resourceQuery: /raw/,             loader: 'raw-loader'           },           {             loader: 'file-loader' // By default, always use file-loader           }         ]       }        config.module.rules.push(svgRule) // Actually add the rule     }   } } 

Теперь подробно остановимся на каждом пункте конфига

  • const imageminMozjpeg = require('imagemin-mozjpeg') const ImageminPlugin = require('imagemin-webpack-plugin').default

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

  • const isDev = process.env.NODE_ENV !== 'production'

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

  • mode: 'universal'

    Задаём явно, что мы хотим получить SSR приложение (а не SPA).

  • ...(!isDev && { modern: 'client' }),

    Используем ES6 синтаксис для динамического расширения объекта. В данном случае !isDev означает, что modern: ‘client’ свойство объекта будет добавлено только в production. modern: ‘client’ говорит Nuxt создать 2 бандла, один из которых использует ES6 Modules синтаксис поддерживаемый последними браузерами, а второй Legacy транспилированный через Babel. В html будет по 2 тега на каждый js скрипт вида:

    <script nomodule src="***" defer></script><script type="module" src="***" defer>

    Браузер будет загружать только один из пары.

  • head: { htmlAttrs: {   lang: 'ru' }, title: 'Nuxt APP', meta: [   { hid: 'description', name: 'description', content: 'Интернет-магазин' } ], link: [   { rel: 'shortcut icon', href: 'favicon.ico' } ] }

    Создаём дефолтный Head, как мы это делали в нашем layout, но в данном случае этот head будет стандартным для всего приложения. Указываем русский язык, дефолтный Title и Description и путь к нашей favicon.ico (которой пока что нет в проекте, но создадим её позже).

  • rootDir: __dirname

    Явно прописываем что считать корнем проекта при использовании абсолютных путей импорта.

  • router: { prefetchLinks: false }

    Отключаем дефолтный механизм Nuxt, который улучшает восприятие UI в некоторых случаях. Этот механизм подгружает страницу как только ссылка на неё попадает в область видимости окна браузера. То есть ещё до клика на ссылку, страница будет уже загружена и пользователь без задержки откроет ссылку. Но в интернет-магазине будут сотни ссылок и нам не нужно, чтобы каждая из них автоматически подгружалась (если у вас слабый сервер, а у клиентов 2G).

  • loading: { color: '#ddd' }

    Бегущая полоска при загрузке страниц, вверху страницы. Это индикатор загрузки, которому можно задать любой цвет или отключить или вставить свой. Он достаточно умный и срабатывает не только при переходе между страницами но при при ожидании ответов по XHR запросам.

  • css: [ 'normalize.css' ]

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

  • modules: [ '@nuxtjs/axios', 'nuxt-trailingslash-module', 'nuxt-webfontloader', 'cookie-universal-nuxt', '@nuxtjs/style-resources' ]

    Подключаем Nuxt модули, которые по сути являются обычными плагинами для Vue, но уже со встроенным механизмом install, который их запускает в нужном месте. Нам не нужно их вручную добавлять во Vue instance, а только прописать для некоторым из них конфиги, о которых будет далее идти речь.

  • webfontloader: { events: false, google: {   families: ['Montserrat:400,500,600:cyrillic&display=swap'] }, timeout: 5000 }

    Задаем конфиг для модуля nuxt-webfontloader. Указываем какой шрифт хотим загрузить. В данном случае берём открытый шрифт из Google Fonts. Когда все шрифты будут загружены, модуль добавит в html тег, класс wf-active. &display=swap означает, что после получения html, когда браузер начнет исполнять js код, это модуль динамически добавит css файл с нужными нам шрифтами. Так же этот модуль умеет вызывать хуки на всех этапах загрузки, но они нам не нужны сейчас, поэтому пропишем events: false.

  • styleResources: { scss: ['./assets/scss/global-variables.scss'], // alternative: scss less: [], stylus: [] }

    Задаем конфиг для модуля @nuxtjs/style-resources. Этот модуль автоматически импортирует указанные файлы стилей для всех компонентов. Обычно используется для подключения глобальных переменных SCSS на весь проект. В данном случае у нас ещё нет файла global-variables.scss, но мы его добавим позже, а модуль не вызовет ошибку, если не найдет файл.

  • axios: { // See https://github.com/nuxt-community/axios-module#options },

    Задаем конфиг для модуля axios. Пока что оставим конфиг пустым.

  • render: { // http2: { //     push: true, //     pushAssets: (req, res, publicPath, preloadFiles) => preloadFiles //     .map(f => `<${publicPath}${f.file}>; rel=preload; as=${f.asType}`) //   }, // compressor: false, resourceHints: false // etag: false, // static: { //  etag: false // } },

    Nuxt из коробки имеет под капотом Web Server, который умеет в http2. Но так как проект сейчас на localhost, а для http2 нужен https, то дабы не городить костыли мы просто не будет включать пока что эту опцию.
    compressor это Gzip сжатие, которое по умолчанию отдаёт всё сжатым (html, js, css, статику). Подробнее об этом можно почитать здесь https://www.npmjs.com/package/compression Я лишь добавлю, что у себя в production я эту опцию отключил compressor: false так как там Nuxt используется только для render, а всю статику отдаёт Nginx с настроенным сжатием, поэтому дабы дважды не делать одну и ту же работу, можно это отключить. Но для примера мы будем использовать Nuxt для отдачи статики и не будем трогать эту опцию.
    То же самое и с etag, если будет использоваться Nginx, то стоит явно отключить etags.
    resourceHints мы явно отключим, чтобы не предзагружать страницы (из той же оперы что и prefetchLinks: false) .

  • optimizeCss: false

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

    filenames: {   app: ({ isDev }) => isDev ? '[name].js' : 'js/[contenthash].js',   chunk: ({ isDev }) => isDev ? '[name].js' : 'js/[contenthash].js',   css: ({ isDev }) => isDev ? '[name].css' : 'css/[contenthash].css',   img: ({ isDev }) => isDev ? '[path][name].[ext]' : 'img/[contenthash:7].[ext]',   font: ({ isDev }) => isDev ? '[path][name].[ext]' : 'fonts/[contenthash:7].[ext]',   video: ({ isDev }) => isDev ? '[path][name].[ext]' : 'videos/[contenthash:7].[ext]' },

    Nuxt сам подставит в Webpack эту часть конфига. Здесь мы задаем то, что во время development хотим видеть красивые имена файлов, а в production build раскидываем файлы по папкам и в качестве имени используем contenthash. Это элегантное решение распространённой проблемы, когда вы скажем обновили приложение на production, а у клиентов все js скрипты и стили в кеше бразуера. Чтобы инвалидировать этот кеш, мы просто каждый раз генерируем у всех файлов название, которое является хеш-функцией контента внутри этого файла. Соответственно если какой-то файл поменяется, то при следующем заходе пользователя на страницу, браузер будет требовать уже файл с другим именем, а те что не поменялись останутся с тем же именем и будут взяты и с кеша браузера.

Как мы видим все файлы и даже html закеширован браузером, что сводит практически к 0 весь трафик между сервером и браузером.

  • ...(!isDev && {   html: {     minify: {       collapseBooleanAttributes: true,       decodeEntities: true,       minifyCSS: true,       minifyJS: true,       processConditionalComments: true,       removeEmptyAttributes: true,       removeRedundantAttributes: true,       trimCustomFragments: true,       useShortDoctype: true     }   } })

    Отключаем для Development всю минификацию html, чтобы ускорить процесс разработки.

  • splitChunks: {   layouts: true,   pages: true,   commons: true },

    Разбиваем на независимые чанки всё приложение.

  • optimization: {   minimize: !isDev },

    Отключаем минификацию js для development.

  • ...(!isDev && {   extractCSS: {     ignoreOrder: true   } }),

    По умолчанию Nuxt автоматически добавляет все стили проекта прямо внутрь html через тег style. Это уменьшает количество запросов к серверу, так как у нас нигде не будет нужно браузеру загружать css файлы отдельно. Но это так же лишает браузер возможности кешировать css стили. Поэтому для development мы включаем inline styles, а в production разбиваем их на чанки для каждого компонента и создаем отдельные файлы.
    ignoreOrder: true Нужно задать, чтобы во время сборки нам не выдавал webpack ложные предупреждения о найденных конфликтах и дубликатах, это своеобразное решение бага #4885.

  • transpile: ['vue-lazy-hydration', 'intersection-observer']

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

  • postcss: {   plugins: {     ...(!isDev && {       cssnano: {         preset: ['advanced', {           autoprefixer: false,           cssDeclarationSorter: false,           zindex: false,           discardComments: {             removeAll: true           }         }]       }     })   },   ...(!isDev && {     preset: {       browsers: 'cover 99.5%',       autoprefixer: true     }   }),    order: 'cssnanoLast' }

    Nuxt из коробки имеет встроенный Postcss для которого здесь мы задаём конфиг. Здесь мы можем подключать полезные плагины для Postcss, задавать их порядок исполнения. В данном случае мы отключаем для development все плагины, а в production весь css будет минифицирован и к нему будут присвоены vendor префиксы для 99.5% браузеров. Так же мы можем прописать плагины, которые будут использоваться сразу в двух средах.

  • extend (config, ctx) {

    Здесь мы можем изменять webpack конфиг, напрямую перехватывать нужные loaders или test.
    По умолчанию Webpack '/\\.(png|jpe?g|gif|svg|webp)$/i' ищет все картинки и svg файлы и обрабатывает их через встроенный url-loader file-loader для того чтобы мы могли в компонентах импортировать эти файлы. Но он никаких их не сжимает и его возможности по работе с svg сильно ограничены. Поэтому мы изменим это поведение.

  • Создадим константы в которых будем хранить новые загрузчики и плагины Webpack, а так же строку для поиска встроенного загрузчика, чтобы позже её перехватить.

    const ORIGINAL_TEST = '/\\.(png|jpe?g|gif|svg|webp)$/i' const vueSvgLoader = [ {     loader: 'vue-svg-loader',     options: {     svgo: false     } } ] const imageMinPlugin = new ImageminPlugin({ pngquant: { quality: '5-30', speed: 7, strip: true }, jpegtran: { progressive: true }, gifsicle: { interlaced: true }, plugins: [ imageminMozjpeg({ quality: 70, progressive: true })] })

  • if (!ctx.isDev) config.plugins.push(imageMinPlugin)

    Добавляем плагин для сжатия только в production

  •   config.module.rules.forEach(rule => {     if (rule.test.toString() === ORIGINAL_TEST) {       rule.test = /\.(png|jpe?g|gif|webp)$/i       rule.use = [         {           loader: 'url-loader',           options: {             limit: 1000,             name: ctx.isDev ? '[path][name].[ext]' : 'img/[contenthash:7].[ext]'           }         }       ]     }   })

  • Перехватываем стандартный загрузчик и убираем из него svg

      const svgRule = {     test: /\.svg$/,     oneOf: [       {         resourceQuery: /inline/,         use: vueSvgLoader       },       {         resourceQuery: /data/,         loader: 'url-loader'       },       {         resourceQuery: /raw/,         loader: 'raw-loader'       },       {         loader: 'file-loader' // By default, always use file-loader       }     ]   }   config.module.rules.push(svgRule) 

    Добавляем наш загрузчик для svg

На этом настройка nuxt.config.js завершена и мы можем запустить проект.

Первый запуск

Для начала запустим Nuxt в режиме development. Для этого пропишем в консоли yarn dev
Заходим в браузере по адресу http://localhost:3000/ и видим, что всё хорошо.

Теперь запустим production сервер, отключаем dev server с помощью Ctrl+C в консоли пишем yarn buildandstart

Сервер будет доступен по тому же порту, но его можно изменить в package.json. Давайте проверим скорость.

Попробуем разобраться почему выдаёт 99/100

  1. Мы используем динамическую инициализацию шрифтов, которая начинается, когда уже сам app.js начинает исполнятся в браузере. После инициализации они подгружаются с Google Fonts. Всё это происходит практически в конце загрузки страницы. Если отключить Webfonts Loader, мы получим 100/100, но мы этого делать не будем.
  2. Мы не используем http2 поэтому браузер начинает загрузку всех ресурсов уже после получения и парсинга html, поэтому в production это процесс примерно на 0,5-1с. начинается раньше (при правильных настройках Nuxt и Nginx).
  3. По умолчанию Lighthouse включает 4x CPU Throttling, чтобы симулировать более медленные процессоры, что сильно увеличивает длительность инициализации скриптов.
  4. Lighthouse игнорирует все возможные кеши и симулирует первый заход на сайт. Наше приложение будет разбито на десятки чанков для кеша, поэтому в реальности для конечного пользователя вторая загрузка страницы будет намного быстрее.
  5. Стандартный вебсервер плохо отдаёт статику, поэтому его рекомендуется заменить на Nginx

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

Наполняем сайт

Создаём Vuex store

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

Для начала создадим в корне папку store и файл index.js с таким содержанием

index.js

// function for Mock API const sleep = m => new Promise(r => setTimeout(r, m)) const categories = [   {     cName: 'Котики',     cSlug: 'cats',     cImage: 'https://source.unsplash.com/300x300/?cat,cats'   },   {     cName: 'Собачки',     cSlug: 'dogs',     cImage: 'https://source.unsplash.com/300x300/?dog,dogs'   },   {     cName: 'Волчки',     cSlug: 'wolfs',     cImage: 'https://source.unsplash.com/300x300/?wolf'   },   {     cName: 'Бычки',     cSlug: 'bulls',     cImage: 'https://source.unsplash.com/300x300/?ox'   } ]  export const state = () => ({   categoriesList: [] }) export const mutations = {   SET_CATEGORIES_LIST (state, categories) {     state.categoriesList = categories   } } export const actions = {   async getCategoriesList ({ commit }) {     try {       await sleep(1000)       await commit('SET_CATEGORIES_LIST', categories)     } catch (err) {       console.log(err)       throw new Error('Внутреняя ошибка сервера, сообщите администратору')     }   } } 

Если вы не знакомы с Vuex под спойлером будет подробное описание файла

Vuex объяснение

  • const sleep = m => new Promise(r => setTimeout(r, m))

    Функция, которая позволит нам симулировать задержку с сервера внутри Async функций.

  • export const state = () => ({ categoriesList: [] })

    Это наше глобальное состояние приложения. В нём мы прописываем дефолтные значения переменных.

  • export const mutations = { SET_CATEGORIES_LIST (state, categories) { state.categoriesList = categories } }

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

  • export const actions = { async getCategoriesList ({ commit }) { try {   await sleep(1000)   await commit('SET_CATEGORIES_LIST', categories) } catch (err) {   console.log(err)   throw new Error('Внутреняя ошибка сервера, сообщите администратору') } } }

    Здесь будут наши actions, это тоже функции, но они не меняют напрямую состояние, но зато получают в качестве первого аргумента объект store из которого в данном случае нам нужна функция comit, которая получает в качестве первого аргумента имя мутации, а второго данные, которые она передаёт в мутацию. А в качестве второго аргумента action получает контекст Nuxt где мы можем обращаться к разным плагинам, например к тому же axios и работать через него с реальным API.
    По сути в actions будет проходить вся работа с API. Мы можем также вызывать другие actions, таким образом создавая цепочки. В данном случае мы симулируем работу с API, но в будущем можем заменить её на реальный API при этом на не придется переписывать код компонентов и страниц так как вся логика работы скрыта.

Создаем блок категорий

В папке components создаём папку commons, в которой мы будем хранить общие компоненты. Далее создаём файл CategoriesList.vue с таким содержанием:

CategoriesList.vue

<template>   <div>     <h2>Список категорий</h2>     <div :class="$style.wrapper">       <div         v-for="category in categories"         :key="category.cSlug"         :class="$style.block"       >         <nuxt-link :to="`/category/${cSlug}`">           <p>{{ category.cName }}</p>           <img :src="category.cImage" />         </nuxt-link>       </div>     </div>   </div> </template>  <script> export default {   props: {     categories: {       type: Array,       default: () => []     }   } } </script>  <style lang="scss" module> .wrapper {   display: flex; } .block { display: flex; flex-direction: column; } </style>

  • <style lang="scss" module> .wrapper { display: flex; } .block { display: flex; flex-direction: column; } </style>

    Для примера будем использовать относительно новую функцию CSS Modules, которая идёт из коробки и превращает например класс wrapper в wrapper_26mY3 , что позволяет вам не задумывать об именовании локальных классов в стилях компонентов.
    К классам придётся обращаться через объект $style в данном случае.

  • props: { categories: {   type: Array,   default: () => [] } }

    Компонент будет получать данные из родительского, это позволит сделать его dump (глупым) и уменьшает сложность приложения. То есть он не работает с API или глобальным состоянием.

  • <nuxt-link :to="`/category/${cSlug}`">

    Nuxt-link это встроенная обвёртка вокруг <router-link> , которая берёт на себя переходы между страницами, а так же имеет дополнительные функции, которые мы отключили ранее (предзагрузка страницы, когда эта ссылка появится в области видимости.
    Мы передаём в атрибут to строковый литерал с переменной cSlug в конце, в которой у нас будут храниться уникальные пути к каждой категории. В итоге в случае первой категории это отрендерится в

    <a href="/category/cats" class=""><p>Котики</p> <img src="https://source.unsplash.com/300x300/?cat,cats"></a>

    И хотя в dom это обычный a href, но на нём висят обработчики кликов и IObserver. Поэтому даже если у пользователя отключен JS в браузере, он сможет переходить по ссылкам и получать отрендеренный контент с сервера.

Перепишем корневой index.vue

Теперь мы хотим отобразить этот компонент на главной странице сайта.

index.vue

<template>   <div>     <h1>Интернет-магазин "Хвостики"</h1>     <CategoriesList :categories="categories" />   </div> </template>  <script> import CategoriesList from '~~/components/common/CategoriesList' import { mapState } from 'vuex' export default {   components: {     CategoriesList   },   async asyncData ({ app, route, params, error, store }) {     try {       await store.dispatch('getCategoriesList')     } catch (err) {       console.log(err)       return error({         statusCode: 404,         message: 'Категории не найдены или сервер не доступен'       })     }   },   computed: {     ...mapState({       categories: 'categoriesList'     })   } } </script> 

  • import CategoriesList from '~~/components/common/CategoriesList'

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

  • import { mapState } from 'vuex'

    Нам нужно получить глобальное состояние в котором храниться список категорий.

    async asyncData ({ app, route, params, error, store }) { try {   await store.dispatch('getCategoriesList') } catch (err) {   console.log(err)   return error({     statusCode: 404,     message: 'Категории не найдены или сервер не доступен'   }) } },

    asyncData Это специальный метод, который вызывает Nuxt на сервере. В нём мы можем получить данные из API напрямую, но в данном случае, мы хотим их получить вызвав определённый action в Vuex. Так же перед функцией есть приставка async , что позволяет нам сказать Nuxt не рендерить страницу пока данные из API не получены.

  • computed: { ...mapState({   categories: 'categoriesList' }) }

    Через обвертку получаем в страницу объект categories привязанный к глобальному состоянию categoriesList

  • <CategoriesList :categories="categories" />

    Выводим этот компонент, передавая ему в props объект categories

Запускаем сервер

При нажатии на категорию nuxt пытается найти маршрут, но он его не найдет. Давайте создадим для каждой категории свой маршрут.

Создаём страницу категории

В папке pages создаём папку categories и в ней файл с именем _CategorySlug.vue такого содержания:

_CategorySlug.vue

<template>   <div>     <h1>{{ category.cName }}</h1>     <p>{{ category.cDesc }}</p>   </div> </template>  <script> import { mapState } from 'vuex' export default {   async asyncData ({ app, params, route, error }) {     try {       await app.store.dispatch('getCurrentCategory', { route })     } catch (err) {       console.log(err)       return error({         statusCode: 404,         message: 'Категория не найдена или сервер не доступен'       })     }   },   computed: {     ...mapState({       category: 'currentCategory'     })   },   head () {     return {       title: this.category.cTitle,       meta: [         {           hid: 'description',           name: 'description',           content: this.category.cMetaDescription         }       ]     }   } } </script> 

Название файла должно начинаться с _, чтобы Nuxt понял, что это динамический маршрут.
Когда мы переходим на страницу http://127.0.0.1:3000/category/cats
Мы можем обратиться к объекту route таким образом route.params.CategorySlug (без нижнего подчеркивания), которое будет равным cats
В этом файле все аналогично index.vue, но есть пару отличий

  • await app.store.dispatch('getCurrentCategory', { route })

    Мы вызываем actions, который мы создадим в следующем шаге, но вторым аргументам передаём вышеупомянутый объект route.

  • head () { return {   title: this.category.cTitle,   meta: [     {       hid: 'description',       name: 'description',       content: this.category.cMetaDescription     }   ] } }

    Прописываем для этой страницы Title и Meta description, которые мы получаем из API.

Изменяем Vuex

index.js

// function for Mock API const sleep = m => new Promise(r => setTimeout(r, m)) const categories = [   {     cTitle: 'Котики',     cName: 'Котики',     cSlug: 'cats',     cMetaDescription: 'Мета описание',     cDesc: 'Описание',     cImage: 'https://source.unsplash.com/300x300/?cat,cats'   },   {     cTitle: 'Собачки',     cName: 'Собачки',     cSlug: 'dogs',     cMetaDescription: 'Мета описание',     cDesc: 'Описание',     cImage: 'https://source.unsplash.com/300x300/?dog,dogs'   },   {     cTitle: 'Волчки',     cName: 'Волчки',     cSlug: 'wolfs',     cMetaDescription: 'Мета описание',     cDesc: 'Описание',     cImage: 'https://source.unsplash.com/300x300/?wolf'   },   {     cTitle: 'Бычки',     cName: 'Бычки',     cSlug: 'bulls',     cMetaDescription: 'Мета описание',     cDesc: 'Описание',     cImage: 'https://source.unsplash.com/300x300/?bull'   } ]  export const state = () => ({   categoriesList: [],   currentCategory: {} }) export const mutations = {   SET_CATEGORIES_LIST (state, categories) {     state.categoriesList = categories   },   SET_CURRENT_CATEGORY (state, category) {     state.currentCategory = category   } } export const actions = {   async getCategoriesList ({ commit }) {     try {       await sleep(1000)       await commit('SET_CATEGORIES_LIST', categories)     } catch (err) {       console.log(err)       throw new Error('Внутреняя ошибка сервера, сообщите администратору')     }   },   async getCurrentCategory ({ commit }, { route }) {     await sleep(1000)     const category = categories.find((cat) => cat.cSlug === route.params.CategorySlug)     await commit('SET_CURRENT_CATEGORY', category)   } } 

getCurrentCategory в этом action на основании текущего route мы ищем нужную категорию и добавляем её в state.

Запускаем

Теперь наши маршруты работают.

Создаём товары для категорий

Я использовал первый попавшийся генератор JSON с такими настройками.

Полученный файл кладем в static/mock. Nuxt предоставляет публичный доступ к файлам, которые лежат в папке static. Поэтому мы сможем подтянуть наши товары используя Axios.

Для начала изменим наш Vuex к такому виду:

index.js

// function for Mock API const sleep = m => new Promise(r => setTimeout(r, m)) const categories = [   {     id: 'cats',     cTitle: 'Котики',     cName: 'Котики',     cSlug: 'cats',     cMetaDescription: 'Мета описание',     cDesc: 'Описание',     cImage: 'https://source.unsplash.com/300x300/?cat,cats',     products: []   },   {     id: 'dogs',     cTitle: 'Собачки',     cName: 'Собачки',     cSlug: 'dogs',     cMetaDescription: 'Мета описание',     cDesc: 'Описание',     cImage: 'https://source.unsplash.com/300x300/?dog,dogs',     products: []   },   {     id: 'wolfs',     cTitle: 'Волчки',     cName: 'Волчки',     cSlug: 'wolfs',     cMetaDescription: 'Мета описание',     cDesc: 'Описание',     cImage: 'https://source.unsplash.com/300x300/?wolf',     products: []   },   {     id: 'bulls',     cTitle: 'Бычки',     cName: 'Бычки',     cSlug: 'bulls',     cMetaDescription: 'Мета описание',     cDesc: 'Описание',     cImage: 'https://source.unsplash.com/300x300/?bull',     products: []   } ] function addProductsToCategory (products, category) {   const categoryInner = { ...category, products: [] }   products.map(p => {     if (p.category_id === category.id) {       categoryInner.products.push({         id: p.id,         pName: p.pName,         pSlug: p.pSlug,         pPrice: p.pPrice,         image: `https://source.unsplash.com/300x300/?${p.pName}`       })     }   })   return categoryInner } export const state = () => ({   categoriesList: [],   currentCategory: {},   currentProduct: {} }) export const mutations = {   SET_CATEGORIES_LIST (state, categories) {     state.categoriesList = categories   },   SET_CURRENT_CATEGORY (state, category) {     state.currentCategory = category   },   SET_CURRENT_PRODUCT (state, product) {     state.currentProduct = product   } } export const actions = {   async getCategoriesList ({ commit }) {     try {       await sleep(1000)       await commit('SET_CATEGORIES_LIST', categories)     } catch (err) {       console.log(err)       throw new Error('Внутреняя ошибка сервера, сообщите администратору')     }   },   async getCurrentCategory ({ commit }, { route }) {     await sleep(1000)     const category = categories.find((cat) => cat.cSlug === route.params.CategorySlug)     const products = await this.$axios.$get('/mock/products.json')      await commit('SET_CURRENT_CATEGORY', addProductsToCategory(products, category))   } } 

Здесь мы переделали action getCurrentCategory , который с помощью axios получает файл products.json. После небольшой обработки мы получаем страницу категории со списком всех товаров и ссылками на них.

Чтобы их отрендерить создадим компонент ProductBrief.vueв components/category.

ProductBrief.vue

<template>   <div :class="$style.wrapper">     <nuxt-link :to="`/product/${product.pSlug}`">       <p>{{ product.pName }}</p>       <img :src="product.image" />     </nuxt-link>     <p>Цена {{ product.pPrice }}</p>   </div> </template>  <script> export default {   props: {     product: {       type: Object,       default: () => {}     }   } } </script>  <style lang="scss" module> .wrapper {   display: flex;   flex-direction: column; } </style>

Это dump компонент, который будет являться карточкой товара.

Изменим нашу страницу категории

_CategorySlug.vue

<template>   <div>     <h1>{{ category.cName }}</h1>     <p>{{ category.cDesc }}</p>     <div :class="$style.productList">       <div         v-for="product in category.products"         :key="product.id"       >         <ProductBrief :product="product" />       </div>     </div>   </div> </template>  <script> import ProductBrief from '~~/components/category/ProductBrief' import { mapState } from 'vuex' export default {   components: {     ProductBrief   },   async asyncData ({ app, params, route, error }) {     try {       await app.store.dispatch('getCurrentCategory', { route })     } catch (err) {       console.log(err)       return error({         statusCode: 404,         message: 'Категория не найдена или сервер не доступен'       })     }   },   computed: {     ...mapState({       category: 'currentCategory'     })   },   head () {     return {       title: this.category.cTitle,       meta: [         {           hid: 'description',           name: 'description',           content: this.category.cMetaDescription         }       ]     }   } } </script> <style lang="scss" module> .productList {   display: flex;   flex-wrap: wrap;   justify-content: space-between; } </style> 

Теперь она будет рендерить все товары в категории в таком виде.

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

Для этого в plugins добавим

vue-lazy-load.js

import Vue from 'vue' import VueLazyload from 'vue-lazyload'  export default async (context, inject) => {   Vue.use(VueLazyload, {     preLoad: 0,     error: 'https://via.placeholder.com/300',     // eslint-disable-next-line     loading: require(`${'~~/assets/svg/download.svg'}`),     attempt: 3,     lazyComponent: true,     observer: true,     throttleWait: 500   }) } 

В котором мы указываем индикатор загрузки и placeholder в случае, если картинка не доступна на сервере.
Подключаем плагин в nuxt.config.js

  plugins: [     { src: '~~/plugins/vue-lazy-load.js' }   ],

И меняем наш img srcна

      <img         v-lazy="product.image"         :class="$style.image"       />

Так же добавляем стиль

.image {   width: 300px;   height: 300px; }

И вуаля

Анимированый индикатор загрузки и ленивая загрузка. Мой смартфон выдохнул.

Конец первой части

На этом я хочу закончить первую часть. Код проекта доступен на Github

Потыкать можно тут.

Edit compassionate-mendel-neeez

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

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

ссылка на оригинал статьи https://habr.com/ru/post/490496/

Управляющий директор 101XP Ольга Макушенко: «5 ключевых ошибок разработки игры как бизнеса»

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

У тех, кто пробует себя в игрострое, возникает ряд вопросов. Как разработчикам показать себя с лучшей стороны и представить свой проект? Как выгодно представить игру издателю и убедить его в будущем успехе? Как затем продать игру?

Мы в Слёрме любим пробовать и узнавать новое. И нам близка тема образовательных проектов.

Тем интереснее было заглянуть на территорию геймдева благодаря управляющему директору направления премиум игр в компании 101XP Ольге Макушенко. Перед вами лекция Ольги, которая которую она прочитала в Высшей школе бизнес-информатики НИУ ВШЭ в рамках образовательной программы Менеджмент игровых проектов, и родилась эта статья.

Когда разработка игры это бизнес: 5 ключевых ошибок

На самом деле, нет универсального рецепта успешной игры. Разработчики и издатели понимают, что никто им не даст стопроцентной гарантии того, что проект «взлетит». Однако существует ряд моментов, которые говорят о потенциале проекта. И именно на них паблишеры и инвесторы обращают внимание в первую очередь.

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

Среди наиболее проблемных моментов, которые мешают продвигать проект и делать его потенциально привлекательным для издателей, можно выделить:

1. Сложности с определением целевой аудитории. Комьюнити игроков – залог успеха игры. Однако очень часто авторы не понимают особенности своей целевой аудитории (ЦА), объем рынка или же его потенциал. Критически важно правильно оценивать ситуацию, проводить анализ похожих игр и понимать, кто именно те люди, которые будут играть в ваш проект.

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

2. Грамотный подход к бюджетированию проекта. Очень часто разработчики думают, что у издателей бесконечная прорва денег, которые бесконтрольно выделяются под любые запросы. Конечно это совсем не так. Любая компания имеет ограниченный бюджет, который хочет потратить максимально эффективно. Планы на разработку десятка фич, уникальных механик и гор контента – это хорошо. Однако студии разработчиков должны понимать, что далеко не все их фантазии можно воплотить в реальность за счёт чужих ресурсов. Более того, часто это все не нужно для релиза игры и доброй частью контента можно и нужно жертвовать в угоду скорости разработки и общего уровня качества игры.

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

3. Отсутствие планирования. Проблема небольших команд — недостаточный фокус на планировании разработки и подготовки документации. Часто нет даже полного роадмапа разработки игры, что делает невозможным определение стоимости, привлечение внешних средств, поиск издателя.

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

4. Уникальность проекта. Чем ваша игра может удивить? Чем она отличается от сотен других аналогичных продуктов? У вас платформер с атмосферой Лавкрафта и крутым сюжетом?

Давайте смотреть правде в глаза. Рынок перенасыщен большим количеством продуктов разного уровня качества. Есть ли хоть что-то, что может выделить вашу игру? В идеале, что может повлиять на коммерческий успех и побудить аудиторию купить проект за фулпрайс?

USP-фишки – это то, что привлечет издателя, инвестора и игроков чуть ли не в первую очередь. Необходимо с самого начала понимать, за счет чего ваш проект будет продаваться. Если этого понимания нет, то проект получится вторичным. Такие игры можно продавать, но это сложно, дорого и часто не имеет смысла.

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

5. Маркетинг – это не просто красивое слово. Разработка игры заключается не только лишь в оформлении технической части проекта, его визуале и сюжете. Огромное значение имеет продвижение проекта. Можно сказать, что даже средний проект имеет шансы на успех, если авторы организуют громкую промо-кампанию. Правда, тут уже разработчики не будут застрахованы от возвратов и негативных отзывов.

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

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

Какие игры продать сложнее всего сегодня?

Почему же из 1000 игр всего 5 заслуживают внимание издателя? Чем плохи остальные продукты? Да и плохи ли они в классическом понимании?

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

Как правило, самые непростые жанры это:

  • Хорроры;
  • Кооперативные игры для вечеринок;
  • Проекты для шлемов виртуальной и дополненной реальности;
  • Нарративные проекты;
  • Платформеры;
  • Игры для всей семьи (детские игры);
  • Игры, поднимающие острые социальные вопросы.

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

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

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

Игры, затрагивающие острые социальные проблемы, хоть и привлекают к себе внимание прессы, однако часто не вызывают желание в них играть. История рассказанная там может быть совсем не близка массовому пользователю. Или же, например, проходить такую игру просто психологически некомфортно и игроки делают возвраты. Конечно же, есть тут и свои исключения. This War of Mine от польской студии 11 Bit Studios трудно назвать комфортной игрой, однако же она продалась миллионными тиражами.

Тем не менее, есть и жанры, которые остаются популярными в течение последних нескольких лет. Это значит, что продавать такую игру будет проще, при должном уровне качества проекта, конечно же. Например, сюда входят survival-игры, симуляторы и шутеры. Несмотря на их немалое число, пользователи продолжают с удовольствием в них играть.

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

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

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

Интересные лекции по игровой индустрии можно послушать в Высшей школе бизнес-информатики НИУ ВШЭ — они проходят на регулярной основе. Вход бесплатный, но необходима предварительная регистрация.

ссылка на оригинал статьи https://habr.com/ru/company/southbridge/blog/490390/

Закон Фарадея или как магнит застревает в медной трубе

Магнит в медной трубе
Изображение взято с сайта «Популярная механика»

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

И если вы дочитали до этого места и не испугались, добро пожаловать под кат — дальше будет интереснее!

Сам я давно подумывал над тем, чтобы хорошенько разобраться в этом вопросе. И вот недавно зашёл разговор с коллегой по работе. Его ребёнку задали сделать научную демонстрацию в школе, на что папа раздобыл кусок медной трубы и неодим-железо-борный магнит. Ребёнок разобрался, произвёл демонстрацию опыта перед классом, дал пояснения, но ни класс ни учитель особо не впечатлились. На конкурсе научных опытов победил вулкан (!) из соды и лимонной кислоты =) Мы с коллегой прикинули на словах и поняли, что дело ясное, что дело тёмное. Да и в литературе не особо много написано по данной тематике. Поговорили и разошлись. Ну а я всё-таки решил попробовать продраться сквозь дебри. В этой статье пишу, что у меня получилось.

Описание эксперимента

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

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

Итак, в опыте мы наблюдаем, как постоянный магнит движется внутри полой медной трубы с постоянной скоростью. Зафиксируем произвольную точку в теле медной трубки и мысленно проведем поперечное сечение. Через данное сечение медной трубы проходит магнитный поток, содаваемый постоянным магнитом. Из-за того, что магнит движется вдоль трубы, в сечении проводника возникает переменный магнитный поток, то ли нарастающий, то ли убывающий в зависимости от того, приближается или отдаляется магнит от точки, где мы мысленно провели сечение. Переменный магнитный поток, согласно уравнениям Максвелла, порождает вихревое электрическое поле, вообще говоря, во всём пространстве. Однако, только там, где есть проводник, это электрическое поле приводит в движение свободные заряды, находящиеся в проводнике — возникает круговой электрический ток, который создает уже своё собственное магнитное поле и взаимодействует с магнитным полем движущегося постоянного магнита. Проще говоря, круговой электрический ток создает магнитное поле того же знака, что и постоянный магнит, и на магнит действует некая диссипативная сила, а если конкретно — сила трения. Читатель может справедливо задать вопрос: «Трение чего обо что?» Трение возникает между магнитным полем диполя и проводником. Да, это трение не механическое. Вернее сказать, тела не соприкасаются. Ну и пусть! Трение всё равно есть!

В целом, на словах всё выглядит более или менее складно, а можно ли это описать на языке математики? Приступим…

Математическое описание

Постоянный магнит в медной трубе

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

$\vec{B}=\frac{\mu_0}{4\pi}\left(\frac{3(\vec{p}_m\cdot\vec{r})\vec{r}}{r^5}-\frac{\vec{p}_m}{r^3}\right)$

Здесь приняты обозначения $\vec{r}=(r,z)$ — радиус-вектор из центра диполя в точку наблюдения, $\vec{p}_m$ — вектор дипольного момента.

Далее, нам нужно записать $z$-компоненту вектора магнитной индукции для вычисления магнитного потока, захваченного в поперечном сечении металла медной трубы. Выпишем $z$-компоненту магнитного поля здесь

$B_z(r,z) = \frac{\mu_0\,p_m}{4\pi}\frac{2z^2 - r^2}{\left(r^2 + z^2\right)^\frac{5}{2}}$

Теперь запишем выражение для магнитного потока через площадь, охватываемую окружностью радиуса $r$ на расстоянии $z$ от диполя.

$\Phi(r,z) = \int_0^{2\pi}\int_0^r B_z(r',z)\,r'\,dr'\,d\varphi = 2\pi\int_0^r \frac{\mu_0\,p_m}{4\pi}\frac{2z^2 - r'^2}{\left(r'^2 + z^2\right)^\frac{5}{2}}\,r'\,dr'$

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

$\Phi(r,z) = \frac{\mu_0\,p_m}{2}\frac{r^2}{\left(r^2 + z^2\right)^\frac{3}{2}}$

Из-за того, что диполь движется вдоль оси $z$ со скоростью $v$, нужно также сделать стандартную подстановку $\Phi(r, z)\rightarrow \Phi(r, z - v t)$
Похоже, пора призвать на помощь одно из великих уравнений Максвелла, а именно, то самое уравнение, которое описывает закон Фарадея:

Изменение потока магнитной индукции, проходящего через незамкнутую поверхность $S$, взятое с обратным знаком, пропорционально циркуляции электрического поля на замкнутом контуре $L$, который является границей поверхности $S$

$\oint_L \vec{E}\,dl = -\frac{\partial}{\partial t}\int_S \vec{B}\,d\vec{s}$

Или, что то же самое,

$2\pi r E_\varphi = -\frac{\partial}{\partial t}\Phi(r,z - v t)$

Здесь мы воспользовались аксиальной симметрией задачи по отношению к оси $z$, а также учли, что индуцированное электрическое поле имеет только азимутальную компоненту $\vec{E} = E_\varphi \vec{e}_\varphi$.
Отсюда можно найти азимутальную компоненту электрического поля, индуцированного магнитом.

$E_\varphi(r,z) = -\frac{1}{2\pi r}\frac{\partial}{\partial t}\Phi(r, z - v t)=-\frac{3\mu_0\,p_m}{4\pi}\frac{rv(z-v t)}{\left(r^2 + (z-v t)^2\right)^\frac{5}{2}}$

Теперь, когда у нас есть выражение для электрического поля, можно вспомнить и о трубе. Как показано на рисунке выше, внутренний радиус трубы равен $a$, а внешний — $b$. Материал трубы — медь. В данный момент нам будет нужна только электрическая проводимость меди. Обозначим проводимость за $\sigma$.
Электрическое поле внутри проводника вызывает электрический ток. Поэтому можем записать закон Ома в дифференциальной форме

$\vec{j} = \sigma \vec{E}$

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

$w = \vec{j}\cdot\vec{E} = \sigma E^2$

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

$\frac{dE_p}{dt} = P$

$-mg\dot{z} = \int_V w\,dV$

$mgv = \int_{-\infty}^{\infty}\int_0^{2\pi}\int_a^b \sigma E^2\,r\,dr\,d\varphi\,dz$

Здесь необходимо заметить, что потенциальная энергия в координатах, изображенных на рисунке выше будет равна $E_p = - mgz$, а чтобы найти полную мощность омических потерь, следует проинтегрировать $w$ по всему объёму проводника. Длину трубы считаем бесконечной. Это не так далеко от истины, если учесть, что в опыте из видеоролика диаметр магнитика много меньше длины трубы.

Последний тройной интеграл выглядит очень сложным. И так оно и есть! Но, во-первых, интегрирование по азимутальному углу $\varphi$ можно заменить просто домножением на $2\pi$ в силу аксиальной симметрии задачи. Во-вторых, порядок интегрирования в данном конкретном интеграле можно изменить и сначала проинтегрировать по $z$, а уж потом по $r$. В-третьих, при интегрировании по $z$ по бесконечным пределам можно смело отбросить слагаемое $-vt$. Оставшийся интеграл берется машиной.

$\int_{-\infty}^\infty \frac{z^2\,dz}{\left(r^2 + z^2\right)^5} = \frac{5\pi}{128r^7}$

В итоге получается ответ для полной мощности омических потерь

$P = \frac{15}{1024}\,\mu_0^2\,p_m^2\,\sigma\left(\frac{1}{a^3} - \frac{1}{b^3}\right)v^2 = k\,v^2$

Здесь после второго знака равенства мы обозначили коэффициент трения

$k = \frac{15}{1024}\,\mu_0^2\,p_m^2\,\sigma\left(\frac{1}{a^3} - \frac{1}{b^3}\right)$

Отметим что, коэффициент трения $k$ зависит только от намагниченности магнита $p_m$, свойств материала проводника $\sigma$ и геометрических размеров трубы $a$ и $b$ — то есть зависит исключительно от параметров магнита и трубы и не зависит от, например, скорости или времени. Это хороший знак для нас и маленький зачётик в копилку найденных формул! Отсюда же становится понятно, почему для демонстрации опыта выбрана именно медная труба, а не, скажем, стальная. Трение зависит от проводимости линейно $\sigma$, а у стали проводимость меньше на порядок.

А что если труба сделана из сверхпроводника?

Это же обстоятельство объясняет и почему магнит левитирует над поверхностью сверхпроводника. Когда мы подносим постоянный магнит к сверхпроводнику, в последнем индуцируются незатухающие внутренние токи, которые создают своё магнитное поле и отталкивают магнитик.

Теперь можно записать

$mgv=kv^2\\mg = kv$

И внезапно (!), перед нами третий закон Ньютона! Сила действия равна силе противодействия. Можем найти установившуюся скорость движения магнита

$v_s = \frac{mg}{k}$

Уравнение движения

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

$ma=mg - kv\\ m\ddot{z} + k\dot{z} = mg$

Решать уравнение для $z(t)$ неинтересно, потому что ну просто координата меняется с постоянной скоростью. Гораздо полезнее знать, как быстро стабилизируется падение, чему равна установившаяся скорость падения. В общем, надо решать это уравнение для скорости

$\dot{v} + \frac{k}{m} v = g$

А решение будет такое

$v(t) = v_0\,e^{-\alpha t} + v_s\left(1 - e^{-\alpha t}\right)$

Здесь $\alpha = k/m$ — коэффициент затухания. Характерное время выхода на установившийся режим падения — $\tau = \alpha^{-1}$. Начальная скорость — $v_0$, установившаяся скорость — $v_s$.

А вообще, это уравнение парашютиста. Вот, наверное, почему статья Популярной Механики называется «Магнитный парашют».

Численный эксперимент

А теперь будет то, ради чего всё это затевалось. Навели тут, понимаешь, теорию. А на что она способна? Вдруг это всего лишь как тень на плетень? Или вообще не работает…

Начинаем снова с магнита. На сайтах интернет магазинов нашёл. Продаются магниты NdFeB, остаточная намагниченность $B_r = 10000 .. 13000$ Гс. Однако, это не означает, что поле непосредственно рядом с магнитом такое же большое. Когда-то доводилось измерять прибором. Поля прямо на поверхности таких магнитов обычно оказываются меньше и составляют 4000 — 5000 Гс. А масса такого магнитика будет равна примерно $m = 8$ г.

Видео у нас из MIT, стало быть, американское. Попробую угадать размеры их демонстрационной установки в дюймах (они же в дюймах любят всё измерять). Размер магнитика похож на $d = 1/2$ дюйма в диаметре. Это из тех какие есть в продаже. Размер медной трубы в длину похож на $l = 12$ дюймов (1 фут), а внутренний и внешний диаметры трубы, скорее всего, $2a = 5/8$ дюйма, $2b = 2$ дюйма.

С геометрией, вроде разобрались. Теперь физические свойства. Проводимость меди $59.5\times 10^6$ См/м. Ну и должен признаться, что как я ни пытался, ничего не смог найти на тему: как связаны остаточная намагниченность и магнитный момент постоянного магнита. Поэтому, держа в уме те измерения, которые когда-то делал, я просто подобрал такое значение $p_m$, чтобы поле на поверхности магнита было где-то между 4500 и 5000 Гс. Получилось $p_m \approx 0.6$ А·м². Чтобы убедиться, что поля похожи на настоящие, построил график магнитной индукции от радиальной координаты на расстоянии половины диаметра от центра шарика.

z-компонента магнитного поля на поверхности постоянного магнита
z-компонента магнитного поля рядом с поверхностью постоянного магнита

Теперь воспользуемся решением уравнения движения, чтобы построить график скорости магнита. Для всех выбранных выше параметров коэффициент трения получается равным $k = 1.032$ Н/(м/с), установившаяся скорость — $v_s = 7.6$ см/с — как раз примерно 3 дюйма в секунду! На видео шарик проходит через трубу длиной в 12 дюймов примерно за 4 секунды.

ЭТО ЗАЧОТ!

Знаю, что правильно «зачёт» писать через «ё», но в данном случае правильнее будет через «о» 😉

А мы продолжаем. Рассеиваемая мощность оказывается равной примерно $P \approx 6$ мВт, а характерное время выхода на установившийся режим — $\tau \approx 8$ мс. Ниже построены графики $v(t)$ для двух разных начальных скоростей: нулевой, и $v_0 = 15$ см/с.

График решения уравнения движения магнитика в медной трубе
График решения уравнения движения магнитика в медной трубе

Вот такое вот получилось исследование. Надеюсь, что было интересно. Оставляйте ваши комментарии. Постараюсь ответить всем. Если вам понравилась статья, поддержите автора лайком или плюсиком в карму. Спасибо, что прочитали.

Литература

  1. Джексон, Дж. Классическая электродинамика: Пер. с англ. Мир, 1965.
  2. Ландау, Л. Д., & Лифшиц, Е. М. (1941). Теория поля. Москва; Ленинград: Государственное издательство технико-теоретической литературы.
  3. Сивухин, Д. В. «Общий курс физики. Том 3. Электричество.» Москва, издательство “Наука”, главная редакция физико-математической литературы (1977).
  4. Яворский, Б. М., and А. А. Детлаф. «Справочник по физике.» (1990).

ссылка на оригинал статьи https://habr.com/ru/post/490310/

OWASP Moscow 2020/1

image

5 марта 2020 года в московском офисе компании OZON пройдёт очередная встреча Московского отделения сообщества OWASP, на которой соберутся специалисты по информационной безопасности.

OWASP (Open Web Application Security Project) объединяет крупные компании, образовательные организации и частных лиц со всего мира. Участники сообщества пишут статьи и учебные пособия, создают документацию, инструменты и технологии для повышения безопасности сервисов и приложений. Все разработки OWASP есть в открытом доступе.

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

Планируется видеотрансляция на YouTube-канале.

Программа

  • 18:30 Регистрация
  • 19:00 «Проекты OWASP: SAMM выпуск 2», Тарас Иващенко, OZON. Недавно состоялся долгожданный выпуск новой версии фреймворка для построения и оценки эффективности программы безопасности. Поговорим про что там нового и что это за проект вообще.
  • 19:15 «Типичные ошибки реализации SMS-аутентификации», Ramazan (r0hack), DETEACT. Многие онлайн-сервисы используют SMS в качестве механизма аутентификации пользователей. Но допускаются маленькие ошибки, которые введут к крупным проблемам. Некоторые рекомендации для снижения уровня риска приложений. Именно об этом пойдет речь в этом выступлении.
  • 19:30 «OWASP Сheat Sheet Series. Microservices-based security architecture documentation», Александр Барабанов. Как в мире микросервисов иметь актуальную информацию о каждом из них с точки зрения безопасности? Попробуем найти решение этой проблемы и презентуем новую памятку в рамках проекта «OWASP Сheat Sheet Series»
  • 19:55 Перерыв
  • 20:10 «Dev, Sec, Oops: How Agile Security increases Attack Surface», Денис Макрушин. В рамках всё более ускоряющегося процесса разработки не обойтись без автоматизации максимального количества контролей безопасности. Но что, если сами сервисы и автоматизированные контроли безопасности окажутся уязвимыми и могут быть использованы для атаки?
  • 20:35 TBA
  • 21:00 Окончание встречи


Страница мероприятия на meetup.com.
OWASP Moscow chapter: OWASP Moscow
OWASP Russia chat: https://t.me/OWASP_Russia
OWASP Russia channel: https://t.me/OWASP_RU

ссылка на оригинал статьи https://habr.com/ru/company/owasp/blog/490470/