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

Javascript

Перевод Простые советы по написанию чистого кода React-компонентов

20.04.2021 12:16:22 | Автор: admin
Автор материала, перевод которого мы публикуем сегодня, делится советами, которые помогают делать чище код React-компонентов и создавать проекты, которые масштабируются лучше, чем прежде.



Избегайте использования оператора spread при передаче свойств


Начнём с анти-паттерна, с приёма работы, которым лучше не пользоваться в тех случаях, когда для этого нет конкретных, обоснованных причин. Речь идёт о том, что следует избегать использования оператора spread ({...props}) при передаче свойств от родительских компонентов дочерним.

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

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


Если функция принимает несколько параметров хорошо будет поместить их все в объект. Вот как это может выглядеть:

export const sampleFunction = ({ param1, param2, param3 }) => {console.log({ param1, param2, param3 });}

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

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

При работе с обработчиками событий используйте функции, возвращающие функции-обработчики


Если вы знакомы с функциональным программированием то знайте, что то, о чём я хочу сейчас рассказать, напоминает каррирование. Суть в том, что тут применяется заблаговременная установка некоторых параметров функции.

Взгляните на этот пример:

export default function SampleComponent({ onValueChange }) {const handleChange = (key) => {return (e) => onValueChange(key, e.target.value)}return (<form><input onChange={handleChange('name')} /><input onChange={handleChange('email')} /><input onChange={handleChange('phone')} /></form>)}

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

Используйте объекты, хранящие пары ключ-значение вместо условного оператора


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

Вот пример, в котором используется if/else:

const Student = ({ name }) => <p>Student name: {name}</p>const Teacher = ({ name }) => <p>Teacher name: {name}</p>const Guardian = ({ name }) => <p>Guardian name: {name}</p>export default function SampleComponent({ user }) {let Component = Student;if (user.type === 'teacher') {Component = Teacher} else if (user.type === 'guardian') {Component = Guardian}return (<div><Component name={user.name} /></div>)}

А вот пример использования объекта, хранящего соответствующие значения:

import React from 'react'const Student = ({ name }) => <p>Student name: {name}</p>const Teacher = ({ name }) => <p>Teacher name: {name}</p>const Guardian = ({ name }) => <p>Guardian name: {name}</p>const COMPONENT_MAP = {student: Student,teacher: Teacher,guardian: Guardian}export default function SampleComponent({ user }) {const Component = COMPONENT_MAP[user.type]return (<div><Component name={user.name} /></div>)}

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

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


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

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

import ConfirmationDialog from 'components/global/ConfirmationDialog';export default function useConfirmationDialog({headerText,bodyText,confirmationButtonText,onConfirmClick,}) {const [isOpen, setIsOpen] = useState(false);const onOpen = () => {setIsOpen(true);};const Dialog = useCallback(() => (<ConfirmationDialogheaderText={headerText}bodyText={bodyText}isOpen={isOpen}onConfirmClick={onConfirmClick}onCancelClick={() => setIsOpen(false)}confirmationButtonText={confirmationButtonText}/>),[isOpen]);return {Dialog,onOpen,};}

Пользоваться этим хуком можно так:

import React from "react";import { useConfirmationDialog } from './useConfirmationDialog'function Client() {const { Dialog, onOpen } = useConfirmationDialog({headerText: "Delete this record?",bodyText:"Are you sure you want to delete this record? This cannot be undone.",confirmationButtonText: "Delete",onConfirmClick: handleDeleteConfirm,});function handleDeleteConfirm() {//TODO: удалить}const handleDeleteClick = () => {onOpen();};return (<div><Dialog /><button onClick={handleDeleteClick} /></div>);}export default Client;

Такой подход к абстрагированию компонентов избавляет программиста от необходимости написания больших объёмов шаблонного кода для управления состоянием приложения. Если вы хотите узнать о нескольких полезных хуках React взгляните на этот мой материал.

Разделяйте код компонентов на части


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

Использование обёрток


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

Вот пример компонента, реализующего drag-and-drop с использованием react-beautiful-dnd:

import React from 'react'import { DragDropContext, Droppable } from 'react-beautiful-dnd';export default function DraggableSample() {function handleDragStart(result) {console.log({ result });}function handleDragUpdate({ destination })console.log({ destination });}const handleDragEnd = ({ source, destination }) => {console.log({ source, destination });};return (<div><DragDropContextonDragEnd={handleDragEnd}onDragStart={handleDragStart}onDragUpdate={handleDragUpdate}><Droppable droppableId="droppable" direction="horizontal">{(provided) => (<div {...provided.droppableProps} ref={provided.innerRef}>{columns.map((column, index) => {return (<ColumnComponentkey={index}column={column}/>);})}</div>)}</Droppable></DragDropContext></div>)}

А теперь взгляните на тот же компонент после того, как мы переместили drag-and-drop-логику в компонент-обёртку:

import React from 'react'export default function DraggableSample() {return (<div><DragWrapper>{columns.map((column, index) => {return (<ColumnComponentkey={index}column={column}/>);})}</DragWrapper></div>)}

Вот код обёртки:

import React from 'react'import { DragDropContext, Droppable } from 'react-beautiful-dnd';export default function DragWrapper({children}) {function handleDragStart(result) {console.log({ result });}function handleDragUpdate({ destination }) {console.log({ destination });}const handleDragEnd = ({ source, destination }) => {console.log({ source, destination });};return (<DragDropContextonDragEnd={handleDragEnd}onDragStart={handleDragStart}onDragUpdate={handleDragUpdate}><Droppable droppableId="droppable" direction="horizontal">{(provided) => (<div {...provided.droppableProps} ref={provided.innerRef}>{children}</div>)}</Droppable></DragDropContext>)}

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

Разделение обязанностей


Тут я хочу рассказать о моём любимом методе разделения больших компонентов на части.

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

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

Например давайте посмотрим на следующий компонент:

import React from 'react'import { someAPICall } from './API'import ItemDisplay from './ItemDisplay'export default function SampleComponent() {const [data, setData] = useState([])useEffect(() => {someAPICall().then((result) => {setData(result)})}, [])function handleDelete() {console.log('Delete!');}function handleAdd() {console.log('Add!');}const handleEdit = () => {console.log('Edit!');};return (<div><div>{data.map(item => <ItemDisplay item={item} />)}</div><div><button onClick={handleDelete} /><button onClick={handleAdd} /><button onClick={handleEdit} /></div></div>)}

Ниже показано то, что получилось после его рефакторинга, в ходе которого его код разделён на части с применением пользовательских хуков. А именно вот сам компонент:

import React from 'react'import ItemDisplay from './ItemDisplay'export default function SampleComponent() {const { data, handleDelete, handleEdit, handleAdd } = useCustomHook()return (<div><div>{data.map(item => <ItemDisplay item={item} />)}</div><div><button onClick={handleDelete} /><button onClick={handleAdd} /><button onClick={handleEdit} /></div></div>)}

Вот код хука:

import { someAPICall } from './API'export const useCustomHook = () => {const [data, setData] = useState([])useEffect(() => {someAPICall().then((result) => {setData(result)})}, [])function handleDelete() {console.log('Delete!');}function handleAdd() {console.log('Add!');}const handleEdit = () => {console.log('Edit!');};return { handleEdit, handleAdd, handleDelete, data }}

Хранение кода каждого компонента в отдельном файле


Часто программисты пишут код компонентов примерно так:

import React from 'react'export default function SampleComponent({ data }) {export const ItemDisplay = ({ name, date }) => (<div><h3><font color="#3AC1EF">{name}</font></h3><p>{date}</p></div>)return (<div><div>{data.map(item => <ItemDisplay item={item} />)}</div></div>)}

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

Итоги


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

Что вы посоветовали бы тем, кто хочет сделать код своих React-компонентов чище?

Подробнее..

Дайджест свежих материалов из мира фронтенда за последнюю неделю 463 (12 18 апреля 2021)

19.04.2021 00:19:54 | Автор: admin
Предлагаем вашему вниманию подборку с ссылками на новые материалы из области фронтенда и около него.


Медиа|Веб-разработка|CSS|JavaScript|Браузеры


Медиа


podcast Новости 512 от CSSSR: Chrome 90, Deno 1.9, анализ производительности JS, сборщики, верстка писем, pnpm 6, ESLint 7.24.0
podcast Подкаст proConf #92: GatsbyConf 2021
podcast Подкаст Сделайте мне красиво 60 Единственный фронтендер, который откладывает яйца
podcast Подкаст Фронтенд Юность #183: Путь от идеи до популярного OpenSource проекта
podcast Подкаст Да как так-то?. Выпуск 2: Тимлиды, проектные менеджеры, тестировщики кто все эти люди?

Веб-разработка


habr Малоизвестные, но крутые атрибуты в HTML
habr Микрофронтенды: разделяй и властвуй
en Полное руководство по созданию шаблонов HTML-писем
en Практическая доступность, часть 2: дайте имя (почти) всему
en Новости платформы: Использование :focus-visible, новый шрифт BBC, Declarative Shadow DOMs, A11Y иплейсхолдеры
en Медленно и осторожно: конвертация всего интерфейса Sentry на TypeScript
en Напряжение между Wix и WordPress растет




CSS


habr Нестандартные шрифты: как подключить и оптимизировать
habr Какие CSS-генераторы можно использовать в 2021 году
habr Пользовательские CSS-переменные, инверсия светлоты цветов и создание тёмной темы за 5 минут
habr CSS: работа с текстом на изображениях
Tailwind CSS: to use, или not to use?
en Tailwind UI: теперь с поддержкой React + Vue
en Проблемы с Overflow в CSS
en Как подружить стили с Fullscreen API
en Скажите привет CSS Container Queries
en CSS это строго типизированный язык
en Руководство для новичков по новым утилитам в Bootstrap 5
en Используйте Reseter.css вместо Normalize и Reset.css. Чтобы улучшить кроссбраузерность.


JavaScript


habr Типобезопасность в JavaScript: Flow и TypeScript
habr Работа с датой и часовыми поясами в JavaScript
en Изменение размера изображения в зависимости от контета с помощью JavaScript
en Работа со строками в современном JavaScript
en Генераторы JavaScript: превосходный async/await
en Другой подход к архитектуре фронтенда





Браузеры


habr Вышел Chrome 90
Включение поддержки HTTP/3 в Firefox намечено на конец мая
В Firefox 90 будет удалён код, обеспечивающий поддержку FTP
Разработчики Vivaldi и Brave отказались использовать FLoC от Google, призванный заменить сторонние cookie
В Microsoft Edge появился специальный детский режим
en В Firefox Nightly и Beta появилась поддержка QUIC и HTTP / 3
en WebKit: Представляем CSS Grid Inspector

Дайджест за прошлую неделю.
Материал подготовили dersmoll и alekskorovin.
Подробнее..

Ontol подборка видео-лекций и каналов для продвинутых программистов

14.04.2021 12:16:12 | Автор: admin
image

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

В перерывах между полетами на реактивном ранце и переводами материалов Y Combinator, я делаю проект Ontol такое место в сети, где максимальная концентрация полезного, апгрейдящего мировоззрение материала (ценного на горизонте 10+ лет, например, такого), которым можно делиться бесплатно в 1 клик. (канал в телеграм: t.me/ontol)

Вот мои предыдущие бесплатные образовательные подборки:



Simple Made Easy 2012 (Rich Hickey)


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



The Mess We're In (Joe Armstrong)


Джо Армстронг один из создателей Erlang. Он работал в лаборатории компьютерных наук Эрикссон в 1986 году и был частью команды, которая разработала и внедрила первую версию Erlang. Он написал несколько книг про Erlang, в том числе Programming Erlang Software for a Concurrent World. Джо имеет докторскую степень в области компьютерных наук Королевского технологического института в Стокгольме, Швеция.



The Unreasonable Effectiveness of Multiple Dispatch (Karpinski)


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

  1. Совместное использование базовых типов в большом количестве пакетов, обеспечивающих разную функциональность;
  2. Совместное использование общих алгоритмов, которые работают с различными реализациями общих абстракций.

Почему общий код в Julia просто работает? Почему кажется, что пакеты Julia разделяют типы с таким незначительным трением? Предполагается, что оба вида повторного использования являются естественными преимуществами объектно-ориентированных языков на основе классов. В конце концов, наследование и инкапсуляция два из четырех столпов ООП. Еще более загадочным является то, что Julia не имеет инкапсуляции и вообще не позволяет наследовать от конкретных типов. Тем не менее, широко распространены оба вида повторного использования кода. Что происходит? В этом выступлении я утверждаю, что оба вида совместного использования напрямую связаны с парадигмой множественного диспетчерского программирования Julia.



Low Level JavaScript


image

Low Level JavaScript это канал YouTube, который глубоко погружается в суть низкоуровневого программирования возится с единицами и нулями но все это делается на чистом JavaScript.

Пример лекции:



David Beazley


Дэвид Бизли автор книг Python Cookbook и Python Essential Reference. Вот его канал.

Пример лекции:



Jacob Sorber


Якоб Сорбер освещает темы, полезные как для новичков, так и для продвинутых: network programming, threads, processes, operating systems, embedded systems и других.

Пример лекции:



Computerphile


Канал Computerphile младший брат Numberphile. Про всякие компьютерные штуки.

Пример лекции:



Category Theory (Bartosz Milewski)


Серия Теория категорий Бартоша Милевски открывает новый взгляд на программирование в целом.

Пример лекции:



Build a 65c02-based computer from scratch (Ben Eater)


Разбираемся, как работают компьютеры. В этих лекциях Ben Eater создет и программирет базовый компьютер с классическим микропроцессором 6502.

Пример лекции:



Building an 8-bit breadboard computer! (Ben Eater)


Попытка построить еще один 8-битный компьютер с нуля.

Пример лекции:



How to Become a Good Backend Engineer (Hussein Nasser)


Прокачиваем Backend.

Пример лекции:



Semicolon&Sons


Хардкорные скринкасты для программистов, которые создают собственный бизнес.

Пример лекции:



Andrew Kelley


Эндрю создает язык системного программирования Zig. Но он также останавливается на других общих проблемах системного программирования, которые не зависят от языка. Перееехал с YouTube на Vimeo из-за бесящей рекламы.

Пример видео:



Jon Gjengset


Мы создаем библиотеки и инструменты на языке программирования Rust. У Джона Дженгсета лучший канал про Rust для учеников среднего и продвинутого уровней. Также он соавтор/создатель Missing Semester

Пример лекции:



George Hotz | Programming | Livecoding SLAM | twitchslam | Part1


8-часовой прямой эфир. Изучение контрактов на блокчейн и обнаружение ошибки безопасности в одном из них.



Jordan Harrod


Аспирантка Гарварда и Массачусетского технологического института, изучает интерфейсы мозг-машина и машинное обучение для медицины (анестезия) и рассказывает про взаимодействие человека с ИИ и алгоритмами.

Пример видео:



ACM SIGPLAN


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

Пример видео:



Fun Fun Function


Канал, где можно узнать и про софт скилы и про трансдьюсеры.

Пример лекции:



GOTO Conferences


Канал от сообщества GOTO

Пример видео:



Javidx9


Для тех, кто занимается программированием игр. Видео от Javidx9 четкие, лаконичные и насыщенные примерами.

Пример видео:



TechLead


Уволенный из Google и Facebook техлид за стаканчиком кофе делится мудростью.



TheCherno


Канал от бывшего разработчика из EA. Видео в основном посвящены C ++ и разработке игровых движков.

Пример видео:



DefogTech


Темы канала: Java concurrency, distributed systems, system design, microservice.

Пример видео:



Simons Institute video archive

.
Больше теории, чем программирования, но много концепций SOTA.

Пример видео:



C Weekly With Jason Turner


Советы и новости про C++. И живое программирование.

Пример видео:



CppCon 2020


CppCon это ежегодная недельная встреча всего сообщества C ++. Канал конференции.

Пример видео:



NDC London 2020


Канал крупнейшей в Европе конфы по .NET & Agile development

Пример видео:



Подробнее..

Микрофронтенды разделяй и властвуй

14.04.2021 16:05:26 | Автор: admin


Всем привет! Меня зовут Аня, я фронтенд-разработчик в Delivery Club. Хочу рассказать про использование микрофронтендов. Поговорим о том, что же это за зверь такой микрофронтенд, почему мы решили использовать этот подход в своих проектах и с какими проблемами столкнулись при внедрении.

Для чего они нам понадобились


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

  • Сложно жить с монолитом. Быстро писать код мешали устаревание подходов, фреймворков и зависимостей, а также связанность логики.
  • Совместная разработка. Долго решать конфликты, противоречивость изменений.
  • Проблема с тестированием. Когда прилетали баги от других задач и команд, было непонятно, в чём причины.
  • Развёртывание было зависимое и блокирующее.

Поэтому нам нужна была возможность:

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

Устройство проекта


Для начала расскажу, как сейчас устроен наш проект.

  • Основное старое приложение на AngularJS, к которому мы планируем подключать новые микроприложения.
  • Dashboard-приложение на Angular 6, подключенное через iframe (но оно со временем разрослось и от описанных выше проблем не избавило). К нему подключаются приложения, здесь хранятся старые страницы.
  • Приложения на VueJS, которые используют самописную библиотеку компонентов на VueJS.





Мы поняли, что ограничения тормозят развитие проекта. Поэтому сформулировали возможные пути:

  • Разделение приложения на страницы по маршрутам.
  • iframe.
  • Микрофронтенды.

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

Что такое микрофронтенды


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


Сейчас наш проект только переходит на микрофронтенды, наша команда стала первопроходцем. И мы столкнулись с рядом проблем.

Проблемы внедрения микрофронтендов


1. Ещё один iframe? Может, уже хватит?


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

Мы видели несколько недостатков:

  • Неудобная навигация. Каждый раз для редиректа на внешнюю ссылку нужно использовать window.postMessage.
  • Сложно верстать в iframe.

К счастью, нам удалось этого всего этого избежать, и микрофрентенд мы подключили как веб-компонент с shadow dom: <review-ui-app></review-ui-app>. Такое решение выгодно с точки зрения изоляции кода и стилей. Веб-компонент мы сделали с помощью модифицированного vue-web-component-wrapper. Почитать подробнее о нём можно здесь.

Что мы сделали:

  1. Написали скрипт, который добавляет ссылку на сборку микрофронтенда в разделе head страницы при переходе на соответствующий маршрут.
  2. Добавили конфигурацию для микрофронтенда.
  3. Добавили в window.customElements тег review-ui-app.
  4. Подключили review-ui-app в dashboard-приложение.

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

2. А где стили?


Ещё одна неприятная проблема нас ждала дальше. Компоненты в микрофронтенде работали, только вот стили не отображались. Мы придумали несколько решений:

  • Первое: импортировать все стили в один файл и передать его во vue-wrapper (но это слишком топорно и пришлось бы добавлять вручную каждый новый файл со стилями).
  • Второе: подключить стили с помощью CSS-модулей. Для этого пришлось подкрутить webcomponents-loader.js, чтобы он вшивал собранный CSS в shadow dom. Но это лучше, чем вручную добавлять новые CSS-файлы :)

3. Теперь про иконки забыли!


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

  1. Сначала мы попытались подрубить спрайт так же, как и стили, через appendChild. Они подключились, но всё равно не отображались.
  2. Затем мы решили подключить через sprite.mount(this.shadowRoot). Добавили в вебпаке в svg-sprite-loader опцию spriteModule: path.resolve(__dirname, './src/renderers/sprite.js). Внутри sprite.js экспортировали BrowserSprite, и иконки начали отображаться! Мы, счастливые, подумали, что победили, но не тут-то было. Да, иконки отображались, и мы даже выкатились с этой версией в прод. Но потом нашли один неприятный баг: иконки пропадали, если походить по вкладкам dashboard-приложения.
  3. Наконец, во vue-wrapper мы подключили DcIconComponent (библиотечный компонент, позволяющий работать с библиотечными иконками) и в нём подключили иконки из нашего проекта. Получили отображение без багов :)

4. Без авторизации никуда!


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

  • Токен с авторизацией передаём с помощью свойств веб-компонента.
  • С помощью AuthRequestInterceptor подключаем токен-запросы для API.
  • Используем токен, пока он не протухнет. После протухания ловим ошибку 401 и кидаем в dashboard-приложение событие обнови токен, пожалуйста (ошибка обрабатывается в AuthResponseInterceptor).
  • Dashboard-приложение обновляет токен. Следим за его изменением внутри main-сервиса, и когда токен обновился, заворачиваем его в промис и подписываемся на обновления токена в AuthResponseInterceptor.
  • Дождавшись обновления ещё раз повторяем упавший запрос, но уже с новым токеном.

