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

Angularjs

Дружим Angular с Google

27.08.2020 18:04:37 | Автор: admin

Дружим Angular с Google


Google ненавидит SPA


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


Одностраничные приложения приятно отличаются динамичностью взаимодействия с пользователем и более сложным UX. Но как не прискорбно обычно пользовательский опыт приносится в жертву SEO оптимизации. Для сеошника сайт на angular это такая себе проблема поискаовикам трудно индексировать страницы с динамическим контентом.


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


site preview


Мы любим JS и Angular. Мы верим, что классный и удобный UX может быть построен с на этом стеке технологий, и мы можем решить все сопутствующие проблемы. В какой-то момент мы наткнулись на Angular Universal. Это модуль Angular для рендеринга на стороне сервера. Сначала нам показалось что вот оно решение, но радость была преждевременной и отсутствие больших проектов, с его применением, было доказательством этого. Шесть месяцев назад мы надеялись найти production ready решение, но поняли, что нет больших проектов, написанных на Universal.


В итоге, мы начали разрабатывать компоненты для интернет-магазина, используя обычный Angular 2, и ждали, когда Universal будет объединен с Angular Core. На данный момент слияния проектов еще не поизошло, и пока не ясно, когда это произойдет (или как итоговый вариант будет совместим с текущей реализацией), однако сам Universal уже перекочевал в github репозиторий Angular.


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


Что такое Angular Universal


Прежде всего, давайте обсудим, что такое Angular Universal. Когда мы запустим наше приложение на Angular 2 и откроем исходный код, увидим что-то вроде этого:


<!DOCTYPE html><html><head>  <meta charset="utf-8">  <title>Angular 2 app</title>  <!-- base url -->  <base href="http://personeltest.ru/aways/habr.com/"><body>  <app></app></body></html>

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

Для решения проблем c индексацией Angular Universal дает нам возможность выполнять рендеринг на стороне сервера. Наша страница будет создаваться на бэкэнд-сервере, написанном на Node.Js, .NET или другом языке, и браузер пользователя получит страницу со всеми привычными тегами в ней -заголовками, мета-тегами и контентом.


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


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


Итак, мы хотим поделиться с вами подводными камнями, с которыми мы столкнулись на нашем пути.


Подводные камни Angular Universal


Не трогайте DOM


Когда мы начали тестировать компоненты нашего магазина с помощью Universal, нам пришлось потратить некоторое время, чтобы понять, почему наш сервер падает при запуске без вывода серверной страницы. Например, у нас есть компонент Session Flow component, который отслеживает активность пользователя во время сессии (перемещения пользователя, клики, рефферер, информация об устройстве пользователя и т.д.). После поиска информации в issues на GitHub мы поняли, что в Universal нет обертки над DOM.


DOM на сервере не существует.


Если вы склонируете этот Angular Universal стартер и откроете browser.module.ts вы увидите, что в массиве providers разработчики Universal предоставляют дваboolean значения:


providers: [    { provide: 'isBrowser', useValue: isBrowser },    { provide: 'isNode', useValue: isNode },    ...  ]

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


@Injectable()export class SessionFlow{    private reffererUrl : string;    constructor(@Inject('isBrowser') private isBrowser){        if(isBrowser){            this.reffererUrl = document.referrer;        }    }}

Universal автоматически добавляет false, если это сервер, и true, если браузер. Может быть, позже разработчики Universal пересмотрят эту реализацию и нам не придется беспокоиться об этом.


Если вы хотите активно взаимодействовать с элементами DOM, используйте сервисы Angular API, такие какElementRef, Renderer или ViewContainer.


Правильный роутинг


Поскольку сервер отражает наше приложение, у нас была проблема с роутингом.


Ваш роутинг на клиенте, написанный на Angular, должен соответствовать роутингу на сервере.


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


    [      { path: '', redirectTo: '/home', pathMatch: 'full' },      { path: 'products', component: ProductsComponent },      { path: 'product/:id', component: ProductComponent}    ]

Тогда нужно создать файл server.routes.ts с массивом роутов сервера. Корневой маршрут можно не добавлять:


export const routes: string[] = [  'products',  'product/:id'];

Наконец, добавьте роуты на сервер:


import { routes } from './server.routes';... other server configurationapp.get('/', ngApp);routes.forEach(route => {  app.get(`/${route}`, ngApp);  app.get(`/${route}/*`, ngApp);});

Пререндеринг стартовой страницы


