Обслуживание разных index.html в сборке Angular для разных языков

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

  • Проблема пользовательского опыта: Если кнопка смены языка изменяет только текст, отображаемый на экране, изменение может быть слишком тонким, чтобы его заметить. Это можно исправить, создав иллюзию перезагрузки.
  • Другие ресурсы: иногда изменение языка влечет за собой изменение направления, стилей и шрифтов. Например, с языка LTR на язык RTL. Или с латинского базового алфавита на русский. Изменение шрифтов означает загрузку нового шрифта поверх существующего. Изменение стилей настолько непредсказуемо, что на несколько миллисекунд может появиться экран без стилей.

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

Код находится на StackBlitz

RTL против LTR

При создании локализованных версий для RTL языков, есть еще пара изменений, которые нужно сделать в index.html, и поскольку теперь у нас есть HTML движки, мы можем заменить не только base href.

Замена стилей

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

// angular.json remove injection
{
"projects": {
  "cr": {
    "architect": {
      "build": {
        "options": {
                    // ...
                    // bundle up styles and styles.rtl
          "styles": [
              {
                  "input": "src/assets/css/styles.css",
                  "bundleName": "styles.ltr",
                  "inject": false
              },
              {
                  "input": "src/assets/css/styles.rtl.css",
                  "bundleName": "styles.rtl",
                  "inject": false
              }
          ],
          // ...
      },
      "configurations": {
        "production": {
          "optimization": {
            "scripts": true,
                        // lets also remove auto inlining of fonts and critical css
            "fonts": false,
            "styles": {
              "inlineCritical": false,
              "minify": true
            }
          },
      // ...
}
Вход в полноэкранный режим Выход из полноэкранного режима

Оптимизация конфигурации для стилей, позволяет inlineCritical. Которая будет инлайнить два включенных css, это не то, что нам сейчас нужно, поэтому мы отключим ее.

В приложении, не основанном на URL:

В нашем экспресс-сервере следующая строка в экспрессе должна быть выше, чем статический модуль экспресса:

app.get('/styles.css', (req, res) => {
  // reroute to either styles.ltr or styles.rtl
  if (res.locals.lang === 'ar') {
     res.sendFile(config.rootPath + 'client/styles.rtl.css');
  } else {
     res.sendFile(config.rootPath + 'client/styles.ltr.css');
  }
});
Войти в полноэкранный режим Выйти из полноэкранного режима

В приложении на основе URL:

// route styles under URL specific language
app.get('/:lang/styles.css', (req, res) => {
  if (res.locals.lang === 'ar') {
     res.sendFile(config.rootPath + 'client/styles.rtl.css');
  } else {
     res.sendFile(config.rootPath + 'client/styles.ltr.css');
  }
});
Войти в полноэкранный режим Выйти из полноэкранного режима

В index.html будет сплошная ссылка:

<link rel="stylesheet" href="styles.css">

Что касается шрифтов, у нас есть два варианта. Если мы используем шрифты Google, предложенные фрагментами кода:

<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin="">
<link href="https://fonts.googleapis.com/css2?family=Signika:wght@300..700&amp;display=swap" rel="stylesheet">
Войти в полноэкранный режим Выход из полноэкранного режима

1. Импортировать URL-адрес шрифта непосредственно в основные стили.

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

2. Импорт в отдельную таблицу стилей (fonts.css)

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

Создадим новый файл fonts.[dir].css, добавим в angular.json, а также пропишем экспресс-путь для fonts.css.

// angular.json
"options": {
    // ...
    // bundle up fonts and fonts.rtl
  "styles": [
     // ...
    {
        "input": "src/assets/css/fonts.css",
        "bundleName": "fonts.ltr",
        "inject": false
    },
    {
        "input": "src/assets/css/fonts.rtl.css",
        "bundleName": "fonts.rtl",
        "inject": false
    }
  ],
Вход в полноэкранный режим Выйдите из полноэкранного режима

Добавьте его как есть в index.html:

<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin="">

<!-- Add the fonts as well well -->
<link rel="stylesheet" href="fonts.css">
<link rel="stylesheet" href="styles.css">
Войти в полноэкранный режим Выйти из полноэкранного режима

В fonts.ltr и fonts.rtl, которые вы создадите в процессе разработки и включите в сборку, есть только один оператор импорта:

/* import the right google font for each language */
@import url('https://fonts.googleapis.com/css2?family=Signika:wght@300..700&display=swap');
Вход в полноэкранный режим Выход из полноэкранного режима

Наконец, пути экспресса аналогичны путям стилей:

// in non-url based (routes.js)
app.get('/fonts.css', (req, res) => {
  if (res.locals.lang === 'ar') {
    res.sendFile(config.rootPath + 'client/fonts.rtl.css');
  } else {
    res.sendFile(config.rootPath + 'client/fonts.ltr.css');
  }
});

// in url-based (routes-url.js)
app.get('/:lang/fonts.css', (req, res) => {
  if (res.locals.lang === 'ar') {
    res.sendFile(config.rootPath + 'client/fonts.rtl.css');
  } else {
    res.sendFile(config.rootPath + 'client/fonts.ltr.css');
  }
});
Enter fullscreen mode Выйти из полноэкранного режима

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

Любопытный случай с inlineCriticalCSS в Angular SSR:

При реализации вышеописанного и запуске для ssr, который рендерится через ngExpressEngine, процесс происходит следующим образом:

  • движок читает код index.html.
  • находит ссылку styles.css
  • открывает её
  • и вставляет критические CSS (с помощью плагина Critters).

Это отличный вариант на стороне сервера, но styles.css не существует (и не должен существовать). Поэтому нам приходится обходиться без него. Единственная проблема — это неприятное сообщение 404, которое появляется в логах сервера.

Чтобы исправить это, после погружения немного вглубь скомпилированных файлов, inline critical css на сервере является недокументированной опцией для ngExpressEngine!

res.render('', {
    req,
    res,
    document: rendered,
        // add this line to skip css file processing
    inlineCriticalCss: false
});
Вход в полноэкранный режим Выход из полноэкранного режима

Прежде чем мы примем эту недокументированную (а значит, ненадежную) возможность и откажемся от приятной функции инлайнинга критических css на сервере, давайте перейдем к другому варианту.

Регенерация index.html собственным шаблонизатором

Эффективность связывания таблицы стилей шрифтов из HTML лучше, чем в двух вышеприведенных вариантах. Но для этого нам нужно регенерировать index.html самостоятельно.

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

Есть несколько хороших движков HTML, таких как PUG, handlebars и mustache, но для наших нужд они избыточны. Давайте создадим два шрифта URL. И раз уж мы взялись за это, давайте сделаем то же самое для стилей.

Мы начнем с этого в index.html.

<!-- in production index.html, add all different styles and fonts, and language specific -->
<html lang="$lang">

<!-- #LTR -->
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Signika:wght@300..700&display=swap">
<link rel="stylesheet" href="styles.ltr.css">
<!-- #ENDLTR -->
<!-- #RTL -->
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Tajawal:wght@300;500&display=swap">
<link rel="stylesheet" href="styles.rtl.css">
<!-- #ENDRTL -->

And in the body we can also specify two loading messages, hey, why not?
<app-root>
 <!-- #LTR -->
  loading
  <!-- #ENDLTR -->
  <!-- #RTL -->
  انتظر
  <!-- #ENDRTL -->
</app-root>
Вход в полноэкранный режим Выйти из полноэкранного режима

Помните: вся наша обработка происходит в производственном индексе.

Вернемся к нашим экспресс-движкам HTML, давайте создадим функцию для обработки index.html.

Для простоты, поскольку экспресс-сервер выходит из-под контроля, мы будем требовать файл config вместо того, чтобы передавать его.

// process the html, remove base href, and styles and fonts
// create the regular expressions to look for in index
// we can have as many languages as we need

const fs = require('fs');
const config = require('./config');

const reLTR = /<!-- #LTR -->([sS]*?)<!-- #ENDLTR -->/gim;
const reRTL = /<!-- #RTL -->([sS]*?)<!-- #ENDRTL -->/gim;

const process = function (html, lang) {
  let contents = html.toString();
  // if config is with URL, change the base href
  if (config.urlBased) {
    contents = contents.replace('<base href="/">', `<base href="/${lang}/">`);
  }
  // if lang is ar, remove ltr and keep rtl
  if (lang === 'ar') {
    contents = contents.replace(reLTR, '');
  } else {
    contents = contents.replace(reRTL, '');
  }
  return contents;
};
Вход в полноэкранный режим Выход из полноэкранного режима

В наших маршрутах мы можем использовать эту функцию, когда нам понадобится. Найдите функции рендерера, сгруппированные в host/server/renderer.js. Чтобы использовать экспортированные функции, мы используем строки, подобные этим:

renderer.htmlEngine(app);

renderer.htmlRender(res);

renderer.ngEngine(req, res);

Вернемся к оптимизации стилей и шрифтов

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

The fonts and the styles are optimized, our comment tags are preserved
<!-- #LTR -->
<style type="text/css">@font-face{font-family:'Signika';font-style:normal...}</style>
<style>:root{...}</style><link rel="stylesheet" href="styles.ltr.css" media="print" onload="this.media='all'"><noscript><link rel="stylesheet" href="styles.ltr.css"></noscript>
<!-- #ENDLTR -->
<!-- #RTL -->
<style type="text/css">@font-face{font-family:'Tajawal';font-style:normal;...}</style>
<style>:root{...}</style><link rel="stylesheet" href="styles.rtl.css" media="print" onload="this.media='all'"><noscript><link rel="stylesheet" href="styles.rtl.css"></noscript>
<!-- #ENDRTL -->
Вход в полноэкранный режим Выход из полноэкранного режима

Атрибут Lang

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

const reLang = /$lang/gim;

const process = function(html, lang) {
   // ... replace $lang when found
   contents = contents.replace(reLang, lang);

   // ...
}
Войти в полноэкранный режим Выйти из полноэкранного режима

И в нашем index.html:

<html lang="$lang">

Строим, обслуживаем… работаем. Двигаемся дальше.

Усовершенствования и корректировки

Как мы выяснили ранее, символы валюты локали являются частью встроенной библиотеки, и если эта конкретная валюта не предоставлена, происходит откат к ее коду. Есть ли способ настроить это поведение? Это, в дополнение к созданию пользовательского интерфейса для переключения языков, будет в следующем эпизоде. 😴

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

РЕСУРСЫ

  • Плагин Critters
  • Проект StackBlitz

Обслуживание разного index.html в сборке Angular для разных языков, Angular — Sekrab Garage

Локализация с помощью Angular

garage.sekrab.com

Оцените статью
devanswers.ru
Добавить комментарий