Русский
Русский
English
Статистика
Реклама

Фича

Особенность Вконтакте

27.06.2020 14:19:52 | Автор: admin

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



Разбираемся


Геолокация по какой-либо причине оказалась включена для приложения, но как она утекла оставалось загадкой. Знаете, где в ВК можно увидеть геолокацию человека? На фотографиях, верно. Вот только фотографий в этом районе я не делал, да и вообще запретил ставить метки смартфону.


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


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


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


Еще раз, пользователь не заходил на уровень ниже в Найти рядом со мной, а функция уже активировалась сама. Об этом пользователя не уведомляют и сам он этого не видит.


Тестировалось на IOS и Android на двух разных аккаунтах. Хотите повторить?


  1. Вам нужно всего лишь 2 телефона и включенная для приложения ВК геолокация
  2. На одном телефоне заходите в "Друзья" "Добавить друга"
  3. На другом в Найти рядом с мной
  4. PROFIT! Вы видите из Найти рядом со мной аккаунт второго телефона, пользователь которого и не думал показывать свое местоположение

Резюме


Благодаря этой особенности:


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

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


Внимание! Перед тем, как рассказать аудитории Хабра об этой проблеме, мной были предприняты попытки сообщить о ней разработчикам через платформу HackerOne. Разработчики посчитали это все не багом, а репорт был закрыт в статусе informative, на мои дальнейшие комментарии они ничего не ответили и игнорировали меня.

Подробнее..

Фичи JavaScript. Часть 1

19.06.2020 08:12:45 | Автор: admin


Доброго времени суток, друзья!

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

Настоятельно рекомендую применить к body следующие стили:

body {    margin: 0;    min-height: 100vh;    overflow: hidden;}

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

const clear = () => document.body.innerHTML = ''clear()

1. Активный элемент


Свойство activeElement позволяет получить элемент, находящийся в фокусе.

const input = document.createElement('input')input.setAttribute('type', 'text')input.setAttribute('placeholder', 'Введите свое имя')input.className = 'username'document.body.append(input)input.focus()console.log(document.activeElement)// <input type="text" placeholder="Введите свое имя" class="username">

2. Редактирование страницы


Свойство designMode позволяет редактировать страницу, открытую в браузере.

document.designMode = 'on'



3. Стили элемента


Метод getComputedStyle() позволяет получить стили элемента. Для получения определенного свойства следует использовать getPropertyValue().

// напишем вспомогательную функцию для получения определенного свойства элемента// мы будем использовать ее в нескольких примерахconst getStyle = (element, property) => getComputedStyle(element).getPropertyValue(property)// возьмем инпут из первого примера// и определим его ширину и высотуconst inputWidth = getStyle(input, 'width')const inputHeight = getStyle(input, 'height')console.log(`Ширина: ${inputWidth}\nВысота: ${inputHeight}`)// Ширина: 156.8px// Высота: 16px// позиционируем элемент, используя полученные данные// предположим, что мы собираемся анимировать элемент// поэтому не хотим использовать transform: translate(-50%, -50%)// допустим также, что мы не знаем размеров элемента// поэтому не можем использовать calc(50% - ширина/высота элемента)input.setAttribute('style',    `position: absolute; top: calc(50% - ${inputHeight.replace('px', '') / 2}px); left: calc(50% - ${inputWidth.replace('px', '') / 2}px);`)

4. Определение браузера


Объект Navidator, в числе прочего, позволяет получить информацию о браузере пользователя.

let browserconst agent = navigator.userAgentif (agent.indexOf('Google')) {    browser = 'Google Chrome'} else if (agent.indexOf('Safari')) {    browser = 'Apple Safari'} else if (agent.indexOf('Opera')) {    browser = 'Opera'} else if (agent.indexOf('Firefox')) {    browser = 'Mozilla Firefox'} else if (agent.indexOf('MSIE')) {    browser = 'Microsoft Interner Explorer'}console.log(browser) // Google Chrome// вероятно, в данном случае надо было использовать switchif (browser === 'Google Chrome' || browser === 'Mozilla Firefox') {    console.log('ok') // ok} else if (browser === 'Opera' || browser === 'Apple Safari') {    console.log('50/50')} else if (browser === 'Microsoft Interner Explorer') {    console.log('!ok')}

5. Получение координат


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