Одной из наиболее важных особенностей Angular Universal является пререндеринг. Из исследования Kissmetrics 47% потребителей ожидают, что веб-страница загрузится за 2 секунды или даже менее. Для нас было очень важно отобразить страницу как можно быстрее. Таким образом, пререндеринг в Universal как раз про нашу задачу. Давайте подробнее рассмотрим, что это такое и как его использовать?


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


Чтобы включить пререндеринг, просто добавьте в конфигурацию сервера preboot: true:


res.render('index', {      req,      res,      preboot: true,      baseUrl: '/',      requestUrl: req.originalUrl,      originUrl: `http://localhost:${ app.get('port') }`    });  });

Добавление мета-тегов


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


Команда Angular Universal создала сервис angular2-meta, чтобы легко манипулировать мета-тегами. Вставьте мета-сервис в ваш компонент и несколько строк кода добавлят мета-теги в вашу страницу:


import { Meta, MetaDefinition } from './../../angular2-meta';@Component({  selector: 'main-page',  templateUrl: './main-page.component.html',  styleUrls: ['./main-page.component.scss']})export class MainPageComponent {  constructor(private metaService: Meta){    const name: MetaDefinition = {      name: 'application-name',      content: 'application-content'    };    metaService.addTags(name);  }}

В следующей версии Angular этот сервис будет перемещен в @angular/platform-server


Кэширование данных


Angular Universal запускает ваш XHR запрос дважды: один на сервере, а другой при загрузке приложения магазина.


Но зачем нам нужно запрашивать данные на сервере дважды? PatricJs создал пример, как сделать Http-запрос на сервере один раз и закэшировать полученные данные для клиента. Посмотреть исходный код примера можно здесь. Чтобы использовать его заинжекте Model service и вызовите метод get для выполнения http-вызовов с кешированием:


    public data;    constructor(public model: ModelService) {        this.universalInit();    }    universalInit() {        this.model.get('/data.json').subscribe(data => {        this.data = data;        });    }

Выводы


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

Подробнее..

Семилетними шагами миграция с JSP Angular JS на Angular 2

26.03.2021 10:08:08 | Автор: admin

Что нужно для перехода от серверного рендеринга к пользовательскому? Чем хорош Angular 2+ и как на него перейти? В этой статье попытаемся разобраться в данных вопросах и описать процесс миграции от серверных технологий рендеринга, таких, как JSP, к клиентским технологиям рендеринга представлений с использованием Angular.

Используемые технологии

Прежде чем приступить к коду, опишем весь стек используемых технологий:

  • Spring Framework - фреймворк для Java-платформы.

  • JSP - технология, позволяющая создавать содержимое, которое имеет как статические, так и динамические компоненты. Страница JSP содержит текст двух типов: статические исходные данные, которые могут быть оформлены в одном из текстовых форматов (например, HTML) и JSP-элементы. Эти элементы конструируют динамическое содержимое, позволяя внедрять Java-код в статичное содержимое страницы.

  • AngularJS и Angular - JavaScript-фреймворки от компании Google для создания клиентских приложений. AngularJS был одним из первых JavaScript-фреймворков, разработанным для создания одностраничных приложений (SPA). Он был выпущен в 2009 году как фреймворк с открытым исходным кодом. В 2016 году был выпущен Angular 2. Он был переписан с нуля на TypeScript и не является обратно совместимым с AngularJS.

Изначально у нас есть приложение, написанное на Spring+JSP с использованием AngularJS.Наша цель - перейти к SPA с использованием современной версии Angular.

Какие перспективы?

Angular является современным и популярным фреймворком и обладает рядом преимуществ по сравнению с JSP и AngularJS.

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

Внедряем технологию

Перейдем непосредственно к описанию процесса перехода и рассмотрим вопросы, возникающие при смене технологий.

Инициализация нового Angular приложения

Вместо JSP-страниц нам теперь понадобится Angular приложение. Для его создания удобно использовать Angular CLI. Angular CLI - официальный инструмент для инициализации и работы с Angular-проектами. Он упрощает создание приложения, его компиляцию.

Angular CLI можно установить, выполнив следующую команду:

npm install @angular/cli

После создадим новый Angular проект с помощью команды:

ng new <название проекта>

Команда ng new генерирует скелет будущего приложения. Angular CLI создает одноименную директорию и помещает в нее исходные файлы "скелета" приложения.

Создание служебных сервисов

