Миграция на Angular 17 + Nx + SSR + Localization

Aleksandr Serenko
F.A.F.N.U.R
Published in
4 min readNov 18, 2023

Недавно вышел релиз Angular 17, который принес следующие изменения:

  • Добавлен билдер esbuild;
  • Universal включен в Angular;
  • Повышен typescript до 5.2.2.

В данной статье хочу рассказать о том, как перейти на новую версию и привести в порядок конфиги.

У Angular есть проект, помогающий мигрировать приложение — https://update.angular.io.

Отмечу, что Nx немного изменился, начиная с 16.8. Если вы не переходили на 17-ю версию, то советую прочитать мою предыдущую статью — Миграция на NX 17, которая позволит вам актуализировать workspace.

Результат можно глянуть на примере моего сайта — fafn.ru.

Миграция до последней версии

Запускаем команду обновления:

yarn nx migration latest

Затем устанавливаем пакеты:

yarn

После выполняем миграции:

yarn nx migrate --run-migrations

Обновление executors

Сначала переключим все executors на более быстрый вариант с esbuild.

В project.json:

{
...
"targets": {
"build": {
"executor": "@angular-devkit/build-angular:application",
...
},
"serve": {
"executor": "@angular-devkit/build-angular:dev-server",
...
},
...
}
}

Также в поменяем свойство main на browser в build.options:

{
...
"targets": {
"build": {
"options": {
"browser": "apps/web/src/main.ts",
...
}
}
}
}

Подключение SCSS и монорепы

С переходом на esbuild изменяется поиск scss файлов в монорепозитории Nx. Теперь нужно явно указывать используемые источники в stylePreprocessorOptions.

Добавим поддержку поиска SCSS в монорепе и node_modules:

{
..,
"targets": {
"build": {
"options": {
"stylePreprocessorOptions": {
"includePaths": ["node_modules", "./"]
},
}
}
}
}

Настройка SSR

В build появились новые свойства, которые теперь заменяют targets из universal:

{
..,
"targets": {
"build": {
"options": {
"localize": ["ru"],
"server": "apps/web/src/main.server.ts",
"prerender": true,
"ssr": {
"entry": "apps/web/server.ts"
}
}
}
}
}

Удалим соответствующие таргеты: server, serve-ssr и prerender.

Файл tsconfig.server.json больше не нужен и также уберем его из проекта.

Для работы SSR сервера необходимо включить новые функции в tsconfig.json:

{
"compilerOptions": {
"target": "es2022",
"esModuleInterop": true,
...
},
}

Добавим в сборку все файлы в tsconfig.app.json:

{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../dist/out-tsc",
"types": ["node", "@angular/localize"]
},
"files": ["src/main.ts", "src/main.server.ts", "server.ts"],
"include": ["src/**/*.d.ts"],
"exclude": ["jest.config.ts", "src/**/*.test.ts", "src/**/*.spec.ts"]
}

Последним шагом, приведем в порядок server.ts.

В целом, просто возьмем и заменим его:

import { APP_BASE_HREF } from '@angular/common';
import { CommonEngine } from '@angular/ssr';
import express from 'express';
import { fileURLToPath } from 'node:url';
import { dirname, join, resolve } from 'node:path';
import bootstrap from './src/main.server';

// The Express app is exported so that it can be used by serverless Functions.
export function app(): express.Express {
const server = express();
const serverDistFolder = dirname(fileURLToPath(import.meta.url));
const browserDistFolder = resolve(serverDistFolder, '../browser');
const indexHtml = join(serverDistFolder, 'index.server.html');

const commonEngine = new CommonEngine();

server.set('view engine', 'html');
server.set('views', browserDistFolder);

// Example Express Rest API endpoints
// server.get('/api/**', (req, res) => { });
// Serve static files from /browser
server.get(
'*.*',
express.static(browserDistFolder, {
maxAge: '1y',
})
);

// All regular routes use the Angular engine
server.get('*', (req, res, next) => {
const { protocol, originalUrl, baseUrl, headers } = req;

commonEngine
.render({
bootstrap,
documentFilePath: indexHtml,
url: `${protocol}://${headers.host}${originalUrl}`,
publicPath: browserDistFolder,
providers: [{ provide: APP_BASE_HREF, useValue: baseUrl }],
})
.then((html) => res.send(html))
.catch((err) => next(err));
});

return server;
}

function run(): void {
const port = process.env['PORT'] || 4000;

// Start up the Node server
const server = app();
server.listen(port, () => {
console.log(`Node Express server listening on http://localhost:${port}`);
});
}

run();

Это базовая версия, которая идет из коробки.

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


const serverDistFolder = dirname(fileURLToPath(import.meta.url));
const locale = serverDistFolder.split('/').at(-1);
const browserDistFolder = resolve(serverDistFolder, '../../browser', locale);

Итоговый сервер:

import { APP_BASE_HREF } from '@angular/common';
import { CommonEngine } from '@angular/ssr';
import express from 'express';
import { dirname, join, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';

import bootstrap from './src/main.server';

// The Express app is exported so that it can be used by serverless Functions.
export function app(): express.Express {
const server = express();

const serverDistFolder = dirname(fileURLToPath(import.meta.url));
const locale = serverDistFolder.split('/').at(-1) ?? '';

const browserDistFolder = resolve(serverDistFolder, '../../browser', locale);
const indexHtml = join(serverDistFolder, 'index.server.html');

const commonEngine = new CommonEngine();

server.set('view engine', 'html');
server.set('views', browserDistFolder);

// Example Express Rest API endpoints
// server.get('/api/**', (req, res) => { });
// Serve static files from /browser
server.get(
'*.*',
express.static(browserDistFolder, {
maxAge: '1y',
}),
);

// All regular routes use the Angular engine
server.get('*', (req, res, next) => {
const { protocol, originalUrl, baseUrl, headers } = req;

commonEngine
.render({
bootstrap,
documentFilePath: indexHtml,
url: `${protocol}://${headers.host}${originalUrl}`,
publicPath: browserDistFolder,
providers: [
{
provide: APP_BASE_HREF,
useValue: baseUrl,
},
],
})
.then((html) => res.send(html))
.catch((err) => next(err));
});

return server;
}

function run(): void {
const port = process.env['PORT'] || 4000;

// Start up the Node server
const server = app();
server.listen(port, () => {
console.log(`Node Express server listening on http://localhost:${port}`);
});
}

run();

Запускаем проект:

yarn nx serve

Сборка

yarn nx build

Ссылки

Демонстрация всего описанного выше можно посмотреть в живую— fafn.ru.

Локализованная версия — en.fafn.ru.

Все исходники находятся на github, в репозитории:

Подписывайтесь на блог, чтобы не пропустить новые статьи про Angular, и веб-разработку. Medium | Telegram| VK |Tw| Ln

--

--

Aleksandr Serenko
F.A.F.N.U.R

Senior Front-end Developer, Angular evangelist, Nx apologist, NodeJS warlock