После прочтения статьи "Порог вхождения в Angular 2 — теория и практика" у меня появилось желание показать, как можно пробросить все роуты Angular 2 через роутер Laravel 5.

Идея следующая

  • в директории resources/views/backend будет лежать представление, являющееся точкой входа для всех роутов Angular 2. Для каждого роута Angular 2, роутер Laravel 5 будет нас перенаправлять на это представление;
  • под каждый Angular 2 роут в директиве @RouteConfig нам придется создать копию в роутере Laravel;
  • все роуты Angular 2, по которым подтягиваются шаблоны, будут иметь вид вида /templates/SomeComponent.main, и будут запрашиваться роутером Laravel 5 по пути вида resources/views/frontend/SomeComponent/main.blade.php

Приступим

Чтобы не запутаться, разобьем все представления в директории resources/views на 2 подпапки:

  • backend — для представлений, которые Laravel 5 использует напрямую;
  • frontend (в статье будет использоваться frontend, но кто-то пожелает переименовать, например в templates) — для всех шаблонов, которые будут использоваться в Angular 2.

1) Front-end

Допустим, у нас есть уже созданный root-компонент app.component.ts, а так же 2 компонента: components/MainComponent.ts, который будет загружаться по умолчанию, и components/EditComponent.ts.

Мы хотим указать в директиве @RouteConfig 2 роута: один '/' с именем Home, ведущий на MainComponent, а второй '/edit' с именем Edit, ведущий на EditComponent.

//app.component.ts
@RouteConfig([
    {
        path: '/',
        name: 'Home,
        component: MainComponent,
        useAsDefault: true
    },
    {
        path: '/edit',
        name: 'Edit',
        component: EditComponent
    }
])
@Component({
    'directives': [ROUTER_DIRECTIVES],
    'selector': 'app',
    'template': '<router-outlet></router-outlet>'
})
export class AppComponent {
    constructor () {}
}

Далее необходимо задать для компонентов их шаблоны:

//components/MainComponent.ts
@Component({
    'selector': 'state-template',
    'templateUrl': '/templates/MainComponent.main'
})
export class MainComponent {
...
}

//components/EditComponent.ts
@Component({
    'selector': 'state-template',
    'templateUrl': '/templates/EditComponent.main'
})
export class EditComponent {
...
}

2) Back-end

Первым делом, в роутере Laravel 5 продублируем все основные роуты Angular 2. Все подобные роуты мы будем направлять в AngularRoutesController.

//routes.php
Route::get('/', [
    'uses' => 'MyApp\AngularRoutesController@index',
    'as' => 'ngHome'
]);

Route::get('/edit', [
    'uses' => 'MyApp\AngularRoutesController@index',
    'as' => 'ngEdit'
]);

В AngularRoutesController мы будем просто рендерить представление, являющееся точкой входа, для Angular 2:

public function index()
{
    return view('backend.content');
}

Представление views/backend/content.blade.php должно содержать в себе тег, в который будет грузиться Angular 2 приложение, у нас это app.

@extends('backend.layout')

@section('backend.content')
    <app>
        @include('backend.loading')
    </app>

    @include('backend.scripts-import')
@stop

Для роутов Angular 2, ведущих к темплейтам, создадим еще 1 Laravel 5 маршрут, который будет перенаправлять запрос на контроллер AngularTemplatesController:

//routes.php
Route::get('/templates/{template}', [
    'uses' => 'MyApp\AngularTemplatesController@index',
    'as' => 'ngTemplates'
]);

Внутри AngularTemplatesController нам необходимо добавить к $template, в котором содержится часть пути к представлению с шаблоном, имя нашей папки с шаблонами Angular 2 — frontend.

public function index($template)
{
    $templatePath = 'frontend.' . $template;

    if (!view()->exists($templatePath)) {
        throw new NotFoundHttpException();
    }

    return view($templatePath);
}

Итог

В результате, все Angular 2 шаблоны, которые будут запрашиваться по URL-адресу вида /templates/SomeComponent.main будут браться по пути вида resources/views/frontend/SomeComponent/main.blade.php. Мы сможем использовать шаблонизатор Blade в шаблонах Angular 2.

