Waltz построен на AngularJS. Поведение по умолчанию для AngularJS заключается в том, что URL-адреса содержат хэш-банг (#), например:

Можно включить красивые URL-адреса, чтобы приведенные выше примеры выглядели следующим образом:

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

К счастью, при некоторой настройке AngularJS поддерживает красивые URL-адреса. Для этого обычно требуется три шага: шаги 1 и 2 выполняются на стороне клиента, а шаг 3 выполняется на сервере. В этой статье описаны эти шаги и то, как мы решили добавить эту функциональность в Waltz.

Шаг 1. Включите режим html5

Включение html5Mode в AngularJS позволяет Angular взаимодействовать с URL-адресом браузера через API истории HTML5. Это суть того, что позволит вашему приложению AngularJS использовать обычные URL-адреса без эквивалента hashbang. Более подробная информация доступна здесь.

$locationProvider.html5Mode({ enabled:true });

Для вальса это делается в waltz-ng/client/routes.js в функции configureRoutes.

Шаг 2. Убедитесь, что относительные ссылки по-прежнему работают

В файле HTML, используемом для начальной загрузки вашего приложения, установите тег ‹base›, чтобы обеспечить правильную работу относительных ссылок в вашем приложении AngularJS. Если тег ‹base› установлен неправильно, вы обнаружите, что попытки загрузить вспомогательные файлы (файлы javascript) будут возвращать ошибки 404 Not Found.

Тег ‹base› должен быть установлен в шапке приложений index.html, в Waltz это устанавливается следующим образом:

<head>
    <meta http-equiv="X-UA-Compatible" content="IE=10,9" >
    <meta charset="utf-8">
    <base href="/" />

Примечание: если ваше приложение не запускается в корневом каталоге, то href следует соответствующим образом скорректировать. Например, если приложение работает под http://domain.com/waltz/, этот href следует настроить следующим образом:

<base href="/waltz/" />

Шаг 3. Исправьте поддержку ссылок на контент

После внесения вышеуказанных изменений, хотя навигация по приложениям работает как положено. Мы обнаружили, что открытие ссылок непосредственно на любую часть Waltz, кроме корневого URL, больше не работает. Например, при ссылке на http://domain.com/waltz/application/10 мы обнаружили, что веб-сервер возвращает ошибку 404 Not Found. Эквивалент приведенного выше URL-адреса в виде хэш-банга выглядит следующим образом: http://domain.com/waltz/#/application/10. Работа версии hashbang несколько отличается и состоит из двух частей:

  1. http://domain.com/waltz/
  2. #/приложение/10

Обработка эквивалента hashbang выглядит следующим образом:

  1. загрузите файл по умолчанию в папку /waltz, в данном случае index.html. Это приведет к тому, что вальс будет загружен.
  2. AngularJS имеет дело со второй частью URL-адреса, которую он интерпретирует как ссылку внутри приложения, а затем старательно загружает правильное представление.

Почему же это не работает без хэш-банга?

Причина в том, что веб-сервер, на котором размещается приложение (tomcat в случае одного клиента), не понимает, что компонент URL /application/10 ссылается конкретному элементу в приложении AngularJS, а не артефакту, развернутому на веб-сервере. На самом деле веб-сервер ничего не знает об AngularJS и возвращается к поиску файла с именем 10 в приложении. каталог, который он, конечно, не найдет в файловой системе.

Итак, как обойти это? Наиболее распространенный подход указывал на Переписать правила. Чтобы доказать, что это решит проблему, мы придумали следующее в качестве примера:

# ignore any files
RewriteCond %{REQUEST_URI} !^.*\.(bmp|css|gif|htc|html?|ico|jpe?g|js|pdf|png|swf|txt|xml|svg|eot|woff|woff2|ttf|map)$
# ignore the /api/ routes to ensure Waltz services still work
RewriteCond %{REQUEST_URI} !^.*/api/.*$
# ignore authentication routes
RewriteCond %{REQUEST_URI} !^.*/sso/.*$
RewriteRule ^(.*)$ %{CONTEXT_PATH} [L]

Так это сработало! Теперь у нас есть установка Waltz с работающими красивыми URL-адресами и поддержкой глубоких ссылок, которая работала так же, как и раньше.

Как мы можем улучшить это?

Есть некоторые проблемы с вышеуказанным подходом.

  1. Ручные шаги в развертывании: это случай с шагом 2, где необходимо добавить базовый тег, чтобы AngularJS знал о своем контексте развертывания и правильно загружался. Это либо нужно будет сделать перед компиляцией в index.ejs, либо упакованный .war нужно будет вручную распаковать и index.html взломать соответственно.
  2. Конфигурация перезаписи может быть сложной, подверженной ошибкам и требующей много времени для правильной настройки. Кроме того, это будет препятствовать возможности легко контейнеризировать Waltz и развертывать, например, как образ докера.

Нам нужен был способ справиться с шагами 2 и 3 таким образом, чтобы не включать дополнительные ручные шаги или сложную настройку сервера и, в идеале, обеспечить простой и относительно экономичный метод развертывания одним щелчком мыши. Мы посмотрели на StaticResourcesEndpoint; это стандартный способ обращения к любым статическим ресурсам, запрашиваемым у Waltz.

Первый шаг – работа с относительными URL-адресами. Для этого убедитесь, что тег ‹base› установлен правильно. Мы решили эту проблему, изменив файл шаблона индекса (index.ejs), чтобы обеспечить наличие тега ‹base href='/' /› по умолчанию, обеспечение правильной работы приложения при разработке на локальном сервере разработки (веб-пакет).Базовый тег должен быть контекстно-зависимым, и для этого требуется манипулирование строками и файловыми потоками, но это означает, что предварительная настройка не требуется при развертывании Waltz.

private InputStream modifyIndexBaseTagIfNeeded(Request request,
                                               String resolvedPath,
                                               InputStream resourceStream) throws IOException {
         if(resolvedPath.endsWith("index.html") && notEmpty(request.contextPath())) {
             List<String> lines = readLines(resourceStream);
 
              for(int i = 0; i < lines.size(); i++) {
                 String line = lower(lines.get(i));
 
                  if (line.contains("<base href=")) {
                     LOG.info("Found <base> tag: " + line + ", adding context path: " + request.contextPath());
                     line = String.format("\t<base href=\"%s/\" />", request.contextPath());
                     LOG.info("Updated <base> tag: " + line);
                     lines.set(i, line);
 
                      // done, exit loop
                     break;
                 }
 
                  if (line.contains("</head>") ) {
                     // don't need to continue if have reached here and no base tag found
                     break;
                 }
             }
 
              // copy amended file into a stream
             try(ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
                  OutputStreamWriter writer = new OutputStreamWriter(outputStream)) {
 
                  for(String line : lines) {
                     writer.write(line);
                     writer.write(System.lineSeparator());
                 }
 
                  writer.flush();
                 ByteArrayInputStream inputStream = new ByteArrayInputStream(outputStream.toByteArray());
                 return inputStream;
             }
         }
         return resourceStream;
}

Приведенный выше код по существу выполняется, когда index.html обслуживается, он ищет тег ‹base› и обеспечивает его установку в соответствии с контекстом развертывания приложения. Затем содержимое файла перепаковывается в поток.

Как насчет поддержки глубоких ссылок? Опять же, StaticResourcesEndpoint может нам помочь. Наш подход заключался в том, чтобы любые запрошенные ресурсы, которые не найдены, возвращались к index.html обслуживаемым. Это гарантирует загрузку приложения перед попыткой доступа к глубинной ссылке. Обратитесь к выделенному коду ниже:

private String resolvePath(Request request) {
    final String indexPath = "static/index.html";
    String path = request.pathInfo().replaceFirst("/", "");
    String resourcePath = path.length() > 0 ? ("static/" + path) : indexPath;
    URL resource = classLoader.getResource(resourcePath);
    if (resource == null) {
        // 404: return index.html
        resource = classLoader.getResource(indexPath);
        resourcePath = indexPath;
    }
    boolean isDirectory = resource
            .getPath()
            .endsWith("/");
    return isDirectory
            ? resourcePath + "/index.html"
            : resourcePath;
}

Пул-реквест на эти изменения доступен здесь, а демо-версию работающего сайта можно посмотреть здесь.