5. Нас волнуют зависимости


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

  • В микрофронтенд-приложении указываем в webpack.config.prod.js в разделе externals те зависимости, которые хотим вынести:

    module.exports = {externals: {vue: Vue},
    

    Здесь мы указываем, что под именем Vue в window можно будет найти зависимость vue.
  • В рамках оболочки (в нашем случаем в dashboard-приложении) выполняем npm install vue (и другие npm install-зависимости).
  • В dashboard-приложении импортируем все зависимости:

    import Vue from vue(window as any).Vue = Vue;
    
  • Получаем удовольствие.

6. Разные окружения


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

Решили мы это следующим образом:

  1. Добавили в микрофронтенд файл, в котором определяем конфигурацию для приложения в runtime браузера. Также добавили в Docker системный пакет, который предоставляет команду envsubst. Она подставляет значения в env.js, который тянет микрофронтенд-приложение, и эти переменные пишутся в window['${APP_NAME}_envConfig'].
  2. Добавили переменные окружения отдельно для прода и отдельно для тестового окружения.

Так мы решили несколько проблем:

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

Выводы


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

Думаю, со временем всё больше проектов будут отказываться писать монолит и переходить на более универсальные подходы. Одним из таких подходов могут стать и микрофронтенды. Надеюсь, эта статья поможет кому-то в реализации :).
Подробнее..

Как портировать SDK Flutter на ТВ-приставку для разработки и запуска приложений Android TV

15.04.2021 16:11:54 | Автор: admin

Недавно мы успешно портировали фреймворк Flutter на ТВ-приставку c открытой программной платформой RDK. В этой статье расскажем о трудностях, с которыми пришлось столкнуться, и предложим решения для успешного запуска и повышения производительности.

Учитывая, что программный стек RDK или Reference Design Kit сейчас активно используется для разработки OTT-приложений, голосового управления приставками и других продвинутых функций для видео по запросу (VoD), мы хотели разобраться, сможет ли Flutter работать на ТВ-приставке. Оказалось, что да, но, как это обычно бывает, есть нюансы.

Далее мы по шагам распишем процесс портирования и запуска Flutter на встраиваемых Linux-платформах и разберемся, как этот SDK с открытым исходным кодом от Google чувствует себя на железе с ограниченными ресурсами и ARM-процессорами.

Но прежде чем переходить непосредственно к Flutter и его преимуществам скажем пару слов об исходном решении, которое было задействовано на ТВ-приставке. На плате работала связка набор библиотек EFL + протокол Wayland, а рисование примитивов было реализовано из node.js на основе плагинного нативного модуля. Это решение неплохо себя показало с точки зрения производительности при отображении кадров, однако сам EFL отнюдь не самый новый фреймворк для отрисовки. А в режиме выполнения node.js со своим огромным event-loopом казался уже не самой перспективной идеей. В то же время Flutter мог позволить нам задействовать более производительную связку рендеринга.

Для тех, кто не в теме: первую версию этого SDK с открытым кодом Google представил еще шесть лет назад. Тогда этот набор средств разработки годился только для Android. Сейчас на нем можно писать приложения для веба, iOS, Linux и даже Google Fuchsia. :-) Рабочий язык для разработки приложений на Flutter Dart, в свое время он был предложен в качестве альтернативы JavaScript.

Перед нами стоял вопрос: даст ли переход на Flutter какой-то выигрыш по производительности? Ведь подход там совершенно иной, хоть в конечном счете и имеется та же графическая подсистема Wayland + OpenGL. Ну и как там с поддержкой процессоров с neon-инструкциями? Были и другие вопросы, например, нюансы по переносу UI на dart или то, что поддержка Linux находится в стадии альфы-беты.

Сборка Flutter Engine для ТВ-приставок на базе ARM

Итак, начнем. Вначале Futter нужно запустить на чужеродной платформе с Wayland + OpenGL ES. В основе рендеринга у Flutter лежит библиотека Skia, которая прекрасно поддерживает OpenGL ES, поэтому в теории все выглядело хорошо.

При сборке Flutter под наши целевые устройства (три ТВ-приставки с RDK), к нашему удивлению, проблемы возникли только на одной. Не будем с ней сражаться, т.к. из-за старой архитектуре intel x86 она для нас не является приоритетной. Лучше сосредоточимся на оставшихся двух ARM-платформах.

Вот, с какими опциями мы собирали Flutter Engine:

./flutter/tools/gn \      --embedder-for-target \      --target-os linux \      --linux-cpu arm \      --target-sysroot DEVICE_SYSROOT      --disable-desktop-embeddings \      --arm-float-abi hard      --target-toolchain /usr      --target-triple arm-linux-gnueabihf      --runtime-mode debugninja -C out/linux_debug_unopt_arm

Большинство опций понятны: собираем под 32-битный ARM-процессор и Linux, выключая при этом все лишнее через --embedder-for-target --disable-desktop-embeddings.

Для сборки в системе должен быть установлен clang версии 9 и выше, т.е. это стандартный сборочный механизм Flutter, инструментарий кросс-компиляции gcc не пойдет. Самое важное подать корректный target-sysroot устройства с RDK.

Честно говоря, мы удивились, что при сборке не возникло вообще никаких нюансов. На выходе получаем заветную библиотеку flutter_engine.so и заголовок с необходимыми функциями для эмбеддера.

Теперь можно собрать целевой проект flutter/dart с нашей библиотекой/движком. Это сделать легко:

flutter --local-engine-src-path PATH_TO_BUILDED_ENGINE_src --local-engine=host_debug_unopt build bundle

Важно! Сборка проекта должна происходить не на устройстве с собранной библиотекой, а на хостовой, т.е. x86_64!

Для этого достаточно еще раз пройти путь сборкой gn и ninja только под x86_64! Именно она указывается в параметре host_debug_unopt.

PATH_TO_BUILDED_ENGINE_src это путь, где находится engine/src/out.

За запуск Flutter Engine под системой обычно отвечает embedder, именно он конфигурирует Flutter под целевую систему и дает основные контексты рендеринга библиотеке Skia и Dart-обработчику. Не так давно в состав Flutter добавили linux-embedder, и, в частности, GTK-embedder, так что можно воспользоваться им из коробки. На нашей платформе на момент портирования это был не вариант, нужно было что-то независимое от GTK.

Рассмотрим некоторые особенности реализации, которые пришлось учесть с кастомным эмбеддером (все, кто любит разбирать не нюансы, а исходники целиком, может сразу перейти к проекту нашего форка с доработками на github.com). К тому же по производительности наш вариант немного выигрывал у версии GTK, что было крайне важно для заказчика, и не тянул за собой весь зоопарк GTK-библиотек.

Так что же вообще нужно от эмбеддера для запуска flutter-приложения? Достаточно, чтобы он просто вызывал из библотеки flutter_engine.so

FlutterEngineRun(FLUTTER_ENGINE_VERSION, &config, &args, display /* userdata */, &engine_);

где в качестве параметров идет передача настроек проекта (директория с собранным flutter bundle) FlutterProjectArgs args и аргументов рендеринга FlutterRendererConfig config.

В первой структуре как раз задается путь bundle-пакета, собранного flutter-утилитой, а во второй используются контексты OpenGL .

// пример использования на github.com/DEgITx/flutter_wayland/blob/master/src/flutter_application.cc

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

Проблемы и их решение

Теперь поговорим о нюансах, с которыми мы столкнулись на этапе портирования. А как же без них? Не только ведь библиотеки собирать :-)

1. Краш эмбеддера и замена очередности вызова функций

Первая проблема, с которой мы столкнулись краш эмбеддера под платформой. Казалось бы, инициализация egl-контекста в других приложения происходит нормально, FlutterRendererConfig инициализирован корректно, но нет эмбеддер не заводится. Значит в связке что-то явно не так. Оказалось, eglBindAPI нельзя вызывать перед eglGetDisplay, на котором происходит особая инициализация nexus-драйвера дисплея (у нас платформа базируется на чипе BCM). В обычном Linux это не проблема, но на целевой платформе оказалась иначе.

Корректная инициализация эмбеддера выглядит так:

egl_display_ = eglGetDisplay(display_);if (egl_display_ == EGL_NO_DISPLAY) {  LogLastEGLError();  FL_ERROR("Could not access EGL display.");  return false;}if (eglInitialize(egl_display_, nullptr, nullptr) != EGL_TRUE) {  LogLastEGLError();  FL_ERROR("Could not initialize EGL display.");  return false;}if (eglBindAPI(EGL_OPENGL_ES_API) != EGL_TRUE) {  LogLastEGLError();  FL_ERROR("Could not bind the ES API.");  return false;}

// github.com/DEgITx/flutter_wayland/blob/master/src/wayland_display.cc корректная реализация, т.е. помогла измененная очередность вызова функций.

Теперь, когда нюанс запуска улажен, мы рады увидеть заветное демо-окно приложения на экране :-).

2. Оптимизация производительности

Настало время проверить производительность. И, честно говоря, она нас не сильно порадовала в режиме отладки (debug mode). Что-то работало шустро, что-то наоборот, имело большие просадки по фреймам и тормозило гораздо больше, чем что-то похожее на EFL+Node.js.

Мы немного расстроились и начали копать дальше. В SDK Flutter есть специальный режим компиляции машинного кода AOT, это даже не jit, а именно компиляция в нативный код со всеми сопутствующими оптимизациями, именно это подразумевается под по релиз-версией Flutter. Такой поддержки у нас в эмбеддере пока не было, добавляем.

Необходимы определенные инструкции, поданные аргументами к FlutterEngineRun

// полная реализация github.com/DEgITx/flutter_wayland/blob/master/src/elf.cc

vm_snapshot_instructions_ = dlsym(fd, "_kDartVmSnapshotInstructions");if (vm_snapshot_instructions_ == NULL) {  error_ = strerror(errno);  break;}vm_isolate_snapshot_instructions_ = dlsym(fd, "_kDartIsolateSnapshotInstructions");if (vm_isolate_snapshot_instructions_ == NULL) {  error_ = strerror(errno);  break;}vm_snapshot_data_ = dlsym(fd, "_kDartVmSnapshotData");if (vm_snapshot_data_ == NULL) {  error_ = strerror(errno);  break;}vm_isolate_snapshot_data_ = dlsym(fd, "_kDartIsolateSnapshotData");if (vm_isolate_snapshot_data_ == NULL) {  error_ = strerror(errno);  break;}
if (vm_snapshot_data_ == NULL || vm_snapshot_instructions_ == NULL || vm_isolate_snapshot_data_ == NULL || vm_isolate_snapshot_instructions_ == NULL) {  return false;}*vm_snapshot_data = reinterpret_cast <  const uint8_t * > (vm_snapshot_data_);*vm_snapshot_instructions = reinterpret_cast <  const uint8_t * > (vm_snapshot_instructions_);*vm_isolate_snapshot_data = reinterpret_cast <  const uint8_t * > (vm_isolate_snapshot_data_);*vm_isolate_snapshot_instructions = reinterpret_cast <  const uint8_t * > (vm_isolate_snapshot_instructions_);
FlutterProjectArgs args;// передаем все необходимое в argsargs.vm_snapshot_data = vm_snapshot_data;args.vm_snapshot_instructions = vm_snapshot_instructions;args.isolate_snapshot_data = vm_isolate_snapshot_data;args.isolate_snapshot_instructions = vm_isolate_snapshot_instructions;

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

$HOST_ENGINE/dart-sdk/bin/dart \--disable-dart-dev \$HOST_ENGINE/gen/frontend_server.dart.snapshot \--sdk-root $DEVICE_ENGINE}/flutter_patched_sdk/ \--target=flutter \-Ddart.developer.causal_async_stacks=false \-Ddart.vm.profile=release \-Ddart.vm.product=release \--bytecode-options=source-positions \--aot \--tfa \--packages .packages \--output-dill build/tmp/app.dill \--depfile build/kernel_snapshot.d \package:lib/main.dart$DEVICE_ENGINE/gen_snapshot                               \    --deterministic                                             \    --snapshot_kind=app-aot-elf                                 \    --elf=build/lib/libapp.so                                   \    --no-causal-async-stacks                                    \    --lazy-async-stacks                                         \    build/tmp/app.dill

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

-Ddart.vm.profile=release \
-Ddart.vm.product=release \

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

output-dill нужен для построения нативной библитеки libapp.so.

Самыми важными для нас являются пути $DEVICE_ENGINE и $HOST_ENGINE два собранных движка под целевую (ARM) и хост-системы (x86_64) соответственно. Тут важно ничего не перепутать и убедиться, что libapp.so получается именно 32-битной ARM-версией:

$ file libapp.so libapp.so: ELF 32-bit LSB shared object, ARM, EABI5 version 1 (SYSV), dynamically linked

Запускаем и-и-и-и... вуаля! все работает!

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

3. Подключение устройств ввода

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

4. Интерфейс на ТВ-приставке под Linux и Android и как увеличить производительность в 23 раза

Коснемся еще нескольких нюансов производительности, с которыми столкнулись в продуктовом UI-приложении. Нас очень обрадовала идентичность работы UI как на целевом устройстве, так и на Linux и Android. Уже сейчас Flutter может вполне может похвастаться очень гибкой портируемостью.

Еще отметим интересный опыт оптимизации самого dart-приложения под целевую платформу. Нас разочаровала довольно низкая производительность продуктового приложения (в отличии от демок). Мы взяли в руки профайлер и начали копать идовольно быстро обнаружили активное использование функций __brcm_cpu_dcache_flush и khrn_copy_8888_to_tf32 во время анимаций (на платформе используется чип процессора Broadcom/BCM ). Явно происходило какое-то очень жесткое пиксельное программное трансформирование или копирование во время анимаций. В итоге виновник был найден: в одной из панелей был задействован эффект размытия:

//...filter: new ImageFilter.blur(sigmaX: 10.0, sigmaY: 10.0),//...

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

Итого

В результате мы получили не просто работающее продуктовое приложение, а работающее приложение с качественным фреймрейтом на Flutter на целевом устройстве. Форк и наша версия эмбеддера под RDK и другие платформы на основе Wayland находится тут: github.com/DEgITx/flutter_wayland

Надеемся, опыт нашей команды в разработке и портировании ПО для ТВ-приставок и Smart TV пригодится вам в своих проектах и послужит отправной точкой для портирования Flutter на других устройствах.

[!?] Вопросы и комментарии приветствуются. На них будет отвечать автор статьи Алексей Касьянчук, наш инженер-программист

Подробнее..

Перевод Сочиняя ПО Почему стоит изучать ФП на JavaScript?

16.04.2021 02:15:22 | Автор: admin

Эта статья - часть серии статей "Составляя ПО" про функциональное программирование и различные техники создания программ на JavaScript ES6+, начиная с азов. Предыдущая часть: Сочиняя ПО: Введение

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

Если вы опытный разработчик, уже знакомый с JavaScript или с каким-то чисто функциональным языком, то вы можете подумать, что JavaScript это весьма забавный способ открыть для себя мир *[ФП]: функциональное программирование. Отставьте эти мысли в сторону и попробуйте посмотреть на текст незашоренным взглядом. Вы можете обнаружить скрытый уровень в программировании на JavaScript, уровень, о котором вы даже не подозревали.

Раз уж эта статья имеет в названии "Сочиняя ПО", и ФП это, очевидно, путь сочинить программу (используя функциональную композицию, функции высшего порядка, и т.д.), то вы можете спросить, почему бы нам не взять какой-нибудь Haskell, ClojureScript, или Elm вместо JavaScript.

JavaScript имеет в своем составе важные особенности, необходимые для ФП:

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

  2. Анонимные функции и лямбда-синтаксис. Например, запись видаx => x * 2является валидным выражением в JavaScript. Такой синтаксис значительно упрощает работу с функциями высшего порядка.

  3. Замыкания. Замыкание - это комбинация функции и ее лексического окружения. Замыкания создаются в момент создания функции. Когда функция создается внутри другой функции, то она имеет доступ к переменным, объявленным во внешней функции, даже после того, как будет осуществлен возврат из этой внешней функции. Замыкания это то, что позволяет работать фиксированным аргументам частичных применений. Фиксированный аргумент - это аргумент, заданный в контексте замыкания возвращаемой функции. Например, в выраженииadd(1)(2)аргумент1является фиксированным аргументом для функции, возвращаемой при вызовеadd(1). Пример:

/* * Более длинный вариант: * const add = function (x) { *     return function (y) { *         return x + y; *     }     * } */const add = x => y => x + y;const summ = add(1)(2);

Что отсутствует в JavaScript

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

Мутация - это изменение значения структуры данных без создания новой переменной и переприсваивания значения. Например:

const foo = {  bar: 'baz'};foo.bar = 'qux'; // мутация

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

Ниже представлен список вещей, которые присущи некоторым функциональным языкам и которых нет в JavaScript:

  1. Чистота. В некоторых ФП языках "чистота" поддерживается самим языком. Выражения с побочными эффектами недопустимы.

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

  3. Рекурсия. Рекурсия - это способность функции вызвать саму себя. Во многих ФП языках рекурсия это единственная возможность выполнить цикл. В таких языках нет конструкций типаfor,whileилиdo ... while.

Чистота: В JavaScript "чистота" может быть достигнута только по соглашению (т.е. все участники должны договориться использовать только чистые функции - прим. перев.). Если вы не используете для большей части своего приложения композицию чистых функций, то вы не следуете функциональному стилю. К сожалению, в JavaScript слишком просто сбиться с пути случайно начав создавать и использовать не "чистые" функции.

Иммутабельность: В ФП языках иммутабельность зачастую дана по умолчанию. В JavaScript отсутствуют эффективные структуры данных, используемые в большинстве ФП языков, но существуют библиотеки, которые могут помочь в этом вопросе, напримерImmutable.jsиMori. Я надеюсь, что в будущих версиях спецификации ECMAScript все же появятся неизменяемые структуры данных.

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

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

Рекурсия: Технически, JavaScript поддерживает рекурсию, однако большинство функциональных языков имеют такую особенность как "оптимизация хвостовой рекурсии". Такая особенность позволяет рекурсивным функциям переиспользовать фреймы стека для последующих рекурсивных вызовов (фактически рекурсия преобразуется в плоскую итерацию - прим. перев.).

Без оптимизации хвостовой рекурсии стек вызовов растет без ограничений и может вызвать переполнение стека. JavaScript, с технической точки зрения, имеет ограниченную хвостовую оптимизацию в стандарте ES6. К сожалению, только один из наиболее распространенных браузеров реализовал эту функциональность, а подобная оптимизация в Babel (наиболее популярный JavaScript компилятор, используемый для компиляции ES6 в ES5), хоть и частичная, была позднее удалена.

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

Что есть в JavaScript такого, чего нет в функциональных языках

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

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

"Монада - это моноид из категории эндофункторов, какие тут проблемы?" ~ Джеймс Айри, как бы цитирующий Филипа Вадлера, перефразируя реальную цитату Сондерса Мак Лейна."Краткая, неполная и в большинстве своем неправильная история языков программирования"

Как правило, пародия преувеличивает смешные вещи, делая из еще забавнее. Цитата выше - это упрощенное определение термина монада, которое звучит примерно так:

"Монада в множестве Х это моноид из категории эндофункторов Х, в котором морфизм, называемый "произведение", заменен композицией эндофункторов, а морфизм, называемый "единица", заменен эндофунктором "тождественность". ~ Сандерс Мак Лейн."Категории для практикующих математиков".

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

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

Согласно Брендану Эйху, такая цель существовала с самого начала:

"... авторы компонентов, кто пишут на С++ или (мы надеемся) на Java, и "скриптовики", начинающие или "про", кто будут писать код, внедренный в HTML."

Исходное намерение Netscape состояло в поддержке двух различных языков, и скриптовый язык, предположительно, должен был напоминать Scheme (диалект языка Lisp). Брендан Эйх:

"Я был нанят компанией Netscape с обещанием "реализовать Scheme" в браузере".

JavaScript должен был стать новым языком:

"Диктат высшего руководства заключался в том, что язык должен быть похож на Java. Это сразу оставило за бортом Perl, Python и Tcl вместе со Scheme."

Таким образом, замысел Брендана Эйха с самого начала был:

  1. Scheme в браузере

  2. Выглядит как Java

Ну и кончилось это все даже большей мешаниной:

"Я совсем не горжусь, но счастлив, что выбрал в качестве основных ингредиентов scheme-подобные функции первого класса и self-подобные (хотя и единичные) прототипы (видимо, имеется ввиду, что в языке Self прототипы сложнее чем в JavaScript - прим. перев.). Влияние Java, особенно проблема y2k, а также разделение на примитивные типы и объекты, было неудачным."

Я бы добавил к списку "неудачных" Java-подобных особенностей языка то, что в конце-концов вошло в JavaScript:

  • Функция-конструктор и ключевое словоnew, с семантикой и способом вызова отличными от функций-фабрик

  • Ключевое словоclassвместе сextendsдля наследования от единственного родителя как основной механизм наследования

  • Тенденция разработчиков думать о классе как о статическом типе, коим он не является.

Мой вам совет: избегайте использования всего этого как только можете.