const success = position => {    // деструктурируем объект    const {        latitude,        longitude,        altitude,        speed    } = position.coords    console.log(`${latitude.toFixed(2)}\n${longitude.toFixed(2)}\n${altitude}\n${speed}`)    // об этом ниже    getCityAndWeather(latitude, longitude)}navigator.geolocation.getCurrentPosition(success)/*    56.90    60.63    null    null*/// вот как мы можем использовать полученные данные// определим город пользователя и погодуconst getCityAndWeather = (latitude, longitude) => {    // прокси для преодоления CORS    const proxy = 'https://cors-anywhere.herokuapp.com/'    // данный сервис был куплен Facebook и станет платным в 2021 году    const api = `${proxy}https://api.darksky.net/forecast/fd9d9c6418c23d94745b836767721ad1/${latitude}, ${longitude}`    fetch(api)        .then(response => response.json())        .then(data => {            console.log(data) // много всего            // получаем город            const city = data.timezone            // получаем температуру            const { temperature, summary } = data.currently            // переводим фаренгейт в цельсий            const celsius = Math.floor((temperature - 32) * (5 / 9)).toFixed()            // выводим результат            console.log(                `${city}\n${celsius}C\n${summary}`            )            /*                Asia/Yekaterinburg                15C                Overcast            */        })}

6. Получение элементов


Как нам получить все элементы DOM? Использовать рекурсию.

const template = `<div>    <p> Lorem ispum        <span>dolor sit amet</span>    </p></div><a href="#">link</a>`document.body.innerHTML = templateconst getElements = element => {    for (const i of element.children) {        console.log(i.tagName)        // 0 -> ! -> false -> ! -> true        if (!!i.children.length) {            console.log('дочерний элемент')            getElements(i)        }    }}getElements(document.body)/*    DIV    дочерний элемент    P      дочерний элемент    SPAN    A*/

7. Разбор URL


Как нам получить отдельные части URL? Это можно сделать двумя способами.

// с помощью регуляркиconst regex = /(\w+):\/\/([\w.]+)\/(\S*)/const url = 'https://example.com/index.html'const result = url.match(regex)// полный адрес (абсолютный путь), протокол, хост, страницаconsole.log(result[0], result[1], result[2], result[3])// https://example.com/index.html https example.com index.html// с помощью конструктора URLconst url2 = new URL('https://example.com/search?query=fetch&page=2#awesome-page')console.log(url2) // много всегоconst {    origin,    protocol,    host,    pathname} = url2console.log(    `${origin} ${protocol} ${host} ${pathname}`)// https://example.com https: example.com /search// рекомендую почитать про свойство searchParams// searchParams.get(), searchParams.append(), searchParams.has(), searchParams.delete() и т.д.

8. Позиционирование одного элемента относительно другого


const toCenter = (element, parent) => {    element.style.position = 'relative'    element.style.left = (parent.clientWidth - element.offsetWidth) / 2 + 'px'    element.style.top = (parent.clientHeight - element.offsetHeight) / 2 + 'px'}const div = document.createElement('div')div.setAttribute('style', 'width: 150px; height: 150px; background: red;')document.body.append(div)const div2 = document.createElement('div')div2.setAttribute('style', 'width: 100px; height: 100px; background: green;')div.append(div2)const div3 = document.createElement('div')div3.setAttribute('style', 'width: 50px; height: 50px; background: blue;')div2.append(div3)toCenter(div, document.body)toCenter(div2, div)toCenter(div3, div2)


9. Ширина и высота документа


Как нам получить полную ширину и высоту документа?

const pageWidth = Math.max(    document.body.scrollWidth, document.documentElement.scrollWidth,    document.body.offsetWidth, document.documentElement.offsetWidth,    document.body.clientWidth, document.documentElement.clientWidth)const pageHeight = Math.max(    document.body.scrollHeight, document.documentElement.scrollHeight,    document.body.offsetHeight, document.documentElement.offsetHeight,    document.body.clientHeight, document.documentElement.clientHeight)// один из вариантов использования// определяем центр страницыconst pageCenter = [pageWidth / 2, pageHeight / 2]console.log(pageCenter)// создаем элемент для позиционированияconst p = document.createElement('p')p.textContent = 'Lorem ipsum dolor sit amet'document.body.append(p)p.style.position = 'absolute'// получаем ширину и высоту элемента, используя getStyleconst elementWidth = getStyle(p, 'width').replace('px', '')const elementHeight = getStyle(p, 'height').replace('px', '')// определяем центр элементаconst elementCenter = [elementWidth / 2, elementHeight / 2]console.log(elementCenter)// позиционируем элементp.style.top = pageCenter[1] - elementCenter[1] + 'px'p.style.left = pageCenter[0] - elementCenter[0] + 'px'

10. Координаты элемента в контексте документа


Метод getBoundingClientRect() возвращает размер элемента и его позицию относительно области просмотра.

// возьмем p из предыдущего примераconsole.log(p.getBoundingClientRect()) // много всегоconsole.log(    `Отступ сверху => ${p.getBoundingClientRect().top.toFixed()}\nОтступ слева => ${p.getBoundingClientRect().left.toFixed()}`)/*    Отступ сверху => 352    Отступ слева => 288*/// создадим два элемента// и определим, в какой части страницы находится каждый из нихconst div = document.createElement('div')div.setAttribute('style', 'width: 100px; height: 100px; background: #222; position: absolute; top: calc(50% - 50px); left: calc(25% - 50px);')document.body.append(div)const div2 = document.createElement('div')div2.setAttribute('style', 'width: 100px; height: 100px; background: #222; position: absolute; top: calc(50% - 50px); left: calc(75% - 50px);')document.body.append(div2)document.querySelectorAll('div').forEach(div => div.addEventListener('click', event => {    const x = event.target.getBoundingClientRect().x    const width = event.target.getBoundingClientRect().width    // расчеты приблизительные    x + width < innerWidth / 2    ? console.log('Элемент находится в левой части страницы.')    : console.log('Элемент находится в правой части страницы.')}))div.click() // Элемент находится в левой части страницы.div2.click() // Элемент находится в правой части страницы.// определим расстояние между нимиconst distanceBetweenDivs = (div, div2) =>console.log((div2.getBoundingClientRect().x - div.getBoundingClientRect().x + div.getBoundingClientRect().width).toFixed())distanceBetweenDivs(div, div2) // 477

11. Координаты курсора


Как нам получить координаты курсора? Очень просто.

// document.addEventListener('click', ev => console.log(`X => ${ev.clientX}\nY => ${ev.clientY}`))/*    X => 348    Y => 304*/// вот как мы можем это использовать// создаем холст и получаем его контекстconst canvas = document.createElement('canvas')document.body.append(canvas)const $ = canvas.getContext('2d')// размер холста - область просмотраcanvas.width = innerWidthcanvas.height = innerHeight// создаем вспомогательную функцию для получения случайного целого числа в заданном диапазонеconst randomInt = (min, max) => Math.floor(min + Math.random() * (max + 1 - min))// создаем вспомогательную функцию для получения случайного цветаconst randomColor = () => `#${((Math.random()*0xfff)<<0).toString(16)}`// давайте порисуем// рисование фигур осуществляется по клику// центр фигуры - место клика// форма фигуры - круг или квадратlet i = 0canvas.addEventListener('click', ev => {    $.beginPath()    // если i - четное число, то рисуем круг    // если нечетное - квадрат    if (i % 2 === 0) {        // $.arc(x, y, радиус, угол)        $.arc(ev.clientX, ev.clientY, randomInt(10, 30), 0, 2 * Math.PI)        $.fillStyle = randomColor()        $.fill()    } else {        let randomSize = randomInt(20, 60)        $.fillStyle = randomColor()        // $.fillRect(x, y, ширина, высота)        $.fillRect(ev.clientX - randomSize / 2, ev.clientY - randomSize / 2, randomSize, randomSize)    }    $.closePath()    i++})

Напоследок реализуем функцию рисования определенного количества фигур.

const manyShapes = number => {    // очищаем холст    $.clearRect(0, 0, canvas.width, canvas.height)    for (let i = 0; i < number; i++) {        let randomX = randomInt(0, innerWidth)        let randomY = randomInt(0, innerHeight)        if (i % 2 === 0) {            $.beginPath()            $.arc(randomX, randomY, randomInt(10, 30), 0, 2 * Math.PI)            $.fillStyle = randomColor()            $.fill()        } else {            let randomSize = randomInt(20, 60)            $.beginPath()            $.rect(randomX, randomY, randomSize, randomSize)            $.fillStyle = randomColor()            $.fill()        }    }}manyShapes(100)



Благодарю за потраченное время. Надеюсь, оно было потрачено не зря.

Продолжение следует
Подробнее..

Фичи JavaScript. Часть 2

27.06.2020 10:22:34 | Автор: admin


Доброго времени суток, друзья!

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

См. Фичи JavaScript. Часть 1.

1. Частое обращение к одним и тем же элементам


Порой при написании кода приходится снова и снова обращаться к одним и тем же элементам. При работе с DOM, например, такими элементами являются document и document.body. Казалось бы, что тут такого? 8 и 13 символов, соответственно, да еще и emmet помогает. Однако, когда кода действительно много, автозавершение начинает предлагать неправильные варианты. Либо, когда работаешь не с html, а, например, с php без правильного синтаксического анализатора, многие привычные вещи приходится набирать вручную. Задумавшись о том, как решить указанную проблему, я вспомнил о canvas. Помните, с чего начинается работа с холстом? Правильно, с его определения и инициализации двумерного контекста рисования:

const C = document.querySelector('canvas')const $ = C.getContext('2d')

Также я подумал об объекте jQuery ($).

Так вот, могу предложить три варианта (один из вариантов я подглядел у разработчиков Facebook при изучении протокола Open Graph):

    // внутри функцииfunction foo() {    const D = document    const B = document.body    const div = D.createElement('div')    B.append(div)    const p = D.createElement('p')    p.textContent = 'Lorem ipsum dolor sit amet...'    div.append(p)    console.log(div)    B.removeChild(div)}foo()// снаружи функцииfunction bar(D, B) {    const div = D.createElement('div')    B.append(div)    const p = D.createElement('p')    p.textContent = 'Lorem ipsum dolor sit amet...'    div.append(p)    console.log(div)    B.removeChild(div)}bar(document, document.body)// IIFE;((D, B) => {    const div = D.createElement('div')    B.append(div)    const p = D.createElement('p')    p.textContent = 'Lorem ipsum dolor sit amet...'    div.append(p)    console.log(div)    B.removeChild(div)})(document, document.body)

Это была разминка, переходим к тренировке.

2. Генератор


Генератор это особая функция, которая работает как фабрика итераторов. Объект является итератором, если он умеет обращаться к элементам коллекции по одному за раз, при этом отслеживая свое текущее положение внутри этой последовательности. Генераторы позволяют определить алгоритм перебора элементов коллекции с помощью единственной функции, поддерживающей собственное состояние.

// пример 1function* takeItem(arr) {    for (let i = 0; i < arr.length; i++) {        yield arr[i]    }}const arr = ['foo', 'bar', 'baz', 'qux']const generator = takeItem(arr)const timer = setInterval(() => {        const item = generator.next()        item.done            ? clearInterval(timer)            : console.log(item.value)    }, 1000)// пример 2async function* range(start, end) {    for (let i = start; i <= end; i++) {        yield Promise.resolve(i)    }};(async () => {    const generator = range(1, 4)    for await (const item of generator) {        console.log(item)    }})()

3. Async/await + fetch


Async/await является альтернативой промисов, позволяя обеспечить синхронность выполнения асинхронных функций. В свою очередь, fetch является альтернативой XMLHttpRequest, представляя собой интерфейс для получения ресурсов (в том числе, по сети).

const url = 'https://jsonplaceholder.typicode.com/users';(async () => {    try {        const response = await fetch(url)        const data = await response.json()        console.table(data)    } catch (er) {        console.error(er)    } finally {        console.info('потрачено')    }})()

4. For await


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

const delayedPromise = (id, ms) => new Promise(resolve => {    const timer = setTimeout(() => {        resolve(id)        clearTimeout(timer)    }, ms)})const promises = [    delayedPromise(1, 1000),    delayedPromise(2, 2000),    delayedPromise(3, 3000)]// старый стильasync function oldStyle() {    for (const promise of await Promise.all(promises)) {        console.log(promise)    }}oldStyle() // все промисы через 3 секунды// новый стильasync function newStyle() {    for await (const promise of promises) {        console.log(promise)    }}newStyle() // каждый промис в свой черед

5. Proxy


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

const person = {    firstname: 'Harry',    lastname: 'Heman',    city: 'Mountain View',    company: 'Google'}const proxy = new Proxy(person, {    get(target, property) {        if (!(property in target)) {            return property                .split('_')                .map(p => target[p])                .sort()                .join(' ')        }        console.log(`получено свойство: ${property}`)        return target[property]    },    set(target, property, value) {        if (property in target) {            target[property] = value            console.log(`изменено свойство: ${property}`)        } else {            console.error('нет такого свойства')        }    },    has(target, property) {        // return property in target        return Object.entries(target)            .flat()            .includes(property)    },    deleteProperty(target, property) {        if (property in target) {            delete target[property]            console.log(`удалено свойство: ${property}`)        } else {            console.error('нет такого свойства')        }    }})console.log(proxy.company_city_firstname_lastname) // Google Harry Heman Mountain Viewproxy.firstname = 'John' // изменено свойство: firstnameproxy.surname = 'Smith' // нет такого свойстваconsole.log(proxy.city) // получено свойство: city Mountain Viewconsole.log('company' in proxy) // truedelete proxy.age // нет такого свойства// proxy + cookieconst getCookieObject = () => {    const cookies = document.cookie.split(';').reduce((cks, ck) => ({        [ck.substr(0, ck.indexOf('=')).trim()]: ck.substr(ck.indexOf('=') + 1),        ...cks    }), {})    const setCookie = (name, value) => document.cookie = `${name}=${value}`    const deleteCookie = name => document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:01 GTM;`    return new Proxy(cookies, {        set: (obj, prop, val) => (setCookie(prop, val), Reflect.set(obj, prop, val)),        deleteProperty: (obj, prop) => (deleteCookie(prop), Reflect.deleteProperty(obj, prop))    })}

6. Reduce


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

const arr = [1, 2, 3]const total = arr.reduce((sum, cur) => sum + cur)console.log(total) // 6// forEachlet total2 = 0arr.forEach(num => total2 += num)console.log(total2) // 6

Однако возможности reduce() этим далеко не исчерпываются:

const devs = [    {        name: 'John',        sex: 'm',        age: 23    },    {        name: 'Jane',        sex: 'f',        age: 24    },    {        name: 'Alice',        sex: 'f',        age: 27    },    {        name: 'Bob',        sex: 'm',        age: 28    }]const men = devs.reduce((newArr, dev) => {    if (dev.sex === 'm') newArr.push(dev.name)    return newArr}, [])console.log(men) // ["John", "Bob"]// filter + mapconst olderThan25 = devs    .filter(dev => dev.age > 25)    .map(dev => dev.name)console.log(olderThan25) // ["Alice", "Bob"]

Сформируем список имен разработчиков одной строкой:

const devsNamesList = `<ul>${devs.reduce((html, dev) => html += `<li>${dev.name}</li>`, '')}</ul>`document.body.innerHTML = devsNamesList// mapconst devsNamesList2 = `<ul>${devs.map(dev => `<li>${dev.name}</li>`).join('')}</ul>`document.body.insertAdjacentHTML('beforeend', devsNamesList2)

Поговорим о группировке:

const groupBy = (arr, criteria) =>    arr.reduce((obj, item) => {        const key = typeof criteria === 'function'            ? criteria(item)            : item[criteria]        if (!obj.hasOwnProperty(key)) obj[key] = ''        obj[key] = item        return obj    }, {})const nums = [6.1, 4.2, 2.3]console.log(groupBy(nums, Math.floor)) // {2: 2.3, 4: 4.2, 6: 6.1}// forEachconst groupBy2 = (arr, criteria, obj = {}) => {    arr.forEach(item => {        const key = typeof criteria === 'function'            ? criteria(item)            : item[criteria]                if (!obj.hasOwnProperty(key)) obj[key] = ''        obj[key] = item        return obj    })    return obj}const words = ['one', 'three', 'five']console.log(groupBy2(words, 'length')) // {3: "one", 4: "five", 5: "three"}

Сделаем выборку:

const cash = {    A: 1000,    B: 2000}const devsWithCash = devs.reduce((arr, dev) => {    const key = dev.name.substr(0,1)        if (cash[key]) {        dev.cash = cash[key]        arr.push(`${dev.name} => ${dev.cash}`)    } else dev.cash = 0    return arr}, [])console.log(devsWithCash) // ["Alice => 1000", "Bob => 2000"]// map + filterconst devsWithCash2 = devs.map(dev => {    const key = dev.name.substr(0,1)    if (cash[key]) {        dev.cash = cash[key]    } else dev.cash = 0    return dev}).filter(dev => dev.cash !== 0)console.log(devsWithCash2)

И последний пример. Помните, как мы формировали список имен разработчиков из массива объектов одной строкой? Но что если у нас имеется такой массив:

const users = [    {        john: {            name: 'John'        }    },    {        jane: {            name: 'Jane'        }    },    {        alice: {            name: 'Alice'        }    },    {        bob: {            name: 'Bob'        }    }]

Как нам сделать тоже самое?

document.body.insertAdjacentHTML('afterbegin', `<ul>${users.reduce((html, el) => html + `<li>${Object.values(el)[0].name}</li>`, '')}</ul>`) // фух!

Давайте рассмотрим что-нибудь попроще.

7. Новые методы работы со строками


// trimStart() trimEnd() trim()const start = '   foo bar'const end = 'baz qux   'console.log(`${start.trimStart()} ${end.trimEnd()}`) // foo bar baz quxconsole.log((`${start} ${end}`).trim()) // тоже самоеconst startMiddleEnd = '   foo  bar   baz  ' // три пробела в начале, два - между foo и bar, три - между bar и baz и два - в конце// при помощи регулярного выражения заменяем два и более пробела одним// затем посредством trim() удаляем пробелы в начале и концеconst stringWithoutRedundantSpaces = startMiddleEnd.replace(/\s{2,}/g, ' ').trim()console.log(stringWithoutRedundantSpaces) // foo bar baz// padStart() padEnd()let str = 'google'str = str.padStart(14, 'https://') // первый аргумент - количество символовconsole.log(str) // https://googlestr = str.padEnd(18, '.com')console.log(str) // https://google.com

8. Новые методы работы с массивами


const arr = ['a', 'b', ['c', 'd'], ['e', ['f', 'g']]]console.log(arr.flat(2)) // ["a", "b", "c", "d", "e", "f", "g"]const arr2 = ['react vue', 'angular', 'deno node']console.log(arr2.map(i => i.split(' ')))/*    [Array(2), Array(1), Array(2)]        0: (2) ["react", "vue"]        1: ["angular"]        2: (2) ["deno", "node"]*/console.log(arr2.flatMap(i => i.split(' '))) // ["react", "vue", "angular", "deno", "node"]

9. Новые методы работы с объектами


const person = {    name: 'John',    age: 30}console.log(Object.getOwnPropertyDescriptor(person, 'name')) // {value: "John", writable: true, enumerable: true, configurable: true}const arr = Object.entries(person)console.log(arr) // [["name", "John"], ["age", 30]]console.log(Object.fromEntries(arr))for (const [key, value] of Object.entries(person)) {    console.log(`${key} => ${value}`) // name => John age => 30}console.log(Object.keys(person)) // ["name", "age"]console.log(Object.values(person)) // ["John", 30]

10. Приватные переменные в классах


class Person {    // значения по умолчанию    static type = 'человек'    static #area = 'Земля'    name = 'John'    #year = 1990    get age() {        return new Date().getFullYear() - this.#year    }    set year(age) {        if (age > 0) {            this.#year = new Date().getFullYear() - age        }    }    get year() {        return this.#year    }    static area() {        return Person.#area    }}const person = new Person()console.log(person) // Person{name: "John", #year: 1990}console.log(person.age) // 30// console.log(person.#year) // errorperson.year = 28console.log(person.year) // 1992console.log(Person.type) // человек// console.log(Person.#area) // errorconsole.log(Person.area()) // Земля

11. Еще парочка нововведений


// промисыconst p1 = Promise.resolve(1)const p2 = Promise.reject('error')const p3 = Promise.resolve(3);(async () => {const result = await Promise.all([p1, p2, p3])console.log(result)})() // Uncaught (in promise) error;(async () => {const result = await Promise.allSettled([p1, p2, p3])console.log(result)})()/*    [{}, {}, {}]        0: {status: "fulfilled", value: 1}        1: {status: "rejected", reason: "error"}        2: {status: "fulfilled", value: 3}*/// приведение к null (nullish coercion)const values = {    undefined: undefined,    null: null,    false: false,    zero: 0,    empty: ''}console.log(values.undefined || 'default undefined')console.log(values.undefined ?? 'default undefined')// default undefinedconsole.log(values.null || 'default null')console.log(values.null ?? 'default null')// default nullconsole.log(values.false || 'default false') // default falseconsole.log(values.false ?? 'default false') // falseconsole.log(values.zero || 'default zero') // default zeroconsole.log(values.zero ?? 'default zero') // 0console.log(values.empty || 'default empty') // default emptyconsole.log(values.empty ?? 'default empty') // ''// опциональная цепочка (optional chaining)const obj1 = {    foo: {        bar: {            baz: {                qux: 'veryDeepInside'            }        }    }}const obj2 = {    foo: {}}// старый стильfunction getValueOld(obj) {    if (obj.foo !== undefined &&    obj.foo.bar !== undefined &&    obj.foo.bar.baz !== undefined &&    obj.foo.bar.baz.qux !== undefined) {        return obj.foo.bar.baz.qux    }}console.log(getValueOld(obj1)) // veryDeepInsideconsole.log(getValueOld(obj2)) // нет ошибки// новый стильconst getValueNew = obj => obj?.foo?.bar?.baz?.quxconsole.log(getValueNew(obj1)) // veryDeepInsideconsole.log(getValueNew(obj2)) // нет ошибки

Благодарю за потраченное время. Надеюсь, оно было потрачено не зря.

Продолжение следует
Подробнее..

Перевод Использование глобального await в JavaScript

19.10.2020 14:23:26 | Автор: admin


Новая возможность, которая может изменить наш подход к написанию кода

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

Одним из предложений по улучшению JavaScript является предложение под названием top-level await (await верхнего уровня, глобальный await). Цель данного предложения состоит в превращении ES модулей в некое подобие асинхронных функций. Это позволит модулям получать готовые к использованию ресурсы и блокировать модули, импортирующие их. Модули, которые импортируют ожидаемые ресурсы, смогут запускать выполнение кода только после получения ресурсов и их предварительной подготовки к использованию.

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

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

Что не так с обычным await?


Если вы попытаетесь использовать ключевое слово await за пределами асинхронной функции, то получите синтаксическую ошибку. Во избежание этого разработчики используют немедленно вызываемые функциональные выражения (Immediately Invoked Function Expression, IIFE).

await Promise.resolve(console.log("")); // Ошибка(async () => {    await Promise.resolve(console.log(""))})();

Указанная проблема и ее решение это лишь вершина айсберга


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

// library.jsexport const sqrt = Math.sqrt;export const square = (x) => x * x;export const diagonal = (x, y) => sqrt((square(x) + square(y)));// middleware.jsimport { square, diagonal } from "./library.js";console.log("From Middleware");let squareOutput;let diagonalOutput;const delay = (ms) => new Promise((resolve) => {    const timer = setTimeout(() => {        resolve(console.log(""));        clearTimeout(timer);    }, ms);});// IIFE(async () => {    await delay(1000);    squareOutput = square(13);    diagonalOutput = diagonal(12, 5);})();export { squareOutput, diagonalOutput };

В приведенном примере мы экспортируем и импортируем переменные между library.js и middleware.js. Вы можете назвать файлы как угодно.

Функция delay возвращает промис, разрешающийся после задержки. Поскольку данная функция является асинхронной, мы используем ключевое слово await внутри IIFE для ожидания ее завершения. В реальном приложении вместо функции delay будет вызов fetch (запроса на получение данных) или другая асинхронная задача. После разрешения промиса, мы присваиваем значение нашей переменной. Это означает, что до разрешения промиса наша переменная будет иметь значение undefined.

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

Давайте взглянем на код, в котором эти переменные импортируются и используются:

// main.jsimport { squareOutput, diagonalOutput } from "./middleware.js";console.log(squareOutput); // undefinedconsole.log(diagonalOutput); // undefinedconsole.log("From Main");const timer1 = setTimeout(() => {    console.log(squareOutput);    clearTimeout(timer1);}, 2000); // 169const timer2 = setTimeout(() => {    console.log(diagonalOutput);    clearTimeout(timer2);}, 2000); // 13

Если вы запустите этот код, то в первых двух случаях получите undefined, а в третьем и четвертом 169 и 13, соответственно. Почему так происходит?

Это происходит из-за того, что мы пытаемся получить значения переменных, экспортируемых из middleware.js, в main.js до завершения выполнения асинхронной функции. Вы помните, что у нас там промис, ожидающий разрешения?

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

Обходные пути

Существует, как минимум, два способа решить обозначенную проблему.

1. Экспорт промиса для инициализации

Во-первых, можно экспортировать IIFE. Ключевое слово async делает метод асинхронным, такой метод всегда возвращает промис. Вот почему в приведенном ниже примере асинхронное IIFE возвращает промис.

// middleware.jsimport { square, diagonal } from "./library.js";console.log("From Middleware");let squareOutput;let diagonalOutput;const delay = (ms) => new Promise((resolve) => {    const timer = setTimeout(() => {        resolve(console.log(""));        clearTimeout(timer);    }, ms);});// обходной маневр или, как еще говорят, костыльexport default (async () => {    await delay(1000);    squareOutput = square(13);    diagonalOutput = diagonal(12, 5);})();export { squareOutput, diagonalOutput };

При получении доступа к экспортируемым переменным в main.js можно подождать выполнения IIFE.

// main.jsimport promise, { squareOutput, diagonalOutput } from "./middleware.js";promise.then(() => {    console.log(squareOutput); // 169    console.log(diagonalOutput); // 169    console.log("From Main");});const timer1 = setTimeout(() => {    console.log(squareOutput);    clearTimeout(timer1);}, 2000); // 169const timer2 = setTimeout(() => {    console.log(diagonalOutput);    clearTimeout(timer2);}, 2000); // 13

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

  • При использовании указанного шаблона приходится искать нужный промис
  • Если в другом модуле также используются переменные squareOutput и diagonalOutput, мы должны обеспечить реэкспорт IIFE

Существует и другой способ.

2. Разрешение промиса IIFE с экспортируемыми переменными

В данном случае, вместо экспорта переменных по-отдельности, мы возвращаем их из нашего асинхронного IIFE. Это позволяет файлу main.js просто ждать разрешения промиса и извлекать его значение.

// middleware.jsimport { square, diagonal } from "./library.js";console.log("From Middleware");let squareOutput;let diagonalOutput;const delay = (ms) => new Promise((resolve) => {    const timer = setTimeout(() => {        resolve(console.log(""));        clearTimeout(timer);    }, ms);});// обходной маневрexport default (async () => {    await delay(1000);    squareOutput = square(13);    diagonalOutput = diagonal(12, 5);    return { squareOutput, diagonalOutput };})();// main.jsimport promise from "./middleware.js";promise.then(({ squareOutput, diagonalOutput }) => {    console.log(squareOutput); // 169    console.log(diagonalOutput); // 169    console.log("From Main");});const timer1 = setTimeout(() => {    console.log(squareOutput);    clearTimeout(timer1);}, 2000); // 169const timer2 = setTimeout(() => {    console.log(diagonalOutput);    clearTimeout(timer2);}, 2000); // 13

Однако у такого решения также имеются некоторые недостатки.

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

Как глобальный await решает данную проблему?


await верхнего уровня позволяет модульной системе заботиться о разрешении промисов и их взаимодействии между собой.

// middleware.jsimport { square, diagonal } from "./library.js";console.log("From Middleware");let squareOutput;let diagonalOutput;const delay = (ms) => new Promise((resolve) => {    const timer = setTimeout(() => {        resolve(console.log(""));        clearTimeout(timer);    }, ms);});// "глобальный" awaitawait delay(1000);squareOutput = square(13);diagonalOutput = diagonal(12, 5);export { squareOutput, diagonalOutput };// main.jsimport { squareOutput, diagonalOutput } from "./middleware.js";console.log(squareOutput); // 169console.log(diagonalOutput); // 13console.log("From Main");const timer1 = setTimeout(() => {    console.log(squareOutput);    clearTimeout(timer1);}, 2000); // 169const timer2 = setTimeout(() => {    console.log(diagonalOutput);    clearTimeout(timer2);}, 2000); // 13

Ни одна из инструкций в main.js не выполняется до разрешения промисов в middleware.js. Это гораздо более чистое решение по сравнению с обходными путями.

Заметка

Глобальный await работает только с ES модулями. Используемые зависимости должны быть указаны явно. Приведенный ниже пример из репозитория предложения хорошо это демонстирует.

// x.mjsconsole.log("X1");await new Promise(r => setTimeout(r, 1000));console.log("X2");// y.mjsconsole.log("Y");// z.mjsimport "./x.mjs";import "./y.mjs";// X1// Y// X2

Данный сниппет не выведет в консоль X1, X2, Y, как можно ожидать, поскольку x и y отдельные модули, не связанные между собой.

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

Реализация


V8

Вы можете протестировать данную возможность уже сейчас.

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

chrome.exe --js-flags="--harmony-top-level-await"

Вы таже можете попробовать данную фичу в Node.js. Прочитайте это руководство, чтобы узнать больше.

ES модули

Убедитесь, что добавили тегу script атрибут type со значением module.

<script type="module" src="./index.js"></script>

Обратите внимание, что в отличие от обычных скриптов, ES6 модули следуют политике общего происхождения (одного источника) (SOP) и совместного использования ресурсов (CORS). Поэтому с ними лучше работать на сервере.

Случаи использования


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

Динамический путь зависимости

const strings = await import(`/i18n/${navigator.language}`);

Это позволяет модулям использовать значения среды выполнения для вычисления путей зависимостей и может быть полезным для разделения разработка/продакшн код, интернационализации, разделения кода в зависимости от среды выполнения (браузер, Node.js) и т.д.

Инициализация ресурсов

const connection = await dbConnector()

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

Запасной вариант

В приведенном ниже примере показано, как глобальный await может использоваться для загрузки зависимости с реализацией запасного варианта. Если импорт из CDN A провалился, осуществляется импорт из CDN B:

let jQuery;try {  jQuery = await import('https://cdn-a.example.com/jQuery');} catch {  jQuery = await import('https://cdn-b.example.com/jQuery');}

Критика


Rich Harris составил список критических замечаний относительно await верхнего уровня. Он включает в себя следующее:

  • Глобальный await может блокировать выполнение кода
  • Глобальный await может блокировать получение ресурсов
  • Отсутствует поддержка CommonJS модулей

Вот какие ответы на эти замечания даются в FAQ предложения:

  • Поскольку дочерние узлы (модули) имеют возможность выполнения, блокировка кода, в конечном счете, отсутствует
  • Глобальный await используется на стадии выполнения графа модулей. На данном этапе все ресурсы получены и связаны, поэтому риска блокировки получения ресурсов не усматривается
  • await верхнего уровня ограничен ES6 модулями. Поддержка CommonJS модулей, как и обычных скриптов, изначально не планировалась

Я снова настоятельно рекомендую ознакомиться с FAQ предложения.

Надеюсь, мне удалось доступно объяснить суть рассматриваемого предложения. Собираетесь ли использовать эту возможность? Делитесь своим мнением в комментариях.
Подробнее..

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

04.02.2021 10:09:11 | Автор: admin
Когда-то мы договорились внутри компании, что будем запускать фичи в приложении под A/B-тестами. Но всё равно были вещи из серии да это же очевидно, что так нужно сделать. Вот история одного из самых долгих и крупных да это же очевидно, помешавшего в итоге пользователям.

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



Возможность появилась в нашем флагманском приложении в 2019 году. Реализована она была через партнёра Smart Engine, решение Smart IDReader. Вот их публикация про применение этого SDK. Партнёра выбирали после долгих тестов: этот обеспечивает очень хорошее качество распознавания и готов юридически на все необходимые процедуры по передаче и защите данных. Они поставляли такие решения чуть ли не таможне.

Сначала подключили фичу только на авиабилеты и начали смотреть, что получается. Важно то, что это была одна из последних фич, когда не было возможности проводить A/B именно в приложении, мы использовали последовательные тесты на iOS/Android и A/B-тесты на вебе. Пока мы узнали только то, что конверсия не падает, а сканирование паспорта использует 10,1% iOS-юзеров и 8,2% Android-юзеров. Всё выглядело хорошо, и мы начали раскатывать фичу дальше.


Раскатка дальше


Тут важно сказать, что чем больше сканирований, тем дешевле единичное распознавание по условиям партнёра. Соответственно, мы раскатали на весь пул пользователей, получили сладкие цены и через некоторое время сделали ещё один контрольный замер после релиза. Тоже всё было хорошо. Единственное, что несколько удивляло, это то, что доля внесения правок после сканирования на Android была 83,7%, на iOS 28,9%. Видимо, речь про не самые хорошие камеры или плохое взаимодействие софта и камеры. Но люди пользовались фичей, и вроде всё было отлично.

Через 2 года нужно было продлевать пакет, и мы решили посмотреть, а что же там в A/B-тестах, благо такая возможность в приложении на тот момент уже давно была. Идея была в том, чтобы понять, насколько увеличивает конверсию такой быстрый и удобный ввод документов и сколько это вообще создаёт добавленной стоимости продукту.

Запустили ухудшающие A/B-тесты (скрывающие от пользователя часть функционала).

Получили результаты.

Удивились.

Выключили фичу.

Результаты тестов


Без сканирования: +8,7% продаж на новых железнодорожных билетах. Это лучший прирост конверсии за 3 года: нужно было просто выпилить фичу, в которую мы все верили.

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

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

Выводы


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

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

Итоговый результат парадоксален: чем больше времени человек проводит в приложении, тем выше вероятность покупки. Сканирование, которое экономит ему около 30-40-50 секунд, ухудшает показатели конверсии. Возможных гипотез две:
  1. Возможно, пользователи, которые приложили больше усилий, больше дорожат этими усилиями и меньше уходят, поэтому ручной ввод работает на цель.
  2. Либо сканированием пользовались те люди, которые меньше были настроены на покупку. Барьер входа ниже, они пробуют и уходят тогда мы создали условия искусственного отбора именно таких.


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

А мораль такая тестируйте даже очевидное. Ещё раз спасибо, Капитан Очевидность, но нам это стоило довольно много времени разработки и, соответственно, денег.
Подробнее..

Перевод Почему никто ещё не скопировал переключатель звука с iPhone и OnePlus?

09.03.2021 18:08:33 | Автор: admin

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

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

Вот почему никто ещё не скопировал

Переключатель на АйфонахПереключатель на Айфонах

Есть одна реальная причина, почему производители не делают выключатель звука в своих телефонах: его разработка требует больше усилий, чем компании готовы приложить. Так, в Android нет стандартного API для таких переключателей, а поэтому компании придется разрабатывать как сам переключатель, так и ПО к нему. И даже если это не так уж сложно, это потребует кучу драгоценного времени, которое бренд лучше потратит на что-нибудь другое.

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

Может быть, переключатель просто низко-приоритетная штука: большинство пользователей звонит не так часто (по результатам нашего опроса). Хотя вы, безусловно, чаще выключаете телефон, чем звоните, это и было одной из причин, почему их вообще сделали. Apple сделала переключатель на оригинальном iPhone в 2007 году, когда приложения и мобильная сеть были на втором плане и люди чаще звонили. Но многое изменилось за эти 14 лет.

но вот, почему все равно следует это сделать

Переключатель на OnePlus (здесь три положения, в отличие от Айфона)Переключатель на OnePlus (здесь три положения, в отличие от Айфона)

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

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

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

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

От переводчика

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

Я сам третий месяц пользуюсь 1+8T, а раньше у меня был Samsung A50, совсем не флагман. Он заметно тормозил и, когда в школе вдруг мне звонили, приходилось судорожно пытаться разблокировать телефон: датчик отпечатка пальцев под экраном лагает, потом лагает интерфейс Теперь руку в карман, и все дела.

Я увидел этот материал и решил попробовать себя в переводе: и английский люблю, и с автором согласен. А что думаете вы насчёт переключателя?

Подробнее..

Как можно при синхронизации облачного хранилища легко создать фантомный файл? (Яндекс.Диск)

22.09.2020 20:11:13 | Автор: admin
Обнаружил великолепную фичу Яндекс-Диска, и делюсь этой прекрасной информацией.

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

Исходные требования: Вам нужно иметь облачное хранилище на Яндекс-Диск, большого объема (вам же нужно много данных?). С этим хранилищем желательно синхронизировать несколько устройств, например десктоп, ноутбук, и что-нибудь еще (можно рабочий комп). На хранилище нужно иметь свободное пространство, для размещения копии данных.

Затем нужно выбрать папку, которая содержит необходимое вам количество данных (например, много. Скажем, несколько гигабайт это много для Яндекс-Диска). Весьма желательно, чтобы внутри этой папки было несколько уровней вложенных папок с файлами.

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


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

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

Этот эксперимент был проведен два раза. Оба раза фича отработала на ура, и было создано много данных.

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

Из песочницы Как приоритизировать фичи в продукте

04.11.2020 14:09:33 | Автор: admin

Приоритизация секретный ингредиент?


Согласно определению из Википедии: Приоритизация понятие, показывающее важность, первенство. Например, приоритет действий определяет порядок их выполнения во времени
Из определения становится понятно, что основная цель приоритизации понять, что важнее чего.

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

Но что делать когда задач больше?

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

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

1) A/B тест лендинга 1 и лендинга 2

2) Дизайн дашборда

3) Интеграция CRM для отдела продаж

4) Добавить базу знаний для пользователей


27) Welcome бонус при регистрации пользователя

Что же первое взять в работу? А второе? Ну дальше? Почему не наоборот?

image

Разумеется, реализовать их все вряд ли получится. Все упирается либо во время, либо в деньги. Но как правильно оценивать и отсеивать фичи?

Существует различные подходы к оценки той или иной задачи. Ниже разберем основные из них.

Быстрая приоритизация. А не фигню ли я делаю?


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

В этом подходе мы используем метод Poker Planning:

image

Сам процесс по шагам выглядит так:

  1. Продакт- менеджер с командой обсуждают пользу каждой фичи и ставят свою оценку от 1 до 3. Где 3 супер полезно, а 1 минимально. Записывается среднее значение в таблицу.
  2. Тоже самое повторяем с оценкой затрат. Важно: затраты нужно обсуждать не с коллегой на кухне, а непосредственно с теми кто выполняет задачу.
  3. Получаем соотношение польза/затраты и видим, что 1 и 3 задача лидируют- значит, их можно взять в ближайший спринт.

Быстрая приоритизация. А не фигню ли я делаю?


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

В этом подходе мы используем метод Poker Planning

image

Сам процесс по шагам выглядит так:

  1. Продакт- менеджер с командой обсуждают пользу каждой фичи и ставят свою оценку от 1 до 3. Где 3 супер полезно, а 1 минимально. Записывается среднее значение в таблицу.
  2. Тоже самое повторяем с оценкой затрат. Важно: затраты нужно обсуждать не с коллегой на кухне, а непосредственно с теми кто выполняет задачу.
  3. Получаем соотношение польза/затраты и видим, что 1 и 3 задача лидируют- значит, их можно взять в ближайший спринт.

RICE


image

Данный подход был разработан в компании Intercom. После тестов ребята остановили на 4 важных факторах:

  • Reach (охват) скольким пользователям мы улучшим жизнь?

Охват измеряется количеством людей / событий за период времени.

  • Impact (эффект) насколько мы улучшим жизнь нашим пользователям

Воздействие трудно измерить довольно точно. Поэтому для удобства можно сделать следующие варианты: 3 для сильного воздействия, 2 для высокого, 1 для среднего, 0,5 для низкого и 0,25 для минимального.

  • Confidence (уверенность) насколько мы уверены, что вообще можем что-то улучшить?

Здесь есть 3 градации уверенности:

100% высокая
80% средняя
50% низкая

  • Effort (усилия) сколько времени нам понадобится, чтобы реализовать задуманное?

В основном измеряется в человеко-месяцах. То есть объем работы, которую человек может выполнить за 1 месяц. Используем по возможности целые числа. Если 1 месяц, то значение 1,0. Если занимает меньше месяца, то значение 0,5.

image

Далее после того как умножим первые 3 параметра друг на друга и поделим на усилия, мы получаем итоговый счет. Чем выше счет тем важнее реализация той или иной задачи.

MoSCoW


Метод MoSCoW позволяет разделить все активности, хотелки и вновь прилетающие задачи на 4 категории, что намного эффективнее.

  • Must то, что необходимо сделать в любом случае. Без выполнения этих задач продукт не будет работать в принципе.
  • Should не самые важные требования, но они тоже должны быть выполнены. Естественно, после реализации must.
  • Could желательные требования, которые можно сделать, если останется время и будут ресурсы.
  • Would требования, которые хотелось бы сделать, но их можно проигнорировать или перенести на следующие релизы без вреда для продукта.

Отсюда как раз идет аббревиатура MoSCoW. Разберем данный подход на примере ремонта новой квартиры в новостройке:

  1. M делаем электричество, трубопровод, клеим обои, кладем плитку или паркет. Устанавливаем туалет, ванну, делаем кухню и базово в спальню хотя бы просто кровать.
  2. S покупаем мебель, шкафы, холодильник, микроволновку, стол, стулья, стиральную машину.
  3. C установка посудомойки, дополнительные шкафы, кран с вытягивающимся шлангом, подсветка по периметру шкафов или на потолке.
  4. W сервоприводы для ящиков, подсветка внутри шкафов, крутящаяся полка для углового шкафа, акустическая система, сетка на окнах от комаров, теплый пол, видеодомофон.

Плюсы: просто, быстро, понятно заказчику (если это не технический специалист)
Минусы: не сильно объективно, не учитывается техническая сложность и риски

ICE Scoring: Как это работает?


Рассчитайте оценку для каждой фичи или идеи, согласно формуле:

image

  • Влияние показывает, насколько ваша идея положительно повлияет на ключевой показатель, который вы пытаетесь улучшить.
  • Легкость реализации это о простоте реализации. Это оценка того, сколько усилий и ресурсов требуется для реализации этой идеи.
  • Уверенность показывает, насколько вы уверены в оценках влияния и легкости реализации.

image

В ICE используется шкала от 1 до 10 чтобы все факторы сбалансированно влияли на итоговый бал. Вы можете подразумевать под 110 то что вам нужно, лишь бы значения были согласованы между собой.

В качестве примера, применим это к фиче Виджеты для Dashboard:

  • Влияние: насколько это будет эффективно? Что это даст нашим пользователям и их целям и задачам?
  • Легкость реализации: насколько легко будет разрабатывать, тестировать и запускать эту фичу?
  • Уверенность: как я могу быть уверен, что эта фича приведет к такому улучшению, которое я описал в Impact и займет столько-то времени?

Недостатки ICE


ICE Scoring иногда подвергается критике за его субъективность:

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

Резюме


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

В заключении приведу краткий алгоритм приоритизации:

  1. Найдите ТОП-3 ключевые метрики для конкретного сервиса.
  2. Соберите гипотезы для прокачки этих метрик: из бэклога или вне его.
  3. Если рынок новый используйте качественные методы: спрашивайте у потенциальных юзеров, чем они сейчас пользуются.
  4. Проведите быструю оценку и отбросьте слабые фичи.
  5. Проведите детальную оценку оставшихся фичей.
Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru