Angular без CLI Туториал

в 11:55, , рубрики: angular, javascript, TypeScript

Фреймворк Angular используется при создании SPA и предлагает большое количество инструментов как для создания, непосредственно, элементов интерфейса, так и CLI для создания и управления структурой файлов, относящихся к приложению.

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

Я задался таким вопросом, и, после изучения этого вопроса я собрал это в туториал.

При написании статьи я использовал следующие технологии:

  • Webpack v5

  • Angular v13

  • NodeJS v14

  • NPM v8

Что нужно знать, чтобы понять этот туториал:

  • javascript, typescript

  • webpack, webpack-cli

  • html, css

Особенности:

  • Приложение разрабатывается для браузера

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

Итак, приступим.

  1. Начнем с того, что создадим каталог с нашим приложением

mkdir angular-no-cli
  1. Добавим package.json и typescript

cd ./angular-no-cli
npm init
npm i -D typescript
npx tsc --init
  1. Создадим angular-подобную структуру каталогов и добавим основые файлы приложения

mkdir src/app
mkdir src/assets
touch webpack.config.js
touch src/index.css
touch src/index.html
touch src/main.ts
touch src/app/app.component.css
touch src/app/app.component.html
touch src/app/app.component.ts
touch src/app/app.module.ts
  1. Добавим необходимые библиотеки

npm i -D webpack webpack-cli webpack-dev-server
npm i @angular/platform-browser @angular/platform-browser-dynamic @angular/common @angular/core rxjs zone.jse.js
npm i -D ts-loader

Для чего нужны эти библиотеки

webpack

основной сборщик

webpack-cli

CLI команды для webpack

webpack-dev-server

development сервер для пошаговой разработки

@angular/platform-browser

библиотека для запуска Angular приложений в браузере

@angular/platform-browser-dynamic

библиотека для запуска Angular приложений в браузере с поддержкой JIT компиляции

@angular/common

библиотека с основными элементами для работы приложения: http-клиент, роутинг, локализация, компоненты, пайпы, директивы и.т.д.

@angular/core

библиотека функций, осуществляющая основную функциональность работы приложения: рендеринг, перехват событий, DI и.т.д.

rxjs

библиотека, реализующая Subscriber-Observer поведение, активно используется пакетами angular

zone.js

библиотека, создающая контекст выполнения функций, который сохраняется в асинхронных задачах

ts-loader

библиотека для сборки .ts файлов

  1. Добавим базовую конфигурацию для webpack

//webpack.config.js
const path = require("path");
module.exports = {
    mode: "development",
    devtool: false,
    context: path.resolve(__dirname),
    entry: {
        app: path.resolve(__dirname, "src/main.ts"),
    },
    stats: 'normal',
    output: {
        clean: true,
        path: path.resolve(__dirname, "dist"),
        filename: "[name].js"
    },
    resolve: {
        extensions: [".ts", ".js"]
    },
    // пока будем собирать только ts файлы
    module: {
        rules: [
            {
                test: /.(js|ts)$/,
                loader: "ts-loader",
                exclude: /node_modules/
            },
        ]
    }
}
  1. Добавим базовую конфигурацию для tsconfig.json

{
    "compilerOptions": {
        "target": "es2016",
        "lib": ["es2020", "dom"],
        "experimentalDecorators": true,
        "emitDecoratorMetadata": true,
        "module": "ES2020",
        "moduleResolution": "node",
        "esModuleInterop": true,
        "forceConsistentCasingInFileNames": true,
        "strict": true,
        "skipLibCheck": true
    }
}
  1. Добавим код в файлы приложения

// src/main.ts
import "zone.js/dist/zone";
import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';
import {AppModule} from './app/app.module';

platformBrowserDynamic()
    .bootstrapModule(AppModule)
    .catch(err => console.error(err));
<!--src/index.html-->
<html lang="ru">
<head>
    <base href="/">
    <title>Angular no cli</title>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<app-root></app-root>
</body>
</html>
<!--src/app/app.component.html-->
<main>Angular no CLI</main>
// src/app/app.component.ts
import {Component} from "@angular/core";

@Component({
    selector: "app-root",
    templateUrl: "./app.component.html",
    styleUrls: ["app.component.css"]
})
export class AppComponent {
}
// src/main.ts
import {NgModule} from "@angular/core";
import {AppComponent} from "./app.component";
import {BrowserModule} from "@angular/platform-browser";

@NgModule({
    declarations: [AppComponent],
    imports: [BrowserModule],
    providers: [],
    bootstrap: [AppComponent]
})
export class AppModule {}
  1. Попорбуем собрать

npx webpack

 Видим файл сборки по пути dist/app.js

  1. Теперь настроим работу dev-server, для этого в конфигурацию webpack добавим следующее

//webpack.config.js
devServer: {
    static: {
        directory: path.resolve(__dirname, "dist")
    },
    port: 4200,
    hot: true,
    open: false
}
  1.  Отлично,проверим работу dev-сервера, запустим его

npx webpack serve

Вместо нашей надписи, мы увидим только ссылку на просмотр файла нашей сборки, кажется мы забыли про index.html, нужно его добавить

  1. За основу возьмем наш src/index.html, чтобы он попал в директорию dist, воспользуемся html-webpack-plugin, установим его и добавим в конфигурацию webpack

npm i -D html-webpack-plugin
//webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin');

plugins: [
    new HtmlWebpackPlugin({
        filename: path.resolve(__dirname, "dist", "index.html"),
        template: path.resolve(__dirname, "src/index.html")
    })
]
  1. Снова запустим сборку, на этот раз в dist добавится index.html, который будет загружать app.js.

  2. Давайте снова запустим dev-server и посмотрим на результат.

  3. Мы видим белый фон страницы, произошла ошибка, давайте откроем консоль и посмотрим, что там

 GET http://localhost:4200/app.component.html 404 (Not Found)

  1. Эта ошибка объясняется тем, что в app.component.ts мы указали параметр templateUrl: "./app.component.html". Соответственно, @angular/core пытается загрузить этот шаблон через обычный HTTP запрос и не находит такого файла.

Тут может возникнуть вопрос, а ведь с использованием CLI мы вообще не видим никаких html файлов в выходной директории, после ng build. Так и есть, это одна из особенностей angular, мы подробнее разберем этот вопрос ниже.

  1. Давайте просто скопируем файл шаблона в dist. Мы можем скопировать файл руками, но лучше отдать эту возможность сборщику. Для этого нам понадобиться еще один плагин.

npm i -D copy-webpack-plugin
//webpack.config.js
const CopyPlugin = require("copy-webpack-plugin");

new CopyPlugin({
    patterns: [
        {
            from: "**/*.html",
            to: path.resolve(__dirname, "dist", "[name].html"),
            context: "src/app/"
        }
    ]
})

Здесь мы попросим плагин скопировать все html файлы в репозитории src/app и поместить c текущим именем в dist.

  1. Опять такая же ошибка, только теперь для app.component.css файла, мы css файлы пока никак не обрабатываем, давайте просто закомментируем.

// src/app/app.component.ts
//styleUrls: ["app.component.css"]
  1. Теперь попробуем добиться схожей структуры файлов в сборочной директории, которую мы обычно видим в проектах, созданных с помощью Angular CLI, список файлов там следующий

  • 3rdpartylicenses.txt - лицензии сторонних библиотек

  • favicon.ico - иконка

  • index.html - основной html файл

  • main.js - код всех необходимых библиотек для запуска и исполнения кода, включая и наш код

  • polyfills.js - полифилы

  • runtime.js - функции загрузки модулей

  1. Для начала выделим runtime.js, для этого добавим новую настройку в наш webpack.config

//webpack.config.js
optimization: {
    runtimeChunk: 'single'
}
  1. Основной скрипт app.js давайте разделим на основную и venod части

