Всем привет!
Современное front-end-приложение на Angular должно включать в себя следующие характеристики:
- Возможность использования типизированного JS — Typescript
- Обеспечение удобства и производительности разработки с помощью HMR (hot module replacement);
- Модульность приложений и возможность отложенной загрузки модулей (Lazy Loading);
- AoT — режим (ahead-of-time), повышающий производительность приложения.
Существует много вариантов сборки, решающих эти задачи (angular cli, A2 seed и т. д.). Обычно они имеют сложную структуру, плохо настраиваются/расширяются и представляют собой монолит, который невозможно изменить.
В статье я расскажу, как совместить Angular 2+ с webpack и разобраться со всеми этапами сборки/разработки.
Вы удивитесь, как это просто.
Постараюсь по максимуму осветить тонкие моменты. Итак, поехали.
1) Создаем проект
Создаем папку с проектом, чтобы никто не догадался, назовем ее angular-project.
(Использую Webstorm, однако можете проделывать то же самое в вашем редакторе)
2) Окружение
Устанавливаем node.js (npm в комплекте по умолчанию).
Создаем package.json, разумеется, количество подключаемых на проект модулей потенциально стремится к бесконечности, однако я оставлю только необходимые, на мой взгляд, для полноценной разработки. Модулей много, постараюсь обосновать, зачем они нужны.
{
"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"
}
}
3) Установка модулей
Через терминал заходим в папку, где лежит package.json, и вводим команду npm i.
4) Установка глобальных модулей
Так как мы используем команды rimraf, webpack и webpack-dev-server в терминале, то придется объяснить их вашему ПК с помощью команды npm i rimraf webpack webpack-dev-server -g
После этих манипуляций наш проект пополнился папкой node_modules.
5) README.md
Создаем README.md, куда кроме ссылки на эту статью можно добавить особенности разработки вашего проекта.
6) Линтер
Создаем tslint.json, тут не буду останавливаться, так как нет серебряной пули.
{
"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"
]
}
}
7) PostCss
Создадим postcss.config.js, чтобы не писать префиксы к стилям
module.exports = {
plugins: [
require('autoprefixer')({
browsers: [
'last 2 versions'
],
cascade: true
})
]
};
Дальше пойдут немного более сложные манипуляции, пожалуйста, сфокусируйтесь.
8) Настройка Typescript tsconfig.json
Так как разработка A2+, на мой взгляд, невозможна без typescript, его надо настроить. Настройки обычные, однако если будут вопросы, спрашивайте в комментариях.
{
//Настраиваем компилятор 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
}
}
9) Настройка Webpack
Самое сложное – дать понять webpack, что мы от него хотим. Для этого создаем webpack.conf.js, без паники, постараюсь все объяснить
"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);
}
10) Структура src
Сейчас наш проект выглядит так, кроме папки src
Создаем структуру в папке src:
Пара комментариев: в папке app будет лежать наше angular приложение, в папке assets вспомогательные файлы, index.html просто кладем в src. В assets поддержим темизацию и разобьем папки для удобной работы со шрифтами, картинками, стилями.
В нашей компании мы используем БЭМ методологию, немного переработанную и более оптимальную, на наш взгляд. base.less – агрегирующий .less файл для base темы:
// 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";
Заметим, что, на наш взгляд, следует разносить функциональную и стилевую части приложения: это решает ряд проблем как сборки, так и поддержки проекта. Если использовать БЭМ и парадигму один блок – один less файл, то проблем у подхода не обнаруживается. Однако есть куча альтернатив. Более подробно покопаться в assets можно в приложении, к этому посту. Вопросы задавайте в комментариях к статье.
11) index.hml
index.html – стал безумно прост в A2+ приложениях
<!DOCTYPE html>
<html>
<head>
<base href="/"> //нужно для A2+ routing
<meta charset="utf-8">
<title>Landing</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="/img/favicon.ico">
</head>
<body>
<app-component>Loading...</app-component>
<script type="text/javascript" src="./ng-app.js"></script>
</body>
</html>
12) Angular app
Вздохнем поглубже, все сложное мы уже сделали, теперь остался сам фреймворк)
Создадим структуру в папке app:
На первый взгляд – ребус.
Однако если вы прошли хотя бы Angular 2+ Tutorial, то все это вам уже знакомо. Для остальных же краткие комментарии: все приложение разбито на модули, фреймворк даже предоставляет такую сущность – module. Есть главный модуль – app.module.ts, есть дополнительные модули, расширяющие функционал приложения. Большая часть приложений будет иметь home, lazy и shared модули. Названия модулей, разумеется, опциональны, однако при соблюдении правил наименования у вас не возникнет проблем с расширяемостью приложения.
Про сам фреймворк говорить много не будем, есть отличная документация. Лучше сосредоточимся на тонких моментах:
ng-main.ts
С него все начинается
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)
}
ng-main-aot.ts для AoT
Для AoT (Ahead-of-Time Compilation) режима создаем другой главный файл ng-main-aot.ts, так нужно…
import …
console.log("******************You are in prod mode******************");
enableProdMode();
platformBrowser()
.bootstrapModuleFactory(<any>AppModuleNgFactory)
.catch(error=>console.log(error));
HMR, стили, hammerjs
HMR, стили нашего приложения (на всякий случай оставил пример подключения картинок) и настройки hammerjs для мобильной разработки подключаем в app.module.ts таким образом:
require("style-loader!../assets/base.less"); //так подключаем стили через webpack
import …
//настраиваем hammer.js
export class MyHammerConfig extends HammerGestureConfig {
overrides = <any>{
'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;
}
}
Lazy loading
Lazy loading модулей подключаем в ng-routing.module.ts
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 { }
После подключения lazy модуля в роутере необходимо в модуле, который мы хотим загружать отложено, сделать (на примере lazy.module.ts) следующее:
import …
const routes: Routes = [
{path: '', component: LazyComponent},
];
@NgModule({
imports: [SharedModule, RouterModule.forChild(routes)],
exports: [LazyComponent],
declarations: [LazyComponent]
})
export class LazyModule {}
Хм… ну вот в принципе и все. Покопаться в app папке можно в приложении к данному посту.
Для разработки с перезагрузкой странички на каждое изменение кода в редакторе, пишем в терминале, находясь папке с package.json: npm run serve
То же, но без перезагрузки странички: npm run hmr
Делаем prod сборку с AoT: npm run prod
Запускаем статический сервер, чтобы посмотреть prod: npm run prodServer
Почистить ./dist папку: npm run clean
Всего несколько шагов и у нас работают: webpack сборка с Angular 4, AoT, HMR, Lazy loading. Все, включая шаблоны и стили, аккуратно кладется в бандл и оптимизируется.
Разумеется, эту конфигурацию можно расширять, улучшать, менять, однако на мой взгляд, ее вполне достаточно, чтобы смело начать разрабатывать с Angular 2+.
P.S.
Небольшая реклама АoT: отличный boost к производительности вашего SPA приложения на Angular.
Спасибо за внимание.
Автор: maxim1006