5 декабря 2016

Настройка Angular 2 и Webpack

В документации к Angular 2 основной упор идет на работу с SystemJS, например, пример быстрого старта приложения от команды Angular также основан на SystemJS. Я же хочу поговорить о настройке Angular 2 с Webpack.

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

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

Что такое Webpack? Это мощное стредство для сборки пакетов (модулей) проекта. С его помощью мы можем подключать различные библиотеки, генерировать CSS на основе SASS или LESS, использовать префиксеры, минимайзеры и т. д.

Итак, для работы нам понадобится в первую очередь Node.js и npm.

Конфигурация проекта

В корне проекта создаем package.json. Тут описаны необходимые модули для работы:

{
  "name": "angular2-webpack",
  "version": "1.0.0",
  "description": "A webpack starter for Angular",
  "scripts": {
    "start": "webpack-dev-server --inline --progress --port 8080",
    "build": "rimraf dist && webpack --config config/webpack.prod.js --progress --profile --bail"
  },
  "license": "MIT",
  "dependencies": {
    "@angular/common": "~2.2.0",
    "@angular/compiler": "~2.2.0",
    "@angular/core": "~2.2.0",
    "@angular/forms": "~2.2.0",
    "@angular/http": "~2.2.0",
    "@angular/platform-browser": "~2.2.0",
    "@angular/platform-browser-dynamic": "~2.2.0",
    "@angular/router": "~3.2.0",
    "core-js": "^2.4.1",
    "rxjs": "5.0.0-beta.12",
    "zone.js": "^0.6.25"
  },
  "devDependencies": {
    "@types/node": "^6.0.45",
    "angular2-template-loader": "^0.4.0",
    "awesome-typescript-loader": "^2.2.4",
    "css-loader": "^0.23.1",
    "extract-text-webpack-plugin": "^1.0.1",
    "file-loader": "^0.8.5",
    "html-loader": "^0.4.3",
    "html-webpack-plugin": "^2.15.0",
    "null-loader": "^0.1.1",
    "raw-loader": "^0.5.1",
    "rimraf": "^2.5.2",
    "style-loader": "^0.13.1",
    "typescript": "^2.0.3",
    "webpack": "^1.13.0",
    "webpack-dev-server": "^1.14.1",
    "webpack-merge": "^0.14.0"
  }
}

Обратите внимание на строчку "start": "webpack-dev-server --inline --progress --port 8080", тут указан порт, на котором будет открываться проект. Можно изменить это значение, если по каким-то причинам, вас не устраивает 8080.

Файл описывает команды для запуска и билда проекта (секция scripts), а также зависимости — пакеты, которые необходимо установить для разработки и запуска проекта. В секции dependencies описаны пакеты, необходимые для работы проекта, в секции devDependencies — пакеты, которые необходимы только на этапе разработки.

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

npm install

Конфигурафия для Typescript

В корне проекта создаем файл tsconfig.json:

{
  "compilerOptions": {
    "target": "es5",
    "module": "commonjs",
    "moduleResolution": "node",
    "sourceMap": true,
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "removeComments": false,
    "noImplicitAny": true,
    "suppressImplicitAnyIndexErrors": true
  }
}

Конфигурация для Webpack

Рассмотрим конфигурацию только для разработки. Создаем папку config, где будем хранить файлы для конфигурации.

config/helpers.js:

var path = require('path');
var _root = path.resolve(__dirname, '..');
function root(args) {
    args = Array.prototype.slice.call(arguments, 0);
    return path.join.apply(path, [_root].concat(args));
}
exports.root = root;

config/common.js:

var webpack = require('webpack');
var HtmlWebpackPlugin = require('html-webpack-plugin');
var ExtractTextPlugin = require('extract-text-webpack-plugin');
var helpers = require('./helpers');