//webpack.config.js
optimization: {
    runtimeChunk: 'single',
    splitChunks: {
        chunks: "all",
        maxAsyncRequests: Infinity,
        minSize: 0,
        name: "vendor"
    }
}
  1. Итак, в сборочной директории мы видим несколько javascript файлов и index.html где они все подключаются, на этом моменте можем еще раз собрать и запустить, чтобы убедиться, что все работает

  2. Теперь давайте избавимся от копирования шаблонов, сделаем так, чтобы они добавились в javascript код, для этого давайте немного разберем webpack конфигурацию, которая создается, когда мы собираем проект с помощью Angular CLI. Нас интересует, какие загрузчики используются для обработки кода, а также как происходит его оптимизация, по ходу разберем небольшие особенности работы самого Angular.

    1. Для того, чтобы посмотреть последовательность выполнения скриптов, можно просто запустить команду ng build в режиме дебага из пакета angular-cli. В рамках даного туториала делать этого не нужно, здесь я вкратце опишу как все работет.

      npm install -g @angular/cli
      ng new my-first-project
      cd my-first-project
      node --inspect-brk .node_modules@angularclibinng build
    2. Начинается с того, что проверяются версии зависимых пакетов, создаются логгеры

    3. Потом читается и проверяется файл конфигурации angular.json

    4. Дальше запускается команда с красноречивым именем validateAndRun

    const command = new description.impl(context, description, logger);
    const result = await command.validateAndRun(parsedOptions); 
    1. Следующий шаг - запуск задачи на сборку, тут @angular/cli делегирует свою работу другому пакету @angular-devkit, который и начинает строить webpack.config

       buildWebpackBrowser(options, context);
      //options - это объект с настройками angular.json
      //context - объект с утилитными функциями angular
    2.  Объект конфигурации создается в несколько этапов

    • Сначала запрашивается конфигурация для tsconfig

    • Потом составляется список браузеров, в которых наш код может выполняться

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

    1. Вот так выглядит вызов метода, который вернет конфигурацию

      //config - объект конфигурации webpack
      const { config, projectRoot, projectSourceRoot, i18n } =
          await webpack_browser_config_1.generateI18nBrowserWebpackConfigFromContext(adjustedOptions, context, (wco) => [
              configs_1.getCommonConfig(wco),
              configs_1.getBrowserConfig(wco),
              configs_1.getStylesConfig(wco),
              configs_1.getTypeScriptConfig(wco),
              wco.buildOptions.webWorkerTsConfig ? configs_1.getWorkerConfig(wco) : {},
          ], { differentialLoadingNeeded });
    2. Там нас интересуют загрузчики, плагины и оптимизация кода, давайте постепенно добавим их в нашу конфигурацию

  3. Найдем в module.rules правила для загрузки html, javascript или typescript файлов

module: {
	rules: [
  	{//*1
  	  	test: /.?(svg|html)$/,
  	  	resourceQuery: /?ngResource/,
  	  	type: "asset/source"
    },
    {//*2
        test: "/.[cm]?[tj]sx?$/",
        resolve: {
            fullySpecified": false
        },
        exclude: ["/[/\](?:core-js|@babel|tslib|web-animations-js|web-streams-polyfill)[/\]/"],
        use: [{
            loader: ".../@angular-devkit/build-angular/src/babel/webpack-loader.js",
            options: {
                cacheDirectory: ".../angular/cache/babel-webpack",
                scriptTarget: 4,
                aot: true,
                optimize: true
            }
        }]
    },
    {//*3
        test: "/.[cm]?tsx?$/",
        loader: "../@ngtools/webpack/src/ivy/index.js",
        exclude: ["/[/\](?:css-loader|mini-css-extract-plugin|webpack-dev-server|webpack)[/\]/"]
    }
  ]
}

Нашли несколько загрузчиков:

1 - обработает файлы, попавшие под выражение ".html?ngResource". В качестве загрузчика выступает raw-loader

2 и 3 - обработает javascript и typescript файлы. В качестве загрузчика выступает @angular-devkit/build-angular и @ngtools/webpack. Это то что нам нужно, но перед тем, как добавлять их в нашу конфигурацию, давайте узнаем о них побольше

  1. Попробуем найти репозитории наших загрузчиков на гитхабе

npm repo @ngtools/webpack
npm repo @angular-devkit/build-angular

Оба ведут в корневую репу angular-cli, там их можно найти в поддиректории packages.

build-angular - содержит в себе файлы с лоадером и плагинами для webpack, в комментариях в коде можно найти такое описание: "This package contains Architect builders used to build and test Angular applications and libraries."

