新增導覽和路由 navigation-routing
瞭解如何使用AEM頁面和SPA Editor SDK支援SPA中的多個檢視。 動態導覽是使用Angular路由實作,並新增到現有的頁首元件。
目標
- 瞭解使用SPA編輯器時可用的SPA模型路由選項。
- 瞭解如何使用Angular路由在SPA的不同檢視之間導覽。
- 實作由AEM頁面階層驅動的動態導覽。
您將要建置的內容
本章將導覽功能表新增至現有的Header元件。 導覽功能表是由AEM頁面階層所驅動,並使用導覽核心元件提供的JSON模型。
先決條件
檢閱設定本機開發環境所需的工具與指示。
取得程式碼
-
透過Git下載本教學課程的起點:
code language-shell $ git clone git@github.com:adobe/aem-guides-wknd-spa.git $ cd aem-guides-wknd-spa $ git checkout Angular/navigation-routing-start -
使用Maven將程式碼庫部署到本機AEM執行個體:
code language-shell $ mvn clean install -PautoInstallSinglePackage如果使用AEM 6.x,請新增
classic設定檔:code language-shell $ mvn clean install -PautoInstallSinglePackage -Pclassic -
安裝傳統WKND參考站台的完成套件。 由WKND參考網站提供的影像會在WKND SPA上重複使用。 可以使用AEM的封裝管理員來安裝封裝。
您一律可以在GitHub上檢視完成的程式碼,或切換至分支Angular/navigation-routing-solution在本機取出程式碼。
檢查HeaderComponent更新 inspect-header
在先前的章節中,HeaderComponent元件已新增為透過app.component.html包含的純Angular元件。 在本章中,HeaderComponent元件已從應用程式移除,並透過範本編輯器新增。 這可讓使用者從AEM中設定HeaderComponent的導覽功能表。
-
在您選擇的IDE中,開啟本章的SPA入門專案。
-
在
ui.frontend模組下方,於ui.frontend/src/app/components/header/header.component.ts檢查檔案header.component.ts。已進行數個更新,包括新增
HeaderEditConfig及MapTo,以便讓元件對應至AEM元件wknd-spa-angular/components/header。code language-js /* header.component.ts */ ... const HeaderEditConfig = { ... }; @Component({ selector: 'app-header', templateUrl: './header.component.html', styleUrls: ['./header.component.scss'] }) export class HeaderComponent implements OnInit { @Input() items: object[]; ... } ... MapTo('wknd-spa-angular/components/header')(withRouter(Header), HeaderEditConfig);記下
items的@Input()註解。items將包含從AEM傳入的導覽物件陣列。 -
在
ui.apps模組中檢查AEMHeader元件的元件定義:ui.apps/src/main/content/jcr_root/apps/wknd-spa-angular/components/header/.content.xml:code language-xml <?xml version="1.0" encoding="UTF-8"?> <jcr:root xmlns:sling="http://sling.apache.org/jcr/sling/1.0" xmlns:cq="http://www.day.com/jcr/cq/1.0" xmlns:jcr="http://www.jcp.org/jcr/1.0" jcr:primaryType="cq:Component" jcr:title="Header" sling:resourceSuperType="wknd-spa-angular/components/navigation" componentGroup="WKND SPA Angular - Structure"/>AEM
Header元件將透過sling:resourceSuperType屬性繼承導覽核心元件的所有功能。
將HeaderComponent新增至SPA範本 add-header-template
-
開啟瀏覽器並登入AEM,http://localhost:4502/。 起始程式碼基底應該已經部署。
-
導覽至SPA頁面範本: http://localhost:4502/editor.html/conf/wknd-spa-angular/settings/wcm/templates/spa-page-template/structure.html。
-
選取最外層的根配置容器,然後按一下它的 原則 圖示。 請注意,not選取 配置容器 已解除製作鎖定。
-
複製目前的原則並建立名為 SPA結構 的新原則:
在允許的元件 > 一般 >下,選取 配置容器 元件。
在允許的元件 > WKND SPA ANGULAR — 結構 >選取 標題 元件下:
在允許的元件 > WKND SPA ANGULAR - Content >選取 影像 和 文字 元件。 您總共應選取4個元件。
按一下「完成」以儲存變更。
-
重新整理頁面。 在解除鎖定的 配置容器 上方新增 Header 元件:
-
選取 Header 元件,然後按一下其 原則 圖示以編輯原則。
-
使用 "WKND SPA標頭" 的 原則標題 建立新原則。
在 屬性 下:
- 將 導覽根目錄 設定為
/content/wknd-spa-angular/us/en。 - 將「排除根層級」設定為「1」。
- 取消勾選收集所有子頁面。
- 將「導覽結構深度」設定為「3」。
這會收集位於
/content/wknd-spa-angular/us/en下方的導覽2層級。 - 將 導覽根目錄 設定為
-
儲存變更後,您應該會看到填入的
Header成為範本的一部分:
建立子頁面
接下來,在AEM中建立其他頁面,這些頁面將用作SPA中的不同檢視。 我們也會檢查AEM所提供JSON模型的階層結構。
-
導覽至 網站 主控台: http://localhost:4502/sites.html/content/wknd-spa-angular/us/en/home。 選取WKND SPA Angular首頁,然後按一下建立 > 頁面:
-
在 範本 下,選取SPA頁面。 在「屬性」下,輸入 Title 的 "Page 1" 和 "page-1" 作為名稱。
按一下「建立」,然後在對話方塊快顯視窗中按一下「開啟」,在AEM SPA編輯器中開啟頁面。
-
將新的 Text 元件新增至主要配置容器。 編輯元件並輸入文字: 「Page 1」 (使用RTE和 H1 元素) (您必須進入全熒幕模式才能變更段落元素)
您可以隨意新增其他內容,例如影像。
-
返回AEM Sites主控台並重複上述步驟,建立名為 「第2頁」 的第二個頁面作為 第1 頁的同層級。 將內容新增至 第2 頁,以便輕鬆識別。
-
最後建立第三個頁面,「第3頁」,但做為 第2 頁的子項。 完成後,網站階層應如下所示:
-
在新標籤中,開啟AEM提供的JSON模型API: http://localhost:4502/content/wknd-spa-angular/us/en.model.json。 SPA首次載入時會請求此JSON內容。 外部結構如下所示:
code language-json { "language": "en", "title": "en", "templateName": "spa-app-template", "designPath": "/libs/settings/wcm/designs/default", "cssClassNames": "spa page basicpage", ":type": "wknd-spa-angular/components/spa", ":items": {}, ":itemsOrder": [], ":hierarchyType": "page", ":path": "/content/wknd-spa-angular/us/en", ":children": { "/content/wknd-spa-angular/us/en/home": {}, "/content/wknd-spa-angular/us/en/home/page-1": {}, "/content/wknd-spa-angular/us/en/home/page-2": {}, "/content/wknd-spa-angular/us/en/home/page-2/page-3": {} } }在
:children底下,您應該會看到每個已建立頁面的專案。 所有頁面的內容都在此初始JSON請求中。 一旦導覽路由實作後,SPA的後續檢視就會快速載入,因為內容在使用者端已可供使用。在初始JSON要求中載入SPA的 所有 內容是不明智的,因為這會減慢初始頁面載入的速度。 接下來,讓我們看看如何收集頁面的階層式深度。
-
導覽至 SPA Root 範本,位於: http://localhost:4502/editor.html/conf/wknd-spa-angular/settings/wcm/templates/spa-app-template/structure.html。
按一下頁面屬性功能表 > 頁面原則:
-
SPA Root範本有一個額外的 階層結構 標籤,可控制所收集的JSON內容。 結構深度決定網站階層中要多深才能收集 root 下的子頁面。 您也可以使用 結構模式 欄位,根據規則運算式篩選出其他頁面。
將 結構深度 更新為"2":
按一下 完成 儲存原則的變更。
-
重新開啟JSON模型http://localhost:4502/content/wknd-spa-angular/us/en.model.json。
code language-json { "language": "en", "title": "en", "templateName": "spa-app-template", "designPath": "/libs/settings/wcm/designs/default", "cssClassNames": "spa page basicpage", ":type": "wknd-spa-angular/components/spa", ":items": {}, ":itemsOrder": [], ":hierarchyType": "page", ":path": "/content/wknd-spa-angular/us/en", ":children": { "/content/wknd-spa-angular/us/en/home": {}, "/content/wknd-spa-angular/us/en/home/page-1": {}, "/content/wknd-spa-angular/us/en/home/page-2": {} } }請注意,頁面3路徑已從初始JSON模型中移除:
/content/wknd-spa-angular/us/en/home/page-2/page-3。稍後,我們將觀察AEM SPA Editor SDK如何以動態方式載入其他內容。
Implement the navigation
Next, implement the navigation menu with a new NavigationComponent. We could add the code directly in header.component.html but a better practice is to avoid large components. Instead, implement a NavigationComponent that could potentially be re-used later.
-
Review the JSON exposed by the AEM
Headercomponent at http://localhost:4502/content/wknd-spa-angular/us/en.model.json:code language-json ... "header": { "items": [ { "level": 0, "active": true, "path": "/content/wknd-spa-angular/us/en/home", "description": null, "url": "/content/wknd-spa-angular/us/en/home.html", "lastModified": 1589062597083, "title": "WKND SPA Angular Home Page", "children": [ { "children": [], "level": 1, "active": false, "path": "/content/wknd-spa-angular/us/en/home/page-1", "description": null, "url": "/content/wknd-spa-angular/us/en/home/page-1.html", "lastModified": 1589429385100, "title": "Page 1" }, { "level": 1, "active": true, "path": "/content/wknd-spa-angular/us/en/home/page-2", "description": null, "url": "/content/wknd-spa-angular/us/en/home/page-2.html", "lastModified": 1589429603507, "title": "Page 2", "children": [ { "children": [], "level": 2, "active": false, "path": "/content/wknd-spa-angular/us/en/home/page-2/page-3", "description": null, "url": "/content/wknd-spa-angular/us/en/home/page-2/page-3.html", "lastModified": 1589430413831, "title": "Page 3" } ], } ] } ], ":type": "wknd-spa-angular/components/header"The hierarchical nature of the AEM pages are modeled in the JSON that can be used to populate a navigation menu. Recall that the
Headercomponent inherits all of the functionality of the Navigation Core Component and the content exposed through the JSON is automatically mapped to the Angular@Inputannotation. -
Open a new terminal window and navigate to the
ui.frontendfolder of the SPA project. Create a newNavigationComponentusing the Angular CLI tool:code language-shell $ cd ui.frontend $ ng generate component components/navigation CREATE src/app/components/navigation/navigation.component.scss (0 bytes) CREATE src/app/components/navigation/navigation.component.html (25 bytes) CREATE src/app/components/navigation/navigation.component.spec.ts (656 bytes) CREATE src/app/components/navigation/navigation.component.ts (286 bytes) UPDATE src/app/app.module.ts (2032 bytes) -
Next create a class named
NavigationLinkusing the Angular CLI in the newly createdcomponents/navigationdirectory:code language-shell $ cd src/app/components/navigation/ $ ng generate class NavigationLink CREATE src/app/components/navigation/navigation-link.spec.ts (187 bytes) CREATE src/app/components/navigation/navigation-link.ts (32 bytes) -
Return to the IDE of your choice and open the file at
navigation-link.tsat/src/app/components/navigation/navigation-link.ts.
-
以下列專案填入
navigation-link.ts:code language-js export class NavigationLink { title: string; path: string; url: string; level: number; children: NavigationLink[]; active: boolean; constructor(data) { this.path = data.path; this.title = data.title; this.url = data.url; this.level = data.level; this.active = data.active; this.children = data.children.map( item => { return new NavigationLink(item); }); } }This is a simple class to represent an individual navigation link. In the class constructor we expect
datato be the JSON object passed in from AEM. This class is used within both theNavigationComponentandHeaderComponentto easily populate the navigation structure.No data transformation is performed, this class is primarily created to strongly type the JSON model. Notice that
this.childrenis typed asNavigationLink[]and that the constructor recursively creates newNavigationLinkobjects for each of the items in thechildrenarray. Recall that JSON model for theHeaderis hierarchical. -
開啟檔案
navigation-link.spec.ts。 This is the test file for theNavigationLinkclass. Update it with the following:code language-js import { NavigationLink } from './navigation-link'; describe('NavigationLink', () => { it('should create an instance', () => { const data = { children: [], level: 1, active: false, path: '/content/wknd-spa-angular/us/en/home/page-1', description: null, url: '/content/wknd-spa-angular/us/en/home/page-1.html', lastModified: 1589429385100, title: 'Page 1' }; expect(new NavigationLink(data)).toBeTruthy(); }); });Notice that
const datafollows the same JSON model inspected earlier for a single link. This is far from a robust unit test, however it should suffice to test the constructor ofNavigationLink. -
開啟檔案
navigation.component.ts。 Update it with the following:code language-js import { Component, OnInit, Input } from '@angular/core'; import { NavigationLink } from './navigation-link'; @Component({ selector: 'app-navigation', templateUrl: './navigation.component.html', styleUrls: ['./navigation.component.scss'] }) export class NavigationComponent implements OnInit { @Input() items: object[]; constructor() { } get navigationLinks(): NavigationLink[] { if (this.items && this.items.length > 0) { return this.items.map(item => { return new NavigationLink(item); }); } return null; } ngOnInit() {} }NavigationComponentexpects anobject[]nameditemsthat is the JSON model from AEM. 此類別公開單一方法get navigationLinks(),它傳回NavigationLink物件的陣列。 -
開啟檔案
navigation.component.html,並以下列專案更新它:code language-html <ul *ngIf="navigationLinks && navigationLinks.length > 0" class="navigation__group"> <ng-container *ngTemplateOutlet="recursiveListTmpl; context:{ links: navigationLinks }"></ng-container> </ul>這會產生初始
<ul>,並從navigation.component.ts呼叫get navigationLinks()方法。<ng-container>用來呼叫名為recursiveListTmpl的範本,並將navigationLinks傳遞為名為links的變數。接著新增
recursiveListTmpl:code language-html <ng-template #recursiveListTmpl let-links="links"> <li *ngFor="let link of links" class="{{'navigation__item navigation__item--' + link.level}}"> <a [routerLink]="link.url" class="navigation__item-link" [title]="link.title" [attr.aria-current]="link.active"> {{link.title}} </a> <ul *ngIf="link.children && link.children.length > 0"> <ng-container *ngTemplateOutlet="recursiveListTmpl; context:{ links: link.children }"></ng-container> </ul> </li> </ng-template>接著會實作導覽連結的其餘轉譯。 請注意,變數
link的型別為NavigationLink,且此類別建立的所有方法/屬性都可供使用。 已使用[routerLink],而非一般的href屬性。 這可讓我們連結至應用程式中的特定路由,不需要重新整理整頁。如果目前的
link具有非空白的children陣列,則導覽的遞回部分也會透過建立另一個<ul>來實作。 -
更新
navigation.component.spec.ts以新增RouterTestingModule的支援:code language-diff ... + import { RouterTestingModule } from '@angular/router/testing'; ... beforeEach(async(() => { TestBed.configureTestingModule({ + imports: [ RouterTestingModule ], declarations: [ NavigationComponent ] }) .compileComponents(); })); ...需要新增
RouterTestingModule,因為元件使用[routerLink]。 -
更新
navigation.component.scss以新增一些基本樣式至NavigationComponent:
@import "~src/styles/variables";
$link-color: $black;
$link-hover-color: $white;
$link-background: $black;
:host-context {
display: block;
width: 100%;
}
.navigation__item {
list-style: none;
}
.navigation__item-link {
color: $link-color;
font-size: $font-size-large;
text-transform: uppercase;
padding: $gutter-padding;
display: flex;
border-bottom: 1px solid $gray;
&:hover {
background: $link-background;
color: $link-hover-color;
}
}
更新標題元件
現在NavigationComponent已實作,必須更新HeaderComponent以參考它。
-
開啟終端機,並導覽至SPA專案中的
ui.frontend資料夾。 啟動webpack開發伺服器:code language-shell $ npm start -
開啟瀏覽器索引標籤並導覽至http://localhost:4200/。
webpack開發伺服器應該設定為從AEM (
ui.frontend/proxy.conf.json)的本機執行個體代理JSON模型。 這可讓我們直接針對教學課程中先前在AEM中建立的內容進行編碼。
HeaderComponent目前已經實作功能表切換功能。 接著,新增導覽元件。 -
返回您選擇的IDE,並在
ui.frontend/src/app/components/header/header.component.ts開啟檔案header.component.ts。 -
更新
setHomePage()方法以移除硬式編碼字串並使用AEM元件傳入的動態prop:code language-js /* header.component.ts */ import { NavigationLink } from '../navigation/navigation-link'; ... setHomePage() { if (this.hasNavigation) { const rootNavigationLink: NavigationLink = new NavigationLink(this.items[0]); this.isHome = rootNavigationLink.path === this.route.snapshot.data.path; this.homePageUrl = rootNavigationLink.url; } } ...NavigationLink的新執行個體是根據items[0]建立的,這是從AEM傳入的導覽JSON模型的根目錄。this.route.snapshot.data.path會傳回目前Angular路由的路徑。 此值用於判斷目前的路由是否為首頁。this.homePageUrl是用來填入 標誌 上的錨點連結。 -
開啟
header.component.html並將導覽的靜態預留位置取代為新建立的NavigationComponent的參考:code language-diff <div class="header-navigation"> <div class="navigation"> - Navigation Placeholder + <app-navigation [items]="items"></app-navigation> </div> </div>[items]=items屬性會將@Input() items從HeaderComponent傳遞至NavigationComponent,以建立導覽。 -
開啟
header.component.spec.ts並為NavigationComponent新增宣告:code language-diff /* header.component.spect.ts */ + import { NavigationComponent } from '../navigation/navigation.component'; describe('HeaderComponent', () => { let component: HeaderComponent; let fixture: ComponentFixture<HeaderComponent>; beforeEach(async(() => { TestBed.configureTestingModule({ imports: [ RouterTestingModule ], + declarations: [ HeaderComponent, NavigationComponent ] }) .compileComponents(); }));由於
NavigationComponent現在已用作HeaderComponent的一部分,因此需要將其宣告為測試平台的一部分。 -
儲存任何開啟檔案的變更並返回webpack開發伺服器: http://localhost:4200/
按一下功能表切換即可開啟導覽,且您應會看到已填入的導覽連結。 您應該能夠導覽至SPA的不同檢視。
瞭解SPA路由
現在導覽已實施,請在AEM中檢查路由。
-
在IDE中,在
ui.frontend/src/app開啟檔案app-routing.module.ts。code language-js /* app-routing.module.ts */ import { AemPageDataResolver, AemPageRouteReuseStrategy } from '@adobe/cq-angular-editable-components'; import { NgModule } from '@angular/core'; import { RouteReuseStrategy, RouterModule, Routes, UrlMatchResult, UrlSegment } from '@angular/router'; import { PageComponent } from './components/page/page.component'; export function AemPageMatcher(url: UrlSegment[]): UrlMatchResult { if (url.length) { return { consumed: url, posParams: { path: url[url.length - 1] } }; } } const routes: Routes = [ { matcher: AemPageMatcher, component: PageComponent, resolve: { path: AemPageDataResolver } } ]; @NgModule({ imports: [RouterModule.forRoot(routes)], exports: [RouterModule], providers: [ AemPageDataResolver, { provide: RouteReuseStrategy, useClass: AemPageRouteReuseStrategy } ] }) export class AppRoutingModule {}routes: Routes = [];陣列定義Angular元件對應的路由或導覽路徑。AemPageMatcher是自訂Angular路由器UrlMatcher,符合此Angular應用程式中AEM任何「看起來」頁面的內容。PageComponent是Angular元件,代表AEM中的頁面,用來呈現相符的路由。 在稍後的教學課程中會檢閱PageComponent。AemPageDataResolver由AEM SPA Editor JS SDK提供,是自訂Angular路由器解析器,用來將路由URL (AEM中包含.html副檔名的路徑)轉換為AEM中的資源路徑(不含副檔名的頁面路徑)。例如,
AemPageDataResolver會將路由的URLcontent/wknd-spa-angular/us/en/home.html轉換為/content/wknd-spa-angular/us/en/home的路徑。 用於根據JSON模型API中的路徑解析頁面內容。由AEM SPA Editor JS SDK提供的
AemPageRouteReuseStrategy是自訂RouteReuseStrategy,可防止跨路由重複使用PageComponent。 否則,頁面「A」中的內容可能會在導覽至頁面「B」時顯示。 -
在
ui.frontend/src/app/components/page/開啟檔案page.component.ts。code language-js ... export class PageComponent { items; itemsOrder; path; constructor( private route: ActivatedRoute, private modelManagerService: ModelManagerService ) { this.modelManagerService .getData({ path: this.route.snapshot.data.path }) .then(data => { this.path = data[Constants.PATH_PROP]; this.items = data[Constants.ITEMS_PROP]; this.itemsOrder = data[Constants.ITEMS_ORDER_PROP]; }); } }PageComponent是處理從AEM擷取的JSON所必需,並當作Angular元件來轉譯路由。由Angular路由器模組提供的
ActivatedRoute包含指出哪個AEM頁面的JSON內容應載入此Angular頁面元件執行個體的狀態。ModelManagerService,根據路由取得JSON資料,並將資料對應到類別變數path、items、itemsOrder。 然後這些資料將傳遞至AEMPageComponent -
在
ui.frontend/src/app/components/page/開啟檔案page.component.htmlcode language-html <aem-page class="structure-page" [attr.data-cq-page-path]="path" [cqPath]="path" [cqItems]="items" [cqItemsOrder]="itemsOrder"> </aem-page>aem-page包含AEMPageComponent。 變數path、items和itemsOrder已傳遞至AEMPageComponent。 透過SPA Editor JavaScript SDK提供的AemPageComponent將接著反複處理此資料,並根據Map Components教學課程中看到的JSON資料動態例項化Angular元件。PageComponent實際上只是AEMPageComponent的Proxy,而且是AEMPageComponent完成大部分繁重工作,以正確將JSON模型對應至Angular元件。
在AEM中檢查SPA路由
-
開啟終端機,並停止webpack開發伺服器 (如果啟動)。 導覽至專案的根目錄,並使用您的Maven技能將專案部署到AEM:
code language-shell $ cd aem-guides-wknd-spa $ mvn clean install -PautoInstallSinglePackagenote caution CAUTION Angular專案已啟用一些非常嚴格的Linting規則。 如果Maven組建失敗,請檢查錯誤,並尋找在列出的檔案中找到個Lint錯誤。。修正篩選器發現的任何問題,並重新執行Maven命令。 -
導覽至AEM中的SPA首頁: http://localhost:4502/content/wknd-spa-angular/us/en/home.html,然後開啟瀏覽器的開發人員工具。 熒幕擷取畫面如下:Google Chrome瀏覽器。
重新整理頁面,您應該會看到
/content/wknd-spa-angular/us/en.model.json的XHR要求(即SPA根目錄)。 請注意,根據教學課程中先前進行之SPA根範本的階層深度設定,僅包含三個子頁面。 這不包括 第3 頁。
-
開啟開發人員工具後,瀏覽至 第3 頁:
請注意,已向
/content/wknd-spa-angular/us/en/home/page-2/page-3.model.json提出新的XHR要求
AEM模型管理員瞭解頁面3 JSON內容無法使用,並自動觸發其他XHR請求。
-
繼續使用各種導覽連結導覽SPA。 請注意,不會提出其他XHR要求,也不會發生完整頁面重新整理。 這可讓一般使用者快速使用SPA,並減少傳回AEM的不必要請求。
-
透過直接導覽至: http://localhost:4502/content/wknd-spa-angular/us/en/home/page-2.html來嘗試深層連結。 請注意,瀏覽器的返回按鈕仍會繼續運作。
恭喜! congratulations
恭喜,您已瞭解如何使用SPA Editor SDK將對應到AEM頁面,以支援SPA中的多個檢視。 已使用Angular路由實作動態導覽,並已新增至Header元件。
您一律可以在GitHub上檢視完成的程式碼,或切換至分支Angular/navigation-routing-solution在本機取出程式碼。
後續步驟 next-steps
建立自訂元件 — 瞭解如何建立要與AEM SPA Editor搭配使用的自訂元件。 瞭解如何開發作者對話方塊和Sling模型,以擴充JSON模型來填入自訂元件。