module.exports = {
    entry: {
        'polyfills': './src/polyfills.ts',
        'vendor': './src/vendor.ts',
        'app': './src/main.ts'
    },

    resolve: {
        extensions: ['', '.ts', '.js']
    },

    module: {
        loaders: [
            {
                test: /\.ts$/,
                loaders: ['awesome-typescript-loader', 'angular2-template-loader']
            },
            {
                test: /\.html$/,
                loader: 'html'
            },
            {
                test: /\.(png|jpe?g|gif|svg|woff|woff2|ttf|eot|ico)$/,
                loader: 'file?name=assets/[name].[hash].[ext]'
            },
            {
                test: /\.css$/,
                exclude: helpers.root('src', 'app'),
                loader: ExtractTextPlugin.extract('style', 'css?sourceMap')
            },
            {
                test: /\.css$/,
                include: helpers.root('src', 'app'),
                loader: 'raw'
            }
        ]
    },

    plugins: [
        new webpack.optimize.CommonsChunkPlugin({
            name: ['app', 'vendor', 'polyfills']
        }),

        new HtmlWebpackPlugin({
            template: 'src/index.html'
        })
    ]
};

В приложении выделяется 3 точки входа (бандла):

  1. polyfills — некий набор скриптов, позволяющий Angular работать в большинстве современных браузеров,
  2. vendor — здесь мы импортируем различные библиотеки, стили и т. д.
  3. app — наше приложение.

Секция resolve отвечает за то, чтобы можно было не указывать расширение при импорте, то есть, вместо:

import { AppComponent } from './app.component.ts';

Мы можем написать:

import { AppComponent } from './app.component';

В эту секцию можно включить и css, и html.

В секции loaders мы «учим» webpack преобразовывать typescript в ES5 код, а также загружать html, изображения и стили, указанные в компонентах. 

Как можно заметить, для css указано 2 паттерна. Первый — загружает все стили, которые не лежат в папках src/app, второй паттерн загружает стили, указанные в свойстве styleUrls компонента.

Последняя секция plugins позволяет управлять зависимостями импорта библиотек в приложении, vendor и polyfills, а также автоматически подключать скрипты и стили в index.html, таким образом, мы не должны прописывать их руками.

config/webpack.dev.js:

var webpackMerge = require('webpack-merge');
var ExtractTextPlugin = require('extract-text-webpack-plugin');
var commonConfig = require('./webpack.common.js');
var helpers = require('./helpers');

module.exports = webpackMerge(commonConfig, {
    devtool: 'cheap-module-eval-source-map',

    output: {
        path: helpers.root('dist'),
        publicPath: 'http://localhost:8080/',
        filename: '[name].js',
        chunkFilename: '[id].chunk.js'
    },

    plugins: [
        new ExtractTextPlugin('[name].css')
    ],

    devServer: {
        historyApiFallback: true,
        stats: 'minimal'
    }
});

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

Далее создадим простейшее приложение.

Директория src

vendor.ts

import '@angular/platform-browser';
import '@angular/platform-browser-dynamic';
import '@angular/core';
import '@angular/common';
import '@angular/http';
import '@angular/router';
import 'rxjs';

polyfills.ts

import 'core-js/es6';
import 'core-js/es7/reflect';
require('zone.js/dist/zone');
Error['stackTraceLimit'] = Infinity;
require('zone.js/dist/long-stack-trace-zone');

main.ts

import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';
platformBrowserDynamic().bootstrapModule(AppModule);

index.html

<!DOCTYPE html>
<html>
<head>
    <base href="/">
    <title>Angular 2 and Webpack</title>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<my-app>Loading...</my-app>
</body>
</html>

Директория src/app

app.module.ts

import { NgModule } from '@angular/core';
import { BrowserModule }  from '@angular/platform-browser';
import { AppComponent } from './app.component';
@NgModule({
    imports: [
        BrowserModule
    ],
    declarations: [
        AppComponent
    ],
    bootstrap: [ AppComponent ]
})
export class AppModule { }

app.component.ts

import { Component } from '@angular/core’;

@Component({
    selector: 'my-app',
    templateUrl: './app.component.html'
})
export class AppComponent { }

app.component.html

<main>
    <h1>Hi, Angular 2!</h1>
</main>

Теперь для запуска приложения, нужно ввести в консоли:

npm start

Если все прошло успешно, в консоли нет ошибок, то по адресу localhost:8080 мы должны увидеть: Hi, angular 2!

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

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

Наиболее часто встречаются https://github.com/AngularClass/angular2-webpack-starter и https://github.com/preboot/angular2-webpack. Я предпочитаю использовать последний.