Мы счастливчики в том, что JavaScript стал настолько богатым языком, потому что в конечном итоге его скриптовый подход выиграл у "компонентного" подхода (на сегодня Java, Flash и ActiveX расширения не поддерживаются в большинстве используемых браузеров).

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

Это означает, что браузеры меньше перегружены и содержат меньше ошибок, потому что им нужно поддерживать только один язык - JavaScript. Вы можете подумать, что WebAssembly - это исключение, но одна из целей разработки WebAssembly - это использование существующей поддержки JavaScript абстрактным синтаксическим деревом (AST). На практике, первой демонстрацией возможностей WebAssembly стало подмножество JavaScript, известное как ASM.js.

Положение единственного стандартного языка программирования общего назначения для веба позволило JavaScript оседлать самую большую волну популярности в истории программного обеспечения:

Приложения съели мир, Интернет съел приложения, а JavaScript сожрал Интернет.

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

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

В JavaScript все эти идеи могут сосуществовать одновременно, позволяя людям использовать язык, что привело к появлению наибольшего реестра ПО с открытым исходным кодом в миреnpm.

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

Я согласен с мнением, что JavaScript не лучший язык для ФП. Однако, ни один из других ФП языков не может похвастаться тем, что подходит каждому и каждый может принять его и начать использовать, а также, как продемонстрировал ES6 - JavaScript может становиться лучше и обслуживать нужды разработчиков, заинтересованных в ФП. Почему бы вместо забвения JavaScript и его удивительной экосистемы, используемой практически в каждой компании в мире, не принять его и постепенно не сделать его лучшим языком для создания ПО?

В текущем состоянии JavaScript достаточно неплох для ФП, разработчики могут создавать всевозможные виды полезных и интересных штук, используя техники функционального программирования. Netflix (и любое приложение на Anglular 2+) использует функциональные утилиты, основанные на библиотеке RxJS. Facebook использует концепт чистых функций, функций высшего порядка, и компонентов высшего порядка для разработки Facebook и Instagram. PayPal, KhanAcademy и Flipkart используют Redux для управления состоянием.

Они не одиноки: Angular, React, Redux и Lodash лидируют в рейтинге используемых фреймворков и библиотек в экосистеме JavaScript, и все они весьма серьёзно вдохновлены ФП, а в случае Lodash и Redux, спроектированы таким образом, чтобы продемонстрировать причины применения паттернов ФП в реальных JavaScript приложениях.

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

В любой момент времени в США открыто около ста тысяч вакансий и еще сотни тысяч по всему миру. Изучение Haskell научит вас многому из ФП, но изучение JavaScript научит вас многому из того, как создавать работающие приложения для реальной работы.

Приложения съели мир, Интернет съел приложения, JavaScript съел Интернет.

Подробнее..

Перевод Вы уверены, что вам нужен API?

16.04.2021 10:20:09 | Автор: admin


От переводчика: При разработке бэкэнда наличие API для фронт-энда стало практически повсеместным стандартом. Однако можем ли мы называть это "настоящим" API? Предлагаем вашему вниманию интересное пятничное чтение, которое, возможно, повлияет на API, которые мы все разрабатываем.


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


Ценность API в сокрытии информации


Предоставляя API, системы решают, какая информация должна быть доступна из внешнего мира, а какая будет держаться в секрете (сокрытие информации).


Почему это важно? Потому что чем объемнее API, тем дороже его поддерживать. Представьте себе крайний случай открытие каждой детали реализации. Каждое изменение в системе может сломать код, который используют ваши клиенты. Именно по этой причине мы стремимся разрабатывать компактные API. Я писал статью (перевод на хабре) по этой теме в контексте описания шаблона Регистрация событий.


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


API как продукт


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


Разделение клиента и сервера


Я готов спорить, что такие API как продукт редкость. Гораздо чаще существование развесистого API признак плохо выбранных границ вследствие нарушения принципа слабая связанность сильное сцепление.


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


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


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


Тяга к отрисовке на клиентской части


Ещё один двигатель страсть к реализации клиентской части с использованием современных JS фреймворков для разработки пользовательского интерфейса, таких как Angular, React или Vue.js. В противоположность отрисовке на сервере (SSR), эти фреймворки отрисовывают интерфейс на клиентской машине (CSR) в браузере и полагаются на сервисы (REST, GraphQL,...), предоставляемые сервером для получения данных и выполнения каких-то действий.


Самодостаточность и CSR не противоречат друг другу


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


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


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


Заключение


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


Если вы решите двигаться по пути создания единой системы, то вам решать, что использовать SSR или CSR. И, хотя зачастую понятия CSR и SPA (одностраничное приложение) часто слепливают вместе, вы можете использовать CSR и разрабатывать модульную клиентскую часть.


Ссылки


У меня появилась идея написания этой статьи, когда я слушал подкаст SoftwareArchitekTOUR Episode 82 (German) с Stefan Tilkov и Eberhard Wolff. Спасибо за вдохновение!


Хорошие источники для более глубокого погружения в самодостаточным системам и микро-фронтам:
Self-Contained Systems
Micro Frontend

Подробнее..

Мой стейт менеджер для React, Preact, Inferno

16.04.2021 14:07:36 | Автор: admin

Люблю изобретать велосипеды и прочие нужные предметы заново. Получается не всегда хорошо, но процесс интересный. Предлагаю вашему вниманию библиотеку управления стейтом для React, Preact (вес всего 4.8Кб). Библиотека еще находится разработке, но уже можно попробовать.

Начнем с примера всеми любимого TODO органайзера. Исходный код на гитхабе. Для начала создадим основной компонент main.js.

// main.jsimport React, { createElement, Component, createContext } from 'react';import ReactDOM from 'react-dom';import {Connect, Provider} from './store'import Input from './InputComp'import TodoList from './TodoList'import LoadingComp from './LoadingComp'const Main = () => (  <Provider>    <h1>Todo:</h1>    <LoadingComp>      <TodoList/>    </LoadingComp>    <hr/>    <Input/>  </Provider>)ReactDOM.render(<Main />, document.getElementById("app"));

Далее стор. Стор нам необходим для инициализации библиотеки, а также тут мы указываем все необходимые файлы с акшенсами. В нашем примере это actions.js и actionsSetup.js

// store.jsimport React, { createElement, Component, createContext } from 'react';import createStoreFactory from 'redoor';// Экспортируем все функции из actions.js и actionsSetup.jsimport * as actions from './actions'import * as actionsSetup from './actionsSetup'// здесь мы указываем необходимые функции библиотеки Reactconst createStore = createStoreFactory({  Component,   createContext,   createElement});// создаем стор в качестве параметра необходимо указать массив объектов// всех используемых акшен функцийconst { Provider, Connect } = createStore([  actions,  actionsSetup]);export { Provider, Connect };

Файл с нашими акшенсами и стейтом проекта

// actions.js// каждый локальный стейт может содержать свой набор переменных // redoor автоматически добавит их глобальный стор// initState зарезервированная переменая она может быть как объект,// так и функция, которая возвращает объект со стейтомexport const initState = {    todos:[],    value:'',}// добавляем в массив новую задачу// переменная state - содержит глобальный стейт// переменная args - зависит от передаваемых значений из компонента// возвращает функция новые переменные стейтаexport const a_enter = ({state,args}) => {  let {value,todos} = state;  todos.push({    id:(Math.random()+"").substr(2),    value:value,    done:false  });  return {    value:'',    todos  }}// помечаем элемент как сделанноеexport const a_done = ({state,args}) => {  let {todos} = state;  let id = args.id;  todos = todos.map(it=>(it.id === id ? (it.done = !it.done, it) : it))  return {    todos  }}// удаляем элемент из спискаexport const a_delete = ({state,args}) => {  let {todos} = state;  let id = args.id;  todos = todos.filter(it=>it.id !== id)  return {    todos  }}

Теперь компоненты отображения

// InputComp.jsimport React from 'react';import {Connect} from './store'// redoor добавляет в пропсы функцию cxRun и все переменные// глобально стораconst Input = ({cxRun, value})=><label className="input">  Todo:    // здесь мы можем поменять стор прямо из компонента  <input onChange={e=>cxRun({value:e.target.value})} value={value} type="text"   />    // по нажатию вызываем акшен a_enter из нашено actions.js  <button onClick={e=>cxRun('a_enter')} disabled={!value.length}>ok</button></label>// соеденяем с redoor наш компонент и экспортируем export default Connect(Input);

cxRun может работать в двух режимах. Первый непосредственно менять содержимое стора, как в случае ввода строки или вызова акшенса из файла actions.js.

И последний компонент выводящий сам список дел.

// TodoList.jsimport React from 'react';import {Connect} from './store'const Item = ({cxRun, it, v})=><div className="item">  // вызываем акшен a_done, где в качестве параметра указываем   // элемент массива в ашенсе эта переменная будет называться args  <div className="item_txt" onClick={e=>cxRun('a_done',it)}>    {v+1}) {it.done ? <s>{it.value}</s> : <b>{it.value}</b>}  </div>  <div className="item_del" onClick={e=>cxRun('a_delete',it)}>    &times;  </div></div>const TodoList = ({cxRun, todos})=><div className="todos">  {    todos.map((it,v)=><Item key={v} cxRun={cxRun} it={it} v={v}/>)  }</div>export default Connect(TodoList);

Теперь по порядку. В нашем проекте в глобальном сторе всего две переменные value и todos. Инициализацией их занимается initState в файле actions.js. initState может быть объектом, так и функцией которая должна вернуть объект со стейтом. Тут важно понимать, что все стейты в акшенс файле помещаются в единый объект и каждый акшенс имеет доступ к любым переменным стейта.

Акшенсы -- это функции которые должны начинаться с префикса "а_" или "action". Имя функции акшенса будет указываться в качестве первого параметра при вызове cxRun. В качестве входного параметра будет объект с переменными state и args.

state -- это весь глобальный стейт проекта

args -- это второй параметр вызова функции cxRun. В нашем проекте при нажатии удалить мы вызываем cxRun('a_delete', it), где первым аргументом будет имя функции акшенса, а вторым сам элемент именно его мы и получаем в args.

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

Что делать если акшен работает асинхронно? Для этого нам необходимо подключить метод setState к локальным переменным файла actions.js с помощью функции bindStateMethods.

//actions.jslet __setState;let __getState;// подключаем методы работы со стейтомexport const bindStateMethods = (getState, setState) => {  __getState = getState;  __setState = setState;};export const a_setup = async ({state,args}) => {  __setState({loading:true});  let data = await loading();  __setState({    loading:false,    todos:data  })}

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

Debugger

Для дебагинга есть инструмент redoor-devtool. Дебаггер это сервер который слушает данные от redoor библиотеки и передает их на одностраничник по адресу localhost:8333. Таким образом дебагер может находится не только в другом браузере, но и на другой машине. Что бывает удобно особенно при разработке для мобильных.

устанавливаем redoor-devtool:

yarn add redoor-devtool

и в отдельной консоли запускаем сервер дебаг консоли

npx redoor-devtool -o

ключик "-o" откроет хром по адресу http://localhost:8333, где будет дебаггер.

Заключение

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

На этом пока все. Будет интерес - постараюсь написать более подробный обзор.

Подробнее..
Категории: Javascript , React , State , Manager , Preact , Inferno

Декомпиляция node.js в Ghidra

16.04.2021 14:07:36 | Автор: admin


Приветствую,


Вам когда-нибудь хотелось узнать, как же именно работает программа, которой вы так активно пользуетесь, игра, в которую часто играете, прошивка какого-нибудь устройства, которое что-то делает по расписанию? Если да, то для этого вам потребуется дизассемблер. А лучше декомпилятор. И если с x86-x64, Java, Python ситуация известная: этих ваших дизассемблеров и декомпиляторов полным-полно, то с другими языками всё обстоит немного сложнее: поисковые машины уверенно утверждают It's impossible.



Что ж, мы решили оспорить данное утверждение и произвести декомпиляцию NodeJS, а именно выхлоп, который выдаёт npm-пакет bytenode. Об этом подробнее мы и расскажем по ходу статьи. Заметим, что это уже вторая статья в серии о нашем плагине для Ghidra (первый материал был также опубликован в нашем блоге на Хабре). Поехали.


Часть нулевая: забегая вперёд


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



К этому:



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


  • Парсинг вывода bytenode. Кроме исполняемого кода, он также парсит пул констант, аргументы функций, области видимости, обработчики исключений, контекстные переменные и многое другое.
  • Полноценная загрузка исследуемого jsc-файла в Ghidra, с отображением пользователю необходимых для реверс-инжиниринга данных.
  • Поддержка всех опкодов, в том числе с различными вариациями их длины расширенных (wide) и экстрарасширенных (extra-wide).
  • Подтягиваются вызовы функций стандартной библиотеки (Intrinsic и Runtime-вызовы).
  • Анализируются все перекрёстные ссылки, даже в обфусцированном коде, что дает возможность исследовать любые NodeJS-приложения.

Конечно, есть и ряд ограничений. О них в конце статьи.

Составные части плагина


Сейчас модуль состоит из четырёх частей:


  1. Загрузчик: парсит файл, создаёт необходимые секции для кода и констант. Тем, кому дизассемблер ближе декомпилятора, пригодится.
  2. Анализатор: работает после загрузчика. Расставляет перекрёстные ссылки, убирает stub-код, сгенерированный компилятором (упрощает анализ), следит за контекстом исполнения.
  3. Дизассемблер и по совместительству декомпилятор. Благодаря технологиям, реализованным в Ghidra, при написании дизассемблера вы одновременно получаете и декомпилятор, что очень удобно. Для этого используется внутренний язык Гидры SLEIGH.
  4. Последняя часть модуля инжектор конструкций на PCode (языке промежуточного представления в Ghidra, аналоге микрокода в IDA). Инжектор формирует промежуточное представление для декомпилятора в тех случаях, когда через SLEIGH это реализовать сложно, или даже невозможно.

Процесс создания


Как это обычно и бывает, когда ты занимаешься реверс-инжинирингом всякой дичи, пришёл один бинарь. В нашем случае он имел расширение .jsc и запускался с помощью node.exe. Гугление по данной связке привело к bytenode пакету для Node.js, позволяющему собрать исходник на JavaScript в виде jsc-файла с байткодом, который гарантированно запустится на той же версии Ноды, если в ней установлен этот самый bytenode.


Бинарь вроде похож на файлы .pyc или .class, где интерпретатор точно так же исполняет собранный под него файл. Проблема только в том, что и для первого, и для второго формата давно существует множество различных файлообменников декомпиляторов, а для формата Ноды нет.


Но ведь в node, скажете вы, уже существует встроенный дизассемблер, и с его помощью можно смотреть на то, что происходит в запускаемом файле. Да, это так. Но он скорее рассчитан на то, что исходный файл у вас всё-таки имеется. У нас же его не было. Поэтому, сделав волевое усилие, мы приняли решение написать этот, будь он проклят, декомпилятор! Знали бы мы тогда, какие мучения нас ждут, взялись бы за проект? Конечно, взялись бы!



(Пример jsc-файла в hex-редакторе. Видны некоторые заголовки и строки)


Погружение и сборка node.js


Итак, что нам необходимо, чтобы начать разбор формата? Теория, в которой рассказывается о принципах работы Ноды? Кто вообще так делает? (Между прочим, данная теория прекрасно изложена в статье по ссылке.) Нам нужны исходники Node! Более того, они должны быть той же версии, в которой собран jsc-файл. Иначе не заработает.


Сделав клон репозитория и попытавшись его собрать, мы наткнулись на первые трудности: не собирается. Конечно, учитывая размеры репозитория и его кроссплатформенность удивляться нечему. Тем более последнюю на момент написания статьи версию Visual Studio 2019 в системе сборки Node.js стали поддерживать не так давно. Поэтому, чтобы собрать именно нужную нам v8.16, пришлось клонировать обе ветки: современную и нужную нам, а затем сравнивать систему сборки.


Система сборки состоит из набора Python-скриптов, батников и sln-проектов. Python-скрипты генерируют проекты для Visual Studio, подставляются дефайны, а затем Студия всё это дело собирает. Действительно, собирает. Но только в режиме сборки Release оно почему-то работает, а в Debug нет. А для наших целей нужна была именно дебажная сборка.


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



(Часть проекта NodeJS в Visual Studio)


Теперь с её помощью можно успешно отлаживать исходный код самой Node.js. Кроме того, появляются дополнительные флаги трассировки при исполнении, которые ещё больше помогают понять, что происходит при исполнении кода.


Парсинг формата


Было установлено огромное количество брейкпоинтов (точек останова) во всех местах, где, по идее, мог начинаться разбор jsc-файла, и наконец-то была запущена отладка.


Некоторые из установленных брейкпоинтов начали срабатывать ещё до того, как начинался разбор самого jsc-файла, что говорит нам о том, что сама Нода использует такой формат для хранения скомпилированных jsc-файлов у себя внутри исполняемого файла. В конце концов дело дошло и до нашего файла.


И тут началось.


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


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


Конечно, навигация по JSON (о нём расскажем чуть позже) та ещё весёлая задача, поэтому было принято не менее важное решение, очень сильно повлиявшее на наши дальнейшие запросы, перейти на Ghidra. Это решение влекло за собой следующие проблемы, которые пришлось решать:


  • перенос парсера с Python на Java и написание загрузчика jsc-формата;
  • создание нового процессорного модуля, который позволит дизассемблировать V8 (именно этот движок используется в Node.js);
  • реализация логики самих опкодов V8 с целью получения декомпилированного листинга.

Загрузчик для Ghidra 1


Первый загрузчик был простым: он разбирал JSON, который генерировался Python-версией парсера, создавал все секции, объекты, загружал байткод. Пока одна часть команды писала разборщик, другая её часть занималась реализацией опкодов на SLEIGH, параллельно создавая концепт плагина для Ghidra. Таким образом, к моменту, когда этот вариант загрузчика был готов, вся команда могла работать совместно.


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



(Пример JSON-выхлопа первого варианта загрузчика. Как видно, он хоть и являлся структурированным, но был сильно неудобным при навигации по коду и данным)


Загрузчик для Ghidra 2


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


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


Внутрянка загрузчика


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


  • разобрать jsc-файл на структуры, которые будут затем использованы дизассемблером, анализатором, декомпилятором;
  • отобразить пользователю плагина все те структуры, которые потребуются при реверс-инжиниринге: код, строки, числа, контексты, обработчики исключений и многое другое;
  • переименовать все объекты, у которых есть названия, и у которых их нет (обычный код, обёрнутый в скобки);
  • идентифицировать встроенные функции Node.js, которые вызываются исключительно по индексам.

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


Загруженный в Ghidra файл обычно выглядит как-то так:



Что на картинке
  1. Слева сверху видны сегменты. В jsc их нет, но нам пришлось создать их, чтобы сгруппировать сходные типы данных, и отделить их от кода.
  2. Слева идёт список функций. В случае с файлом, изображённым на скриншоте выше, они все обфусцированные, поэтому имеют одинаковые имена. Но это никак не мешает плагину выстраивать перекрёстные ссылки.
  3. В центре скриншота виден дизассемблерный листинг. Вам в любом случае придётся с ним работать, так как декомпилятор не всесилен.
  4. Справа виден декомпилированный C-подобный листинг. Как видим, он значительно облегчает анализ jsc-приложения.

Дизассемблер


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


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


  • Байт-код V8 интерпретируемый, что усложняет процесс реализации. Тем не менее во многих моментах нам очень помогали исходные коды других процессорных модулей, например виртуальной машины Java.
  • При реализации некоторых типов регистров (например, для работы с локальными переменными могут использоваться регистры с индексом от 0 до 0x7FFFFFFF-4) пришлось пойти на компромисс в виде максимально отображаемых в дизассемблере.


(Типичный дизазм V8)


Анализатор


Наверное, второй самой объёмной частью плагина после загрузчика является анализатор. Его основной задачей является наведение порядка после работы загрузчика. Он подразумевает:


  • создание перекрёстных ссылок на код и данные (constant pool),
  • отслеживание контекста исполнения (codeflow) программы,
  • упрощение представления асинхронного кода,
  • накопление информации для декомпилятора.

Создание ссылок и работа с constant pool


Если вы занимались реверс-инжинирингом Java-классов, то наверняка знаете, что самым главным объектом в каждом классе является constant pool. Вкратце, это что-то типа словаря, в котором по индексам в качестве ключей хранятся ссылки или значения в виде абсолютно любых объектов, например:


  • строк, чисел, списков, кортежей и других примитивов;
  • функций, областей видимости и других элементов кода.


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


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


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


  1. имеют ссылки на constant pool,
  2. имеют ссылки на контекст исполнения или изменяют его,
  3. выполняют runtime-функции (встроенные).

Код для работы с первым типом инструкций достаточно объёмный, так как в constant pool может храниться практически всё. И на это всё нужно проставить ссылки. Но рассказать мы бы хотели только об одном из сложных случаев: SwitchOnSmiNoFeedback.


SwitchOnSmiNoFeedback


