Фреймворк Angular используется при создании SPA и предлагает большое количество инструментов как для создания, непосредственно, элементов интерфейса, так и CLI для создания и управления структурой файлов, относящихся к приложению.
Для создания проекта с использованием библиотеки Angular, официальный сайт предлагает нам установить пакет angular-cli и далее из консоли запустить определенные команды, которые скачают нужные пакеты, создадут нужные файлы и останется только запустить приложение, однако что если мы не хотим использовать коробочное решение, мы хотим сами создать структуру папок, заполнить ее файлами, подключить нужные библиотеки и собрать, в общем полностью контролировать процесс создания приложения.
Я задался таким вопросом, и, после изучения этого вопроса я собрал это в туториал.
При написании статьи я использовал следующие технологии:
-
Webpack v5
-
Angular v13
-
NodeJS v14
-
NPM v8
Что нужно знать, чтобы понять этот туториал:
-
javascript, typescript
-
webpack, webpack-cli
-
html, css
Особенности:
-
Приложение разрабатывается для браузера
-
Для того, чтобы не потеряться, какую настройку куда добавлять, в некоторых файлах с кодом путь к целевому файлу будет подписан
Итак, приступим.
-
Начнем с того, что создадим каталог с нашим приложением
mkdir angular-no-cli
-
Добавим package.json и typescript
cd ./angular-no-cli
npm init
npm i -D typescript
npx tsc --init
-
Создадим 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
-
Добавим необходимые библиотеки
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 файлов |
-
Добавим базовую конфигурацию для 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/
},
]
}
}
-
Добавим базовую конфигурацию для tsconfig.json
{
"compilerOptions": {
"target": "es2016",
"lib": ["es2020", "dom"],
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"module": "ES2020",
"moduleResolution": "node",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true
}
}
-
Добавим код в файлы приложения
// 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 {}
-
Попорбуем собрать
npx webpack
Видим файл сборки по пути dist/app.js
-
Теперь настроим работу dev-server, для этого в конфигурацию webpack добавим следующее
//webpack.config.js
devServer: {
static: {
directory: path.resolve(__dirname, "dist")
},
port: 4200,
hot: true,
open: false
}
-
Отлично,проверим работу dev-сервера, запустим его
npx webpack serve
Вместо нашей надписи, мы увидим только ссылку на просмотр файла нашей сборки, кажется мы забыли про index.html, нужно его добавить
-
За основу возьмем наш 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")
})
]
-
Снова запустим сборку, на этот раз в dist добавится index.html, который будет загружать app.js.
-
Давайте снова запустим dev-server и посмотрим на результат.
-
Мы видим белый фон страницы, произошла ошибка, давайте откроем консоль и посмотрим, что там
GET http://localhost:4200/app.component.html 404 (Not Found)
-
Эта ошибка объясняется тем, что в app.component.ts мы указали параметр templateUrl: "./app.component.html". Соответственно, @angular/core пытается загрузить этот шаблон через обычный HTTP запрос и не находит такого файла.
Тут может возникнуть вопрос, а ведь с использованием CLI мы вообще не видим никаких html файлов в выходной директории, после ng build. Так и есть, это одна из особенностей angular, мы подробнее разберем этот вопрос ниже.
-
Давайте просто скопируем файл шаблона в 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.
-
Опять такая же ошибка, только теперь для app.component.css файла, мы css файлы пока никак не обрабатываем, давайте просто закомментируем.
// src/app/app.component.ts
//styleUrls: ["app.component.css"]
-
Теперь попробуем добиться схожей структуры файлов в сборочной директории, которую мы обычно видим в проектах, созданных с помощью Angular CLI, список файлов там следующий
-
3rdpartylicenses.txt - лицензии сторонних библиотек
-
favicon.ico - иконка
-
index.html - основной html файл
-
main.js - код всех необходимых библиотек для запуска и исполнения кода, включая и наш код
-
polyfills.js - полифилы
-
runtime.js - функции загрузки модулей
-
Для начала выделим runtime.js, для этого добавим новую настройку в наш webpack.config
//webpack.config.js
optimization: {
runtimeChunk: 'single'
}
-
Основной скрипт app.js давайте разделим на основную и venod части
//webpack.config.js
optimization: {
runtimeChunk: 'single',
splitChunks: {
chunks: "all",
maxAsyncRequests: Infinity,
minSize: 0,
name: "vendor"
}
}
-
Итак, в сборочной директории мы видим несколько javascript файлов и index.html где они все подключаются, на этом моменте можем еще раз собрать и запустить, чтобы убедиться, что все работает
-
Теперь давайте избавимся от копирования шаблонов, сделаем так, чтобы они добавились в javascript код, для этого давайте немного разберем webpack конфигурацию, которая создается, когда мы собираем проект с помощью Angular CLI. Нас интересует, какие загрузчики используются для обработки кода, а также как происходит его оптимизация, по ходу разберем небольшие особенности работы самого Angular.
-
Для того, чтобы посмотреть последовательность выполнения скриптов, можно просто запустить команду 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
-
Начинается с того, что проверяются версии зависимых пакетов, создаются логгеры
-
Потом читается и проверяется файл конфигурации angular.json
-
Дальше запускается команда с красноречивым именем validateAndRun
const command = new description.impl(context, description, logger); const result = await command.validateAndRun(parsedOptions);
-
Следующий шаг - запуск задачи на сборку, тут @angular/cli делегирует свою работу другому пакету @angular-devkit, который и начинает строить webpack.config
buildWebpackBrowser(options, context); //options - это объект с настройками angular.json //context - объект с утилитными функциями angular
-
Объект конфигурации создается в несколько этапов
-
Сначала запрашивается конфигурация для tsconfig
-
Потом составляется список браузеров, в которых наш код может выполняться
-
Потом выполняются проверки на корректность версий, корректность значений настроек и многое другое, каждая проверка в случае ошибки подробно опишет пользователю, что пошло не так.
-
Вот так выглядит вызов метода, который вернет конфигурацию
//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 });
-
Там нас интересуют загрузчики, плагины и оптимизация кода, давайте постепенно добавим их в нашу конфигурацию
-
-
Найдем в 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. Это то что нам нужно, но перед тем, как добавлять их в нашу конфигурацию, давайте узнаем о них побольше
-
Попробуем найти репозитории наших загрузчиков на гитхабе
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.
-
Давайте установим нужные пакеты, которые советуют в 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
-
Итак, после установки давайте изменим поля 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
})
]
-
Копирование html шаблонов можем закоммментировать, сам плагин нам еще понадобиться, а вот шаблоны нет,
/* new CopyPlugin({
patterns: [
{
from: "**!/!*.html",
to: path.resolve(__dirname, "dist", "[name].html"),
context: "src/app/"
}
]
}),*/
-
На этом моменте можем запустить dev-server, чтобы убедиться, что все собирается, как надо
-
Давайте теперь добавим стили, css файлы у нас есть, добавим в них правила
/*файл app.component.css*/
main {
color: red;
}
/* файл index.css */
html {
background: lightcyan;
}
-
Отлично, теперь поставим нужные лоадеры и плагины для работы со стилями
npm i -D css-loader mini-css-extract-plugin postcss-loader
-
Давайте раскомментируем ссылку на наши стили в app.component.ts
//файл app.component.ts
styleUrls: ["app.component.css"]
-
Еще немного изменим конфигурацию 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',
}),
]
-
Снова запустим dev-server, увидим, что наша надпись стала красного цвета, а фон - голубого, продолжим
-
Давайте добавим любую картинку, допишем код в нашем шаблоне
<!-- app.component.html-->
<!-- У меня это waiter.svg, положил я его в src/assets/waiter.svg -->
<img src="/assets/waiter.svg" alt="waiter">
-
Раскомментируем CopyPlugin и изменим его конфигурацию, чтобы он добавил наши assets в dist
new CopyPlugin({
patterns: [
{
context: "src/assets/",
from: "**/*",
to: "assets/",
}
]
})
-
Снова проверим dev-server, теперь видим и картинку, все работет
-
Теперь давайте разберем часть webpack конфигурации, связанной с оптимизацией кода и начнем с того, что просто взглянем на вес production сборок vendor части, ng-cli и нашей
Сборка ng cli - 100 Кбайт (main.js + polifills.js)
Наша сборка - 357 Кбайт (app.js + vendor.js)
-
Заметная разница, но раз мы используем одинаковые лоадеры, дело тут будет в минификации кода, давайте посмотрим, что 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
-
На этом моменте вес сборки должен уменьшиться, у меня он сократился до 150 Кбайт
-
Теперь раздробим наш 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('@', '')}`;
}
},
}
}
}
-
Теперь в сборочной директории можно увидеть все скрипты, которые мы используем для запуска нашего приложения, запустим dev-server и посмотрим, что все успешно работает
-
Добавим переменную окружения, чтобы разграничить 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"
},
-
Добавим скрипты запуска в package.json
"start": "webpack serve --env development ",
"build": "webpack --progress --env production",
"build:dev": "webpack --progress"
-
Отлично, мы сделали все, что нужно, теперь можем разрабатывать наше приложение
Выводы:
-
Создание такой сборки своими руками - долго, однако при этом вы полностью контролируете процесс и в дальнейшем можно будет просто копировать конфигурацию
-
Контроль над процессом дает понимание того, зачем мы устанавливаем тот или иной пакет
-
Мы потеряли возможность использовать ng update, чтобы обновлять версию angular
Что еще можно сделать с таким приложением:
Можно пойти в ширину и создать таким образом не только SPA, но и отдельную библиотеку или модуль, которые можно будет потом импортировать в приложение. Грубо говоря, создать что-то вроде личного кабинета с lazy-loading и использованием сторонних API, можно также использовать webpack.externals и другие возможности webpack
Что не вошло в данный туториал:
Во время изучения Angular я углубился в сам процесс его работы, узнал как работает Ivy компилятор, что такое AOT режим и на что конкретно он влияет, как обрабатываются шаблоны, что такое ngcc, ngtsc, для чего нужны те или иные библиотеки. Объем информации получился довольно большой, поэтому эту часть я не стал включать в эту статью, но мог бы включить в следующую, если эта будет полезна.
Также у меня есть планы на статью, в которой я создам более полноценное приложение на этой базе.
Спасибо за внимание.
Источники:
Deep Dive into the Angular Compiler
Автор: Ivan Kolesov