Настройка среды разработки Webpack 3 + Angular 4: от сложного к простому

в 13:01, , рубрики: angular, angular 4, AngularJS, hmr, javascript, node.js, TypeScript, webpack, Блог компании Netcracker

Всем привет!

Современное 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, однако можете проделывать то же самое в вашем редакторе)

Настройка среды разработки Webpack 3 + Angular 4: от сложного к простому - 1

2) Окружение

Устанавливаем node.js (npm в комплекте по умолчанию).

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

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.

Настройка среды разработки Webpack 3 + Angular 4: от сложного к простому - 2

5) README.md

Создаем README.md, куда кроме ссылки на эту статью можно добавить особенности разработки вашего проекта.

6) Линтер

Создаем tslint.json, тут не буду останавливаться, так как нет серебряной пули.

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, чтобы не писать префиксы к стилям

postcss.config.js

module.exports = {
    plugins: [
        require('autoprefixer')({
            browsers: [
                'last 2 versions'
            ],
            cascade: true
        })
    ]
};

Дальше пойдут немного более сложные манипуляции, пожалуйста, сфокусируйтесь.

8) Настройка Typescript tsconfig.json

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

tsconfig.json

{
//Настраиваем компилятор 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, без паники, постараюсь все объяснить

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

Настройка среды разработки Webpack 3 + Angular 4: от сложного к простому - 3

Создаем структуру в папке src:

Настройка среды разработки Webpack 3 + Angular 4: от сложного к простому - 4

Пара комментариев: в папке app будет лежать наше angular приложение, в папке assets вспомогательные файлы, index.html просто кладем в src. В assets поддержим темизацию и разобьем папки для удобной работы со шрифтами, картинками, стилями.

В нашей компании мы используем БЭМ методологию, немного переработанную и более оптимальную, на наш взгляд. base.less – агрегирующий .less файл для base темы:

base.less

// 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+ приложениях

index.html

<!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:

Настройка среды разработки Webpack 3 + Angular 4: от сложного к простому - 5

На первый взгляд – ребус.

Однако если вы прошли хотя бы Angular 2+ Tutorial, то все это вам уже знакомо. Для остальных же краткие комментарии: все приложение разбито на модули, фреймворк даже предоставляет такую сущность – module. Есть главный модуль – app.module.ts, есть дополнительные модули, расширяющие функционал приложения. Большая часть приложений будет иметь home, lazy и shared модули. Названия модулей, разумеется, опциональны, однако при соблюдении правил наименования у вас не возникнет проблем с расширяемостью приложения.

Про сам фреймворк говорить много не будем, есть отличная документация. Лучше сосредоточимся на тонких моментах:

ng-main.ts

С него все начинается

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, так нужно…

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 таким образом:

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

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) следующее:

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.

Настройка среды разработки Webpack 3 + Angular 4: от сложного к простому - 6

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

Автор: maxim1006

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js