По названию может показаться, что это обычные свитчи, только для V8. В действительности это специальная конструкция для работы с асинхронным кодом, то есть с тем, который помечен с помощью ключевых слов await/async. Работает оно так:


  1. В инструкции SwitchOnSmiNoFeedback указываются два индекса: первый начальный индекс в constant pool, по которому лежат ссылки на функции, и второй количество этих функций.
  2. Сами функции представляют из себя автоматически сгенерированные пролог (тело, эпилог) для кода, который требуется исполнять асинхронно (делается обёртка в виде переключения контекста, его сохранения, выгрузки). В нашем плагине этот шаблонный код заменяется на NOP-инструкции (No OPeration).

Возьмём в качестве примера следующий код:


async function handler() {    try {        const a = await 9;        const b = await 10;        const c = await 11;        let d = await 12;    } catch (e) {        return 123;    }    return 666;}console.log(handler());


(Так обычно и выглядит функция с SwitchOnSmiNoFeedback без оптимизации)



(А так с оптимизацией...)


Анализатор контекстов


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


В случае V8 для этого были введены три специальных сущности:


  1. регистр контекста,
  2. значение глубины для обращения к контексту,
  3. сохранение контекста в стек контекстов (выгрузка из него).

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


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


Runtime-функции


Основное, что здесь стоит отметить, это удаление (nop) из листинга обращений к функциям, также связанных со SwitchOnSmiNoFeedback, а именно async_function_promise_release, async_function_promise_create, promise_resolve. То есть плагин просто делает читаемость листинга декомпилятора выше.


Ссылки


GitHub: https://github.com/PositiveTechnologies/ghidra_nodejs/
Релизы: https://github.com/PositiveTechnologies/ghidra_nodejs/releases
Серьёзный разбор формата сериализации V8 движка в NodeJS: http://personeltest.ru/aways/habr.com/ru/company/pt/blog/551540/


Недостатки


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


Релиз


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


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


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


Спасибо за внимание

Подробнее..

Типобезопасность в JavaScript Flow и TypeScript

16.04.2021 16:20:16 | Автор: admin
Все, кто имеют дело с разработкой UI в кровавом enterprise наверняка слышали о типизированном JavaScript, подразумевая под этим TypeScript от Microsoft. Но кроме этого решения существует как минимум ещё одна распространённая система типизации JS, и тоже от крупного игрока IT-мира. Это flow от Facebook. Из-за личной нелюбви к Microsoft раньше всегда использовал именно flow. Объективно это объяснял хорошей интеграцией с существующими утилитами и простотой перехода.

К сожалению, надо признать, что в 2021 году flow уже значительно проигрывает TypeScript как в популярности, так и в поддержке со стороны самых разных утилит (и библиотек), и пора бы его закопать поставить на полку, и перестать жевать кактус перейти на де-факто стандарт TypeScript. Но под этим хочется на последок сравнить эти технлогии, сказать пару (или не пару) прощальных слов flow от Facebook.

Зачем нужна безопасность типов в JavaScript?


JavaScript это замечательный язык. Нет, не так. Экосистема, построенная вокруг языка JavaScript замечательная. На 2021 год она реально восхищает тем, что вы можете использовать самые современные возможности языка, а потом изменением одной настройки системы сборки транспилировать исполняемый файл для того, чтобы поддержать его выполнение в старых версиях браузеров, в том числе в IE8, не к ночи он будет помянут. Вы можете писать на HTML (имеется ввиду JSX), а потом с помощью утилиты babel (или tsc) заменить все теги на корректные JavaScript-конструкции вроде вызова библиотеки React (или любой другой, но об этом в другом посте).

