[Из песочницы] Как я перестал любить Angular |
Много лет я работал с AngularJS и по сей день использую его в продакшене. Несмотря на то, что идеальным, в силу своей исторически сложившейся архитектуры, его назвать нельзя — никто не станет спорить с тем, что он стал просто вехой в процессе эволюции не только JS фреймворков, но и веба в целом.
На дворе 2017ый год и для каждого нового продукта/проекта встает вопрос выбора фреймворка для разработки. Долгое время я был уверен, что новый Angular 2/4 (далее просто Angular) станет главным трендом enterprise разработки еще на несколько лет вперед и даже не сомневался что буду работать только с ним.
Сегодня я сам отказываюсь использовать его в своем следующем проекте.
Дисклеймер: данная статья строго субъективна, но таков мой личный взгляд на происходящее и касается разработки enterprise-level приложений.
За годы эволюции большинство недостатков фреймворка были устранены, библиотеки доведены до весьма стабильного состояния, а размеры комьюнити стремятся к бесконечности. Пожалуй, можно сказать, что в первом ангуляре мало что можно сильно улучшить не сломав при этом сотни существующих приложений.
Несмотря на колоссальные труды и общее ускорение фреймворка (особенно после 1.5),
основным его недостатком я бы все же назвал скорость работы. Конечно это простительно, учитывая что спустя столько лет, худо-бедно до сих пор поддерживается обратная совместимость.
И вот наконец Angular был переписан с нуля дабы стать основной для многих будущих веб-приложений.
Конечно путь к этому был долог и полон Breaking Changes,
но на сегодняшний день Angular 4 стабилен и позиционируется как полностью production-ready.
Одна из наиболее крутых вещей, которую дал нам новый Angular — популяризация TypeScript.
Лично я был с ним знаком и работал еще до того, как он стал основным для моего любимого фреймворка,
но многие узнали о нем именно благодаря Angular.
Не буду подробно останавливаться на TypeScript, т.к. это тема для отдельной статьи,
да и написано о нем уже больше чем нужно. Но для enterprise разработки TypeScript дает огромное количество преимуществ. Начиная с самой статической типизации и областями видимости и заканчивая поддержкой ES7/8 даже для IE9.
Главное преимущество работы с TypeScript — богатый инструментарий и прекрасная поддержка IDE. По нашему опыту, юнит тестов с TS приходится писать существенно меньше.
Если вы читаете данную статью, то с вероятностью 95% вы уже знаете что это такое.
Но для тех 5% кто еще не знает — Vue.js это крайне легковесный (но очень богатый по функционалу) фреймворк, вобравший в себя многое хорошее, как из AngularJS, так и из React.
Фактически больше он похож все же на React, но шаблоны практически идентичны AngularJS (HTML + Mustache).
В реальности от AngularJS, он отличается очень даже разительно, но в поверхносном смысле,
понять как он работает, если у вас уже был опыт работы с React или AngularJS, будет несложно.
Последний мой проект, который совсем недавно вышел в production, мы писали на AngularJS 1.5-1.6.
Вопреки наличию стабильной версии Angular уже долгое время, мы приняли решение не мигрировать на него по ряду причин (не столько технических, сколько политических).
Но одной из фич современного веба мы пользовались с самого начала — это TypeScript.
Вот пример нашего компонента из данного проекта:
import {Component} from "shared-front/app/decorators";
import FileService, {ContentType, IFile} from "../file.service";
import AlertService from "shared/alert.service";
@Component({
template: require("./file.component.html"),
bindings: {
item: "<",
},
})
export default class FileComponent {
public static $inject = ["fileService"];
public item: IFile;
constructor(private fileService: FileService, private alertService: AlertService) {
}
public isVideo() {
return this.item.contentKeyType === ContentType.VIDEO;
}
public downloadFile() {
this.fileService.download(this.getFileDownloadUrl()).then(() => {
this.alertService.success();
});
}
private getFileDownloadUrl() {
return `url-for-download${this.item.text}`;
}
}
На мой взгляд выглядит очень даже приятно, не слишком многословно, даже если вы не фанат TS.
К тому же все это замечательно тестируется как Unit-тестами, так и Е2Е.
Продолжай AngularJS развиваться, как тот же React, и будь он побыстрее, можно было бы и по сей день продолжать писать крупные проекты на нем.
В принципе это по прежнему вполне разумно, если ваша команда очень хорошо знакома с AngularJS. Но думаю, что большинство все же предпочитает двигаться в ногу со временем и захочет выбрать нечто более современное.
Так мы и поступили, рационально выбрав Angular 2 (позже 4) для нашего нового проекта несколько месяцев назад.
Выбор казался довольно очевидным, учитывая, что вся наша команда имеет большой опыт работы с первой версией. К тому же, лично я ранее работал с alpha-RC версиями и тогда проблемы фреймворка списывались на 0.х номер версии.
К сожалению, как теперь стало понятно, многие из этих проблем заложены в архитектуру и не изменятся в ближайшее время.
А вот пример компонента из проекта на Angular:
import {Component} from '@angular/core';
import FileService, {ContentType, IFile} from "../file.service";
import AlertService from "shared/alert.service";
@Component({
selector: 'app-file',
templateUrl: './file.component.html',
styleUrls: ['./file.component.scss']
})
export class FileComponent {
Input() item: IFile;
constructor(private fileService: FileService, private alertService: AlertService) {
}
public isVideo() {
return this.item.contentKeyType === ContentType.VIDEO;
}
public downloadFile() {
this.fileService.download(this.getFileDownloadUrl()).subscribe(() => {
this.alertService.success();
});
}
private getFileDownloadUrl() {
return `url-for-download${this.item.text}`;
}
}
Возможно чуть чуть более многословно, но гораздо чище.
Первое, что вы установите при разработке нового Angular 4 приложения это Angular CLI
Нужен он CLI для того, чтобы упростить создание новых компонентов/модулей/тестов итд.
На мой взгляд это едва ли не лучшее, что есть в новом Angular. Тула действительно очень удобная в использовании и сильно ускоряет процесс разработки.
Это то, чего так сильно не хватало в AngularJS, и все решали проблему отсутствия данной тулзы по-своему. Множество различных сидов (стартеров), сотни разных подходов к одному и тому же, в общем анархия. Теперь с этим покончено.
Конечно CLI тоже имеет ряд недостатков в части настроек и конфигурации "под себя", но все же он на голову выше аналогичных утилит для React (create-react-app) или Vue (vue-cli). Хотя второй, благодаря своей гибкости, становится лучше с каждым днем.
Изначально я не хотел писать очередную хейтерскую статью вроде
Angular 2 is terrible (нашелся даже перевод).
Однако, несмотря на то, что статья выше была написана для уже весьма устаревшей версии,
по большинству пунктов она совершенно в точку.
Я бы даже сказал, что местами автор был слишком мягок.
В целом не совсем разделяю взгяд автора на RxJS, т.к. библиотека невероятно мощная.
An Ajax request is singular, and running methods like Observable.prototype.map when there will only ever be one value in the pipe makes no semantic sense. Promises on the other hand represent a value that has yet to be fulfilled, which is exactly what a HTTP request gives you. I spent hours forcing Observables to behave before giving up using Observable.prototype.toPromise to transform the Observable back to a Promise and simply using Promise.all, which works much better than anything Rx.js offers.
В реальности, благодаря RxJS, со временем становится очень приятно воспринимать любые данные,
пусть даже не совсем подходящие для Observable, как единую шину.
Но суровая правда в том, что Object.observe нативно мы все же не увидим:
After much discussion with the parties involved, I plan to withdraw the Object.observe proposal from TC39 (where it currently sits at stage 2 in the ES spec process), and hope to remove support from V8 by the end of the year (the feature is used on 0.0169% of Chrome pageviews, according to chromestatus.com).
И несмотря на огромное количество фич, которые Rx позволяет делать — делать его ядром фреймворка не самый правильный подход. Думаю желающие, вроде меня, могли бы подключить его и отдельно без особых проблем.
Также не соглашусь в целом по поводу TypeScript'а, так как это все же замечательный язык, но об этом ниже.
Статья крайне рекомендуется к ознакомлению, если вы уже используете или планируете использовать Angular
Однако все же изложу несколько собственных мыслей (к сожалению, все больше негативных), не упомянутых в статье выше.
Пожалуй самое болезненное разочарование для меня — это то, во что превратили работу с TypeScript'ом в Angular.
Ниже несколько примеров наиболее неудачных решений.
Одной из основных проблем использования TypeScript в Angular я считаю крайне спорные API.
Сам по себе TypeScript идеально подходит для написания максимально строгого кода, без возможностей случайно сделать шаг не в ту сторону. Фактически он просто создан для того, чтобы писать публичный API, но команда Angular сделала все, чтобы данное преимущество превратилось в недостаток.
Примеры:
По какой-то причине команда Angular решила сделать класс HttpParams
иммутабельным.
Иммутабельность это здорово, но если вы думаете, что большинство классов в Angular являются таковыми, то это вовсе не так.
Например код вида:
let params = new HttpParams();
params.set('param', param);
params.set('anotherParam', anotherParam);
...
this.http.get('test', {params: params});
Не будет добавлять параметры к запросу. Казалось бы почему?
Ведь никаких ошибок, ни TypeScript ни сам Angular не отображает.
Только открыв сам класс в TypeScript можно найти комментарий
This class is immuatable — all mutation operations return a new instance.
Конечно, это совершенно неочевидно.
В вот и вся документация про них:
http
.post('/api/items/add', body, {
params: new HttpParams().set('id', '3'),
})
.subscribe();
Начнем с того, что документация по Angular вообще не имеет толкового разбора и описания Observable
и того, как с ними работать.
Нет даже толковых ссылок на документацию по RxJS. И это при том, что Rx является ключевой частью фреймворка, а само создание Observable
уже отличается:
// rx.js
Rx.Observable.create();
vs
// Angular
new Observable()
Ну да и черт с ним, здесь я хотел рассказать о Rx + TypeScript + Angular.
Допустим вы хотите использовать некий RxJS оператор, вроде do
:
observable.do(event => {...})
В коде никакой ошибки не произойдет, это сработает и замечательно запустится.
Вот только, во время выполнения возникнет такая ошибка:
ERROR TypeError: observable.do is not a function
Потому что вы очевидно (потратили кучу времени на поиск проблемы) забыли заимпортировать сам оператор:
import 'rxjs/add/operator/do';
Почему это ломается в рантайме, если у нас есть TypeScript? Не знаю. Но это так.
Претензий к новому ангуляровскому роутеру у меня накопилось уже великое множество, но его API — одна из основных.
Теперь для работы с параметрами предлагается подписываться на события роутера.
Ок, пускай, но приходят всегда все события, независимо от того, какие нам нужны.
А проверять предлагается через instanceof
(снова новый подход, отличный от большинства других мест):
this.router.events.subscribe(event => {
if(event instanceof NavigationStart) {
...
}
}
В очередной раз странным решением было сделать всю работу с роутами командами — причем массивом из них. Простые и наиболее распространенные переходы будут выглядеть как-то так:
this.router.navigate(['/some']);
...
this.router.navigate(['/other']);
Почему это плохо?
Потому что команды в данном случае имеют сигнатуру any[]
.
Для незнакомых с TypeScript — это фактически отключение его фич.
Это при том, что роутинг — наиболее слабо связанная часть в Angular.
Например мы в нашем AngularJS приложении наоборот старались типизировать роуты,
чтобы вместо простых строк они были хотя бы enum.
Это позволяет находить те или иные места, где используется данный роут без глобального поиска по строке 'some'.
Но нет, в Angular это преимущество TypeScript не используется никак.
Этот раздел также мог бы пойти на отдельную статью, но скажу лишь, что в данном случае
игнорируются любые фичи TypeScript и название модуля пишется как строка, в конце через #
{
path: 'admin',
loadChildren: 'app/admin/admin.module#AdminModule',
},
Для начала — в Angular есть два типа форм: обычные
и реактивные.
Само собой, работать с ними нужно по-разному.
Однако лично меня раздражает именно API reactive forms:
// Зачем нужен первый пустой параметр?
// Почему name это массив c валидатором??
this.heroForm = this.fb.group({
name: ['', Validators.required ],
});
или из документации
// Почему пустое поле это имя??
this.heroForm = this.fb.group({
name: '', // <--- the FormControl called "name"
});
и так далее
this.complexForm = fb.group({
// Почему понадобился compose ?
// Неужели нельзя без null ??
'lastName': [null, Validators.compose([Validators.required, Validators.minLength(5), Validators.maxLength(10)])],
'gender' : [null, Validators.required],
})
А еще — нельзя просто использовать атрибуты типа [disabled] с реактивными формами...
Это далеко не все примеры подобных откровенно спорных решений, но думаю, что для раздела про API этого хватит
К сожалению использование горячо любимого мною TypeScript'а в Angular слишком сильно завязано на декораторы.
Декораторы — прекрасная вещь, но к сожалению в рантайме нет самой нужной их части, а именно __metadata
.
__metadata
просто напросто содержит информацию о классе/типе/методе/параметрах, помеченном тем или иным декоратором,
для того чтобы позже эту информацию можно было получить в рантайме.
Без метаданных декораторы тоже можно использовать — во время компиляции, но толку в таком случае от них не очень много.
Впрочем, мы в нашем AngularJS приложении использовали такие декораторы, например @Component
:
export const Component = (options: ng.IComponentOptions = {}) => controller => angular.extend(options, {controller});
Он фактически просто оборачивает наши TypeScript классы в компоненты AngularJS и делает их контроллерами.
Но в Angular, несмотря на экспериментальность фичи, это стало частью ядра фреймворка,
что делает необходимым использование полифила reflect-metadata в совершенно любом случае. Очень спорное решение.
Обилие внутренних классов и абстракций, а также всякие хитрости завязанные на TypeScript,
также не идут на пользу его принятию комьюнити. Да еще и портят впечатление о TS в целом.
Самый яркий пример подобных проблем — это Dependency Injection в Angular.
Сама по себе концепция замечательная, особенно для unit тестирования.
Но практика показывает, что большой нужды делать из фронтенда нечто Java-подобное нет.
Да, в нашем AngularJS приложении мы очень активно это использовали, но поработав с тестированием Vue компонентов, я серьезно начал сомневаться в пользе DI.
В Angular для большинства обычных зависимостей, вроде сервисов, DI будет выглядеть очень просто, с получением через конструктор:
constructor(heroService: HeroService) {
this.heroes = heroService.getHeroes();
}
Но так работает только для TypeScript классов, и если вы хотите добавить константу, необходимо будет использовать @Inject
:
constructor(@Inject(APP_CONFIG) config: AppConfig) {
this.title = config.title;
}
Ах да, сервисы которые вы будете инжектить должны быть проанотированы как @Injectable()
.
Но не все, а только те, у которых есть свои зависимости, если их нет — можно этот декоратор не указывать.
Consider adding @Injectable() to every service class, even those that don't have dependencies and, therefore, do not technically require it.
Here's why:
Future proofing: No need to remember @Injectable() when you add a dependency later.
Consistency: All services follow the same rules, and you don't have to wonder why a decorator is missing.
Почему бы не сделать это обязательным сразу, если потом это рекомендуется делать всегда?
Еще прекрасная цитата из официальной документации по поводу скобочек:
Always write@Injectable()
, not just@Injectable
. The application will fail mysteriously if you forget the parentheses.
Короче говоря, создается впечатление, что TypeScript в Angular явно используется не по назначению.
Хотя еще раз подчеркну, что сам по себе язык обычно очень помогает в разработке.
Синтаксис шаблонов — основная претензия к Angular. И по вполне объективным причинам.
Пример не только множества разных директив, но еще и разных вариантов их использования:
style using ngStyle
Hello Wordl!
CSS class using property syntax, this text is blue
object of classes
array of classes
string of classes
Изначально разработчики позиционировали новый синтакисис шаблонов, как спасение от множества директив.
Обещали, что будет достаточно только [] и ().
Binding | Example |
---|---|
Properties | |
Events | |
Two-way |
К сожалению в реальности директив едва ли не больше чем в AngularJS.
И да, простое правило запоминания синтаксиса two-way binding про банан в коробке
из официальной документации:
Visualize a banana in a box to remember that the parentheses go inside the brackets.
Вообще писать про документацию Angular даже нет смысла, она настолько неудачная,
что достаточно просто попытаться ее почитать, чтобы все стало понятно.
Контрпример — доки Vue.
Мало того, что написаны подробно и доходчиво, так еще и на 6 языках,
в т.ч. русском.
Angular позволяет использовать так называемый View encapsulation.
Суть сводится к эмуляции Shadow DOM или использовании нативной его поддержки.
Сам по себе Shadow DOM — прекрасная вещь и действительно потенциально позволяет использовать даже разные CSS фреймворки для разных копмонентов без проблем.
Однако нативная поддержка на сегодняшний день совершенно печальна.
По умолчанию включена эмуляция Shadow DOM.
Вот пример простого CSS для компонента:
.first {
background-color: red;
}
.first .second {
background-color: green;
}
.first .second .third {
background-color: blue;
}
Angular преобразует это в:
.first[_ngcontent-c1] {
background-color: red;
}
.first[_ngcontent-c1] .second[_ngcontent-c1] {
background-color: green;
}
.first[_ngcontent-c1] .second[_ngcontent-c1] .third[_ngcontent-c1] {
background-color: blue;
}
Совершенно не ясно зачем делать именно так.
Например Vue делает то же самое, но гораздо чище:
.first[data-v-50646cd8] {
background-color: red;
}
.first .second[data-v-50646cd8] {
background-color: green;
}
.first .second .third[data-v-50646cd8] {
background-color: blue;
}
Не говоря уже о том, что в Vue это не дефолтное поведение и включается добавлением простого scoped к стилю.
Так же хотелось бы отметить, что Vue (vue-cli webpack) подобным же образом позволяет указывать SASS/SCSS, тогда как для Angular CLI нужны команды типа ng set defaults.styleExt scss
.
Не очень понятно зачем все это, если внутри такой же webpack.
Но это все ерунда, реальные проблемы у нас начались, когда нам потребовалось использовать сторонние компоненты.
В частности мы использовали один из наиболее популярных UI фреймворков — PrimeNG,
а он иногда использует подобные селекторы:
body .ui-tree .ui-treenode .ui-treenode-content .ui-tree-toggler {
font-size: 1.1em;
}
Они по определению имеют больший приоритет нежели стили компонента, который использует в себе тот или иной сторонний элемент.
Что в итоге приводит к необходимости писать нечто подобное:
body :host >>> .ui-tree .ui-treenode .ui-treenode-content .ui-tree-toggler {
font-size: 2em;
}
Иногда и вовсе приходилось вспомнить великий и ужасный !important
.
Безусловно все это связано конкретно с PrimeNG и не является как таковой проблемой фреймворка, но это именно та проблема, которая скорее всего возникнет и у вас при реальной работе с Angular.
В примере выше мы использовали >>>
— как и /deep/
это алиас для так называемого shadow-piercing селектора.
Он позволяет как бы "игнорировать" Shadow DOM и для некоторых сторонних компонентов порой просто незаменим.
В одном из относительно свежих релизов Angular создатели фреймворка решили,
в соответствии со стандартом, задепрекейтить /deep/
и >>>
.
Никаких ошибок или ворнингов их использование не принесло, они просто перестали работать.
Как выяснилось позже, теперь работает только ::ng-deep
— аналог shadow-piercing селектора в Angular вселенной.
Обновление это было отнюдь не мажорной версии (4.2.6 -> 4.3.0), просто в один прекрасный момент наша верстка во многих местах поползла (спасибо и NPM за версии с шапочкой ^
).
Конечно, не все наши разработчики ежедневно читают ChangeLog Angular 4, да и за трендами веб разработки не всегда можно уследить. Само собой сначала грешили на собственные стили — пришлось потратить немало времени и нервов для обнаружения столь неприятной особенности.
К тому же скоро и ::ng-deep
перестанет работать.
Как в таком случае править стили кривых сторонних компонентов, вроде тех же PrimeNG, ума не приложу.
Наш личный вывод: дефолтная настройка — эмуляция Shadow DOM порождает больше проблем чем решает.
Это вполне потянет на отдельную статью, но вкратце суть в том, что команда Angular действительно написала свой HTML парсер. По большей части это было необходимо для обеспечения регистрозависимости.
Нет смысла холиварить на тему ухода Angular от стандартов, но по мнению многих это довольно странная идея, ведь в том же AngularJS обычного HTML (регистронезависимого) вполне хватало.
С AngularJS нередко бывало такое: добавили вы некий а тест не написали.
Прошло некоторое время и модуль который содержит логику данного компонента был удален/отрефакторен/итд.
Так или иначе — теперь ваш компонент не отображается.
Теперь он сразу определяет неизвестные теги и выдает ошибку если их не знает.
Правда теперь любой сторонний компонент требует либо полного отключения этой проверки,
либо включения CUSTOM_ELEMENTS_SCHEMA
которая пропускает любые тэги с -
Сорцы можете оценить самостоятельно
...
const TAG_DEFINITIONS: {[key: string]: HtmlTagDefinition} = {
'base': new HtmlTagDefinition({isVoid: true}),
'meta': new HtmlTagDefinition({isVoid: true}),
'area': new HtmlTagDefinition({isVoid: true}),
'embed': new HtmlTagDefinition({isVoid: true}),
'link': new HtmlTagDefinition({isVoid: true}),
'img': new HtmlTagDefinition({isVoid: true}),
'input': new HtmlTagDefinition({isVoid: true}),
'param': new HtmlTagDefinition({isVoid: true}),
'hr': new HtmlTagDefinition({isVoid: true}),
'br': new HtmlTagDefinition({isVoid: true}),
'source': new HtmlTagDefinition({isVoid: true}),
'track': new HtmlTagDefinition({isVoid: true}),
'wbr': new HtmlTagDefinition({isVoid: true}),
'p': new HtmlTagDefinition({
closedByChildren: [
'address', 'article', 'aside', 'blockquote', 'div', 'dl', 'fieldset', 'footer', 'form',
'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'header', 'hgroup', 'hr',
'main', 'nav', 'ol', 'p', 'pre', 'section', 'table', 'ul'
],
closedByParent: true
}),
...
'td': new HtmlTagDefinition({closedByChildren: ['td', 'th'], closedByParent: true}),
'th': new HtmlTagDefinition({closedByChildren: ['td', 'th'], closedByParent: true}),
'col': new HtmlTagDefinition({requiredParents: ['colgroup'], isVoid: true}),
'svg': new HtmlTagDefinition({implicitNamespacePrefix: 'svg'}),
'math': new HtmlTagDefinition({implicitNamespacePrefix: 'math'}),
'li': new HtmlTagDefinition({closedByChildren: ['li'], closedByParent: true}),
'dt': new HtmlTagDefinition({closedByChildren: ['dt', 'dd']}),
'dd': new HtmlTagDefinition({closedByChildren: ['dt', 'dd'], closedByParent: true}),
'rb': new HtmlTagDefinition({closedByChildren: ['rb', 'rt', 'rtc'
...
А теперь ключевой момент — все эти ошибки происходят в консоли браузера в рантайме,
нет, ваш webpack билд не упадет, но и увидеть что либо кроме белого экрана не получится.
Потому что по умлочанию используется JIT компилятор.
Однако это решается прекомпиляцией шаблонов благодаря другому, AOT компилятору.
Нужно всего лишь собирать с флагом --aot
, но и здесь не без ложки дегтя, это плохо работает с ng serve
и еще больше тормозит и без того небыструю сборку. Видимо поэтому он и не включен по умолчанию (а стоило бы).
Наличие двух различно работающих компиляторов само по себе довольно опасно звучит,
и на деле постоянно приводит к проблемам.
У нас с AOT было множество разнообразных ошибок, в том числе весьма неприятные и до сих пор открытые, например нельзя использовать экспорты по умолчанию
Обратите внимание на элегантные предлагаемые решения проблемы:
don't use default exports :)
Just place both export types and it works
Или нечто подобное описанному здесь (AOT не всегда разбирает замыкания)
Код подобного вида вызывает очень странные ошибки компилятора AOT:
@NgModule({
providers: [
{provide: SomeSymbol, useFactor: (i) => i.get('someSymbol'), deps: ['$injector']}
]
})
export class MyModule {}
Приходится подстраиваться под компилятор и переписывать код в более примитивном виде:
export factoryForSomeSymbol = (i) => i.get('someSymbol');
@NgModule({
providers: [
{provide: SomeSymbol, useFactor: factoryForSomeSymbol, deps: ['$injector']}
]
})
export class MyModule {}
Также хотелось бы отметить, что текст ошибок в шаблонах зачастую совершенно неинформативен.
Одна из концептуально очень крутых вещей которые появились в Angular это Zone.js.
Он позволяет отслеживать контекст выполнения для асинхронных задач, но новичков отпугивают огромной длины стактрейсы. Например:
core.es5.js:1020 ERROR Error: Uncaught (in promise): Error: No clusteredNodeId supplied to updateClusteredNode.
Error: No clusteredNodeId supplied to updateClusteredNode.
at ClusterEngine.updateClusteredNode (vis.js:47364)
at VisGraphDataService.webpackJsonp.../../../../../src/app/services/vis-graph-data.service.ts.VisGraphDataService.updateNetwork (vis-graph-data.service.ts:84)
at vis-graph-display.service.ts:63
at ZoneDelegate.webpackJsonp.../../../../zone.js/dist/zone.js.ZoneDelegate.invoke (zone.js:391)
at Object.onInvoke (core.es5.js:3890)
at ZoneDelegate.webpackJsonp.../../../../zone.js/dist/zone.js.ZoneDelegate.invoke (zone.js:390)
at Zone.webpackJsonp.../../../../zone.js/dist/zone.js.Zone.run (zone.js:141)
at zone.js:818
at ZoneDelegate.webpackJsonp.../../../../zone.js/dist/zone.js.ZoneDelegate.invokeTask (zone.js:424)
at Object.onInvokeTask (core.es5.js:3881)
at ClusterEngine.updateClusteredNode (vis.js:47364)
at VisGraphDataService.webpackJsonp.../../../../../src/app/services/vis-graph-data.service.ts.VisGraphDataService.updateNetwork (vis-graph-data.service.ts:84)
at vis-graph-display.service.ts:63
at ZoneDelegate.webpackJsonp.../../../../zone.js/dist/zone.js.ZoneDelegate.invoke (zone.js:391)
at Object.onInvoke (core.es5.js:3890)
at ZoneDelegate.webpackJsonp.../../../../zone.js/dist/zone.js.ZoneDelegate.invoke (zone.js:390)
at Zone.webpackJsonp.../../../../zone.js/dist/zone.js.Zone.run (zone.js:141)
at zone.js:818
at ZoneDelegate.webpackJsonp.../../../../zone.js/dist/zone.js.ZoneDelegate.invokeTask (zone.js:424)
at Object.onInvokeTask (core.es5.js:3881)
at resolvePromise (zone.js:770)
at zone.js:696
at zone.js:712
at ZoneDelegate.webpackJsonp.../../../../zone.js/dist/zone.js.ZoneDelegate.invoke (zone.js:391)
at Object.onInvoke (core.es5.js:3890)
at ZoneDelegate.webpackJsonp.../../../../zone.js/dist/zone.js.ZoneDelegate.invoke (zone.js:390)
at Zone.webpackJsonp.../../../../zone.js/dist/zone.js.Zone.run (zone.js:141)
at zone.js:818
at ZoneDelegate.webpackJsonp.../../../../zone.js/dist/zone.js.ZoneDelegate.invokeTask (zone.js:424)
at Object.onInvokeTask (core.es5.js:3881)
Некоторым Zone.js нравится и
наверняка в продакшене нам это еще не раз поможет, но в нашем проекте большой пользы мы пока не извлекли.
А ошибок в консоли зачастую больше одной и поиск сути проблемы постоянно осложняется анализом таких стактрейсов.
Не говоря уж о том, что нередко длины консоли хрома часто просто не хватает чтобы вместить все сразу.
И сама суть проблемы остается где-то далеко наверху, если ошибка происходит слишком много раз.
Еще один пункт, который формально не относится к самому фреймворку — это довольно скудное количество готовых наборов UI компонентов. Думаю, что написание большинства компонентов с нуля — непозволительная роскошь для многих веб-разработчиков. Зачастую проще, быстрее и разумнее взять готовый UI framework вместо написания собственных гридов и деревьев.
Да, я понимаю, что выбирать фреймворк по наличиую UI компонентов в корне неверно,
но при реальной разработке это необходимо.
Вот список основных UI фреймворков для Angular: https://angular.io/resources (раздел UI components).
Рассмотрим наиболее популярные бесплатные варианты.
Безусловно, наибольшие надежды я возлагал на Angular Material 2 ввиду того, что разрабатывается он командой Angular и наверняка будет соответствовать всем гайдлайнам.
К сожалению, несмотря на его возраст, набор компонентов крайне мал.
На момент начала написания данной статьи, и уж тем более, когда мы выбирали фреймворк для проекта — гридов не было.
И вот совсем недавно он-таки появился. Но функционал все же пока довольно базовый.
Я считаю, что Angular Material 2 подойдет лишь небольшим или, в лучшем случае, средним проектам, т.к. до сих пор нет, например, деревьев. Часто очень нужны компоненты вроде multiple-select, коих тоже нет.
Отдельно стоит сказать про очень скупую документацию и малое количество примеров.
Перспективы развития тоже довольно печальные.
Feature | Status |
---|---|
tree | In-progress |
stepper | In-progress, planned Q3 2017 |
sticky-header | In-progress, planned Q3 2017 |
virtual-repeat | Not started, planned Q4 2017 |
fab speed-dial | Not started, not planned |
fab toolbar | Not started, not planned |
bottom-sheet | Not started, not planned |
bottom-nav | Not started, not planned |
По тем же причинам, что и выше не буду останавливаться на Bootstrap фреймворках типа
ng2-bootstrap (получше) и ngx-bootstrap.
Они очень даже неплохи, но простейшие вещи можно сделать и обычным CSS, а сложных компонентов тут нет (хотя наверняка многим будет достаточно modal, datepicker и typeahead).
Это на сегодняшний день наиболее популярый фреймворк содержащий множество сложных компонентов. В том числе гриды и деревья (и даже Tree Table!).
Изначально я вообще довольно скептически относился к PrimeFaces т.к. у меня был давний опыт работы с JSF, и много неприятных воспоминаний. Да и выглядят PrimeNG визуально так же (не очень современно). Но на момент начала нашего проекта достойных альтернатив не было, и все же хочется сказать спасибо разработчикам за то, что в короткие сроки был написан дейтсивтельно широчайший инструментарий.
Однако проблем с этим набором компонентов у нас возникало очень много.
Часто документация совершенно не помогает.
Забыли добавить некий модуль — что-то просто молча не работает и никаких ошибок не возникает.
Приходится дебажить и выясняется, что сорцы совершенно без комментариев.
В общем, несмотря на наличие большого количества готовых компонентов,
по возможности, я бы не рекомендовал выбирать PrimeNG в качестве базы.
Луч света в темном царстве — это относительно молодая (меньше года от роду) библиотека Clarity от vmware.
Набор компонентов впечатляет,
документация крайне приятная, да и выглядят здорово.
Фреймворк не просто предоставляет набор UI компонентов, но и CSS гайдлайны.
Эдакий свой bootstrap. Благодаря этому достигается консистентный и крайне приятный/минималистичный вид компонентов.
Гриды очень функциональные и стабильные, а сорцы говорят сами за себя
(о боже, комментарии и юнит тесты, а так можно было?).
Однако пока что очень слабые формы,
нет datepicker'а и select2-подобного компонента.
Работа над ними идет в данный момент:
DatePicker,
Select 2.0
(как всегда дизайн на высоте, и хотя с разработкой не торопятся я могу быть уверен, что делают на совесть).
Пожалуй, "Clarity Design System" — единственная причина почему я еще верю в жизнь Angular
(и вообще единственный фреймворк который не стыдно использовать для enterprise разработки).
Как никак VMware серьезнейший мейнтейнер и есть надежда на светлое будущее.
Мы только начали его использовать и наверняка еще столкнемся с проблемами,
но на сегодняшний день он нас полностью устраивает и просто прекрасно работает.
Да, я считаю что для Angular на сегодняшний день есть лишь один достойный UI фреймворк.
О чем это говорит?
Полноценно разрабатывать такие фреймворки для Angular могут лишь серьезнейшие компании вроде той же VMware. Нужен ли вам такой суровый enterprise? Каждый решает сам.
А теперь давайте посмотрим, что происходит с одним из свежих конкурентов.
Для сравнения мощные уже существующие фреймворки для Vue.js с теми же гридами:
Element (~15k stars), Vue Material
(существенно младше Angular Material 2 но уже содержит в разы больше),
Vuetify (снова Material и снова множество компонентов),
Quasar,
также надо отметить популярные чисто китайские фреймворки типа
iView и Muse-UI
(iView выглядит очень приятно, но документация хромает).
Вот и простой, но очевидный, пример того, что писать компоненты на Vue гораздо проще.
Это позволяет даже выбирать наиболее приглянувшийся из множества наборов компонентов,
нежели надяться на один, который поддерживает огромная команда.
Благодаря Clarity есть надежда на то, что наш Angular проект в дальнейшем будет становится только лучше.
Однако для нас стало очевидно, что количество всевозможных проблем с Angular,
а также многочисленные его усложнения совершенно не приносят пользы даже для больших проектов.
В реальности все это лишь увеличивает время разработки, не снижая стоимость поддержки и рефакторинга.
Поэтому для нашего нового проекта мы выбрали Vue.js.
Достаточно просто развернуть базовый webpack шаблон для vue-cli и оценить скорость работы библиотеки.
Несмотря на то, что лично я всегда был сторонником фреймворков all-in-one,
Vue без особых проблем делает почти все то же, что и Angular.
Ну и конечно, множество тех же UI framework'ов также играет свою роль.
Единственное чего немного не хватает — чуть более полноценной поддержки TypeScript,
уж очень много головной боли он нам сэкономил за эти годы.
Но ребята из Microsoft уже вот вот вмержает ее.
А потом она появится и в webpack шаблоне.
Почему мы не выбрали React? После AngularJS наша команда гораздо проще вошла в Vue,
ведь все эти v-if
, v-model
и v-for
уже были очень знакомы.
Лично мне во многом, но не во всем, нравится Aurelia но уж больно она малопопулярна,
а в сравнении с взрывным ростом популярности Vue она кажется совсем неизвестной.
Надеюсь, что через год-два Angular под давлением community все-таки избавится от всего лишнего,
исправит основные проблемы и станет наконец тем enterprise framework'ом, которым должен был.
Но сегодня я рекомендую вам посмотреть в сторону других, более легковесных и элегантных решений.
И поверьте, спустя 4 года работы с Angular, вот так бросить его было очень нелегко.
Но достаточно один раз попробовать Vue...
Метки: author MooooM разработка веб-сайтов javascript html css angularjs angular angular 4 vue vue.js typescript |
ICO для моей игры: ошибки и инсайты |
Мы делаем игру, в которой каждый сможет построить свой успешный бизнес на базе реальной криптовалюты, а заработанные в игре деньги тратить в реальной жизни. И все это — в пиратской атмосфере 17 века.
Метки: author Alienter финансы в it gamedev indiedev ico финансирование стартапов |
ICO для моей игры: ошибки и инсайты |
Мы делаем игру, в которой каждый сможет построить свой успешный бизнес на базе реальной криптовалюты, а заработанные в игре деньги тратить в реальной жизни. И все это — в пиратской атмосфере 17 века.
Метки: author Alienter финансы в it gamedev indiedev ico финансирование стартапов |
[Из песочницы] Что «под капотом» у LinkedList? |
LinkedList numbers = new LinkedList<>();
// модификатор transient указывает на то, что данное свойство класса нельзя
// сериализировать
transient int size = 0;
transient Node first;
transient Node last;
numbers.add(8);
private static class Node {
E item;
Node next;
Node prev;
Node(Node prev, E element, Node next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
numbers.add(5);
numbers.add(1, 13);
numbers.remove(Integer.valueOf(5));
numbers.remove(5);
Метки: author AsteriskIT программирование java linkedlist |
Learnopengl. Урок 2.4 — Текстурные карты |
Ранее мы обсуждали возможность каждого объекта иметь уникальный материал, чтобы по-разному реагировать на свет. Это отлично подходит для того, чтобы придать каждому объекту уникальный вид относительно других объектов на сцене. Но этого все еще не дает нам большой гибкости в настройке внешнего вида объекта.
В предыдущем уроке мы определили материал для целого объекта, но в реальном мире объекты обычно состоят не из одного, а из нескольких материалов. Представьте себе машину: ее внешний корпус блестящий; окна частично отражают окружающую среду; у машины также есть матовые покрышки, а еще есть сверкающие обода (сверкающие, если вы хорошо моете свою машину). Так вот, каждый объект имеет разные свойства материала для каждой своей части.
Итак, наша система материалов из предыдущего урока не подходит для более или менее сложных объектов, по этому нам нужно расширить ее, введя диффузную и бликовую карты. Это даст нам возможность влиять на диффузный (и, косвенным образом, на фоновый, так как это почти всегда одно и то же) и бликовый компоненты объекта с большей точностью.
Все, что нам нужно — это способ установить диффузный цвет для каждого фрагмента объекта. Что может повлиять на значение цвета, основываясь на позиции фрагмента?
Припоминаете? Это — текстуры, которые мы интенсивно обсуждали в одном из предыдущих уроков. Карты освещения – всего лишь другое название того же принципа: используя изображение, нанесенное на поверхность объекта, мы можем делать выборки цвета для каждого фрагмента. В сценах с освещением это обычно называется диффузной картой (как правило ее так называют 3D художники), так как текстурное изображение представляет все диффузные цвета объекта.
Для демонстрации диффузных карт мы будем использовать изображение деревянного контейнера с железной рамкой:
Использование диффузных карт в шейдерах очень похоже на использование текстур в одном из предыдущих уроков. Однако теперь мы заменим ранее определенный вектор vec3
диффузного цвета диффузной картой sampler2D
.
Имейте в виду, что
sampler2D
это, так называемый, непрозрачный тип данных. Это значит, что мы не можем создать экземпляр такого типа, мы можем только определить его как uniform. Если мы попытаемся использовать этот тип не как uniform (например как параметр функции), то GLSL выведет странные ошибки. Это правило также распространяется и на любую структуру, которая содержит непрозрачный тип.
Мы также удалили вектор ambient
, так как он в большинстве случаев совпадает с диффузным цветом, так что нам не нужно хранить его отдельно:
struct Material {
sampler2D diffuse;
vec3 specular;
float shininess;
};
...
in vec2 TexCoords;
Если вы такой упрямый и все еще хотите установить фоновый цвет отличным от диффузного, то можете оставить вектор
ambient
. Но фоновый цвет будет один для всего объекта. Чтобы получить различные значения фонового цвета для каждого фрагмента объекта, вам нужно использовать отдельную текстуру для значений фонового цвета.
Обратите внимание, что нам снова нужны текстурные координаты во фрагментном шейдере, по этому мы объявляем дополнительную входящую переменную. Затем мы просто делаем выборку из текстуры, чтобы извлечь значение диффузного цвета фрагмента:
vec3 diffuse = light.diffuse * diff * vec3(texture(material.diffuse, TexCoords));
Также не забудьте установить фоновый цвет материала таким же, как и диффузный:
vec3 ambient = light.ambient * vec3(texture(material.diffuse, TexCoords));
Это все, что нам нужно для использования диффузной карты. Как вы могли заметить, в этом нет ничего нового, но это дает впечатляющий рост визуального качества. Чтобы это заработало, нам нужно добавить текстурные координаты к данным вершин и передать их в качестве вершинного атрибута во фрагментный шейдер, загрузить текстуру и связать ее с соответствующим текстурным блоком.
Обновленные вершинные данные можно найти здесь. Теперь они включают в себя позиции вершин, векторы нормалей и текстурные координаты для каждой вершины куба. Давайте обновим вершинный шейдер, чтобы он мог принимать текстурные координаты, как вершинный атрибут и передавать их во фрагментный шейдер:
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aNormal;
layout (location = 2) in vec2 aTexCoords;
...
out vec2 TexCoords;
void main()
{
...
TexCoords = aTexCoords;
}
Удостоверьтесь, что обновили вершинные атрибуты обоих VAO (прим. переводчика: имеются ввиду VAO текстурированного куба и VAO куба-лампы), так чтобы они совпадали с новыми вершинными данными, и загрузили изображение контейнера в текстуру. Перед тем, как нарисовать контейнер нам нужно присвоить переменной material.diffuse
предпочтительный текстурный блок и связать с ним текстуру контейнера:
lightingShader.setInt("material.diffuse", 0);
...
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, diffuseMap);
Используя диффузную карту, мы снова получили огромный прирост детализации и теперь, с добавленным освещением, наш контейнер действительно начал блистать (в буквальном смысле). Вероятно, теперь он выглядит вот так:
Вы можете найти полный исходный код приложения здесь.
Вероятно, вы заметили, что блик выглядит немного странно, ведь наш объект — это контейнер, который большей частью состоит из дерева. А, как мы знаем, дерево не дает такого зеркального блеска. Мы можем исправить это, установив вектор specular
в структуре Material
равным vec3(0.0)
, но это значит, что железная рамка контейнера тоже перестанет давать блики, а мы знаем, что металл должен, хотя бы немного блестеть. И снова мы хотели бы контролировать, какие части объекта должны блестеть и с какой силой. Эта проблема очень похожа на обсуждение диффузных карт. Совпадение? Не думаю.
Мы снова можем воспользоваться текстурной картой, только теперь для зеркальных бликов. Это значит, что нам нужно создать черно-белую (или цветную, если хотите) текстуру, которая определит силу блеска каждой части объекта. Вот пример бликовой карты:
Интенсивность блеска определяется яркостью каждого пикселя изображения. Каждый пиксель такой карты может быть представлен как цветовой вектор, где черный цвет — это vec3(0.0)
, а серый — vec3(0.5)
, например. Затем, во фрагментном шейдере мы отбираем соответствующее цветовое значение и умножаем его на интенсивность бликового цвета. Соответственно, чем "белее" пиксель, тем больше получается результат умножения, а, следовательно, и яркость блика на фрагменте объекта.
Так как контейнер большей частью состоит из дерева, а дерево — это материал, который не дает бликов, то вся "деревянная" часть текстуры закрашена черным. Черные части вообще не блестят. Поверхность стальной рамки контейнера имеет переменную силу зеркального блеска: сама сталь дает довольно интенсивные блики, в то время как трещины и потертости – нет.
Технически, дерево тоже имеет зеркальные отражения, хотя и с намного более низкой силой блеска (свет сильнее рассеивается), но в образовательных целях, мы сделаем вид, что дерево никак не реагирует на зеркальный свет.
Используя инструменты, такие как Photoshop или Gimp, довольно просто превратить диффузную текстуру в бликовую. Достаточно просто вырезать некоторые части, сделать изображение черно-белым и увеличить яркость/контрастность.
Карта зеркальных отражений — это самая обычная текстура, по этому код ее загрузки очень похож на загрузку диффузной карты. Удостоверьтесь, что вы правильно загрузили изображение и сгенерировали текстурный объект. Так как мы используем новую текстуру в том же фрагментом шейдере, нам нужно использовать другой текстурный блок для карты бликов. Давайте свяжем эту текстуру с соответствующим текстурным блоком перед визуализацией:
lightingShader.setInt("material.specular", 1);
...
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, specularMap);
Затем обновим свойства материала во фрагментном шейдере, чтобы отражающий компонент принимался в качестве sampler2D
, а не vec3
:
struct Material {
sampler2D diffuse;
sampler2D specular;
float shininess;
};
И, наконец, нам нужно провести выборку карты бликов, чтобы получить соответствующую интенсивность блика для каждого фрагмента объекта:
vec3 ambient = light.ambient * vec3(texture(material.diffuse, TexCoords));
vec3 diffuse = light.diffuse * diff * vec3(texture(material.diffuse, TexCoords));
vec3 specular = light.specular * spec * vec3(texture(material.specular, TexCoords));
FragColor = vec4(ambient + diffuse + specular, 1.0);
Используя карту бликов, мы можем с чрезвычайной точностью определить, какие части объекта дают зеркальные блики, и установить соответствующую их интенсивность. Таким образом, карта бликов дает нам дополнительный уровень контроля поверх диффузной карты.
Вы также можете использовать в бликовой карте цвета, которые определяют не только интенсивность блика, но и его цвет. Однако в реальности, цвет блика в значительной степени (а в большинстве случаев и полностью) зависит от источника света, поэтому использование цветных карт бликов не даст реалистичных результатов (вот почему эти изображения обычно черно-белые — нас интересует только интенсивность блика).
Если вы запустите приложение, то увидите, что материал контейнера очень похож, на деревянный контейнер с железной рамкой:
Вы можете найти полный исходный код приложения здесь.
Используя диффузные и бликовые карты, мы можем добавить огромное количество деталей в относительно простые объекты. Мы можем добавить еще больше деталей, используя другие текстурные карты, такие как карты нормалей/рельефа и/или карты отражений, но их мы прибережем для следующих уроков. Покажите ваш контейнер друзьям и семье и помните, что однажды наш контейнер может стать еще более привлекательным, чем сейчас!
Метки: author eanmos разработка игр программирование c++ opengl 3 glsl перевод |
Настройка среды разработки Webpack 3 + Angular 4: от сложного к простому |
{
"name": "angular-project",
"version": "1.0.0",
"description": "angular scaffolding",
"author": "maxim1006",
"license": "MIT",
"dependencies": {
//блок необходимых для Angular модулей
"@angular/animations": "^4.3.6",
"@angular/common": "^4.3.6",
"@angular/compiler": "^4.3.6",
"@angular/compiler-cli": "^4.3.6",
"@angular/core": "^4.3.6",
"@angular/forms": "^4.3.6",
"@angular/http": "^4.3.6",
"@angular/platform-browser": "^4.3.6",
"@angular/platform-browser-dynamic": "^4.3.6",
"@angular/router": "^4.3.6",
//модули для hmr
"@angularclass/hmr": "^2.1.1",
"@angularclass/hmr-loader": "^3.0.2",
//polyfills для es5
"core-js": "^2.5.0",
//модуль для работы декораторов в браузере
"reflect-metadata": "^0.1.8",
//модуль для работы с реактивным программированием
"rxjs": "^5.4.3",
//типизация и доп. возможности для js
"typescript": "2.3.4",
//зоны в js, очень интересно, обязательно почитайте
"zone.js": "^0.8.17"
},
"devDependencies": {
//для сборки AoT (Ahead-of-Time Compilation) angular
"@ngtools/webpack": "^1.6.2",
//поддержка типизации, чтобы не ругался typescript
"@types/es6-shim": "^0.31.35",
"@types/jasmine": "^2.5.54",
"@types/node": "^7.0.43",
//routing в приложении
"angular-router-loader": "^0.6.0",
//так как на выходе получится бандл со встроенными темплейтами
"angular2-template-loader": "^0.6.2",
//чтобы не писать префиксы в css
"autoprefixer": "^6.3.7",
//для оптимизации работы typescript в webpack
"awesome-typescript-loader": "^3.2.3",
//если вдруг надо скопировать папку/файл
"copy-webpack-plugin": "^4.0.1",
//для работы с css
"css-loader": "^0.28.5",
"css-to-string-loader": "^0.1.2",
//es6 polyfills
"es6-shim": "^0.35.1",
//для мобильной разработки
"hammerjs": "^2.0.8",
//чтобы webpack работал с html
"html-webpack-plugin": "^2.29.0",
//препроцессор для более удобной работы со стилями
"less": "^2.7.2",
"less-loader": "^4.0.3",
//по завершению сборки сможем вызвать коллбек
"on-build-webpack": "^0.1.0",
//вставляет результат работы webpack на страничку
"raw-loader": "^0.5.1",
//для работы со стилями
"postcss-loader": "^1.3.3",
"style-loader": "^0.17.0",
//линтер
"tslint": "^5.7.0",
//если надо что-нибудь удалить
"rimraf": "^2.6.1",
//чтобы вставить картинки в css в виде base64
"url-loader": "^0.5.8",
//webpack
"webpack": "^3.5.5",
//и его встроенный express сервер
"webpack-dev-server": "^2.7.1"
},
//когда введем в терминале эти команды с помощью npm run __command__ (например npm run serve)выполняться соответствующие команды)
"scripts": {
//Запускаем сервер. При каждом сохранении в вашем редакторе при работе с файлами проекта страничка будет перезагружаться, и вы будете видеть результат. Расскажем подробнее о команде. Для начала запускаем веб-сервер с данными настройками. Если мы хотим видеть в консоли, что с ним происходит (что бандлится и т. д.), используем (флаг --profile); если хотим, чтобы при сохранении в редакторе webpack автоматически обновлял результат, используем (--watch); ну а если хотим видеть проценты компиляции, можем опционально использовать (флаг –-progress).
"serve": "webpack-dev-server --config ./webpack.config.js --profile --watch --progress",
//то же, что и serve, но без перезагрузки страницы
"hmr": "webpack-dev-server --config ./webpack.config.js --profile --watch --progress",
//создаем prod папочку с нашим проектом
"prod": "npm run aot",
//посмотреть как наш проект выглядит в prod, мало ли что
"prodServer": "webpack-dev-server --config ./webpack.config.js --open",
//очищаем ./dist на всякий случай
"clean": "rimraf ./dist",
//нужно, чтобы в webpack.js понять, что мы делаем aot. Делать это необязательно, но для наглядности нужно.
"aot": "webpack",
//тесты для приложения
"test": "karma start"
}
}
{
"rules": {
"no-unused-variable": true,
"curly": true,
"no-console": [
true,
"log",
"error",
"debug",
"info"
],
"no-debugger": true,
"no-duplicate-variable": true,
"no-eval": true,
"no-invalid-this": true,
"no-shadowed-variable": true,
"no-unsafe-finally": true,
"no-var-keyword": true,
"triple-equals": [
true,
"allow-null-check",
"allow-undefined-check"
],
"semicolon": [
true,
"always",
"ignore-interfaces"
],
"variable-name": [
true,
"ban-keywords",
"check-format",
"allow-leading-underscore"
]
}
}
module.exports = {
plugins: [
require('autoprefixer')({
browsers: [
'last 2 versions'
],
cascade: true
})
]
};
{
//Настраиваем компилятор typescript
"compilerOptions": {
"target": "es5",
"module": "es2015",
"declaration": false,
"moduleResolution": "node",
"sourceMap": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"removeComments": false,
"noImplicitAny": false,
"suppressImplicitAnyIndexErrors": true,
"skipLibCheck": true,
"lib": ["es6", "dom"],
"outDir": "./dist/",
"typeRoots": [
"./node_modules/@types/"
]
},
"compileOnSave": false,
"buildOnSave": false,
//наше приложение будет лежать в папке ./src
"include": [
"./src/**/*"
],
//запрещаем typescript обращать внимание на:
"exclude": [
"node_modules/*",
"dist/*",
"dist-serve/*",
"node/*",
"**/*.spec.ts"
],
//настраиваем loader для webpack
"awesomeTypescriptLoaderOptions": {
"forkChecker": true,
"useWebpackText": true,
"useCache": true
},
//нужно для AoT
"angularCompilerOptions": {
"genDir": ".",
"skipMetadataEmit" : true
}
}
"use strict";
//это node модули и webpack плагины, которые понадобятся нам в разработке
const path = require('path');
const fs = require('fs');
const webpack = require('webpack');
const WebpackOnBuildPlugin = require('on-build-webpack');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const AotPlugin = require('@ngtools/webpack').AotPlugin;
//помните, в package.json были команды serve, hmr, prod и т. д.? так вот, текущую команду (например, если вы введете npm run serve, то команда будет называться ‘serve’) можно получить и обработать вот так:
const ENV = process.env.npm_lifecycle_event ? process.env.npm_lifecycle_event : '';
const isStatic = ENV === 'serve';
const isHmr = ENV === 'hmr';
const isProd = ENV === 'prod';
const isTest = ENV === 'test';
const isAot = ENV.includes('aot');
const isProdServer = ENV.includes('prodServer');
//в зависимости от команды, мы будем объяснять webpack что делать
//обычно из webpack.conf.js экспортируется функция, возвращающая объект с настройками
module.exports = function makeWebpackConfig() {
console.log(`You are in ${ENV} mode`); //напомнить что мы запустили
let config = {}; //главный объект с настройками
//если вдруг кто-то выполнит команду npm run prodServer, не выполнив предварительно npm run prod, кидаем напоминалку
if (isProdServer) {
if (!fs.existsSync('./dist')) {
throw "Can't find ./dist, please use 'npm run prod' to get it.";
}
}
//подключаем sourcemaps
if (isHmr || isStatic) {
config.devtool = 'inline-source-map';
} else {
config.devtool = 'source-map';
}
//обозначаем главный файл, который будет создавать webpack. Этот файл доступен в index.html по пути “./ng-app.js”
config.entry = {
'ng-app': './src/app/ng-main.ts'
};
//специально для AoT режима нужно создать другой файл с другим наполнением, так надо…
if (isAot) {
config.entry['ng-app'] = './src/app/ng-main-aot.ts';
}
// Имя файла, который создаст webpack будет 'ng-app’, так как задали filename: '[name].js', также когда запустим prod сборку, результирующий бандл попадет в папку './dist', это указали с помощью path: root('./dist')
config.output = isTest ? {} : {
path: root('./dist'), //root – всего лишь функция, для создания правильных путей относительно папки, в которой находится webpack.config.js
filename: '[name].js'
};
//в свойстве entry при настройке webpack обязательно нужно задать какой-нибудь файл, иначе возникнет ошибка, но в режиме prodServer нам нужно лишь посмотреть на нашу prod сборку. По этой причине и создаем поддельный файл, чтобы сервер ни на что, кроме статики, не отвлекался. Можно в корень проекта, рядом с webpack.conf.js, положить пустой файл webpack-prod-server.js, чтобы в логи сервера не попадала ошибка, что этого файла нет, хотя и без него сервер будет работать.
if (isProdServer) {
config.entry = {
'server': './webpack-prod-server.js'
};
config.output = {};
}
//указываем расширения файлов, с которыми webpack будет работать
config.resolve = {
extensions: ['.ts', '.js', '.json', '.html', '.less', '.svg']
};
//определяем так называемые loaders: если будут вопросы по ним, отвечу в комментариях. Если коротко, тут готовый пример для превращения ts в js, html вставляем в js бандл, less компилируем в css и вставляем в js бандл, картинки до 10 кб в base64 и вставляем в js бандл.
config.module = {
rules: [
{
test: /\.ts$/,
use: isAot ? [{loader: '@ngtools/webpack'}] : [
{
loader: 'awesome-typescript-loader?'
},
{
loader: 'angular2-template-loader'
},
{
loader: 'angular-router-loader'
}
].concat(isHmr ? '@angularclass/hmr-loader?pretty=' + !isProd + '&prod=' + isProd : []),
exclude: [/\.(spec|e2e|d)\.ts$/]
},
{
test: /\.html$/, loader: 'raw-loader',
exclude: [/node_modules\/(?!(ng2-.+))/, root('src/index.html')]
},
{
test: /\.(png|jpe?g|gif|svg|woff|woff2|ttf|eot|ico)(\?v=[0-9]\.[0-9]\.[0-9])?$/,
loader: "url-loader?name=[name].[ext]&limit=10000&useRelativePath=true"
},
{
test: /\.less$/,
use: [
{loader: "css-to-string-loader"},
{loader: "css-loader"},
{loader: "postcss-loader"},
{loader: "less-loader"}
]
}
]
};
//если работаем не в режиме тестирования, то подключаем webpack плагины
if (!isTest) {
config.plugins = [
//не останавливать webpack warcher при ошибках
new webpack.NoEmitOnErrorsPlugin(),
//передать текущий режим в наши .ts файлы, как их получить в .ts файлах увидите чуть позже
new webpack.DefinePlugin({
'process.env': {
'STATIC': isStatic,
'HMR': isHmr,
'PROD': isProd,
'AOT': isAot
}
}),
//сделать что-то по окончании сборки
new WebpackOnBuildPlugin((stats) => {
console.log('build is done');
})
]
//если работаем в режиме hmr, то подключить плагин для hmr
.concat(isHmr ? new webpack.HotModuleReplacementPlugin() : []);
}
//если вы вызовете команду ‘npm run prod’, то запустите процесс создания prod сборки с AoT
if (isAot) {
config.plugins = [
//нужно для AoT режима
new AotPlugin({
tsConfigPath: './tsconfig.json',
entryModule: root('src/app/app.module.ts#AppModule')
}),
//Оптимизируем полученный бандл
new webpack.optimize.UglifyJsPlugin({
compress: {
warnings: false,
screw_ie8: true,
conditionals: true,
unused: true,
comparisons: true,
sequences: true,
dead_code: true,
evaluate: true,
if_return: true,
join_vars: true
},
output: {
comments: false
},
sourceMap: true
}),
//Копируем нужные нам файлы в ./dist папку (js бандл туда положит сам webpack, а мы перенесем то, что нам понадобится дополнительно)
new CopyWebpackPlugin([
{from: 'index.html', context: './src'},
{from: 'assets/themes/base/fonts/**/*', context: './src'},
{from: 'assets/themes/base/images/other-images/**/*', context: './src'},
]),
new WebpackOnBuildPlugin((stats) => {
console.log('build in aot is done');
})
];
}
//Ну и наконец настроим наш webpack-dev-server
config.devServer = {
contentBase: isProdServer ? "./dist" : "./src",//корневая папка сервера, в prod режиме в ./dist, в режиме разработки в ./src
headers: {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, PATCH, OPTIONS",
"Access-Control-Allow-Headers": "X-Requested-With, content-type, Authorization"
}, //стандартные заголовки для rest запросов
historyApiFallback: true, //включаем HTML5 history api, очень удобно 1ой строкой
compress: true,//включаем gzip
quiet: false, //ничего лишнего нам выводить в логи не нужно
inline: isHmr || isStatic || isProdServer, //inline mode
hot: isHmr, //включаем hmr, если в hmr режиме
stats: "minimal",
port: 9000,
//модное окно смерти при ошибке от Webpack
overlay: {
errors: true
},
//Опции для webpack warcher
watchOptions: {
aggregateTimeout: 50,
ignored: /node_modules/
}
};
return config;
};
//делаем правильный путь от текущей директории
function root(__path = '.') {
return path.join(__dirname, __path);
}
// Common
@import "themes/base/styles/common/normalize";
@import "themes/base/styles/common/colors";
@import "themes/base/styles/common/common";
@import "themes/base/styles/common/fonts";
@import "themes/base/styles/common/vars";
// Blocks
// (please, add new blocks in alphabetical order)
@import "themes/base/styles/blocks/app-component";
//нужно для A2+ routing
Loading...
import './ng-polyfills'; //чтобы работало в ie 9+
import …
//в настройках webpack мы прокидывали переменные, тут их ловим
if (process.env.STATIC) {
//console.log("******************You are in Dev mode******************");
platformBrowserDynamic().bootstrapModule(AppModule).then(():any => {});
} else if (process.env.HMR) {
//нужно для hmr в Angular
//console.log("******************You are in HMR mode******************");
bootloader(main);
}
export function main() {
return platformBrowserDynamic()
.bootstrapModule(AppModule)
}
import …
console.log("******************You are in prod mode******************");
enableProdMode();
platformBrowser()
.bootstrapModuleFactory(AppModuleNgFactory)
.catch(error=>console.log(error));
require("style-loader!../assets/base.less"); //так подключаем стили через webpack
import …
//настраиваем hammer.js
export class MyHammerConfig extends HammerGestureConfig {
overrides = {
'swipe': {velocity: 0.4, threshold: 20}
}
}
@NgModule({
declarations: [
AppComponent,
],
imports: [
BrowserModule,
HomeModule,
NgRoutingModule
],
providers: [
],
bootstrap: [
AppComponent
]
})
export class AppModule {
constructor(public appRef: ApplicationRef) {}
hmrOnInit(store) {
if (!store || !store.state) return;
if ('restoreInputValues' in store) {
store.restoreInputValues();
}
this.appRef.tick();
delete store.state;
delete store.restoreInputValues;
}
hmrOnDestroy(store) {
let cmpLocation = this.appRef.components.map(cmp => cmp.location.nativeElement);
store.disposeOldHosts = createNewHosts(cmpLocation);
store.state = {data: 'yolo'};
store.restoreInputValues = createInputTransfer();
removeNgStyles();
}
hmrAfterDestroy(store) {
store.disposeOldHosts();
delete store.disposeOldHosts;
}
}
import …
const routes: Routes = [
{path: '', redirectTo: '/home', pathMatch: 'full'},
{path: 'home', component: HomeComponent},
//так подключаем lazy модули, отдельные .js файлы webpack для них создаст сам
{path: 'lazy', loadChildren: './modules/lazy/lazy.module#LazyModule'},
{path: '**', component: PageNotFoundComponent},
];
@NgModule({
imports: [
RouterModule.forRoot(routes)
],
exports: [
RouterModule
]
})
export class NgRoutingModule { }
import …
const routes: Routes = [
{path: '', component: LazyComponent},
];
@NgModule({
imports: [SharedModule, RouterModule.forChild(routes)],
exports: [LazyComponent],
declarations: [LazyComponent]
})
export class LazyModule {}
Метки: author maxim1006 javascript angularjs блог компании netcracker angular angular 4 webpack typescript node.js hmr |
«Молодые хотят покупать на бирже акции знакомых стартапов »: тренды финансов по мнению основателя сервиса Robinhood |
Метки: author itinvest финансы в it блог компании itinvest robinhood биржа финансы |
«Молодые хотят покупать на бирже акции знакомых стартапов »: тренды финансов по мнению основателя сервиса Robinhood |
Метки: author itinvest финансы в it блог компании itinvest robinhood биржа финансы |
CaptureManager SDK |
Метки: author Xirexel python c++ c# видео аудио |
CaptureManager SDK |
Метки: author Xirexel python c++ c# видео аудио |
[Из песочницы] Исправляем 7 распространенных ошибок обработки исключений в Java |
Метки: author Skwazer программирование java java core exeption |
[Из песочницы] Исправляем 7 распространенных ошибок обработки исключений в Java |
Метки: author Skwazer программирование java java core exeption |
Учим webworkers хорошим манерам |
const worker = new Worker('test.worker.js');
worker.onmessage = (data) => {
if(data.eventName === 'someFuncResult')
someFunc();
else if(data.eventName === 'someFunc2Result')
someFunc2();
};
worker.postMessage({eventName: 'someFunc'});
self.addEventListener('message', (data) => {
if(data.eventName === 'someFunc') {
doSomeFunc();
self.postMessage('someFuncResult');
}
if(data.eventName === 'someFunc2') {
doSomeFunc();
self.postMessage('someFunc2Result');
}
})
Метки: author kwolfy javascript webworkers |
Учим webworkers хорошим манерам |
const worker = new Worker('test.worker.js');
worker.onmessage = (data) => {
if(data.eventName === 'someFuncResult')
someFunc();
else if(data.eventName === 'someFunc2Result')
someFunc2();
};
worker.postMessage({eventName: 'someFunc'});
self.addEventListener('message', (data) => {
if(data.eventName === 'someFunc') {
doSomeFunc();
self.postMessage('someFuncResult');
}
if(data.eventName === 'someFunc2') {
doSomeFunc();
self.postMessage('someFunc2Result');
}
})
Метки: author kwolfy javascript webworkers |
10 лет с Колибри: как это было |
Метки: author Leency программирование open source kolibri kolibrios kolibrin дизайн интерфейс иконки ассемблер с-- |
10 лет с Колибри: как это было |
Метки: author Leency программирование open source kolibri kolibrios kolibrin дизайн интерфейс иконки ассемблер с-- |
Использование Akka в Spring-приложении |
Метки: author andd3dfx java akka multithreading spring jmock |
Использование Akka в Spring-приложении |
Метки: author andd3dfx java akka multithreading spring jmock |
[Из песочницы] Параллелизм против многопоточности против асинхронного программирования: разъяснение |
Метки: author Aliaxandr высокая производительность asp .net асинхронное программирование многопоточное программирование параллелизм потоки задачи |
[Из песочницы] Параллелизм против многопоточности против асинхронного программирования: разъяснение |
Метки: author Aliaxandr высокая производительность asp .net асинхронное программирование многопоточное программирование параллелизм потоки задачи |