На JSP-страницах использовалась информация, содержащая Java-выражения, а также специфичные теги: http://www.springframework.org/tags, в частности, для локализации и http://java.sun.com/jsp/jstl/core. Также в некоторых ситуациях может существовать необходимость в кастомизируемом ролевом доступе, информацией о котором, помимо сервера, должен владеть и клиент. Для получения этой информации создадим дополнительные контроллеры с методами, возвращающими нужную информацию. Запросы, возвращающие сообщения для локализации, должны быть доступны без аутентификации. Это связано с тем, что эти сообщения отображаются на всех страницах, включая доступные неавторизованным пользователям. Поправим конфигурацию Spring Security, чтобы она игнорировала запросы по шаблону "/messages/*":

@Overridepublic void configure(WebSecurity web) {    web.ignoring().antMatchers("/messages/*");}

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

Проксирование

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

Для этого добавим в корневую папку проекта файл proxy.conf.json с конфигурацией прокси-сервера:

{  "/api": {    "target": "http://localhost:8099",    "secure": false,    "cookieDomainRewrite": "localhost",    "changeOrigin": true  }}

Также поправим package.json, чтобы данная конфигурация применялась, указав в стартовом скрипте "start": "ng serve --proxy-config proxy.conf.json".

Перенос JSP страниц в компоненты Angular

Angular приложение состоит из компонентов, у каждого из которых своя независимая логика. Компонент - полноценная сущность, у которой есть своя логика TypeScript, разметка HTML и свой стиль CSS. Соответственно каждая JSP-страница будет разбиваться на отдельные компоненты, в которых в отдельные файлы выносится html, typescript и css. Для отправки запросов к бэкенду создадим отдельные сервисы. Заменим конструкции AngularJS на аналогичные в Angular.

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

При открытии JSP-страницы выполняется проверка на наличие привилегий. В случае отсутствия привилегий пользователь перенаправляется на страницу входа.

<%   if (!SecurityUtils.hasRole(SecurityConstants.VIEW_USER)) {       response.sendRedirect("login.jsp");   }%>

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

Все guard-ы должны возвращать либо true, либо false. И происходить это может как в синхронном режиме (тип Boolean), так и в асинхронном режиме (Observable<boolean> или Promise<boolean>). Данный пример разрешает переход при наличии у пользователя привилегии VIEW_SETTINGS. Метод hasPrivilege отправляет запрос на сервис и возвращает Observable<boolean>.

@Injectable({ providedIn: 'root'})export class SettingsGuard { readonly  SECURITY_CONSTANTS = SecurityConstants; constructor(private router: Router,             private securityConstantService: SecurityConstantService) { } canActivate(next: ActivatedRouteSnapshot, state: RouterStateSnapshot):   Observable<boolean|UrlTree> | Promise<boolean|UrlTree> | boolean|UrlTree {   return this.securityConstantService.hasPrivilege(this.SECURITY_CONSTANTS.VIEW_SETTINGS); }}

Обработка ошибок

Следующий шаг - обработка ошибок, возвращаемых сервером. Добавим HttpInterceptor, который обеспечивает перехват http-запросов и ответов для преобразования или обработки их перед передачей. При получении ошибок 401 или 403 будем перенаправлять пользователя на страницу входа:

public handleError(err: HttpErrorResponse): Observable<any> {  if (err.url != null && (err.status === 401 || err.status === 403)) {    this.router.navigate(['/login']);  }  return throwError(err.error);}

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

Кэширование

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

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

cache = new Map<string, RequestCacheEntry>();get(req: HttpRequest<any>): HttpResponse<any> | undefined {   const url = req.url + req.body.key;   const cached = this.cache.get(url);   if (!cached) {     return undefined;   }   const isExpired = cached.lastRead < (Date.now() - maxAge);   return isExpired ? undefined : cached.response; }put(req: HttpRequest<any>, response: HttpResponse<any>): void {   const url = req.url + req.body.key;   const newEntry = { url, response, lastRead: Date.now() };   this.cache.set(url, newEntry);   const expired = Date.now() - maxAge;   this.cache.forEach(entry => {     if (entry.lastRead < expired) {       this.cache.delete(entry.url);     }   }); }

CachingInterceptor реализует интерфейс HttpInterceptor, перехватывает запросы и решает, нужно их кэшировать или нет. Любой класс, реализующий интерфейс HttpInterceptor, должен иметь метод intercept.

