Фреймворк 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\@angular\cli\bin\ng 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
ссылка на оригинал статьи https://habr.com/ru/post/656529/
Добавить комментарий