Чем хорош JavaScript как скриптовый язык, исполняемый в вашем браузере?

  • JavaScript не нужно компилировать. Вы просто добавляете конструкции JavaScript и браузер обязан их понимать. Это сразу даёт кучу удобных и почти бесплатных вещей. Например, отладку прямо в браузере, за работоспособность которой отвечает не программист (который должен не забыть, например, включить кучу отладочных опций компилятора и соответствующие библиотеки), а разработчик браузера. Вам не нужно ждать по 10-30 минут (реальный срок для C/C++), пока ваш проект на 10к строк скомпилируется, чтобы опробовать написать что-то по другому. Вы просто меняете строку, перегружаете страницу браузера и наблюдаете за новым поведением кода. А в случает использования, например, webpack, страницу еще и за вас перезагрузят. Многие браузеры позволяют менять код прямо внутри страницы с помощью своих devtools.
  • Это кросс-платформенный код. В 2021 году уже почти можно забыть о разном поведении разных браузеров. Вы пишете код под Chrome/Firefox, заранее запланировав, например, от 5% (enterprise-код) до 30% (UI/мультимедиа) своего времени, чтобы потом подрихтовать результат под разные браузеры.
  • В языке JavaScript почти не нужно думать о многопоточности, синхронизации и прочих страшных словах. Вам не нужно думать о блокировках потоков потому что у вас один поток (не считая worker'ов). До тех пор, пока ваш код не будет требовать 100% CPU (если вы пишете UI для корпоритавного приложения), то вполне достаточно знать, что код исполняется в одном единственном потоке, а асинхронные вызовы успешно оркестрируются с помощью Promise/async/await/etc.
  • При этом даже не рассматриваю вопрос, почему JavaScript важен. Ведь с помощью JS можно: валидировать формы, обновлять содержимое страницы без перезагрузки её целиком, добавлять нестандартные эффекты поведения, работать с аудио и видео, да можно вообще целиком клиент своего enterprise-приложения написать на JavaScript.

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

Но и, разумеется, это плохо. Потому что сам факт наличия чего-нибудь где-нибудь это плохо. И было бы здорово до того, как код попадёт на сайт, видимый пользователям, проверить все-все скрипты на сайте и убедиться, что они хотя бы компилируются. А в идеале и работают. Для этого используются самые разные наборы утилит (мой любимый набор npm + webpack + babel/tsc + karma + jsdom + mocha + chai).

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

  • Что вы корректно используете синтаксис языка JavaScript. Проверяется, что набранный вами текст может быть понят интерпретатором языка JavaScript, что вы не забыли закрыть открытую фигурную скобку, что строковые лексемы корректно ограничены кавычками и прочее, и прочее. Эту проверку выполняют почти все утилиты сборки/траспилирования/сжатия/обфускации кода.
  • Что семантика языка используется корректно. Можно попытаться проверить, что те инструкции, которые записаны в написанном вами скрипте могут быть корректно поняты интерпретатором. Например, пусть есть следующий код:
    var x = null;x.foo();
    

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

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

console.log( input.value ) // 1console.log( input.value + 1 ) // 11

Существующие средства статического анализа кода (eslint, например), может попытаться отследить значительное количество потенциальных ошибок, которые программист допускает в своём коде. Например:


Обратите внимание, что все эти правила по сути представляют собой ограничения, которые линтер накладывает на программиста. То есть линтер фактически уменьшает возможности языка JavaScript, чтобы программист допускал меньше потенциальных ошибок. Если включить все-все правила, то будет нельзя делать присваивания в условиях (хотя JavaScript это изначально разрешает), использовать дубликаты ключей в литералах объектов и даже нельзя будет вызывать console.log().

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

image
Попытка умножить число на строку

Попытка обратиться к несуществующему (неописанному в типе) свойству объекта
Попытка обратиться к несуществующему (неописанному в типе) свойству объекта

Попытка обратиться к несуществующему (неописанному в типе) свойству объекта
Попытка вызвать функцию с несовпадающим типом аргумента

Если мы напишем этот код без средств проверки типов, то код упешно траспилируется. Никакие средства статического анализа кода, если они не используют (явно или неявно) информацию о типах объктов, не смогут эти ошибки найти.

То есть добавление типизации в JavaScript добавляет дополнительные ограничения на код, который пишет программист, но зато позволяет найти такие ошибки, которые в противном случае случились бы во время выполнения скрипта (то есть скорее всего у пользователя в браузере).

Возможности типизации JavaScript


Flow TypeScript
Возможность задать тип переменной, аргумента или тип возвращаемого значения функции
a : number = 5;function foo( bar : string) : void {    /*...*/} 
Возможность описать свой тип объекта (интерфейс)
type MyType {    foo: string,    bar: number}
Ограничение допустимых значений для типа
type Suit = "Diamonds" | "Clubs" | "Hearts" | "Spades";
Отдельный type-level extension для перечислений
enum Direction { Up, Down, Left, Right }
Сложение типов
type MyType = TypeA & TypeB;
Дополнительные типы для сложных случаев
$Keys<T>, $Values<T>, $ReadOnly<T>, $Exact<T>, $Diff<A, B>, $Rest<A, B>, $PropertyType<T, k>, $ElementType<T, K>, $NonMaybeType<T>, $ObjMap<T, F>, $ObjMapi<T, F>, $TupleMap<T, F>, $Call<F, T...>, Class<T>, $Shape<T>, $Exports<T>, $Supertype<T>, $Subtype<T>, Existential Type (*)
Partial<T>, Required<T>, Readonly<T>, Record<K,T>, Pick<T, K>, Omit<T, K>, Exclude<T, U>, Extract<T, U>, NonNullable<T>, Parameters<T>, ConstructorParameters<T>, ReturnType<T>, InstanceType<T>, ThisParameterType<T>, OmitThisParameter<T>, ThisType<T>

Оба движка для поддержки типов в JavaScript обладают примерно одинаковыми возможностями. Однако если вы пришли из языков со сторогой типизацией, даже в типизированном JavaScript есть очень важное отличие от той же Java: все типы по сути описывают интерфейсы, то есть список свойств (и их типы и/или аргументы). И если два интерфейса описывают одинаковые (или совместимые) свойства, то их можно использовать вместо друг-друга. То есть следующий код корректен в типизированным JavaScript, но явно некорректен в Java, или, скажем, C++:

type MyTypeA = { foo: string; bar: number; }type MyTypeB = { foo: string; }function myFunction( arg : MyTypeB ) : string {    return `Hello, ${arg.foo}!`;}const myVar : MyTypeA = { foo: "World", bar: 42 } as MyTypeA;console.log( myFunction( myVar ) ); // "Hello, World!"

Данный код является корректным с точки зрения типизированного JavaScript, так как интерфейс MyTypeB требует наличие свойства foo с типом string, а у переменной с интерфейсом MyTypeA такое свойство есть.

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

type MyTypeB = { foo: string; }function myFunction( arg : MyTypeB ) : string {    return `Hello, ${arg.foo}!`;}const myVar = { foo: "World", bar: 42 };console.log( myFunction( myVar ) ); // "Hello, World!"

Тип переменной myVar в данном примере это литеральный интерфейс { foo: string, bar: number }. Он по прежнему совместим с ожидаемым интерфейсом аргумента arg функции myFunction, поэтому данный код не содержит ошибок с точки зрения, например, TypeScript.

Подобное поведение значительно уменьшает количество проблем при работе с разными библиотеками, пользовательским кодом и даже просто с вызовом функций. Типичный пример, когда некоторая библиотека определяет допустимые опции, а мы передаём их в виде объекта options:

// Где-то внутри библиотекиinterface OptionsType {    optionA?: string;    optionB?: number;}export function libFunction( arg: number, options = {} as OptionsType) { /*...*/ }

// В пользовательском кодеimport {libFunction} from "lib";libFunction( 42, { optionA: "someValue" } );

Обратите внимание, что тип OptionsType не экспортирован из библиотеки (и не импортирован в пользовательский код). Но это не мешает вызывать функцию с использованием литерального интерфейса для второго аргумента options функции, а для системы типизации проверить этот аргумент на совместимость типов. Попытка сделать что-то подобное в Java вызовет у компилятора явное непонимание.

Как это работает с точки зрения браузера?


Ни TypeScript от Microsoft, ни flow от Facebook не поддерживаются браузерами. Как впрочем и самые новые расширения языка JavaScript пока не нашли поддержки в некоторых браузерах. Так как же этот код, во-первых, проверяется на корректность, а во-вторых, как он исполняется браузером?

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

/* удалено: type MyTypeA = { foo: string; bar: number; } *//* удалено: type MyTypeB = { foo: string; } */function myFunction( arg /* удалено: : MyTypeB */ ) /* удалено: : string */ {    return `Hello, ${arg.foo}!`;}const myVar /* удалено: : MyTypeA */ = { foo: "World", bar: 42 } /* удалено: as MyTypeA */;console.log( myFunction( myVar ) ); // "Hello, World!"

т.е.
function myFunction( arg ) {    return `Hello, ${arg.foo}!`;}const myVar = { foo: "World", bar: 42 };console.log( myFunction( myVar ) ); // "Hello, World!"


Такое преобразование обычно делается одним из следующих способов.
  • Для удаления информации о типах от flow используется плагин для babel: @babel/plugin-transform-flow-strip-types
  • Для работы с TypeScript можно использовать одно из двух решений. Во-первых можно использовать babel и плагин @babel/plugin-transform-typescript
  • Во-вторых вместо babel можно использовать собственный транспилер от Microsoft под названием tsc. Эта утилита встраивается в процесс сборки приложения вместо babel.


Примеры настроек проекта под flow и под TypeScript (с использованием tsc).
Flow TypeScript
webpack.config.js
{  test: /\.js$/,  include: /src/,  exclude: /node_modules/,  loader: 'babel-loader',},
{  test: /\.(js|ts|tsx)$/,  exclude: /node_modules/,  include: /src/,  loader: 'ts-loader',},
Настройки транспилера
babel.config.js tsconfig.json
module.exports = function( api ) {  return {    presets: [      '@babel/preset-flow',      '@babel/preset-env',      '@babel/preset-react',    ],  };};
{  "compilerOptions": {    "allowSyntheticDefaultImports": true,    "esModuleInterop": false,    "jsx": "react",    "lib": ["dom", "es5", "es6"],    "module": "es2020",    "moduleResolution": "node",    "noImplicitAny": false,    "outDir": "./dist/static",    "target": "es6"  },  "include": ["src/**/*.ts*"],  "exclude": ["node_modules", "**/*.spec.ts"]}
.flowconfig
[ignore]<PROJECT_ROOT>/dist/.*<PROJECT_ROOT>/test/.*[lints]untyped-import=offunclear-type=off[options]

Разница между подходами babel+strip и tsc с точки зрения сборки небольшая. В первом случае используется babel, во-втором будет tsc.


Но есть разница, если используется такая утилита как eslint. У TypeScript для линтинга с помощью eslint есть свой набор плагинов, которые позволяют найти ещё больше ошибок. Но они требуют, чтобы в момент анализа линтером у него была информация о типах переменных. Для этого в качестве парсера кода необходимо использовать только tsc, а не babel. Но если для линтера используется tsc, то использовать для сборки babel будет уже неправильно (зоопарк используемых утилит должен быть минимальным!).


Flow TypeScript
.eslint.js
module.exports = {  parser: 'babel-eslint',  parserOptions: {    /* ... */
module.exports = {  parser: '@typescript-eslint/parser',  parserOptions: {    /* ... */

Сравнение flow и TypeScript


Попытка сравнить flow и TypeScript. Отдельные факты собраны из статьи Nathan Sebhastian TypeScript VS Flow, часть собрана самостоятельна.

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

Различные линейки
Flow TypeScript
Основной contributor Facebook Microsoft
Сайт flow.org www.typescriptlang.org
GitHub github.com/facebook/flow github.com/microsoft/TypeScript
GitHub Starts 21.3k 70.1k
GitHub Forks 1.8k 9.2k
StackOverflow frequent 2289 49 353
StackOverflow unanswered 123 11 451

Смотря на эти цифры у меня просто не остаётся морального права рекомендовать flow к использованию. Но почему же я использовал его сам? Потому что раньше была такая штука, как flow-runtime.

flow-runtime


flow-runtime это набор плагинов для babel, который позволяет встроить типы от flow в runtime, использовать их для определения типов переменных в runtime, и, самое главное для меня, позволяло проврять типы переменных в runtime. Что позволяло в runtime во время, например, автотестов или ручного тестирования отловить дополнительные баги в приложении.

То есть прямо во время выполнения (в debug-сборке, разумеется), приложение явно проверяло все-все типы переменных, аргументов, результаты вызова сторонних функций, и всё-всё-всё, на соответствие тех типов.

К сожалению, под новый 2021 год автор репозитория добавил информацию о том, что он перестаёт заниматься развитием данного проекта и в целом переходит на TypeScript. Фактически последняя причина оставаться на flow для меня стала deprecated. Ну что же, добро пожаловать в TypeScript.
Подробнее..

Перевод Работа с датой и часовыми поясами в JavaScript

16.04.2021 16:20:16 | Автор: admin

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

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

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

Статья немаленькая и делится логически на две части:

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

  • Обзор подхода к работе со временем, принятого в JS, объекта Date, его методов и лучших практик в клиент-серверной разработке, и вывод почему для полноценной работы с датой вам не обойтись без задействования специальных библиотек.

Поехали!

Обработка часового пояса в JavaScript

Недавно я работал над задачей добавления функции часового пояса в TOAST UI Calendar, библиотеку календарей JavaScript, созданную моей командой. Я хорошо знал, что поддержка часовых поясов в JavaScript довольно таки слабая и надеялся, что абстрагирование существующих инструментов легко решило бы многие проблемы.

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

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

Что такое часовой пояс (time zone)?

Часовой пояс - это регион, который использует единый, законодательно установленный стандарт времени. У многих стран свой уникальный часовой пояс, а в некоторых крупных странах, таких как США или Канада, даже несколько часовых поясов. Интересно, что хотя Китай достаточно велик, чтобы иметь несколько часовых поясов, он использует только один часовой пояс. По этой причине солнце встает в западной части Китая около 10:00 утра.

GMT, UTC и Offset

GMT (время по Гринвичу)
Местное время в Корее обычно GMT+09:00. GMT - это сокращение от среднего времени по Гринвичу, которое является временем на часах Королевской Обсерватории в Гринвиче, Великобритания, расположенной на долготе 0. Система GMT начала распространяться 5 февраля 1925 года и была мировым стандартом времени до 1 января 1972 года.

UTC (универсальное глобальное время)
Многие считают GMT и UTC одним и тем же, и во многих случаях они взаимозаменяемы, но на самом деле у них есть существенные отличия. UTC было создано в 1972 году для компенсации проблемы замедления вращения Земли. Эта система времени основана на Международном атомном времени, которое использует атомную частоту цезия для установки стандарта времени. Другими словами, UTC - более точная система. Хотя фактическая разница во времени между ними мала, UTC является более точным выбором для разработчиков программного обеспечения.

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

Offset (смещение часового пояса относительно часового пояса UTC)
+09:00 в UTC+09:00 означает, что местное время на 9 часов опережает стандартное время UTC. Это означает, что когда в Корее 21:00, в регионе UTC+00:00 - полдень, 12:00. Разница во времени между стандартным временем UTC и местным временем называется смещением (offset), которое выражается следующим образом: +09:00, -03:00 и т. д.

Часто страны называют свои часовые пояса своими уникальными именами. Например, часовой пояс Кореи называется KST (стандартное время Кореи) и имеет определенное значение смещения, которое выражается как KST = UTC+09:00. Однако смещение +09:00 также используется не только Кореей, но и Японией, Индонезией и многими другими, что означает, что отношение между смещениями и именами часовых поясов не 1:1, а 1:N. Список стран со смещением +09:00 можно найти в википедии на странице UTC+09:00.

Некоторые смещения не производятся строго на почасовой основе. Например, в Северной Корее в качестве стандартного времени используется +08:30, а в Австралии +08:45 или +09:30, в зависимости от региона.

Полный список смещений UTC и их названия можно найти здесь: Список смещений времени UTC.

Time zone !== offset?
Как я уже упоминал ранее, мы используем названия часовых поясов (KST, JST) взаимозаменяемо со смещением, не различая их. Но это неправильно рассматривать время и смещение в определенном регионе одинаково, по следующим причинам:

Летнее время (DST)
Хотя это понятие может быть неизвестно в некоторых странах, во многих странах летнее время официально принято - в частности, в США, Великобритании и странах Европы. На международном уровне летнее время обычно называется Daylight Saving Time (DST). Во время перехода на DST мы переводим стрелки часов на один час вперед от стандартного времени в летнее время.

Например, в Калифорнии в США зимой используется PST (стандартное тихоокеанское время, UTC-08:00), а летом - PDT (тихоокеанское летнее время, UTC-07:00). Регионы Северной Америки, в которых используются два часовых пояса, вместе называются Тихоокеанским временем (PT), и это название принято во многих регионах США и Канады.

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

Например, до 2006 года в США и Канаде летнее время начиналось с первого воскресенья апреля в 02:00 и длилось до последнего воскресенья октября в 12:00, а с 2007 года стало начинаться во второе воскресенье марта с 02:00 и длиться до 2:00 первого воскресенья ноября. В европейских странах летнее время применяется единовременно по всей стране, в то время как в США летнее время поочередно применяется к часовым поясам.

Меняется ли часовой пояс?

Как я вкратце упомянул ранее, каждая страна имеет собственное право определять, какой часовой пояс использовать, а это означает, что ее часовой пояс может быть изменен по любым политическим или экономическим причинам. Например, в штатах период перехода на летнее время был изменен в 2007 году, поскольку президент Джордж Буш подписал энергетическую политику в 2005 году. Египет и Россия использовали летнее время, но перестали его использовать с 2011 года.

В некоторых случаях страна может изменить не только летнее время, но и стандартное время. Например, Самоа использовало смещение UTC-10:00, но позже перешло на UTC+14:00, чтобы уменьшить потери в торговле, вызванные разницей во времени между Самоа и Австралией и Новой Зеландией. Это решение привело к тому, что страна пропустила весь день 30 декабря 2011 года, и об этом сообщили газеты по всему миру.

Нидерланды использовали смещение +0:19:32.13, которое является излишне точным с 1909 года, но изменили его на смещение +00:20 в 1937 году, а затем снова изменили на смещение +01:00 в 1940 году, и придерживаются его до сих пор.

1 часовой пояс : N смещений

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

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

Но это не может быть реализовано с помощью пары простых правил. Например, поскольку штаты изменили даты начала и окончания летнего времени в 2007 г., 31 мая 2006 г. вам следовало жить по PDT (тихоокеанское летнее время, UTC-07:00), а после 31 марта 2007 г. уже по PST (стандартное тихоокеанское время, UTC-08:00). Это означает, что для обращения к определенному часовому поясу вы должны знать все исторические данные о стандартных часовых поясах или момент времени, когда правила летнего времени были изменены.

Вы не можете просто сказать: Часовой пояс Сан-Франциско - PST, UTC-08:00. Вы должны быть более конкретными и сказать: Сан-Франциско в настоящее время использует PST как стандартное время.

Пример, как страны Северной Америки стандартизировали эти расхождения для себя - это объединение PST и PDT в PT, учитывающее текущее стандартное время и его летнее время. Однако, это только североамериканская практика и перед нами продолжает стоять задача работы с датами в прошлом и будущем, на которые влияют произошедшие и ожидаемые изменения в правилах, во всех часовых поясах. Хорошо бы чтобы все это было оформлено в международный стандарт.

База данных часовых поясов IANA

По правде говоря, часовые пояса - это скорее база данных, чем набор правил, потому что они должны содержать все соответствующие исторические изменения. Существует несколько стандартных баз данных, предназначенных для обработки проблем с часовыми поясами, и наиболее часто используемой из них является База данных часовых поясов IANA. База данных часовых поясов IANA, также называемая базой данных tz (или tzdata), содержит исторические данные о местном стандартном времени по всему миру и изменениях летнего времени. Эта база данных организована так, чтобы содержать все исторические данные, которые в настоящее время можно проверить, чтобы гарантировать точность времени, начиная со времени Unix (1970.01 / 01 00:00:00). В ней также есть данные до 1970 года, но их точность не гарантируется.

Соглашение об именовании соответствует правилу Area/Location. Area обычно относится к названию континента или океана (Азия, Америка, Тихий океан), в то время как Location - к названию крупных городов, таких как Сеул и Нью-Йорк, а не к названию стран (это потому, что продолжительность жизни страны намного короче, чем города). Например, часовой пояс Кореи - Азия / Сеул, а часовой пояс Японии - Азия / Токио. Хотя эти две страны находятся географически в регионе, где принят стандартный offset UTC+09:00, они имеют разную историю изменений часовых поясов. Вот почему в этом стандарте они обрабатываются с использованием разных часовых поясов.

База данных часовых поясов IANA поддерживается многочисленными сообществами разработчиков и историков. Новые исторические факты и политические решения сразу же попадают в базу данных, что делает ее наиболее надежным источником. Более того, многие ОС на базе UNIX, включая Linux и macOS, а также популярные языки программирования, включая Java и PHP, используют эту базу данных.

Обратите внимание, что Windows отсутствует в приведенном выше списке поддержки. Это потому, что Windows использует собственную базу данных под названием Microsoft Time Zone Database. Однако эта база данных неточно отражает исторические изменения и поддерживается только Microsoft. Следовательно, она менее точна и надежна, чем IANA.

JavaScript и База данных часовых поясов IANA

Как я вкратце упомянул ранее, поддержка часового пояса в JavaScript довольно плохая. Поскольку по умолчанию он следует часовому поясу определенного региона (точнее, часовому поясу, выбранному во время установки ОС), возможности изменить его на новый часовой пояс нет. Кроме того, его спецификация для стандарта баз данных так же плохо определена, вы заметите это, если внимательно ознакомитесь с документацией ES2015. В отношении местного часового пояса и доступности летнего времени существует лишь несколько неопределенных заявлений. Например, летнее время определяется следующим образом: ECMAScript 2015 - Настройка летнего времени.

В зависимости от реализации, алгоритм использует наилучшую доступную информацию о часовых поясах для определения местной поправки на летнее время DaylightSavingTA (t), измеряемой в миллисекундах. Ожидается, что реализация ECMAScript сделает все возможное, чтобы определить местное летнее время.

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

ПРИМЕЧАНИЕ. рекомендуется использовать информацию о часовых поясах из базы данных часовых поясов IANA http://www.iana.org/time-zones/.

Спецификации ECMA признается, что не имеет специальной стандартной базы данных в JavaScript и рекомендует использовать базу данных часовых поясов IANA. В результате разные браузеры используют свои собственные решения для расчета часовых поясов, и они часто несовместимы друг с другом. Позже в ECMA-402 добавили возможность использовать часовой пояс IANA в виде Intl.DateTimeFormat для ECMAScript Internationalization API. Однако этот вариант по-прежнему гораздо менее надежен, чем в других языках программирования.

Часовой пояс в серверно-клиентской среде

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

Однако здесь есть то, что нужно учесть. Что, если некоторые из клиентов, обращающихся к серверу, находятся в разных часовых поясах? Расписание, зарегистрированное на 11 марта 2017 г. в 11:30 в Сеуле, должно отображаться как 10 марта 2017 г. в 21:30 при просмотре расписания в Нью-Йорке. Чтобы сервер поддерживал клиентов из разных часовых поясов, расписание, хранимое на сервере, должно иметь абсолютные значения, на которые не влияют часовые пояса. Каждый сервер имеет свой способ хранения абсолютных значений, и это выходит за рамки данной статьи, поскольку все зависит от сервера или среды базы данных. Однако для того, чтобы это работало, дата и время, передаваемые от клиента на сервер, должны быть значениями, основанными на том же смещении (обычно в формате UTC) или значениями, которые также включают данные часового пояса клиентской среды.

Обычно такие данные передаются в форме времени Unix на основе UTC или ISO-8601, содержащего информацию о смещении. В приведенном выше примере, если 11:30 утра 11 марта 2017 г. в Сеуле необходимо преобразовать во время Unix, это будет целочисленный тип со значением 1489199400. В соответствии с ISO-8601 это будет строковый тип, значение которого: 2017-03-11T11:30:00+09:00.

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

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

Объект даты в JavaScript

В JavaScript задачи, связанные с датой или временем, обрабатываются с помощью объекта Date. Это собственный объект, определенный в ECMAScript, также, как Array или Object. Он в основном реализован в собственном коде, таком как C++. Его API хорошо описан в документации MDN. На это большое влияние оказывает класс Java java.util.Date. В результате он наследует некоторые нежелательные черты, такие как характеристики изменяемых данных и месяц, начинающийся с 0.

Объект Date в JavaScript внутренне управляет данными времени, используя абсолютные значения, такие как время Unix. Однако конструкторы и методы, такие как функции parse(), getHour(), setHour() и т.д. находятся под влиянием местного часового пояса клиента (точнее, часовой пояса операционной системы, в которой запущен браузер). Следовательно, если вы создаете объект Date с использованием данных, вводимых пользователем, данные будут напрямую отражать местный часовой пояс клиента.

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

Создание объекта даты с пользовательским вводом

Вернемся к первому примеру. Предположим, что пользователь вошел в 11:30 11 марта 2017 г. на устройстве, которое соответствует часовому поясу Сеула. Эти данные хранятся в 5 целых числах 2017, 2, 11, 11 и 30, каждое из которых представляет год, месяц, день, час и минуту соответственно. (Поскольку месяц начинается с 0, значение должно быть 31 = 2.) С помощью конструктора вы можете легко создать объект Date, используя числовые значения.

const d1 = new Date(2017, 2, 11, 11, 30);d1.toString(); // Sat Mar 11 2017 11:30:00 GMT+0900 (KST)

Если вы посмотрите на значение, возвращаемое d1.toString(), то узнаете, что абсолютное значение созданного объекта составляет 11:30 11 марта 2017 г. на основе смещения +09:00 (KST).

Вы также можете использовать конструктор вместе со строковыми данными. Если вы передаете строку при создании Date, он внутренне вызывает Date.parse() и вычисляет правильное значение. Эта функция поддерживает спецификации RFC2888 и ISO-8601. Однако, как описано в документе MDN Date.parse(), возвращаемое значение этого метода варьируется от браузера к браузеру, и формат типа строки может повлиять на предсказание точного значения. Таким образом, рекомендуется не использовать этот метод.

Например, строка вида 2015-10-12 12:00:00 возвращает NaN в Safari и Internet Explorer, в то же время эта строка возвращает местный часовой пояс в Chrome и Firefox. В некоторых случаях она возвращает значение, основанное на стандарте UTC.

Создание объекта даты с использованием данных сервера

Предположим теперь, что вы собираетесь получать данные с сервера. Если данные имеют числовое значение времени Unix, вы можете просто использовать конструктор для создания объекта Date. Когда конструктор Date получает единственный числовой параметр, он распознается, как значение времени Unix в миллисекундах. (Внимание: JavaScript обрабатывает время Unix в миллисекундах. Это означает, что стандартное числовое значение времени Unix необходимо умножить на 1000) Результат выполнения следующего кода будет таким же, как и в предыдущем примере.

const d1 = new Date(1489199400000);d1.toString(); // Sat Mar 11 2017 11:30:00 GMT+0900 (KST)

Что если вместо времени Unix использовать строковый тип, такой как ISO-8601? Как я объяснил в предыдущем абзаце, метод Date.parse() ненадежен, и его лучше не использовать. Однако, поскольку в ECMAScript 5 и более поздних версиях указана поддержка ISO-8601, вы можете использовать строки в формате, указанном в ISO-8601, для конструктора Date в Internet Explorer 9.0 или более поздней версии, который поддерживает ECMAScript 5 при осторожном использовании.

Если вы поддерживаете старые браузеры, не забудьте добавить букву Z в конце. Без неё старые браузеры иногда интерпретируют строку на основе вашего местного времени, а не UTC. Ниже приведен пример запуска в Internet Explorer 10.

const d1 = new Date('2017-03-11T11:30:00');const d2 = new Date('2017-03-11T11:30:00Z');d1.toString(); // "Sat Mar 11 11:30:00 UTC+0900 2017"d2.toString(); // "Sat Mar 11 20:30:00 UTC+0900 2017"

Согласно техническим характеристикам результирующие значения в обоих случаях должны быть одинаковыми. Однако, как вы можете видеть, результирующие значения отличаются как d1.toString() и d2.toString(). В последней версии браузера эти два значения будут одинаковыми. Чтобы избежать проблем такого рода, всегда следует добавлять Z в конце строки, если нет данных о часовом поясе.

Создание данных для передачи на сервер

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

Если это время Unix, вы можете просто использовать метод getTime() для выполнения этого. (Обратите внимание на использование миллисекунд.)

const d1 = new Date(2017, 2, 11, 11, 30);d1.getTime(); // 1489199400000

А как насчет строк формата ISO-8601? Как объяснялось ранее, Internet Explorer 9.0 или выше, который поддерживает ECMAScript 5 или выше, поддерживает формат ISO-8601. Вы можете создавать строки формата ISO-8601, используя метод toISOString() или toJSON(). toJSON() может использоваться для рекурсивных вызовов с JSON.stringify() или другими. Оба метода дают одинаковые результаты, за исключением случая, когда они обрабатывают недопустимые данные:

const d1 = new Date(2017, 2, 11, 11, 30);d1.toISOString(); // "2017-03-11T02:30:00.000Z"d1.toJSON();   // "2017-03-11T02:30:00.000Z"const d2 = new Date('Hello');d2.toISOString(); // Error: Invalid Dated2.toJSON();   // null

Вы также можете использовать метод toGMTString() или toUTCString() для создания строк в формате UTC. Поскольку они возвращают строку, удовлетворяющую стандарту RFC-1123, вы можете использовать это по мере необходимости.

Объекты Date включают toString(), toLocaleString() и их методы расширения. Однако, поскольку они в основном используются для возврата строки, основанной на местном часовом поясе, и возвращают различные значения в зависимости от используемого браузера и ОС, они не очень полезны.

Изменение местного часового пояса

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

Давайте продолжим предыдущий пример, предполагая, что часовой пояс браузера установлен на Сеул. Пользователь входит в 11:30 11 марта 2017 г. по сеульскому времени, но хочет видеть местное время Нью-Йорка. Сервер передает данные времени Unix в миллисекундах и вы можете преобразовать их, если знаете смещение местного часового пояса, а мы знаем, что для Нью-Йорка это -05:00

Для вычисления смещений вы можете использовать метод getTimeZoneOffset(). Этот метод - единственный API в JavaScript, который можно использовать для получения информации о местном часовом поясе. Он возвращает значение смещения текущего часового пояса в минутах относительно часового пояса UTC.

const seoul = new Date(1489199400000);seoul.getTimeZoneOffset(); // -540

Возвращаемое значение -540 означает, что часовой пояс Сеула опережает UTC на 540 минут. Обратите внимание, что знак минус перед значением противоположен знаку плюса в стандартном обозначении смещения (+09:00). Не знаю почему, но вот так это отображается. Если мы вычислим смещение Нью-Йорка с помощью этого метода, мы получим 60 * 5 = 300. Преобразуйте разницу 840 в миллисекунды и создайте новый объект Date. Затем вы можете использовать get-методы этого объекта для преобразования значения в любой формат по вашему выбору. Давайте создадим простую функцию форматирования для сравнения результатов.

function formatDate(date) {return date.getFullYear() + '/' +(date.getMonth() + 1) + '/' +date.getDate() + ' ' +date.getHours() + ':' +date.getMinutes();}const seoul = new Date(1489199400000);const ny = new Date(1489199400000 - (840 * 60 * 1000));formatDate(seoul); // 2017/3/11 11:30formatDate(ny);   // 2017/3/10 21:30

formatDate() показывает правильную дату и время в соответствии с разницей часовых поясов между Сеулом и Нью-Йорком. Похоже, мы нашли простое решение. Тогда можем ли мы преобразовать его в местный часовой пояс, если мы знаем смещение региона? К сожалению, ответ отрицательный. Помните, что я сказал ранее? Что данные часового пояса - это своего рода база данных, содержащая историю всех изменений смещения? Чтобы получить правильное значение часового пояса, вы должны знать значение смещенияна момент даты (а не на текущую дату).

Проблема преобразования местного часового пояса

Если вы продолжите работать с приведенным выше примером еще немного, вы скоро столкнетесь с проблемой. Пользователь хочет проверить время по местному времени Нью-Йорка, а затем изменить дату с 10-го на 15-е. Если вы используете метод setDate() объекта Date, вы можете изменить дату, не изменяя другие значения.

ny.setDate(15);formatDate(ny);  // 2017/3/15 21:30

Выглядит достаточно просто, но здесь есть ловушка. Что бы вы сделали, если бы вам пришлось передать эти данные обратно на сервер? Поскольку данные были изменены, вы не можете использовать такие методы, как getTime() или getISOString(). Следовательно, вы должны отменить преобразование, прежде чем отправлять его обратно на сервер.

const time = ny.getTime() + (840 * 60 * 1000); // 1489631400000

Некоторые из вас могут задаться вопросом, почему я добавил использование преобразования данных, когда мне все равно нужно преобразовать их обратно перед возвратом. Выглядит так, будто я могу просто обработать их без преобразования и временно создать преобразованный объект Date только при форматировании. Однако не все так просто. Если вы измените дату объекта Date по сеульскому времени с 11-го на 15-е, добавляются 4 дня (24 * 4 * 60 * 60 * 1000). Однако по местному времени Нью-Йорка, поскольку дата была изменена с 10-го на 15-е, в результате было добавлено 5 дней (24 * 5 * 60 * 60 * 1000). Это означает, что для получения корректного результата вы должны рассчитывать даты на основе местного смещения часового пояса относительно часового пояса UTC.

Проблемы на этом не заканчиваются. Вот еще одна, где вы не получите желаемое значение, просто добавив или вычтя смещения часового пояса. Поскольку 12 марта является датой начала летнего времени по местному времени Нью-Йорка, смещение часового пояса 15 марта 2017 г. должно быть -04:00, а не -05:00. Поэтому, когда вы отключаете преобразование, вы должны добавить 780 минут, что на 60 минут меньше, чем раньше.

const time = ny.getTime() + (780 * 60 * 1000); // 1489627800000

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

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

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

Moment timezone

Moment - это хорошо зарекомендовавшая себя библиотека JavaScript, которая является почти стандартом для обработки даты. Предоставляя различные API-интерфейсы для дат и форматирования, в последнее время многие пользователи признают moment стабильным и надежным. И есть модуль расширения Moment Timezone, который решает все проблемы, описанные выше. Этот модуль расширения содержит данные базы данных часовых поясов IANA для точного расчета смещений и предоставляет различные API-интерфейсы, которые можно использовать для изменения и форматирования часового пояса.

В этой статье я не буду подробно обсуждать, как использовать библиотеку или структуру библиотеки. Я просто покажу вам, насколько просто решить проблемы, которые я обсуждал ранее. Если кому-то интересно, см. Документацию Moment Timezone.

Давайте решим проблему, показанную на картинке, с помощью Moment Timezone.

const seoul = moment(1489199400000).tz('Asia/Seoul');const ny = moment(1489199400000).tz('America/New_York');seoul.format(); // 2017-03-11T11:30:00+09:00ny.format();  // 2017-03-10T21:30:00-05:00seoul.date(15).format(); // 2017-03-15T11:30:00+09:00ny.date(15).format();   // 2017-03-15T21:30:00-04:00

Как вы видите в результате, смещение seoul останется прежним, в то время как смещение ny было изменено с -05:00 на -04:00. И если вы используете функцию format(), вы можете получить строку в формате ISO-8601, которая точно применила смещение. Видите насколько это просто по сравнению с тем, что мы делали ранее.

Заключение

До сих пор мы обсуждали API-интерфейсы часовых поясов, поддерживаемые JavaScript, и их проблемы. Если вам не нужно вручную изменять местный часовой пояс, вы можете реализовать необходимый функционал даже с помощью базовых API-интерфейсов при условии, что вы используете Internet Explorer 9 или выше. Однако, если вам нужно вручную изменить местный часовой пояс, все становится очень сложным. В регионе, где нет летнего времени и политика часовых поясов практически не меняется, вы можете частично реализовать ее, используя getTimezoneOffset() для преобразования данных. Но если вам нужна полная поддержка часовых поясов, не создавайте ее с нуля. Лучше используйте такую библиотеку, как Moment Timezone.

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

Заключение от переводчика

Очевидно, здесь повествование обрывается, и хотелось бы углубиться в особенности различных библиотек для работы с датами в JS, в частности в Moment.JS, Luxon и Date FNS. Возможно в дальнейшем здесь появится ссылка на статью посвященную этим библиотекам.

Благодарю за помощь в подготовке перевода Степана Омельченко и Марию Пилипончик. Это было не просто, но мы смогли!

Подробнее..

Botfather универсальный фреймворк для автоматизации

16.04.2021 20:09:46 | Автор: admin

Привет, Хабр!

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

Общие сведения

Называется эта программа Botfather. Скачать ее можно с официального сайта. Написана она с использованием библиотеки Qt и доступна как для Windows, так и для GNU/Linux. Для дистрибутивов GNU/Linux приложение доступно только в виде пакета flatpak. На официальном сайте имеется некоторое количество скриптов и довольно неплохая документация.

Итак, устанавливаем программу и запускаем. Нас встречает примерно такое окно:

Я уже добавил двух ботов. Самый первый в списке позволяет вести поиск заданного объекта на изображении. Второй умеет заходить на сайт botfather.io под определенным логином и паролем. Можно добавлять новых ботов из имеющихся в списке или создавать своих. Вот список готовых ботов:

Вызывается этот список по нажатию на "Add a bot". В программе имеется встроенный браузер, но своего редактора кода нет. Писать код для бота можно в любом текстовом редакторе, который вам по душе. Писать придется на языке JavaScript. Также в панели инструментов можно заметить кнопку "Android". С ее помощью можно подключить свой телефон или планшет и запускать ботов на мобильных устройствах. Теперь подробнее об уже добавленных мной ботах.

Image Detection Demo

Как уже говорилось, этот бот умеет искать указанный объект на изображении. Откроем его папку, перейдя во вкладку "Settings" и нажав на кнопку "Open bot folder". Мы увидим вот это:

Мы видим сам файл скрипта find_boxes.js, изображение box.png, которое следует искать и изображение screenshot.png, в котором нужно искать. Посмотрим на скрипт:

// Read the Image and Vision APIs documentation to lear more.// https://botfather.io/docs/var screenshot = new Image("screenshot.png");var box_template = new Image("box.png");var matches = Vision.findMatches(screenshot, box_template, 0.99);Helper.log(matches.length, "boxes have been found:")for (var i = 0; i < matches.length; i++) {Helper.log(matches[i]);}var output = Vision.markMatches(screenshot, matches, new Color("red"));output.save("output.png");Helper.log("The matches have been marked red on a newly created image.");Helper.log("That output image which has been saved as 'output.png'");Helper.log("Open the bots folder to view it.");

Здесь я бы хотел отметить переменную output. Как можно видеть, у нас в папке бота, после его отработки, должно появиться еще одно изображение, в котором красной рамкой будут выделены все найденные элементы. Проверим! Запускаем бот и смотрим логи:

Судя по логам, изображение должно быть в папке. Откроем ее:

Да, действительно, бот отработал на отлично:

Посмотрим теперь на следующего бота.

Website Login Demo

Немного рассмотрим сначала код этого бота:

function main(){// Validate the user script configurationif (!Config.getValue("username") || !Config.getValue("password")){Helper.log("Please provide a Botfather username and password in the bots 'Config' tab.");return;}// Load the Botfather login pagevar browser = new Browser("Main Browser");browser.loadUrl("https://botfather.io/accounts/login/");browser.finishLoading();// Fill out the username and password fieldvar u = Config.getValue("username");var p = Config.getValue("password");browser.executeJavascript("document.getElementById('id_username').value = '" + u + "';");browser.executeJavascript("document.getElementById('id_password').value = '" + p + "';");Helper.sleep(4);// Submit the formbrowser.executeJavascript("document.getElementById('id_password').form.submit();");Helper.sleep(2);browser.finishLoading();// Tell the script user whether login succeeded or notif (browser.getUrl().toString().indexOf("/accounts/login") !== -1){Helper.log("Looks like login failed. Open the browser to check for yourself.");return;}Helper.log("Success! You're logged in. Open the browser to check for yourself.")}main();

Как видно, этот бот заходит на официальный сайт botfather.io под логином и паролем. Эти данные нужно заранее ввести во вкладке "Config":

После запуска бота во вкладке "Browsers" появится браузер. Оттуда его можно и запустить. Так как у меня нет аккаунта на сайте botfather.io, то мне в логах было показано это:

После регистрации на сайте, по указанным во вкладке "Config" данным, запускаю бота снова. Получаю следующее:

Встроенный браузер подтверждает успешный вход:

Теперь немного пробежимся по API этого фреймворка.

Небольшой обзор API

Android

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

Можно сделать скриншот:

Android.takeScreenshot();

Получить список всех пакетов:

Android.listPackages();

Запустить приложение:

Android.startApp(package);

Сымитировать тап:

Android.sendTap(location);

Свайп:

Android.sendSwipe(start, end, duration_in_ms);

И многое другое.

Desktop

Получить позицию курсора:

var mousePositionPoint = Desktop.getCursorPosition();

Клик левой кнопкой мыши:

var position = new Point(300, 300);// A simple left clickDesktop.pressMouse(position);// This is equivalent toDesktop.pressMouse(position, "left");// And also equivalent toDesktop.holdMouse(position, "left");Desktop.releaseMouse(position, "left");

Пример функции перетаскивания:

function dragAndDrop(from, to) {    Desktop.holdMouse(from, "left");    Helper.msleep(750);    // Drag is triggered by first moving the element a little    var dragTriggerOffset = from.pointAdded(new Point(25, 25));    Desktop.warpCursor(dragTriggerOffset);    Helper.msleep(750);    Desktop.releaseMouse(to, "left");}// The function is then called like this:dragAndDrop(new Point(60, 60), new Point(400, 60));

Ввод простых символов:

// Entering lowercase "a"Desktop.press("a");// Entering uppercase "A"Desktop.holdKey("shift");Desktop.pressKey("a");Desktop.releaseKey("shift");

И так далее. В разделе документации на сайте много примеров.

Browser

Загрузить страницу:

Browser.loadUrl(url);

Вызвать перезагрузку:

Browser.reload();

Выполнить код на странице:

Browser.executeJavascript(javascript_code);

Остановить загрузку браузера:

Browser.stopLoading();

Ожидание загрузки браузера:

Browser.finishLoading(timeout_seconds);

Пример создания браузеров и загрузки страниц:

var browser1 = new Browser("Browser Name 1");var browser2 = new Browser("Browser Name 2", new Size(1200, 600));browser1.loadUrl("https://google.com/");browser2.loadUrl("https://youtube.com/");browser1.finishLoading(); // (default) waits max 30 seconds for the website to loadbrowser2.finishLoading(10); // waits max 10 seconds for the website to load

На этом все! Надеюсь, что вам было интересно. До встречи в следующих постах!


Дата-центр ITSOFT размещение и аренда серверов и стоек в двух дата-центрах в Москве. За последние годы UPTIME 100%. Размещение GPU-ферм и ASIC-майнеров, аренда GPU-серверов, лицензии связи, SSL-сертификаты, администрирование серверов и поддержка сайтов.

Подробнее..

Есть ли жизнь после жизни?

17.04.2021 00:07:00 | Автор: admin

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

Историческая справка

В далеком 1970 году английский математик Джон Хортон Конвей, увлеченный темой клеточных автоматов, с командой единомышленников описал вымышленный мир, объекты которого эволюционировали по определенным правилам. В дальнейшем, эта работа обрела большую популярность, благодаря ее практической реализации в виде игры под названием "Жизнь".

Конвей и игра Жизнь, 1974 годКонвей и игра Жизнь, 1974 год

Алгоритм игры настолько прост и в то же время увлекателен, что более полувека привлекает внимание не только учёных, но и простых обывателей:

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

  • Каждая клетка на этой поверхности может находиться в двух состояниях: быть живой (заполненной) или быть мёртвой (пустой). Клеткаимеет восемь соседей, окружающих её.

  • Распределение живых клеток в начале игры называется первым поколением. Каждое следующее поколение рассчитывается на основе предыдущего по таким правилам:

    • в пустой (мёртвой) клетке, рядом с которой ровно три живые клетки, зарождается жизнь;

    • если у живой клетки есть две или три живые соседки, то эта клетка продолжает жить; в противном случае, если соседей меньше двух или больше трёх, клетка умирает (от одиночества или от перенаселённости)

  • Игра прекращается, если

    • на поле не останется ни одной живой клетки

    • конфигурация на очередном шаге в точности (без сдвигов и поворотов) повторит себя же на одном из более ранних шагов (складывается периодическая конфигурация)

    • при очередном шаге ни одна из клеток не меняет своего состояния (складывается стабильная конфигурация; предыдущее правило, вырожденное до одного шага назад)

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

  • Устойчивые фигуры: фигуры, которые остаются неизменными

  • Долгожители: фигуры, которые долго меняются, прежде чем стабилизироваться[2];

  • Периодические фигуры: фигуры, у которых состояние повторяется через некоторое число поколений, большее 1;

  • Двигающиеся фигуры: фигуры, у которых состояние повторяется, но с некоторым смещением;

  • Ружья: фигуры с повторяющимися состояниями, дополнительно создающиедвижущиеся фигуры;

  • Паровозы: двигающиеся фигуры с повторяющимися состояниями, которые оставляют за собой другие фигуры в качестве следов;

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

  • Отражатели:устойчивыеилипериодическиефигуры, способные при столкновении с нимидвижущихся фигурпоменять их направление;

  • Размножители: конфигурации, количество живых клеток в которых растёт как квадрат количества шагов;

  • Фигуры, которые при столкновении с некоторыми фигурами дублируются.

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

Планерное ружьёГоспера первая бесконечно растущая фигураПланерное ружьёГоспера первая бесконечно растущая фигура

Что дальше?

Благодаря развитию компьютерных технологий, на сегодняшний день существует столько реализаций игры "Жизнь" как в классическом варианте, так и в виде ее многочисленных модификаций, что тема кажется исчерпанной. Каждый, кто в достаточной мере проявлял интерес к математике или информатике, знаком с данной игрой или даже ей увлекался.

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

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

Демонстрация первой версииДемонстрация первой версии

Столь бурная детская реакция стала причиной не останавливаться на достигнутом и продолжить работу. В проект был добавлен редактор состояния клеток, панель управления процессом эволюции (запуск и остановка). И после очередного утомительного набора в редакторе фигуры "ружье Госпера" возникла идея реализовать палитру готовых фигур. Затем потребовалось, чтобы эти фигуры можно было поворачивать и делать зеркальное отражение, а в саму палитру добавлять новые фигуры не модифицирую код проекта... Новые идеи не иссякали, а Остапа все несло и несло...

Палитра фигур и управление процессом эволюцииПалитра фигур и управление процессом эволюции

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

Мир треугольниковМир треугольников

Итоги

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

Проект доступен на GitHub для конструктивной критики и свободного использования.

Всем добра! Спасибо за внимание!

Подробнее..

Автоматизируем сервис-воркер с Workbox 6. Доклад в Яндексе

17.04.2021 12:09:16 | Автор: admin
Задеплоил сервис-воркер нужно покупать новый домен, известная шутка о том, как сложно писать собственную логику кеширования. С приходом шестой версии библиотеки Workbox для прогрессивных веб-приложений (PWA) больше не нужен компромисс между гибкостью и удобством автоматизации сетевых задач. Максим Сальников рассказал, как начать работу с Workbox 6, реализовать типовую функциональность для офлайнового веб-приложения и пойти дальше, добавив собственную логику кеширования.

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

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

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



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

Что сегодня происходит с прогрессивными веб-приложениями? Американские горки какие-то новости радуют, какие-то не очень.

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

Компания Google радует теперь прогрессивные веб-приложения можно монетизировать, используя API для продажи цифровых товаров на их площадке Google Play. Для этого, правда, придется PWA обернуть в нативную оболочку мобильного приложения. К счастью, сделать это можно легко, используя инструмент от той же компании Google Trusted Web Activity, TWA.

Компания Apple на своей конференции для разработчиков анонсировала, что 16 API, связанных с доступом к аппаратным возможностям устройств, в ближайшее время не будут реализованы в WebKit (и соответственно, в Safari), потому что по наличию или отсутствию этих API и их возможностей можно составить цифровой профиль пользователя и тем самым нарушить его приватность.

Хорошие новости от компании Microsoft. Инструмент PWA Builder, позволяющий создавать дистрибутивы, которые мы можем загружать в магазины приложений, живет и развивается. И всё проще и проще становится отправлять наши прогрессивные веб-приложения в натуральном виде в магазин приложений Microsoft Store.

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


Ссылка со слайда

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

Технически прогрессивные улучшения или, как механизм для этого, feature detection то, что нам известно в мире фронтенда с незапамятных времен.


Ссылки со слайда: первая, вторая

Как понять все эти термины? PWA что это, как это связано с сервис-воркером, что включает в себя сервис-воркер? Я сделал простую диаграмму, чтобы задать контекст нашей сегодняшней дискуссии. Под идеей PWA понимаются два концепта, связанных с веб-приложениями: возможность их установки на устройства пользователей и интересные возможности через набор новых web-API.

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

Из многообразия этого подуровня API что-то связано с кешированием, но не все.


Ссылка со слайда

Если вам нужна мотивация в плане количественных показателей популярности сервис-воркеров 1% из выборки сайтов, которые находятся в HTTP Archive, используют сервис-воркеры. Рост заметен, не взрывной на текущий момент времени, но и останавливаться он не планирует.


Ссылка со слайда

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

Почему наблюдается такая популярность сервис-воркеров именно со стороны известных веб-ресурсов?



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

  • Действительно, почему бы не сделать так, что веб-приложения будут доступны всегда, вне зависимости от статуса подключения, если соответствующие API имеются?
  • Почему бы не настроить действительно умное кеширование и переиспользовать данные, которые мы когда-либо загрузили?
  • Почему бы не сделать так, что действия пользователей, которые они совершили офлайн, никуда не пропадают, а тщательно записываются и, если нужно, отправляются на сервера автоматически при первой возможности подключения. Например, для аналитики это очень важно?
  • Если соединение нестабильно, почему бы не сделать так, чтобы это не нарушало пользовательский опыт? Все возможности имеются.
  • Следующая идея обновление приложения и данных, которые использует приложение. К этому мы привыкли при использовании нативных мобильных приложений. То есть они где-то в фоне себя потихоньку обновляют, так что мы всегда работаем с самой последней версией.
  • Кроме того, современные устройства оснащены массой интересных API и аппаратных, и программных, так что не использовать их странно, если это релевантно для конкретного приложения и конкретного бизнес-сценария.

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

Если все это так удобно для пользователей, почему каждое приложение не использует сервис-воркер?


Ссылка со слайда

Не все так просто с точки зрения разработчиков. Шутка из 2019 года все еще имеет под собой основания. Я вольно ее перевел и возьму смелость нарушить идею о том, что хорошая шутка в объяснении не нуждается все же объясню, что тут имеется в виду.


Ссылка со слайда

Дело в том, что в теории работа с сервис-воркером выглядит достаточно просто. Что такое сервис-воркер? Во-первых, это JavaScript-код. Во-вторых, это код, который исполняется в отдельном контексте относительно основного кода нашего приложения. В-третьих, это код, который исполняется только как ответ на какие-то события (events).

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

Событие install кладем нужные ресурсы в кеш: index.html, главный бандл JavaScript, главный бандл CSS, может быть, что-то еще.


Ссылка со слайда

Событие activate управляем версиями, если приложение обновилось.


Ссылка со слайда

Событие fetch тут мы немного обманываем основное приложение тем, что в ответ на все явные и неявные HTTP, a точнее HTTPS-запросы мы можем выдавать данные не из интернета, а те, что мы до этого закешировали. Это в теории.


Ссылка со слайда

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

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

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

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


Ссылка со слайда

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


Ссылка со слайда

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

  • Уровень абстракций настолько комфортен для разработчика, насколько это возможно. У нас все еще есть полный контроль, полное понимание того, что именно мы делаем то есть не ждите большой кнопки сделать круто. При этом мы избавлены от копания в глубоких технических деталях HTTP-запросов, алгоритмов кеширования, если нам это не нужно. Если нужно, то можем опуститься и на этот уровень.
  • Где уместно, возвращается декларативность, применимая к идее кеширования, что тоже очень значительно упрощает и делает комфортной саму разработку.
  • Модульность помогает нам и системам сборки оптимизировать размер итогового файла сервис-воркера, неиспользуемый код не окажется в production.
  • Если нужно, мы всегда можем расширить текущие модули и текущие методы.
  • Функциональность из коробки настолько широка, что покроет, я думаю, процентов 90 всех возможных сценариев, которые потребуются при создании, в частности, офлайн-приложения.
  • Мощный инструментарий: инструменты командной строки, модули для Node, плагины для систем сборки.
  • Очень важный момент бесплатность, открытый исходный код, активная разработка и поддержка со стороны Google и сообщества разработчиков.

Я надеюсь, что убедил вас попробовать. Давайте сделаем это незамедлительно.



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



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

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

Нативные импорты JavaScript в сервис-воркерах пока еще не работают, поэтому сервис-воркер будет необходимо собрать.

В конце концов нужно сообщить приложению, что теперь оно работает под управлением сервис-воркера.



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



Это модуль для Node, соответственно добавим немного JavaScript-кода.



В конфигурации мы укажем тот самый исходный сервис-воркер и адрес итогового сервис-воркера.



Самое главное мы должны объяснить Workbox, какие именно файлы составляют ту самую программную оболочку, тот самый application shell. К счастью, это делается очень удобным методом glob-шаблонов. Я гарантирую, что вы сможете настроить необходимую выборку ресурсов для вашего даже сложного по конфигурации веб-приложения. В этом конфиге есть много других свойств, которые позволяют вам совершенно точно затюнинговать этот процесс.



Вызов метода injectManifest с говорящим названием.



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



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



В общем случае нам понадобятся три плагина.


Ссылка со слайда

Как их настроить, можно посмотреть на этом слайде. Базовая настройка требуется только в плагине rollup-replace, который позволяет выбирать режим Workbox, development или production, заменой строки в исходном коде Workbox.

Чем отличаются режимы? В режиме production вся отладочная информация будет полностью исключена, в режиме development вы увидите детальный лог в консоли браузера, который позволяет точно прослеживать, что именно делает Workbox. Очень удобно.

Terser сделает минификацию, чтобы получить примерно такой вид итогового сервис-воркера.



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

Осталось интегрировать сборку сервис-воркера в общий билд приложения.


Ссылка со слайда

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

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

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



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

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

Исходный код именно для этой части сервис-воркера есть в репозитории по адресу aka.ms/workbox6. Там находится код сегодняшнего демонстрационного приложения и ссылка на его онлайн-версию, чтобы вы могли с ним поиграть и посмотреть, какие процессы там происходят. Открывайте DevTools, вкладку Application, что я и сделаю прямо сейчас.

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



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

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

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

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



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



Если мы в блоге используем аватары из сервиса Gravatar, для этого мы можем настроить самую консервативную стратегию всегда брать их из кеша, если они там доступны с предыдущей загрузки. Как вы видите, этот метод очень декларативен, то есть это выглядит как настройка роутинга. Для адресов, который попадают под этот шаблон, в данном случае я его задаю через RegExp, мы просим Workbox использовать стратегию Cache First.



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



Самая интересная стратегия предлагается для конкретной статьи или конкретного поста. Называется Stale While Revalidate, и говорит она следующее: Workbox, попробуй взять данные для этого адреса из кеша, если они там есть, и отправь их в приложение, чтобы оно их мгновенно показало. В это же самое время сходи и посмотри, есть ли по этому адресу в сети обновление. Если есть загрузи и положи его в кеш для следующего использования.

В то же время в моем примере кода вы можете увидеть, что используется плагин Broadcast Update, который дает нам возможность отправить сообщение приложению. Если данные обновились, можно спросить пользователя: Не желаете ли получить обновленную статью?



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


Ссылка со слайда

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

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


Ссылка со слайда

Аналогично с картинками. Есть хорошие практики для динамического кеширования картинок, и Workbox позволяет эти практики применять буквально одной строчкой. В данном случае я задаю, что хочу кешировать не более чем 10 элементов.



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



Кеширование самих страниц.



Кеширование статических ресурсов, а именно JavaScript- и CSS-файлов.



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


Ссылка со слайда

Что делать, если нашему приложению нужна своя собственная стратегия? Workbox 6 имеет инструменты и для этого случая, когда нужно задать свою логику запросов, при этом сохранить подход, при котором мы можем использовать ее в роутинге, и все другие плюшки, например стандартные плагины Workbox.

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

Итак, что нам нужно сделать? Нам нужно расширить базовый класс в стратегии, назовем его CacheNetworkRace.


Ссылка со слайда

В этом классе нужно реализовать метод с названием _handle, куда мы передаем сам HTTP-запрос и очень важно экземпляр класса StrategyHandler


Ссылка со слайда


Ссылка со слайда

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


Ссылка со слайда

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

Что нам остается? Реализовать ту самую гонку в виде: Какой промис первым выполнится успешно, тот мы и отдаем. Тут же можно сделать обработку ошибок.



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

Друзья, мне осталось показать только некоторые интересные и полезные ресурсы. Вернемся к репозиторию, где вы найдете исходный код нашего демо-приложения, как оно работает, можете сразу увидеть ссылку на онлайн-версию. Все это настроено и работает на Azure Static Web Apps. Я вам рекомендую попробовать этот способ хостинга статических веб-приложений, который максимально автоматизирует весь цикл приложения вы указываете только GitHub-репозиторий, его ветку, и через несколько мгновений ваше приложение, собранное и задеплоенное, находится уже в сети.

Продолжить общение на тему прогрессивных веб-приложений и сервис-воркеров я вам предлагаю в русскоязычном сообществе pwa_ru в телеграме. Еще одна интересная ссылка ведет на другой GitHub-репозиторий, где мы собрали максимально широкий набор ресурсов о прогрессивных веб-приложениях, сервис-воркерах, кешировании на русском языке. Там же вы найдете целую кучу примеров PWA в production и из глобального интернета, и из рунета. Я вам обещаю, многие посещаемые вами сайты, которые вы используете достаточно часто, это прогрессивные веб-приложения или, по крайней мере, они используют части сервис-воркеров для оптимизации вашего пользовательского опыта.

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

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

17.04.2021 14:23:59 | Автор: admin

Лежу я ночью, пытаюсь уснуть. И как обычно тысяча мыслей, и среди них я сумел зацепился за одну. А звучала она так: "почему бы не сделать анализатора футбольных матчей, где нужно будет лишь ввести участников игры и получить выборку из их статистики общей и какие-то описание, чего ждать в грядущем матче". Действительно, почему нет?!

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

Гугл дал свой результат, впрочем как всегда. Я нашел кучу калькуляторов ставок, которые продается за 3-5к рублей, и прочие таблицы расчетов в свободном доступе. Я как бы и так помнил расчеты тоталов голов, но мне нужно было их улучшить и получить на выходе собственно целого "мага/колдуна/вангу" спортивных событий. Или хотя бы формулку, которая выдаст результат после ввода данных.

Это что, писать парсер?!

Мне не хотелось сильно углубляться в код. Во-первых, я не кодер, а скорее человек, который с ним постоянно сталкивается в работе, и совсем чуть-чуть в нем может разобраться. Во-вторых, мне просто было лень, я искал простые решения. И вспомнил, что чудо Google Sheets может парсить таблички, xml, html-страницы, и делается это прост формулами: IMPORTDATA, IMPORTFEED, IMPORTHTML, IMPORTXML. Вот ссылка на справку гугла, там все подробно описано, останавливаться на этом я пожалуй не буду.

Нашел источник футбольной статистики, что было очень сложно. Ведь мне нужно не только спарсить разок, а обновлять мои данные постоянно, поскольку футбольные матчи идут и идут, и данные нужно актуализировать. Остановился на зарубежном сборище футбольной инфы fbref.com, все в некрасивых таблицах 2002 года. "Как раз то, что мне нужно!", - вскрикнул я, после 3-его часа ресерча источника статистических данных. Ведь мне нужны были не простые, а всякие XG, XGa и прочие радости профессиональных футбольных "аналистов". Далее с помощью API Google Sheets и query запросов, по сути урезанным sql, я кидался данными из вкладки во вкладку, разбивая на те таблицы, которые мне будут нужны для расчетов.

Секунду, а как я инфу из Google Sheets на сайте смогу отобразить?!

Да, этот вопрос у меня появился после прекрасных дней ковыряния в данных и структурирования всей информации, которую я сумел спарсить. И я чутка приуныл, потому что помнил, что могу вывести айфреймом. Но, черт возьми, это так некрасиво и попахивает прошлым веком. Пришлось ковыряться дальше. Блог, куда я хотел это все засунуть, у меня стоит на обыкновенном Wordpress, но найти адекватный плагин, который выводил бы инфу в красивом виде на страницу ультра-сложно, чтобы ещё и работал нормально адекватно, конечно. В итоге, я нашел, даже с эстетикой выводимых таблиц я смирился. Взял плагин Inline Google Spreadsheet Viewer. Банальный до нельзя, но все же, мои таблички по крайней мере выглядели не совсем стыдно:

Пфф, я что не смогу найти скрипт для отправки данных в Google Sheets?

Не смогу. Потрачено кучу времени на поиск, весь стэкоферфлоу и гитхаб русскоязычный, англоязычный, все перерыл вдоль и поперёк. Думал я =). А оказалось, что я был рядом с решением моей проблемы. Проблема заключалась в следующем: нужно было дать возможность выбора футбольных команд пользователю, даже если это буду я (ибо трафика на блоге особо нет, да и я не парюсь), и при этом, отправить их в Google Sheets по API. Что оказалось не совсем легко.

Решение моей проблемы было у меня под носом. На одной из тысячи просмотренных мною страниц был заголовок "Отправка на почту через html форму, используя Google Apps". Но как и в других 999 страниц, я подумал, что это не имеет отношения ко мне, а ведь я был не прав.

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

Ниже я добавлю весь основной код. Не знаю, могу ли публиковать не свой код, но под постом оставлю естественно ссылку на источник, ибо там есть ещё прекрасная доскональная инструкция. Да и весь код закоменчен, за что автору огромное спасибо. И Google Apps Script с этим всем справился на ура.

/****************************************************************************** * This tutorial is based on the work of Martin Hawksey twitter.com/mhawksey  * * But has been simplified and cleaned up to make it more beginner friendly   * * All credit still goes to Martin and any issues/complaints/questions to me. * ******************************************************************************/// if you want to store your email server-side (hidden), uncomment the next line// var TO_ADDRESS = "example@email.net";// spit out all the keys/values from the form in HTML for email// uses an array of keys if provided or the object to determine field orderfunction formatMailBody(obj, order) {  var result = "";  if (!order) {    order = Object.keys(obj);  }    // loop over all keys in the ordered form data  for (var idx in order) {    var key = order[idx];    result += "<h4 style='text-transform: capitalize; margin-bottom: 0'>" + key + "</h4><div>" + sanitizeInput(obj[key]) + "</div>";    // for every key, concatenate an `<h4 />`/`<div />` pairing of the key name and its value,     // and append it to the `result` string created at the start.  }  return result; // once the looping is done, `result` will be one long string to put in the email body}// sanitize content from the user - trust no one // ref: https://developers.google.com/apps-script/reference/html/html-output#appendUntrusted(String)function sanitizeInput(rawInput) {   var placeholder = HtmlService.createHtmlOutput(" ");   placeholder.appendUntrusted(rawInput);     return placeholder.getContent(); }function doPost(e) {  try {    Logger.log(e); // the Google Script version of console.log see: Class Logger    record_data(e);        // shorter name for form data    var mailData = e.parameters;    // names and order of form elements (if set)    var orderParameter = e.parameters.formDataNameOrder;    var dataOrder;    if (orderParameter) {      dataOrder = JSON.parse(orderParameter);    }        // determine recepient of the email    // if you have your email uncommented above, it uses that `TO_ADDRESS`    // otherwise, it defaults to the email provided by the form's data attribute    var sendEmailTo = (typeof TO_ADDRESS !== "undefined") ? TO_ADDRESS : mailData.formGoogleSendEmail;        // send email if to address is set    if (sendEmailTo) {      MailApp.sendEmail({        to: String(sendEmailTo),        subject: "Contact form submitted",        // replyTo: String(mailData.email), // This is optional and reliant on your form actually collecting a field named `email`        htmlBody: formatMailBody(mailData, dataOrder)      });    }    return ContentService    // return json success results          .createTextOutput(            JSON.stringify({"result":"success",                            "data": JSON.stringify(e.parameters) }))          .setMimeType(ContentService.MimeType.JSON);  } catch(error) { // if error return this    Logger.log(error);    return ContentService          .createTextOutput(JSON.stringify({"result":"error", "error": error}))          .setMimeType(ContentService.MimeType.JSON);  }}/** * record_data inserts the data received from the html form submission * e is the data received from the POST */function record_data(e) {  var lock = LockService.getDocumentLock();  lock.waitLock(30000); // hold off up to 30 sec to avoid concurrent writing    try {    Logger.log(JSON.stringify(e)); // log the POST data in case we need to debug it        // select the 'responses' sheet by default    var doc = SpreadsheetApp.getActiveSpreadsheet();    var sheetName = e.parameters.formGoogleSheetName || "responses";    var sheet = doc.getSheetByName(sheetName);        var oldHeader = sheet.getRange(1, 1, 1, sheet.getLastColumn()).getValues()[0];    var newHeader = oldHeader.slice();    var fieldsFromForm = getDataColumns(e.parameters);    var row = [new Date()]; // first element in the row should always be a timestamp        // loop through the header columns    for (var i = 1; i < oldHeader.length; i++) { // start at 1 to avoid Timestamp column      var field = oldHeader[i];      var output = getFieldFromData(field, e.parameters);      row.push(output);            // mark as stored by removing from form fields      var formIndex = fieldsFromForm.indexOf(field);      if (formIndex > -1) {        fieldsFromForm.splice(formIndex, 1);      }    }        // set any new fields in our form    for (var i = 0; i < fieldsFromForm.length; i++) {      var field = fieldsFromForm[i];      var output = getFieldFromData(field, e.parameters);      row.push(output);      newHeader.push(field);    }        // more efficient to set values as [][] array than individually    var nextRow = sheet.getLastRow() + 1; // get next row    sheet.getRange(nextRow, 1, 1, row.length).setValues([row]);    // update header row with any new data    if (newHeader.length > oldHeader.length) {      sheet.getRange(1, 1, 1, newHeader.length).setValues([newHeader]);    }  }  catch(error) {    Logger.log(error);  }  finally {    lock.releaseLock();    return;  }}function getDataColumns(data) {  return Object.keys(data).filter(function(column) {    return !(column === 'formDataNameOrder' || column === 'formGoogleSheetName' || column === 'formGoogleSendEmail' || column === 'honeypot');  });}function getFieldFromData(field, data) {  var values = data[field] || '';  var output = values.join ? values.join(', ') : values;  return output;}

Это просто спасло мне кучу времени и жизнь, ведь не реализовать как следует свою идею, это верх мучений. Как жить то потом?

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

function update() {   var sheetName1 = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Tournament");   var sheetName2 = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("TheMeets");   var sheetName3 = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("TheMeets_sort");   var cellFunction1 = '=IMPORTHTML("https://fbref.com/en/comps/12/La-Liga-Stats","table",1)';  var cellFunction2 = '=sort({IMPORTHTML("https://fbref.com/en/comps/12/schedule/La-Liga-Scores-and-Fixtures","table",1);IMPORTHTML("https://fbref.com/en/comps/12/3239/schedule/2019-2020-La-Liga-Scores-and-Fixtures","table",1);IMPORTHTML("https://fbref.com/en/comps/12/1886/schedule/2018-2019-La-Liga-Scores-and-Fixtures","table",1);IMPORTHTML("https://fbref.com/en/comps/12/1652/schedule/2017-2018-La-Liga-Scores-and-Fixtures","table",1)},3,FALSE)';  var cellFunction3 = '=sort(IMPORTHTML("https://fbref.com/en/comps/12/schedule/La-Liga-Scores-and-Fixtures","table",1),3,TRUE)';      sheetName1.getRange('A1').setValue(cellFunction1);     sheetName2.getRange('A2').setValue(cellFunction2);    sheetName3.getRange('A1').setValue(cellFunction3);}

Выбираем таблицы и обновляем их. Так же добавляем в Google Apps Script триггер, на развертывание данного скрипта ежедневно. И вуаля!

УРА!

Я победил и смог довести дело до конца, сейчас все это выглядит в более ли менее адекватном виде:

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

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

Весь код, инструкция и как с помощью Google Apps Script складировать свои данные в Google Sheets здесь.

В двух словах на спортс.ру расписал, что я планировал учитывать в расчетах и как считать.

Подробнее..

Перевод Тонкости работы консоли разработчика Chrome

17.04.2021 14:23:59 | Автор: admin

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

const numbers = [2, 3, 4, 5];console.log(numbers);// Square the numbersfor (let i = 0; i<numbers.length; i++) {    numbers[i] = numbers[i]**2;}console.log(numbers);

Более осторожный подход будет использовать Array.map() для обработки массива вместо цикла for...of. (Таким образом, ваши изменения будут применены неразрушающим образом к новому массиву.) Но есть причина, по которой я выбрал этот подход. Он демонстрирует первый пример некой странной причуды в консоли разработчика.

Чтобы увидеть проблему в действии, откройте эту страницу в браузере на базе Chromium (например, Chrome или Edge), затем откройте консоль разработчика и затем разверните списки массивов в консоли. Вы увидите два массива, но оба они будут экземплярами измененного массива:

Почему это происходит? Если окно консоли закрыто во время выполнения кода, и вы регистрируете объект, срабатывает шаблон ленивого вычисления. Ваша команда console.log() фактически регистрирует ссылку на массив. В интересах экономии памяти и оптимизации производительности Chrome не пытается извлечь информацию из массива, пока вы не развернете ее в консоли, то есть после того, как она будет преобразована в окончательную форму.

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

console.log(numbers.toString());

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

const objects = [ {name: 'Sadie', age: 12},{name: 'Patrick', age: 18}];console.log(objects);objects[0].age = 14;console.log(objects);

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

Как решить эту проблему? Вы можете преобразовать массив в строку, но не с помощью простого вызова toString(). Вместо этого вам нужно будет написать алгоритм, который будет делать это самостоятельно, или использовать библиотеку, или использовать JSON. Но, конечно, настоящая проблема в том, что эта ошибка может возникнуть в иерархии вложенных объектов, когда вы этого не ожидаете, и вы невольно примете информацию в консоли разработчика, если она окажется неточной. Лучшая защита - знать о возможности ленивой оценки. Еще одна хорошая практика - написать целевой код ведения консоли для вывода определенных свойств с примитивными типами данных. Не регистрируйте целые объекты и полагайтесь на расширяемое представление в консоли, если это не является абсолютно необходимым.

Подробнее..

Аэродинамика из блендера

18.04.2021 04:18:58 | Автор: admin

Зачем (затем, что нужно кормить баллистическую модель)

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

Есть несколько способов получить эти цифры:

  1. CFD. Всякие ANSYS, floEFD, solidWorks flow simulation и так далее. Большие и серьезные программные пакеты с серьезным ценником. И для стартапа, пилящего свой шаттл в гараже, такой софт обойдется приблизительно во столько же, во сколько и сам гараж.

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

  3. Приближенные методы на базе локальных параметров обтекания. Занимают промежуточное положение между первыми двумя и основаны на разбиении геометрии исследуемого ЛА на фрагменты, взаимодействием между которыми можно пренебречь. Поскольку возмущения в потоке не могут распространяться быстрее скорости звука и за пределы скачков уплотнения, то подобные методы лучше всего работают на больших скоростях (M ~ 8-10 и выше). Ими мы и займемся

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

Н.Ф. Краснов. Аэродинамика. том 1.Тэта - угол отклонения скачка, бэта - угол наклона поверхности, создающей скачок, дельта - параметр, зависящий от показателя адиабаты. Решив это уравнение методом Ньютона, получим необходимый нам угол скачка и параметры преобразования потока. Н.Ф. Краснов. Аэродинамика. том 1.Тэта - угол отклонения скачка, бэта - угол наклона поверхности, создающей скачок, дельта - параметр, зависящий от показателя адиабаты. Решив это уравнение методом Ньютона, получим необходимый нам угол скачка и параметры преобразования потока.

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

Основы метода Ньютона.Н.С. Аржаников, Г.С. Садекова. "Аэродинамика летательных аппаратов" Основы метода Ньютона.Н.С. Аржаников, Г.С. Садекова. "Аэродинамика летательных аппаратов"

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

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

Оценка трения эквивалентной пластины по критерию РейнольдсаОценка трения эквивалентной пластины по критерию Рейнольдса

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

Все превратится в треугольники

Теперь нужно получить данные о геометрии ЛА. Есть множество форматов, но самым удобным кажется STL. Каждая запись исчерпывающе описывает элементарную площадку на поверхности тела через три точки, которые ее формируют, и ориентированный вектор нормали. А еще Blender, которым я достаточно сносно владею, умеет экспортировать в него модели. Однако есть нюанс - STL, создаваемый Blender-ом - это бинарный файл, чтение которого немного отличается от работы с привычными текстовыми файлами (csv, json и так далее). Но для таких оказий в NodeJS есть класс Buffer. А сам бинарный STL снабжен подробной документацией.

Для начала через поставляемый в fs метод open открываем на чтение бинарник, после чего из полученных сведений выбираем поле size и создаем нужный нам буфер для чтения данных.

Это код работы с STL
Ничего особенного, просто каждый раз сползаем еще на 50 байтов вправоНичего особенного, просто каждый раз сползаем еще на 50 байтов вправо

Дальнейшие действия определяются спецификацией бинарного STL. Первые 80 байтов - это заголовок со сведениями о программе, в которой был создан бинарник. Их можно пропустить. Следующие 4 байта критичны - это 32-разрядный Unsigned Int, хранящий количество треугольников в составе модели. Как только мы узнали количество треугольников - начнем их считывать.

Каждый треугольник состоит из идущий подряд 32-разрядных Float Little Endian. Первые три числа - приведенная к единичному вектору нормаль. Затем тройки точек (X, Y, Z), задающих плоскость. После 48 значащих байтов идет еще 2 байта с 16-разрядным Unsigned Int, который некоторые редакторы используют для сохранения цвета поверхности. Но для расчета обтекания цвет нам явно не потребуется.

Тест 1. Притупленное тело

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

Это модель
Это - тестовая модель. Направление потока - справа налевоЭто - тестовая модель. Направление потока - справа налево

На качественном уровне поведение коэффициентов нормальной и продольной силы совпадает с теоретическим расчетом. На количественном уровне есть расхождение в 5-10% относительно теории:

Теория и модель
Сплошные линии - расчетные значения, точки - теоретический расчетСплошные линии - расчетные значения, точки - теоретический расчетИсходные данные для запуска расчета. Все еще "Аэродинамика..." Аржаникова и СадековойИсходные данные для запуска расчета. Все еще "Аэродинамика..." Аржаникова и Садековой

Тест 2. Аполлон

Следующий шаг - "Аполлон". Сравним аэродинамическое качество из статьи DSMC Simulations of Apollo Capsule Aerodynamics... (которой я пользовался в посте про капсульные корабли) с модельным.

Аэродинамика "Аполлона"
Линия - модельный расчет, сквозные точки - данные из статьи AIAAЛиния - модельный расчет, сквозные точки - данные из статьи AIAAИнтересующий нас график - черный, для сплошной среды. Точки взяты с негоИнтересующий нас график - черный, для сплошной среды. Точки взяты с него

График качества с высоты 85 км (где в полной мере применимы методы сплошной среды) с модельным расчетом. Как и в предыдущем случае, видна уверенная сходимость со средней погрешностью в ~5%. Кстати, обратим внимание на красный и синий графики качества из статьи AIAA - аэродинамическое качество для разреженных потоков быстро уменьшается.

Тест 3. Крыло с тонким профилем

Главное ограничение метода - малые скорости, для которых уже нельзя пренебрегать взаимодействиями между разными участками обтекаемого тела. Особенно это заметно при решении задачи обтекания конуса при малых (M ~2 - 3) скоростях. Здесь метод будет давать завышенные коэффициенты ( особенно сопротивления).

Расчет в диапазоне скоростей M = 2 - 6 (коэффициент подъемной силы от угла атаки)
Красный график - теория тонкого профиля, синий - модельный расчетКрасный график - теория тонкого профиля, синий - модельный расчет

Первый график - Мах 2, второй - Мах 4, третий - Мах 6. Угол атаки - в градусах

По предварительным оценкам, полученная модель лучше всего подходит для определения характеристик КА при скоростях M > 3,5 - 4 и для высот до ~ 90 км. Однако расчет показывает хорошую сходимость начиная с M=4, а полученные цифры аэродинамического качества хорошо коррелируют с "барьером Кюхемана" .

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

А теперь - немного хулиганства (и спойлер к циклу следующих статей). Кажется, я начинаю понимать, как нам одолеть Илона Маска. И это - нулевой шаг к захвату мира.

Девайс для захвата мира, если что - то аэродинамическое качество пепелаца порядка 2,7 - 3,5. Ну и мне нравится "Скайлон" как прототип, но мы обойдемся без теплообменниковДевайс для захвата мира, если что - то аэродинамическое качество пепелаца порядка 2,7 - 3,5. Ну и мне нравится "Скайлон" как прототип, но мы обойдемся без теплообменников

А пока у меня появился инструмент, позволяющий оценивать характеристики самых разных пепелацев, причем в наиболее интересном с точки зрения атмосферного полета космических аппаратов коридоре высот и скоростей - с достаточно хорошей (~5%) точностью.

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

Подробнее..

FSTB работа с файлами в Node.js без боли

19.04.2021 04:23:17 | Автор: admin

Когда я работаю с файлами в Node.js, меня не оставляет мысль, что я пишу очень много однотипного кода. Создание, чтение и запись, перемещение, удаление, обход файлов и подкаталогов, всё это обрастает неимоверным количеством бойлерплейта, который еще усугубляется странными названиями функций модуля fs. Со всем этим можно жить, но меня не оставляла мысль, что можно сделать удобнее. Хотелось, чтобы такие элементарные вещи, как, например, чтение или запись текста (или json) в файл можно было написать в одну строчку.

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

Предыстория

Работа с файлами в ноде проходит в несколько этапов: отрицание, гнев, торг... сначала мы получаем каким-либо образом путь к объекту файловой системы, потом проверяем его существование (при необходимости), потом работаем с ним. Работа с путями в ноде вообще вынесена в отдельный модуль. Самая классная функция для работы с путями, это path.join. Реально крутая штука, которая, когда я стал ей пользоваться, сэкономила мне кучу нервных клеток.

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

Главная проблема, это то, что объект файловой системы может иметь любое имя из разрешённых символов. Если, я сделаю у этого объекта методы для работы с ним, то получится, что, например, такой код: root.home.mydir.unlink будет двусмысленным - а что, если у в директории mydir есть директория unlink? И что тогда? Я хочу удалить mydir или обратиться к unlink?

Однажды я экспериментировал с яваскриптовым Proxу и придумал интересную конструкцию:

const FSPath = function(path: string): FSPathType {  return new Proxy(() => path, {    get: (_, key: string) => FSPath(join(path, key)),  }) as FSPathType;};

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

FSPath(__dirname).node_modules //работает аналогично path.join(__dirname, "node_modules")FSPath(__dirname)["package.json"] //работает аналогично path.join(__dirname, "package.json")FSPath(__dirname)["node_modules"]["fstb"]["package.json"] //работает аналогично path.join(__dirname, "node_modules", "fstb", "package.json")

Как результат, получаем функцию, которая при вызове возвращает сформированный путь. Например:

const package_json = FSPath(__dirname).node_modules.fstb["package.json"]console.log(package_json()) // <путь к скрипту>/node_modules/fstb/package.json

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

Так и появилась библиотека FSTB расшифровывается как FileSystem ToolBox.

Пробуем в деле

Установим FSTB:

npm i fstb

И подключим в проект:

const fstb = require('fstb');

Для формирования пути к файлу можно воспользоваться функцией FSPath, либо использовать одно из сокращений: cwd, dirname, homeили tmp(подробнее про них смотрите в документации). Также пути можно подтягивать из переменных окружения при помощи метода envPath.

Чтение текста из файла:

fstb.cwd["README.md"]().asFile().read.txt().then(txt=>console.log(txt));

FSTB работает на промисах, так что можно использовать в коде async/await:

(async function() {  const package_json = await fstb.cwd["package.json"]().asFile().read.json();  console.log(package_json);})();

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

Если бы я писал это с помощью стандартных функций, получилось бы что-то такое:

const fs = require("fs/promises");const path = require("path");(async function() {  const package_json_path = path.join(process.cwd(), "package.json");  const file_content = await fs.readFile(package_json_path, "utf8");  const result = JSON.parse(file_content);  console.log(result);})();

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

Другой пример. Допустим нужно прочитать текстовый файл построчно. Тут мне даже придумывать не надо, вот пример из документации Node.js:

const fs = require('fs');const readline = require('readline');async function processLineByLine() {  const fileStream = fs.createReadStream('input.txt');  const rl = readline.createInterface({    input: fileStream,    crlfDelay: Infinity  });  // Note: we use the crlfDelay option to recognize all instances of CR LF  // ('\r\n') in input.txt as a single line break.  for await (const line of rl) {    // Each line in input.txt will be successively available here as `line`.    console.log(`Line from file: ${line}`);  }}processLineByLine();

Теперь попробуем сделать это при помощи FSTB:

(async function() {  await fstb.cwd['package.json']()    .asFile()    .read.lineByLine()    .forEach(line => console.log(`Line from file: ${line}`));})();

Да, да я читер. В библиотеке есть эта функция, и под капотом работает тот самый код из документации. Но здесь интересно, что на ее выходе реализован итератор, который умеет filter, map, reduce и т.д. Поэтому, если надо, например, читать csv, просто добавьте .map(line => line.split(',')).

Запись в файл

Естественно, куда же без записи. Здесь тоже все просто. Допустим у нас есть строка и мы ее хотим записать в файл:

(async function() {  const string_to_write = 'Привет хабр!';  await fstb.cwd['habr.txt']()    .asFile()    .write.txt(string_to_write);})();

Можно дописать в конец файла:

await fstb.cwd['habr.txt']()    .asFile()    .write.appendFile(string_to_write, {encoding:"utf8"});

Можно сериализовать в json:

(async function() {  const object_to_write = { header: 'Привет хабр!', question: 'В чем смысл всего этого', answer: 42 };  await fstb.cwd['habr.txt']()    .asFile()    .write.json(object_to_write);})();

Ну и можно создать стрим для записи:

(async function() {  const file = fstb.cwd['million_of_randoms.txt']().asFile();  //Пишем в файл  const stream = file.write.createWriteStream();  stream.on('open', () => {    for (let index = 0; index < 1_000_000; index++) {      stream.write(Math.random() + '\n');    }    stream.end();  });  await stream;  //Проверяем количество записей  const lines = await file.read.lineByLine().reduce(acc => ++acc, 0);  console.log(`${lines} lines count`);})();

Кстати, ничего странного не заметили? Я об этом:

await stream; // <= WTF?!!

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

Что еще можно делать с файлами

Итак, мы посмотрели, как можно писать и читать из файлов. Но что еще можно с ними делать при помощи FSTB? Да все тоже, что при помощи стандартных методов модуля fs.

Можно получить информацию о файле:

const stat = await file.stat()console.log(stat);

Получим:

  Stats {    dev: 1243191443,    mode: 33206,    nlink: 1,    uid: 0,    gid: 0,    rdev: 0,    blksize: 4096,    ino: 26740122787869450,    size: 19269750,    blocks: 37640,    atimeMs: 1618579566188.5884,    mtimeMs: 1618579566033.8242,    ctimeMs: 1618579566033.8242,    birthtimeMs: 1618579561341.9297,    atime: 2021-04-16T13:26:06.189Z,    mtime: 2021-04-16T13:26:06.034Z,    ctime: 2021-04-16T13:26:06.034Z,    birthtime: 2021-04-16T13:26:01.342Z }

Можно посчитать хэш-сумму:

const fileHash = await file.hash.md5();console.log("File md5 hash:", fileHash);// File md5 hash: 5a0a221c0d24154b850635606e9a5da3

Переименовывать:

const renamedFile = await file.rename(`${fileHash}.txt`);

Копировать:

//Получаем путь к директории, в которой находится наш файл и // создаем в ней директорию "temp" если она не существуетconst targetDir = renamedFile.fsdir.fspath.temp().asDir()if(!(await targetDir.isExists())) await targetDir.mkdir()  //Копируем файлconst fileCopy = await renamedFile.copyTo(targetDir)  const fileCopyHash = await fileCopy.hash.md5();console.log("File copy md5 hash:", fileCopyHash);// File md5 hash: 5a0a221c0d24154b850635606e9a5da3

И удалять:

await renamedFile.unlink();

Также можно проверить, существует ли файл, доступен ли он на чтение и запись:

console.log({     isExists: await file.isExists(),     isReadable: await file.isReadable(),     isWritable: await file.isWritable() });

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

Директории: вишенка на торте и куча изюма

На мой взгляд, самая вкусная часть проекта это работа с директориями. Когда я ее реализовал и попробовал в деле, мне самому жутко понравился результат. Давайте посмотрим, что может делать FSTB с директориями. Для работы с каталогами используется объект FSDir, а получить его можно таким вот образом:

//Создем объект FSDir для node_modules:const node_modules = fstb.cwd.node_modules().asDir();

Что можно с этим делать? Ну во-первых, мы можем итерировать подкаталоги и файлы в директории:

// Выводим в консоль все имена подкаталоговawait node_modules.subdirs().forEach(async dir => console.log(dir.name));

Здесь доступны методы filter, map, reduce, forEach, toArray. Можно, для примера посчитать объем подкаталогов, названия которых начинаются с символа @ и отсортировать их по убыванию.

const ileSizes = await node_modules  .subdirs()  .filter(async dir => dir.name.startsWith('@'))  .map(async dir => ({ name: dir.name, size: await dir.totalSize() })).toArray();fileSizes.sort((a,b)=>b.size-a.size);console.table(fileSizes);

Получим что-то в этом роде:

 (index)          name           size       0           '@babel'        6616759     1     '@typescript-eslint'  2546010     2           '@jest'         1299423     3           '@types'        1289380     4       '@webassemblyjs'    710238      5          '@nodelib'       512000      6          '@rollup'        496226      7           '@bcoe'         276877      8           '@xtuc'         198883      9        '@istanbuljs'       70704     10          '@sinonjs'        37264     11         '@cnakazawa'       25057     12        '@size-limit'       14831     13           '@polka'         6953   

Бабель, конечно же, на первом месте ))

Усложним задачу. Допустим нам надо посмотреть, в каких модулях при разработке используется typescript и вывести версии. Это немного посложнее, но тоже получится довольно компактно:

const ts_versions = await node_modules  .subdirs()  .map(async dir => ({    dir,    package_json: dir.fspath['package.json']().asFile(),  }))  //Проверяем наличие package.json в подкаталоге  .filter(async ({ package_json }) => await package_json.isExists())  // Читаем package.json  .map(async ({ dir, package_json }) => ({    dir,    content: await package_json.read.json(),  }))  //Проверяем наличие devDependencies.typescript в package.json  .filter(async ({ content }) => content.devDependencies?.typescript)  // Отображаем имя директории и версию typescript  .map(async ({ dir, content }) => ({    name: dir.name,      ts_version: content.devDependencies.typescript,    }))    .toArray();  console.table(ts_versions);

И получим:

     (index)             name                   ts_version               0                'ajv'                   '^3.9.5'              1             'ast-types'                 '3.9.7'              2             'axe-core'                 '^3.5.3'              3             'bs-logger'                  '3.x'               4               'chalk'                  '^2.5.3'              5        'chrome-trace-event'            '^2.8.1'              6             'commander'                '^3.6.3'              7          'constantinople'              '^2.7.1'              8             'css-what'                 '^4.0.2'              9             'deepmerge'                '=2.2.2'             10             'enquirer'                 '^3.1.6'        ...

Что же еще можно делать с директориями?

Можно обратиться к любому файлу или поддиректории. Для этого служит свойство fspath:

//Создаем объект FSDir для node_modules:const node_modules = fstb.cwd.node_modules().asDir();//Получаем объект для работы с файлом "package.json" в подкаталоге "fstb"const package_json = node_modules.fspath.fstb["package.json"]().asFile()

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

Создание директории производится с помощью метода mkdir. Для копирования и перемещения директории есть методы copyTo и moveTo. Для удаления - rmdir (для пустых директорий) и rimraf (если надо удалить директорию со всем содержимым).

Давайте посмотрим на примере:

// Создадим временную директориюconst temp_dir = await fstb.mkdtemp("fstb-");if(await temp_dir.isExists()) console.log("Временный каталог создан")// В ней создадим три директории: src, target1 и target2const src = await temp_dir.fspath.src().asDir().mkdir();const target1 = await temp_dir.fspath.target1().asDir().mkdir();const target2 = await temp_dir.fspath.target2().asDir().mkdir();//В директории src создадим текстовый файл:const test_txt = src.fspath["test.txt"]().asFile();await test_txt.write.txt("Привет, хабр!");  // Скопируем src в target1const src_copied = await src.copyTo(target1);// Переместим src в target2const src_movied = await src.moveTo(target2);// Выведем получившуюся структуру // subdirs(true)  для рекурсивного обхода подкаталогов await temp_dir.subdirs(true).forEach(async dir=>{  await dir.files().forEach(async file=>console.log(file.path))})// Выведем содержимое файлов, они должны быть одинаковы console.log(await src_copied.fspath["test.txt"]().asFile().read.txt())console.log(await src_movied.fspath["test.txt"]().asFile().read.txt())// Удалим временную директорию со всем содержимымawait temp_dir.rimraf()if(!(await temp_dir.isExists())) console.log("Временный каталог удален")

Получим следующий вывод в консоли:

Временный каталог созданC:\Users\debgger\AppData\Local\Temp\fstb-KHT0zv\target1\src\test.txtC:\Users\debgger\AppData\Local\Temp\fstb-KHT0zv\target2\src\test.txtПривет, хабр!Привет, хабр!Временный каталог удален

Как видите, получается лаконичный, удобный в написании и использовании код. Большинство типовых операций пишутся в одну строчку, нет кучи joinов для формирования сложных путей, проще выстраивать последовательность операций с файлами и директориям.

Заключение

Когда я начинал писать эту библиотеку, моей целью было упростить работу с файловой системой в Node.js. Считаю, что со своей задачей я справился. Работать с файлами при помощи FSTB гораздо удобнее и приятнее. На проекте, в котором я ее обкатывал, объем кода, связанный с файловой системой, уменьшился раза в два.

Если говорить о плюсах, которые дает FSTB, можно выделить следующее:

  • Сокращается объем кода

  • Код получается более декларативный и менее запутанный

  • Снижается когнитивная нагрузка при написании кода для работы с файловой системой.

  • Библиотека хорошо типизирована, что при наличии поддержки тайпингов в вашей IDE заметно упрощает жизнь.

  • Нет внешних зависимостей, так что она не притащит за собой в ваш проект ничего лишнего

  • Поддержка Node.js начиная с 10-й версии, поэтому можно использовать даже в проектах с довольно старой кодовой базой

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

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

Исходный код библиотеки доступен в GitHub: https://github.com/debagger/fstb

С документацией можно ознакомиться здесь: https://debagger.github.io/fstb/

Благодарю за внимание!

Подробнее..
Категории: Javascript , Typescript , Node.js , Toolkit , Library , Filesystem

Frontend Meetup 2004

19.04.2021 16:09:25 | Автор: admin

Вместе со спикерами из Devexperts, Почты России, Леруа Мерлен и Райффайзенбанка узнаем об опыте разработки продуктов: как найти подход к Blazor, использовать плагин Figma для работы с white label, разрабатывать картографический раздел отделений и внедрять микрофронтенды.

О чем поговорим

Blazor поневоле

Александр Кильганов, Райффайзенбанк

О докладе: Я не Frontend-разработчик, но с Blazor пришлось познакомиться: подход к нему получилось найти не сразу, об этом и расскажу с какими болями и трудностями при работе столкнулся и как их решал. Также расскажу, как смешивал несмешиваемые компоненты Blazor, C# и HTML и что это из этого получилось. Ну, и отвечу на главный вопрос: стоит ли Blazor вообще использовать?

Плагин в Figma для работы с white label

Наталья Ильина, Devexperts

О докладе:Мы занимаемся разработкой финтех продуктов и поставляем клиенту white label. В свое время мы пришли к пониманию, что нам нужно автоматизировать рутинные процессы. Так был разработан плагин Хамелеон, который интегрирует содержимое макетов в Figma со сборками. В докладе расскажу, как плагин работает, с какими сложностями столкнулись в Figma, как решили их и какой у плагина есть потенциал.

Опыт отрисовки отделений на карте в Почте России

Михаил Вовренчук, Почта России

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

Эволюция микрофронтендной платформы в Леруа Мерлен

Роман Соколов, Леруа Мерлен

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


Начнем митап в 19:00 (мск).
Зарегистрируйтесь, чтобы получить ссылку на трансляцию: письмо придет вам на почту.

Подробнее..

Пишем юнит тесты на TypeScriptе (на примере котиков)

19.04.2021 18:14:05 | Автор: admin

Как писать модульные тесты в проекте с TypeScript'ом? В этой статье я постараюсь ответить на этот вопрос а также покажу как создать среду модульного тестирования под проекты использующие TypeScript.

Юнит тесты, что это?

Unit tests ( модульные тесты) - тесты применяемые в различных слоях приложения, тестирующие наименьшую делимую логику приложения: например модуль, класс или метод.

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

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

Настройка окружения

Итак теперь ближе к делу. Предположим у нас есть некий проект со следующей структурой:

project| node_modules| src| package.json| tsconfig.json

В ./src лежит некий модуль cat.module.ts который содержит простой класс Cat.

export class Cat {  public name: string;  public color: string;  constructor(name: string, color: string) {    this.name = name;    this.color = color;  }  public move(distanceMeter: number) : string {    return `${this.name} moved ${distanceMeter}m.`;  }  public say() : string {    return `Cat ${this.name} says meow`;  }}

Как видно, наш класс содержит в себе конструктор который принимает в себя значения имени и цвета а также пару методов. Этот класс и будет являтся нашим объектом тестирования (SUT - system under test).

Создадим в корне проекта, папку test, в которой будем хранить наши тесты.

Далее установим необходимые npm пакеты:

npm install --save-dev ts-node mocha @testdeck/mocha nyc chai @types/chai

Краткое описание пакетов:
ts-node - пакет для исполнения TypeScript и REPL в среде node.js.

mocha - популярный, гибкий тестовый фреймворк, позволяет разрабатывать тесты любого уровня. Будем использовать его как основу наших тестов. Вместе с ним используем @testdeck/mocha - имплементацию декоратора testdeck для Мокки, чтобы писать наши тесты в ООП стиле.

nyc - современный CLI популярной утилиты Istanbul, которая расчитывает текущее покрытие тестами кода.

chai - популярная библиотека для проверки утрверждений(assertions), подходит для многих тестовых фреймворков. Мы же будет ее использовать в паре с Моккой. Добавим так же @types/chai чтобы наш чаи мог свободно работать с типами typescript'а

После установки всех необходимых пакетов, создадим в нашей папке test, файл tsconfig.json, в который добавим конфиги TS которые будут вызыватся отдельно для наших тестов.

{  "extends": "../tsconfig.json",  "compilerOptions": {    "baseUrl": "./",    "module": "commonjs",    "experimentalDecorators": true,    "strictPropertyInitialization": false,    "isolatedModules": false,    "strict": false,    "noImplicitAny": false,    "typeRoots" : [      "../node_modules/@types"    ]  },  "exclude": [    "../node_modules"  ],  "include": [    "./**/*.ts"  ]}

В строчке include мы указываем включать все файлы в папке test с расширешием .ts

Затем создадим в корне проекта, файл register.js в котором опишем иснтрукции для ts-node, откуда запускать и транспилировать наши тесты.

const tsNode = require('ts-node');const testTSConfig = require('./test/tsconfig.json');tsNode.register({  files: true,  transpileOnly: true,  project: './test/tsconfig.json'});

Далее создадим там в корне, файл .mocharc.json, со следующим содержимым:

{  "require": "./register.js",  "reporter": "list"}

И файл .nyrc.json с конфигом нашей утилиты для анализа тестового покрытия.

{  "extends": "@istanbuljs/nyc-config-typescript",  "include": [    "src/**/*.ts"  ],  "exclude": [    "node_modules/"  ],  "extension": [    ".ts"  ],  "reporter": [    "text-summary",    "html"  ],  "report-dir": "./coverage"}

После добавления всех необходимых конфигов, дерево нашего проекта будет выглядить следующим образом:

project| node_modules| src| test| --- tsconfig.json| .mocharc.json| .nyrc.json| package.json| register.js| tsconfig.json

Теперь необходимо добавить скрипт запуска в package.json

"test": "nyc ./node_modules/.bin/_mocha 'tests/**/*.test.ts'"

С настройкой закончили, теперь можно написать первый тест

Пишем тесты

Создаем файл в папке ./test файл cat.unit.test.ts и пишем в нем следующий код:

import { Cat } from '../src/cat.module';import { suite, test } from '@testdeck/mocha';import * as _chai from 'chai';import { expect } from 'chai';_chai.should();_chai.expect;@suite class CatModuleTest {  private SUT: Cat;  private name: string;  private color: string;  before() {    this.name = 'Tom';    this.color = 'black';    this.SUT = new Cat(this.name, this.color);  }}

Сейчас мы импортировали наш класс Cat из модуля cat.module.ts, добавили импорты необходимых тестовых библиотек, а также создали необходимые переменные чтобы инициализировать наш класс.

В секции before задали параметры необходимые для класс и создали инстанст класса Cat, на котором будут проходит тесты.

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

import { Cat } from '../src/cat.module';import { suite, test } from '@testdeck/mocha';import * as _chai from 'chai';import { expect } from 'chai';_chai.should();_chai.expect;@suite class CatModuleTest {  private SUT: Cat;  private name: string;  private color: string;  before() {    this.name = 'Tom';    this.color = 'black';    this.SUT = new Cat(this.name, this.color);  }  @test 'Cat is created' () {    this.SUT.name.should.to.not.be.undefined.and.have.property('name').equal('Tom');  }}

Запускаем тест командой npm test и получаем примерно такой результат в консоле:

Как видим покрытие у нас не полное, остались не протестированными строчки 11-15. Это как раз методы класса Cat move и say.

Дописываем еще два теста для этих методов и получаем в итоге такой файл с тестами:

import { Cat } from '../src/cat.module';import { suite, test } from '@testdeck/mocha';import * as _chai from 'chai';import { expect } from 'chai';_chai.should();_chai.expect;@suite class CatModuleTest {  private SUT: Cat;  private name: string;  private color: string;  before() {    this.name = 'Tom';    this.color = 'black';    this.SUT = new Cat(this.name, this.color);  }  @test 'Cat is created' () {    this.SUT.name.should.to.not.be.undefined.and.have.property('name').equal('Tom');  }  @test 'Cat move 10m' () {    let catMove = this.SUT.move(10);    expect(catMove).to.be.equal('Tom moved 10m.');  }  @test 'Cat say meow' () {    expect(this.SUT.say()).to.be.equal('Cat Tom says meow');  }}

Снова запускаем наши тесты и видим что теперь класс Cat имеет полное тестовое покрытие.

Итог

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

PS: Написано по мотивам статьи How setting up unit test with TypeScript.

Подробнее..
Категории: Javascript , Typescript , Unit test

Категории

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

© 2006-2021, personeltest.ru