В этом методе мы сначала проверим, нужно ли кэшировать данные из запроса (метод isCacheable), и, если нужно, попробуем извлечь данные из нашего кэша. Если у нас есть кэшированные данные для конкретного запроса, то мы вернем Observable с этими данными. Иначе мы отправим запрос на сервер для извлечения данных, в нашем случае, используя метод sendRequest. Этот метод также кэширует запрос для дальнейшего использования.

intercept(req: HttpRequest<any>, next: HttpHandler) { if (!isCacheable(req)) { return next.handle(req); } const cachedResponse = this.cache.get(req); if (req.headers.get('x-refresh')) {   const results$ = sendRequest(req, next, this.cache);   return cachedResponse ?     results$.pipe( startWith(cachedResponse) ) :     results$; } return cachedResponse ?   of(cachedResponse) : sendRequest(req, next, this.cache);}

Заключение

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

Подробнее..

Экосистема JavaScript тренды в 2021 году. Всё ли так однозначно?

01.04.2021 12:23:08 | Автор: admin

В конце прошлого года на сайте State of JS 2020 было опубликовано исследование о состоянии экосистемы JavaScript в 2020 году с ретроспективой на предыдущие годы развития. Исследование основывалось на многочисленных опросах, в которых суммарно приняли участие более 23 тысяч человек из 137 стран мира.

Географическое распределение числа опрошенных.Географическое распределение числа опрошенных.

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

Языки, расширяющие возможности JavaScript

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

Тренды использования языка.Тренды использования языка.

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

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

Фреймворки

Наверно, многие помнят, когда в начале бурного развития фронтенд-экосистемы количество фреймворков росло, словно грибы после дождя. Часть из них уже канула в лету (Press F for Backbone.js, Marrionete.js, Prototype.js, [type anything].js), и за последние годы мы могли наблюдать стабилизацию позиций трёх основных конкурентов: React, Angular, Vue. И с каждым годом их доля присутствия на рынке только росла.

Тренды использования технологии.Тренды использования технологии.

Однако здесь не всё так однозначно. В 2019 году ворвался молодой Svelte, который за 2020 год в два раза увеличил свою долю использования среди разработчиков. И при этом в рейтингах проявления интереса и удовлетворённости от использования со стороны IT-сообщества Svelte занимает первое место. Фреймворк стал глотком свежего воздуха в подходе к созданию веб-приложений, и поэтому следует ожидать, что он будет наращивать своё присутствие в 2021 году всё больше и больше.

Тренды интереса к технологии.Тренды интереса к технологии.Тренды удовлетворённости от использования технологии.Тренды удовлетворённости от использования технологии.

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

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

Управление данными

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

Тренды использования технологии.Тренды использования технологии.

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

Тренды удовлетворённости от использования технологии.Тренды удовлетворённости от использования технологии.Тренды интереса к технологии.Тренды интереса к технологии.

Инструменты для тестирования

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

Mocha по-прежнему является достойной альтернативной. Но отсутствие явной привязанности к конкретному фреймворку смещает её на вторую позицию.

Jasmine является инструментом тестирования по умолчанию для проектов на Angular. И, возможно, спад его популярности связан с определенным спадом самого Angular.

Тренды использования технологии.Тренды использования технологии.

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

Тренды интереса к технологии.Тренды интереса к технологии.

Заключение

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

Подробнее..

Решаем вопрос сортировки в JavaScript раз и навсегда

30.05.2021 14:10:27 | Автор: admin

Вступление

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

С чего все началось

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

Список требований:

  • использовать несколько выражений как ключ для сортировки

  • возможность указать направление сортировки независимо по каждому из ключей

  • возможность сортировать строки без учета регистра, и с учетом локали

  • устойчивость сортировки

Что нам предлагает AngularJS для сортировки? документация по filter:orderBy

{{ orderBy_expression | orderBy : expression : reverse : comparator }}$filter('orderBy')(collection, expression, reverse, comparator)Example:<tr ng-repeat="friend in friends | orderBy:'-age'">...</tr>