Нюансы

Поскольку в шаблонизаторе Blade и в шаблонах Angular 2 используется одинаковый синтаксис с двойными фигурными скобками {{ ... }}, для корректной обработки двойных фигурных скобок, относящихся к Angular 2, необходимо будет ставить перед ними символ @.
Этот символ позволит шаблонизатору Blade игнорировать такие конструкции, для последующего из разбора Angular 2.

<div>{{ bladeVariable }}</div>
<li *ngFor="#file of files" class="list-group-item">
    @{{ file.name }}
</li>

Репозиторий с примером.

Комментарии (25)


  1. ivanuzzo
    04.03.2016 17:16
    +1

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

    (раньше я тоже таким занимался, когда пытался наладить связь angular + Symfony, в итоге мне подсказали интересную идею, которую я описал выше. Правда, для работы приложения придется дополнительно настроить CORS, но конкретно в Симфони с этим проблем нету)


    1. ElianL
      04.03.2016 18:57

      Можно так же поднять для фронта маленький сервачок на node.js — использовать как проксю(отпадает необходмость в CORS) и, если нужно, ддя серверного рендеринга.

      Хотя если все на одно домене то ни прокси, ни CORS вообще не нужны.

      Первую схему мы юзаем для dev окружения, то есть мне как фронтеннд разработчику не нужно поднимать локально бекенд, вторая схема для production


  1. vintage
    04.03.2016 17:21
    -5

    Есть решение проще: просто не вводить своими роутами ни пользователя, ни браузер, ни бэкенд в заблуждение, а использовать # — с ним у вас не будет никаких проблем.


  1. 4ertovo4ka
    04.03.2016 17:24
    +1

    Спасибо! Такого примера не хватало, когда я делать начинала.
    Почему-то все всегда опускали этот момент.
    Но разделение на фронт и бэк, сейчас думаю, правильнее. Две независимые сущности — при замене одной не страдает другая.
    Но еще раз спасибо за статью!


  1. lowadka
    04.03.2016 17:27
    +3

    Похоже на какой-то особый вид извращения


    1. 4ertovo4ka
      04.03.2016 17:32

      Зря вы так.
      Если посмотреть туториалы по связке 2 означенных вещей — везде будет то, что они работают на одном домене в разных вариациях, но нигде нет подсказки как их подружить вместе в части роутов.
      Может и извиащение, но в начале пути это ставит в тупик


      1. lowadka
        04.03.2016 19:18
        +1

        Проблема в том, что это заведомо плохое решение. А когда о таком пишут статью, якобы чтобы "просветить других", то значит, что таких "других" будет только больше


        1. 4ertovo4ka
          04.03.2016 21:15

          Причинно-следственная связь: в своем посте я подняла вопрос о том, что у меня была проблема роутинга. Решение проблемы нигде не проскакивало. В итоге потратив время я пришла к тому, что разнесла laravel и angular по разным доменам.
          Автор этого поста дал вариант как можно было бы сделать не разнося по проектам.
          В итоге имеем: есть 2 варианта решения одной проблемы. Тут нет соревнования, что хуже, а что лучше — а есть одно видение, и автор тут рассказал, как можно было сделать. Теперь проблема, которая не поднималась в источниках — была поднята, предложено 2 варианта решения этой проблемы. 2 рабочих варианта.
          И у каждого остается право выбора что использовать.
          И я верю, что для кого-то очевиден выбор в пользу разнесения и вообще очевидны многие вещи. Но давайте не будем списывать со счетов блондинок и новичков, у которых такие моменты могут вызвать вопросы.


          1. ElianL
            04.03.2016 21:24
            +1

            Даже если оставлять все один проектом, то есть решени куда лучше:
            определить какие урлы должен обрабатывать бекенд(статика, api) все остальное уже "редеректить" на html файл где урл подхватит клиент.
            Идея копипастить урлы из одного файла в другой это просто ужас.


            1. 4ertovo4ka
              04.03.2016 21:28

              Есть замечтательное: "критикуя предлагай".
              Предложите, пожалуйста, свой вариант решения. Как можно сделать изящнее?


              1. ElianL
                04.03.2016 21:32

                "определить какие урлы должен обрабатывать бекенд(статика, api) все остальное уже "редеректить" на html где урл подхватит клиент"

                чем не решение?


                1. 4ertovo4ka
                  04.03.2016 21:46

                  Знаете в чем разница между вашим решением и решением которое вам не нравится?
                  В его технической реализации.
                  Решение автора наглядно и его можно пощупать и сделать свой вывод. Ваше решение — описательное. Я предлагаю — а вы уж думайте как сделать. Теории много везде. Живой практики нехватка. Ее отсутствие и толкнуло на поднятие вопроса и предложения решений.
                  Мне хотелось бы ваше в виде кода так же увидеть. Как новичок, который просто танцует на граблях — ценю пример с возможностью пощупать.


                  1. ElianL
                    04.03.2016 21:56

                    я не могу предоставить вам рабочий кусок кода, потому что я не знаком с данным фреймворком но примерно это может выглядеть так (пcевдокод)

                    Route::get('/templates/{template}', [
                        'uses' => 'MyApp\AngularTemplatesController@index',
                    ])
                    
                    Route::get('/templates/{template}', [
                        'uses' => 'MyApp\AngularTemplatesController@index',
                    ])
                    
                    Route::get('/assets/*', [
                        'uses' => 'MyApp\staticController@index',
                    ]);
                    
                    Route::get('/api/*', [
                        'uses' => 'MyApp\apiContoller@index',
                    ]);
                    
                    Route::get('*', [
                        'uses' => 'MyApp\IndexHTMLController@index',
                    ]);


              1. vintage
                04.03.2016 21:42

                Более изящные решения почему-то лихо минусуются. :-D


                1. 4ertovo4ka
                  04.03.2016 21:49

                  Почему-то тут минусуют решение, которое описали не в 2 словах.
                  Минусуют практическую реализацию.
                  Ваш комментарий же он не решение, он как теория… А теория — это не практическое решение.
                  И, как по мне, но когда знаешь как сделать лучше — покажи. А нет времени, так не стоит пробегаючи говорить, что все идиоты, а я дартаньян.


                  1. vintage
                    04.03.2016 22:00

                    https://angular.io/docs/ts/latest/guide/router.html#!#-hashlocationstrategy-

                    Реально, это решение как этой, так и многих других проблем.


                1. ElianL
                  04.03.2016 22:02

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

                  History Api придумали не просто от нечего делать.

                  Плюс ссылки с решеткой плохо индексируются поиковиками. Обращаю ваше внимание, что Angular2 (и ReactJS к примеру) могут отлично сами рендериться на сервере

                  Ваше решени это просто костыль и использование инструментов(решетки в урле) не по назначению


                  1. vintage
                    04.03.2016 22:29

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

                    Аргумент к скромности пропустим.

                    Ссылки с решёткой отлично индексируются поисковиками. Примеры надо?

                    Сами по себе они отрендериться на сервере не смогут, тем более на каком-нибудь Java-сервере. Необходимо реализовать серверную подготовку данных. И тут без разницы будет ли фронт-контроллер перехватывать /lalala или ?_escaped_fragment_=/lalala

                    "Моё" решение как раз таки не костыль, а чёткое разграничение зон ответственности: что обрабатывается на сервере, а что на клиенте. А вот "ваше" идёт вразрез с архитектурой веба. Главным образом появляется куча проблем с кешированием, ведь браузер не знает, что по разным псевдостатическим ссылкам скрывается фактически одна и та же страница.


                    1. ElianL
                      04.03.2016 22:47

                      "чёткое разграничение зон ответственности" — как раз таки будет разбиения проекта на два отдельных =)

                      "Сами по себе они отрендериться на сервере не смогут" — для этого есть nodejs

                      "куча проблем с кешированием" — при полноценном серверном рендере страницы не статичны, а в случае если вы отдаете браузеру просто пустой html-заглушку то это не большая проблема, так как все "большие" части(js/css) все равно закешируются


                      1. vintage
                        04.03.2016 23:03

                        Да, и это хорошо. Но при этом вовсе не обязательно разносить всё по разным доменам.

                        Ну да, куда ж без ноды-то в java-бэкенде ;-)

                        1. Бессмысленное для пользователя генерирование на сервере html с данными при старте каждой сессии.
                        2. Лишний блокирующий загрузку всего остального запрос за html, которого могло бы и не быть.
                        3. Засранный по самое неболуйся application cache.
                        4. Невозможность выложить на статический сервер типа github.io.
                        5. Невозможность без плясок с бубном завернуть в cordova или chrome app.
                        6. Костыли для обеспечения кроссбраузерности.
                        7. Невозможность раздачи через CDN.

                        Но всё это, конечно, пустяки. Зато мы можем укоротить урл на один символ.


                        1. ElianL
                          04.03.2016 23:57

                          Никто и не говорил про разные домены. На каком домене это все крутиться вообще не при чем. Мы говорим о разбиении на два логический проекта(на уровне репозиториев)

                          Бессмысленное для пользователя генерирование на сервере html с данными при старте каждой сессии

                          Не бесмыленное — визуально перформанс растет. C сервера приходит уже готовый html
                          плюс такие страницы индексируются поисковиками

                          Лишний блокирующий загрузку всего остального запрос за html, которого могло бы и не быть.

                          нода не блокирует загрузку за счет асинхроности js

                          Невозможность выложить на статический сервер типа github.io.

                          и? сколько страниц в интернете генерится, к примеру, php их тоже нельзя выложить

                          Невозможность без плясок с бубном завернуть в cordova или chrome app.

                          лично заворачивал в cordova приложение где фронт был написан с прослойкой на node (c reactjs) — проблем никаких. Потому что фронт умеет рендериться там где есть js

                          Костыли для обеспечения кроссбраузерности.

                          при чем тут нода?

                          Невозможность раздачи через CDN

                          аналогично с "невозможность выложить на статический сервер типа github.io."

                          Плюс выноса фронта в отдельный проект в том, что сам фронт и решает каким ему быть после сборки — статикой или легким nodejs сервером. Он никак не завазян на язык/фреймворк бекенда. Мы можем переписывать любую часть, другой это коснется

                          Он полностью абстагирован от бека, как бы это было например с iOS или Android приложением.

                          Я уж молчу на сколько это удобно когда бекендерам вообще не приходиться думать, о html/css/js файлах — в их репе их просто нет.
                          А фронтедрам не нужно поддерживать локальную версию бека, заботиться о миграциях, установки зависимотей бекнда и так далее. Они спокойно забирают данные с удаленного сервера и все. + Всегда могут локально просить данные с продакшен сервера(ведь часто баги появляются именно на рабочем контенте)

                          На эту тему советую почитать вот эту статью https://www.nczonline.net/blog/2013/10/07/node-js-and-the-new-web-front-end/


                          1. vintage
                            05.03.2016 09:43

                            Эти 7 пунктов были про всевдостатику, а не про ноду.


        1. ivanuzzo
          04.03.2016 21:21
          +2

          чтобы прийти к «заведомо хорошему решению» нужно не раз споткнуться о грабли. Хорошо, когда есть с кем посоветоваться. А если человек — самоучка, и некоторые моменты (например, эта статья) ему покажутся очевиднее, чем разделение на фронт и бэк? Автор не позиционировал статью, как истину в последней инстанции и эта статья вполне имеет право на жизнь. А если бы комментарии озвучивали альтернативы данному подходу — это было бы куда полезнее, чем просто негативная оценка.


          1. 4ertovo4ka
            04.03.2016 21:26

            У меня плюсы закончились на сегодня.
            ivanuzzo, плюсую ))))
            В идеале здорово, когда есть что с чем сравнивать. Автор ее предложил именно потому, что у меня не хватило на это знаний.
            И вообще такие вещи они уже больше относятся к архитектуре, к решениям, с которыми потом жить. Всегда надо иметь представление как можно сделать по разному и что за каждым вариантом может стоять дальше, какие плюсы и минусы.


  1. ElianL
    04.03.2016 19:22
    +3

    под каждый Angular 2 роут в директиве @RouteConfig нам придется создать копию в роутере Laravel;

    Вот уже в этот момент, автор должен понять, что что-то он делает не так