ngtools/webpack - тажке видим загрузчик и плагины, но важнее то, что есть файл README, который говорит нам, что это загрузчик, который можно использовать, если мы хотим собрать проект на базе Angular-фреймворка, как раз наш случай. В описании также сказано, что нужно будет подключить babel-loader с Linker Ivy плагином и AngularWebpackPlugin.

  1. Давайте установим нужные пакеты, которые советуют в README

# При установке может возникнуть ошибка с peerDependency, который хочет 
# определенную версию typescript, можем проигнорировать это и 
# добавить флаг --legacy-peer-deps
npm i -D @ngtools/webpack babel-loader @angular/compiler-cli @angular/compiler @angular-devkit/build-angular

# можно сразу удалить, т.к. мы будем использовать другой загрузчик
npm rm ts-loader 
  1. Итак, после установки давайте изменим поля module.rules и plugins в конфигурации webpack

const AngularWebpackPlugin = require('@ngtools/webpack').AngularWebpackPlugin;

module: {
    rules: [
        {
            test: /.?(svg|html)$/,
            resourceQuery: /?ngResource/,
            type: "asset/source"
        },
        {
            test: /.[cm]?[tj]sx?$/,
            exclude: //node_modules//,
            use: [
                {
                    loader: 'babel-loader',
                    options: {
                        cacheDirectory: true,
                        compact: true,
                        plugins: ["@angular/compiler-cli/linker/babel"],
                    },
                },
                {
                    loader: "@angular-devkit/build-angular/src/babel/webpack-loader",
                    options: {
                        aot: true,
                        optimize: true,
                        scriptTarget: 7
                    }
                },
                {
                    loader: '@ngtools/webpack',
                },
            ],
        },
    }],
    plugins: [
        new AngularWebpackPlugin({
            tsconfig: path.resolve(__dirname, "tsconfig.json"),
            jitMode: false,
            directTemplateLoading: true
        })
    ]
  1. Копирование html шаблонов можем закоммментировать, сам плагин нам еще понадобиться, а вот шаблоны нет,

/* new CopyPlugin({
    patterns: [
        {
            from: "**!/!*.html",
            to: path.resolve(__dirname, "dist", "[name].html"),
            context: "src/app/"
        }
    ]
}),*/
  1. На этом моменте можем запустить dev-server, чтобы убедиться, что все собирается, как надо

  2.  Давайте теперь добавим стили, css файлы у нас есть, добавим в них правила

/*файл app.component.css*/
main {
    color: red;
}

/* файл index.css */
html {
    background: lightcyan;
}
  1. Отлично, теперь поставим нужные лоадеры и плагины для работы со стилями

npm i -D css-loader mini-css-extract-plugin postcss-loader
  1. Давайте раскомментируем ссылку на наши стили в app.component.ts

//файл app.component.ts
styleUrls: ["app.component.css"]
  1.  Еще немного изменим конфигурацию webback, добавим mini-css-extract-plugin, чтобы экспортировать наши стили в отдельный файл и изменим entry, тчтобы подключить сборку стилей

const MiniCssExtractPlugin = require('mini-css-extract-plugin');

entry: {
    index: ["./src/main.ts", "./src/index.css"]
},
module: [
    rules: {
        test: /.(css)$/,
        exclude: //node_modules//,
        oneOf: [
            {
                resourceQuery: {
                    not: [/?ngResource/]
                },
                use: [MiniCssExtractPlugin.loader, "css-loader", "postcss-loader"]
            },
            {
                type: "asset/source",
                loader: "postcss-loader"
            }
        ]
    }
],
plugins: [
    new MiniCssExtractPlugin({
        filename: '[name].css',
    }),
]
  1.  Снова запустим dev-server, увидим, что наша надпись стала красного цвета, а фон - голубого, продолжим

  2.   Давайте добавим любую картинку, допишем код в нашем шаблоне

<!-- app.component.html-->
<!-- У меня это waiter.svg, положил я его в src/assets/waiter.svg -->
<img src="/assets/waiter.svg" alt="waiter">
  1.  Раскомментируем CopyPlugin и изменим его конфигурацию, чтобы он добавил наши assets в dist

new CopyPlugin({
    patterns: [
        {
            context: "src/assets/",
            from: "**/*",
            to: "assets/",
        }
    ]
})
  1. Снова проверим dev-server, теперь видим и картинку, все работет

  2. Теперь давайте разберем часть webpack конфигурации, связанной с оптимизацией кода и начнем с того, что просто взглянем на вес production сборок vendor части, ng-cli и нашей