У меня возникло несколько замечаний по поводу этого фильтра. Для начала, знак - в этом примере не может быть математической операцией, потому что есть значения для которых это бессмысленная операция, например строки. В документации говорится что это префикс, который указывает направление сортировки. Если продолжить разбор, что это вообще за выражение? Это, вроде как, похоже на JS, но в то же время не очень. Это синтаксис выражений для AngularJS, который так же опасен как eval, но при этом имеет свои ограничения. То что этот синтаксис исключителен для AngularJS значит что эти знания невозможно перенести на другие проекты на JS. Кроме того, нельзя использовать TypeScript для проверки этих выражений. expression кроме того может принимать не только строку, но и функцию, которая возвращает ключ для сортировки. Но если указывать функцию, то направление сортировки указать нельзя и теряется гибкость. Так же можно указать несколько критериев сортировки, если задать массив из строк или функций.

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

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

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

lodash

Посмотрим на библиотеку lodash, В ней есть функция _.sortBy, которая позволяет сортировать массив по ключу.

var users = [    { 'user': 'fred',   'age': 48 },    { 'user': 'barney', 'age': 36 },    { 'user': 'fred',   'age': 40 },    { 'user': 'barney', 'age': 34 }]; _.sortBy(users, [(o) => o.user]);// => objects for [['barney', 36], ['barney', 34], ['fred', 48], ['fred', 40]] _.sortBy(users, ['user', 'age']);// => objects for [['barney', 34], ['barney', 36], ['fred', 40], ['fred', 48]]

Хм, эта функция не позволяет указывать направление сортировки, почему так? Из-за этого я хотел сразу отбросить lodash, но потом увидел _.orderBy.

This method is like _.sortBy except that it allows specifying the sort orders of the iteratees to sort by.

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

// Sort by `user` in ascending order and by `age` in descending order._.orderBy(users, ['user', 'age'], ['asc', 'desc']);// => objects for [['barney', 36], ['barney', 34], ['fred', 48], ['fred', 40]]

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

В целом, _.orderBy это терпимый метод сортировки.

Array#sort

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

items.sort(function(a, b) {    if (b.salary < a.salary) {      return -1;    }    if (b.salary > a.salary) {      return 1;    }    if (a.id < b.id) {      return -1;    }    if (a.id > b.id) {      return 1;    }    return 0;});// Для сравнения, эквивалентный код с использованием `lodash`// намного короче и его проще читать:lodash.orderBy(items, ['salary', 'id'], ['desc', 'asc']);

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

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

SQL / SEQUEL

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

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

SELECT EMPNO,NAME,SALFROM EMPWHERE DNO 50ORDER BY EMPNO

SQL позволяет указывать несколько критериев сортировки, а так же независимо указывать направление сортировки по разным критериям:

SELECT EMPNO,NAME,SALFROM EMPORDER BY SAL DESC, EMPNO ASC

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

Haskell и Rust

Haskell и Rust предоставляют довольно элегантные методы для сортировки по ключу:

Haskell sortOn:

import Data.Ord (Down)import Data.Sort (sortOn)sortOn (\employee -> (Down (salary employee), employee_id employee)) employees

Rust slice::sort_by_key:

use std::cmp::{Reverse};slice.sort_by_key(|employee| (Reverse(employee.salary), employee.id))

Здесь сортировка по нескольким критериям достигается за счет лексикографического порядка кортежей, а сортировка по убыванию за счет оберточных типов (newtype) Down и Reverse, которые инвертирует порядок сортировки своего содержания. Это очень простой для использовани интерфейс, и он полностью совместим со всеми требованиями.

Python

В Python у списков есть встроенный метод list.sort и гобальный метод sorted, в котором можно указать критерий сортировки через именованный аргумент key.

Ранее эти методы так же принимали аргумент cmp, но его убрали потому что он не нужен.

sorted(employees, key=lambda employee: (employee.salary, employee.id))

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

from ord_reverse import Reversesorted(employees, key=lambda employee: (Reverse(employee.salary), employee.id))

Java и C#

В Java метод Arrays.sort принимает Comparator (который почти состоит одной функции сравнения двух элементов). Но Comparator так же позволяет строить компараторы, добавляя новые критерии сравнения, используя метод thenComparing. Можно обратить направление сортировки используя метод reversed.

Comparator<Employee> comparator =  Comparator.comparing(Employee.getSalary).reversed()    .thenComparing(Employee.getId);Arrays.sort(array, comparator);

Здесь есть небольшой недостаток нет простого способа указать обратное направление сортировки для отдельного критерия. Давайте попробуем написать компаратор вида ORDER BY SALARY ASC, ID DESC:

// Вариант 1, создавать два компаратора, и складывать ихComparator<Employee> comparator =  Comparator.comparing(Employee.getSalary)    .thenComparing(Comparator.comparing(Employee.getId).reversed());// Вариант 2, инвертирует компаратор дважды. Таким образом первый компаратор// будет использовать прямое направиление сортировки.Comparator<Employee> comparator =  Comparator.comparing(Employee.getSalary).reversed()    .thenComparing(Employee.getId).reversed();

Если не учитывать LINQ Query, который есть прямым наследником SQL, в C# для сортировки используется Enumerable.OrderBy и Enumerable.OrderByDescending, а так же Enumerable.ThenBy и Enumerable.ThenByDescending для добавления новых критериев сортировки.

IEnumerable<Employee> query =    employees    .OrderByDescending(employee => employee.Salary)    .ThenBy(employee => employee.Id);

По сравнению с Java здесь легче указать обратною сортировку для индивидуальных ключей. Но есть и недостатки не очевидно когда именно будет происходить сортировка, и слишком множатся методы: IEnumerable 4 метода, по сравнению с 1 в Haskell/Rust/Python. Количество методов в C# можно было бы свести к двум, используя простой класс для инверсии сравнения.

В целом, как Java, так и C# удовлетворяют требования сортировки. К сожалению, оба языка используюь более громоздкий подход с использованием ООП.

C и C++

C qsort:

#include <stdlib.h>int cmp_employee(const void *p1, const void *p2){      const employee *a = (employee*)p1;      const employee *b = (employee*)p2;      if (b->salary < a->salary) {        return -1;      }      if (b->salary > a->salary) {        return 1;      }      if (a->id < b->id) {        return -1;      }      if (a->id > b->id) {        return 1;      }    return 0;  }  /* ... */  qsort(employees, count, sizeof(employee), cmp_employee);

C++ std::sort:

#include <algorithm>/* ... */std::sort(employees.begin(), employees.end(), [](const employee &a, const employee &b) {  return (b->salary < a->salary) || (a->id < b->id);});

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

В C и в C++ нет встроенных способов сортировки по ключу и отсутствуют воспомагательные классы для сборки компараторов из нескольких частей. Array#sort, который мы рассматривали ранее имеет те же недостатки как и эти две функции.

Выбираем то что лучше подходит

Из всех перечисленных интерфейсов, наиболее компактная и выразительная сортировка в Haskell и Rust. Можем ли мы перенести ее в JavaScript?

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

sortBy(array, (employee) => [{ reverse: employee.salary }, employee.id]);

Определяем линейный порядок в JavaScript

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

Определяем с нуля:

  1. null меньше всех значений. Это альтернативно использованию типа Maybe или Option.

  2. Если типы разные, сравнение бросает ошибку.

  3. Особое значение NaN меньше всех других чисел.

  4. Остальные числа, строки, були и BigInt сравниваются между собой как определено в JavaScript.

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

  6. Если оба значения имеют форму { reverse: xxx }, то значение xxx будет рекурсивно сравниваться в обратном порядке. Это равносильно использованию Down / Reverse

  7. Если оба значения имеют форму { localeCompare: sss, collator: ccc }, строки sss сравниваются используя коллатор ccc. Коллаторы в обоих значений должны быть равны.

  8. Все остальное бросает ошибку.

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

Как только мы выбрали интерфейс для сортировки и определили линейный порядок, осталось дело за малым воплотить это в виде библиотеки: better-cmp

Бонус: почему я не использовал библиотеку X?

  • orderBy: не смотря на "Inspired by Angular's orderBy filter", эта библиотека довольно хороша. Но я предпочел пойти дальше.

  • thenby: довольно хорошая библиотека, копирует интерфейс Java для комбинации компараторов, но я решил копировать другой язык из-за эргономики.

  • multisort: _

    if (/[^\(\r\n]*\([^\(\r\n]*\)$/.test(nextKey)) {    var indexOfOpenParenthesis = nextKey.indexOf("(");    var args = JSON.parse("[" + nextKey.slice(indexOfOpenParenthesis+1, -1) + "]");    nextKey = nextKey.slice(0, indexOfOpenParenthesis);}
    

Заключение

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

  • Довольно странно что для такой распространенной операции как сортировка разные языки используют разные интерфейсы.

  • Ещё более странно странно что в "текущем году" в JavaScript нет широко известной и адекватной сортировки по ключу.

  • Лучшее решение для JavaScript что смог сделать теперь воплощено в виде библиотеки better-cmp, доступной на npm.

Подробнее..

Категории

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

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