Сборка ng cli - 100 Кбайт (main.js + polifills.js)

Наша сборка - 357 Кбайт (app.js + vendor.js)

  1. Заметная разница, но раз мы используем одинаковые лоадеры, дело тут будет в минификации кода, давайте посмотрим, что Angular CLI использует в качестве оптимизации и скопируем это себе

optimization: {
    minimize: true,
    minimizer: [
        new JavaScriptOptimizerPlugin({
            advanced: true,
            define: {ngDevMode: false, ngI18nClosureMode: false, ngJitMode: false},
            keepNames: false,
            removeLicenses: true,
            sourcemap: false,
            target: 7
        }),
        new TransferSizePlugin(),
        new CssOptimizerPlugin({
            esbuild: {
                alwaysUseWasm: false,
                initialized: false
            }
        })
    ]

JavaScriptOptimizerPlugin - переопределяют работу стандартного terser-plugin

TransferSizePlugin - записывает вес ассета

CssOptimizerPlugin - убирает пробелы из css

  1. На этом моменте вес сборки должен уменьшиться, у меня он сократился до 150 Кбайт

  2. Теперь раздробим наш vendor на отдельные куски с кодом используемых библиотек, добавим следующее в webpack config

optimization: {
    minimize: true,
    runtimeChunk: 'single',
    splitChunks: {
        chunks: "all",
        maxAsyncRequests: Infinity,
        minSize: 0,
        cacheGroups: {
            defaultVendors: {
                test: /[\/]node_modules[\/]/,
                name(module) {
                    const name = module.context.match(/[\/]node_modules[\/](.*?)([\/]|$)/)[1];
                    return `${name.replace('@', '')}`;
                }
            },
        }
    }
}
  1.  Теперь в сборочной директории можно увидеть все скрипты, которые мы используем для запуска нашего приложения, запустим dev-server и посмотрим, что все успешно работает

  2. Добавим переменную окружения, чтобы разграничить prod и dev конфигурации, заменим экспорт объекта конфигурации webpack на экспорт функции и в соответствии с этим изменим некоторые поля конфигурации

module.exports = (env) => {}
mode: env.production ? "production" : "development",
devtool: env.production ? false : "eval",
output: {
    clean: true,
    path: path.resolve(__dirname, "dist"),
    filename: env.production ? "[name].[chunkhash].js" : "[name].js"
},
  1. Добавим скрипты запуска в package.json

"start": "webpack serve --env development ",
"build": "webpack --progress --env production",
"build:dev": "webpack --progress"
  1. Отлично, мы сделали все, что нужно, теперь можем разрабатывать наше приложение

Ссылка на репозиторий с кодом

Выводы:

  • Создание такой сборки своими руками - долго, однако при этом вы полностью контролируете процесс и в дальнейшем можно будет просто копировать конфигурацию

  • Контроль над процессом дает понимание того, зачем мы устанавливаем тот или иной пакет

  • Мы потеряли возможность использовать ng update, чтобы обновлять версию angular

Что еще можно сделать с таким приложением:

Можно пойти в ширину и создать таким образом не только SPA, но и отдельную библиотеку или модуль, которые можно будет потом импортировать в приложение. Грубо говоря, создать что-то вроде личного кабинета с lazy-loading и использованием сторонних API, можно также использовать webpack.externals и другие возможности webpack

Что не вошло в данный туториал:

Во время изучения Angular я углубился в сам процесс его работы, узнал как работает Ivy компилятор, что такое AOT режим и на что конкретно он влияет, как обрабатываются шаблоны, что такое ngcc, ngtsc, для чего нужны те или иные библиотеки. Объем информации получился довольно большой, поэтому эту часть я не стал включать в эту статью, но мог бы включить в следующую, если эта будет полезна.

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

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

Источники:

Документация по webpack

Документация по typescript

Документация по angular

How Angular works

Deep Dive into the Angular Compiler

Исследование Ivy — нового компилятора Angular

Описание AOT

Использование Ivy Linker

Архитектура движка Ivy

Автор: Ivan Kolesov

Источник

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


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