AEM集成SPA(二)集成React完整教程

发布时间:2025-12-10 11:39:25 浏览次数:10

前言

这篇文章是对官方教程的整理,并实践动手排坑后做的总结,主要以SPA框架——React为代表,集成进AEM体系中。前端技术版本更新的太快,因此如果发现有问题,最好在 package.json 中为包设置成一致的版本。
文档和源码:pa空气n.b空气aidu.co空气m/s/1QHnzBa5saUVp_a63BLq5Hw 提取码:yoko

文章目录

  • 前言
  • 5 AEM SPA React完整教程
    • 5.1 Overview
    • 5.2 Project Setup
      • 5.2.1 说明
      • 5.2.2 配置aem-clientlibs-generator
      • 5.2.3 配置frontend-maven-plugin
      • 5.2.4 集成React App到Page
    • 5.3 Editable Components
      • 5.3.1 说明
      • 5.3.2 SPA Editor SDK的安装和集成
      • 5.3.3 Text Component
      • 5.3.4 Image Component
      • 5.3.5 HierarchyPage Sling Model
    • 5.4 Front End Development
      • 5.4.1 说明
      • 5.4.2 安装Sass
      • 5.4.3 通过代理获取JSON
      • 5.4.4 通过Mock获取JSON
      • 5.4.5 Header组件
      • 5.4.6 更新Image组件
      • 5.4.7 更新Text组件
      • 5.4.8 集成Responsive Grid
      • 5.4.9 集成Styleguidist
    • 5.5 Navigation and Routing
      • 5.5.1 说明
      • 5.5.2 SPA的路由方式
      • 5.5.3 安装React Router
      • 5.5.4 List Component
      • 5.5.5 更新Header组件
      • 5.5.6 添加Font Awesome图标
      • 5.5.7 重定向到首页

5 AEM SPA React完整教程

文档:

  • Getting Started with React and AEM SPA Editor
  • GitHub: WKND Events SPA Editor Project 每章教程都有对应Github的代码

5.1 Overview

https://helpx.adobe.com/experience-manager/kt/sites/using/getting-started-spa-wknd-tutorial-develop/react.html

技能要求:

  • Node.js
  • npm
  • webpack
  • Scss - todo
  • React Styleguidist - todo

环境要求:

  • Java 1.8
  • AEM 6.5 or AEM 6.4 + SP2
  • Apache Maven (3.3.9 or newer)
  • Node.js v10+
  • npm 6+

开发工具:

  • IntelliJ with AEM IDE Tooling - 这是那个用不起来的工具。。
  • Visual Studio - AEM Sync

Starter项目下载(建议从这里开始,每一章进行实操练习):

  • WKND Events React Starter Project
git clone git@github.com:Adobe-Marketing-Cloud/aem-guides-wknd-events.gitcd aem-guides-wknd-eventsgit checkout react/start

5.2 Project Setup

https://helpx.adobe.com/experience-manager/kt/sites/using/getting-started-spa-wknd-tutorial-develop/react/chapter-0.html

5.2.1 说明

这篇教程主要是项目初始化相关的内容,涉及到了打包插件、前端maven插件,以及示例的演示等。

新增的工程子模块:

  • react-app:React应用的webpack工程,后续的章节中此webpack工程将被转换成Maven模块作为client library发布到AEM

主要技术要求:

  • AEM editable template

  • create-react-app手脚架

  • aem-clientlib-generator:将react工程中编译后的css和js转化成AEM client library

    需要配置文件: clientlib.config.js

    此文件描述了react-app中的css和js位置,并发布到指定的aem clientlibs中

  • frontend-maven-plugin:通过Maven build来调用NPM命令行,确保项目依赖和持续集成发布

SPA模块和core模块相似,都是嵌入到了ui.apps模块再发布到AEM,详见下图:

5.2.2 配置aem-clientlibs-generator

1)React工程初始化/启动/编译,在react-app模块下打开控制台

#淘宝镜像npm config set registry https://registry.npm.taobao.org # 安装依赖npm install# 示例项目使用了create-react-app,启动手脚架npm run start# 编译npm run build

2)安装aem-clientlib-generator,最新版本是1.7.3(写文档时测试过程出现过错误),教程中是1.4.1

cd <src>/aem-guides-wknd-events/react-app# 默认安装最新的1.7.3npm install aem-clientlib-generator --save-dev# 或跟着教程版本1.4.1npm install aem-clientlib-generator@1.4.1 --save-dev

这里可能会在编译时报错:Browserslist: caniuse-lite is outdated ,解决如下:

# 尝试更新所有包,没用npm cache clean --forcenpm update# 尝试更新部分包,没用npm update caniuse-lite browserslist# 通过强制更新工具:https://www.npmjs.com/package/npm-update-all,太慢了npm install npm-update-all -gnpm-update-all# 通过npx更新嵌套的包npx browserslist@latest --update-db# 实际测试可行npx browserslist@4.10 --update-db

3)aem-clientlib-generator的配置文件编写,此文件描述了react-app中的css和js位置,并发布到指定的aem clientlibs中,在react-app根目录创建文件:clientlib.config.js

module.exports = {// default working directory (can be changed per 'cwd' in every asset option)context: __dirname,// path to the clientlib root folder (output)clientLibRoot: "./../ui.apps/src/main/content/jcr_root/apps/wknd-events/clientlibs",libs: {name: "react-app",allowProxy: true,categories: ["wknd-events.react"],serializationFormat: "xml",jsProcessor: ["min:gcc"],assets: {js: ["build/static/**/*.js"],css: ["build/static/**/*.css"]}}};

4)修改package.json中的npm启动scripts,目的是在build时触发aem-clientlib-generator工具

//package.json..."scripts": {"build": "react-scripts build && clientlib --verbose",...}...

5)编译测试

npm run build# 成功后在/ui.apps/src/main/content/jcr_root/apps/wknd-events/clientlibs/目录下应该会有一个react-app的文件夹,里面有着打包后的css和js

6)在ui.apps/.gitignore中排除react-app,主要为了确保此目录每次都是动态编译生成的(可选,示例项目代码中默认有)

# ui.apps/.gitignore# Ignore React generated client libraries from source controlreact-app

5.2.3 配置frontend-maven-plugin

1)在root工程pom中添加react-app子模块

<modules><!--添加react-app子模块,注意位置一定在apps前面,此顺序是maven的编译顺序--><module>react-app</module><module>core</module><module>ui.apps</module><module>ui.content</module></modules>

2)查看本地node和npm的版本号,并作为properties配置到root的pom中

# 查看版本号node -v # v12.13.1npm -v # 6.12.1 <properties>...<!--frontend-maven-plugin相关配置开始--><frontend-maven-plugin.version>1.6</frontend-maven-plugin.version><node.version>v12.13.1</node.version><npm.version>6.12.1</npm.version><!--frontend-maven-plugin相关配置结束-->...</properties>

3)在react-app子模块下创建pom.xml文件,在里面配置了frontend-maven-plugin

<?xml version="1.0" encoding="UTF-8"?><project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"><modelVersion>4.0.0</modelVersion><!-- ====================================================================== --><!-- P A R E N T P R O J E C T D E S C R I P T I O N --><!-- ====================================================================== --><parent><groupId>com.adobe.aem.guides</groupId><artifactId>aem-guides-wknd-events</artifactId><version>0.0.1-SNAPSHOT</version><relativePath>../pom.xml</relativePath></parent><!-- ====================================================================== --><!-- P R O J E C T D E S C R I P T I O N --><!-- ====================================================================== --><!--<artifactId>aem-guides-wknd-events.react</artifactId>--><artifactId>aem-guides-wknd-events.react-app</artifactId><packaging>pom</packaging><name>WKND Events - React App</name><description>UI React application code for WKND Events</description><!-- ====================================================================== --><!-- B U I L D D E F I N I T I O N --><!-- ====================================================================== --><build><plugins><plugin><groupId>com.github.eirslett</groupId><artifactId>frontend-maven-plugin</artifactId><version>${frontend-maven-plugin.version}</version><executions><execution><id>install node and npm</id><goals><goal>install-node-and-npm</goal></goals><configuration><nodeVersion>${node.version}</nodeVersion><npmVersion>${npm.version}</npmVersion></configuration></execution><execution><id>npm install</id><goals><goal>npm</goal></goals><!-- Optional configuration which provides for running any npm command --><configuration><arguments>install</arguments></configuration></execution><execution><id>npm run build</id><goals><goal>npm</goal></goals><configuration><arguments>run build</arguments></configuration></execution><!--此命令行非必须,用于更新依赖,Bug已修复,这里可删除--><execution><id>npm update</id><goals><goal>npm</goal></goals><configuration><arguments>update</arguments></configuration></execution></executions></plugin></plugins></build></project>

4)通过maven命令测试

cd <src>/aem-guides-wknd-events/react-appmvn clean install # 通过frontend-maven-plugin插件,将自动运行配置的NPM脚本,从而编译react-app

5)最后将整个项目部署到AEM

(1)可以通过命令行,工程root下运行

cd <src>/aem-guides-wknd-eventsmvn -PautoInstallPackage -Padobe-public clean install

(2)推荐通过IDEA的运行配置,上面命令行对应的IDEA运行配置如下:

注意:由于npm中版本的不同,因此react-app编译时会有问题(有个依赖过期了),导致无法进行完整流程的编译和部署,目前此问题已修复,若未修复则需要单独编译完再打包。各个子模块的编译顺序:

  • react-app
  • core
  • apps
  • content

注意: IDEA的运行配置设置中需要激活Profile:adobe-public(用于解决运行时依赖问题),如下图:

5.2.4 集成React App到Page

其实就是将SPA导出的Webpack工程集成到AEM的structure/page页面模板中

1)修改headerlibs:apps/wknd-events/components/structure/page/customheaderlibs.html

主要是meta和css设置,将导入到页首,具体的分析可以参考前文章节[4.9 SPA Page Component](#4.9 SPA Page Component)

<!--/*Custom Headerlibs for React Site*/--><meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" /><!--/*指定JSON传输*/--><meta property="cq:datatype" data-sly-test="${wcmmode.edit || wcmmode.preview}" content="JSON" /><!--/*通知SPA Editor是否是edit模式*/--><meta property="cq:wcmmode" data-sly-test="${wcmmode.edit}" content="edit" /><!--/*通知SPA Editor是否是preview模式*/--><meta property="cq:wcmmode" data-sly-test="${wcmmode.preview}" content="preview" /><!--/*引入自定义的slingmodel,其中获取rootUrl是示例写的方法*/--><meta property="cq:pagemodel_root_url"data-sly-use.page="com.adobe.aem.guides.wkndevents.core.models.HierarchyPage"content="${page.rootUrl}" /><!--/*引入react-app相关的css*/--><sly data-sly-use.clientlib="/libs/granite/sightly/templates/clientlib.html" /><sly data-sly-call="${clientlib.css @ categories='wknd-events.react'}" /><!--/*原内容备份*/--><!--/*<sly data-sly-use.clientLib="/libs/granite/sightly/templates/clientlib.html"data-sly-call="${clientlib.css @ categories='wknd-events.base'}"/><sly data-sly-resource="${'contexthub' @ resourceType='granite/contexthub/components/contexthub'}"/>*/-->

2)修改footerlibs:apps/wknd-events/components/structure/page/customfooterlibs.html

主要是js依赖设置,将导入到页尾

<!--/*Custom footer React libs*/--><sly data-sly-use.clientLib="${'/libs/granite/sightly/templates/clientlib.html'}"></sly><!--/*开发环境判断,是否调用pagemodel的messaging,这个库就是SPA Editor发送改变时的消息通道*/--><sly data-sly-test="${wcmmode.edit || wcmmode.preview}"data-sly-call="${clientLib.js @ categories='cq.authoring.pagemodel.messaging'}"></sly><!--/*引入react-app相关的js,这里包括了react相关的js依赖,因为是通过webpack打包的*/--><sly data-sly-call="${clientLib.js @ categories='wknd-events.react'}"></sly><!--/*原内容备份*/--><!--/*<sly data-sly-use.clientlib="/libs/granite/sightly/templates/clientlib.html"/><sly data-sly-call="${clientlib.js @ categories='wknd-events.base'}"/>*/-->

3)创建React入口页面body.html

在apps/wknd-events/components/structure/page下创建:body.html

<!--/*- body.html- includes p that will be targeted by SPA- SPA(这里是React)页面入口,React会在这里动态的插入DOM元素- 对应的可以参考:/react-app/src/index.js,里面的代码如下:- ReactDOM.render(<App />, document.getElementById('root'));*/--><p ></p>

4)安装与部署,不重复赘述了;然后访问以下页面测试:

http://localhost:4502/editor.html/content/wknd-events/react/home.html

5.3 Editable Components

https://helpx.adobe.com/experience-manager/kt/sites/using/getting-started-spa-wknd-tutorial-develop/react/chapter-1.html

5.3.1 说明

这篇教程在基于上一篇(项目初始化)的基础上,进行SPA的Editable Components开发。

重点:

  • 三个AEM SPA Editor JS SDK的安装和使用(集成)
  • SPA Editor SDK的概念和原理建议参考最先前的文章(4.6和4.8)
  • Text组件示例
  • Image组件示例
  • 示例为我们提供了一个SlingModel:HierarchyPage,并作出详细分析

5.3.2 SPA Editor SDK的安装和集成

SPA Editor SDK的概念和原理建议参考最先前的文章(4.6和4.8)。简要流程:AEM通过Sling Model导出JSON内容,SPA Editor SDK将JSON和React组件映射关联。

1)安装AEM SPA Editor JS SDK

# 打开控制台,进入react-app根目录cd <src>/aem-guides-wknd-events/react-app# 安装AEM SPA Editor JS SDK(3个)npm install @adobe/cq-spa-component-mappingnpm install @adobe/cq-spa-page-model-managernpm install @adobe/cq-react-editable-components# 安装一些其它的依赖(4个)npm install react-fast-comparenpm install typescript --save-devnpm install ajv --save-devnpm install clone --save-dev# 安装后检查package.json中的dependencies(7个)和devDependencies(4个)是否齐全
  • @adobe/cq-spa-component-mapping - 映射AEM Components和SPA Components,此模块不依赖特定的SPA框架
  • @adobe/cq-spa-page-model-manager - 提供API,来管理用于组成SPA(页面)的AEM Pages的Model表示,此模块不依赖特定的SPA框架
  • @adobe/cq-react-editable-components - 提供通用的React helpers和Components支持AEM authoring;此模块还封装了cq-spa-page-model-manager和cq-spa-component-mapping以支持React框架

2)现在可以开始将AEM SPA editor JS SDK集成进React中了。首先是通过JSON Model(来自AEM)初始化App,修改:react-app/src/index.js,就是react的入口JS

import React from 'react';import ReactDOM from 'react-dom';import { ModelManager, Constants } from '@adobe/cq-spa-page-model-manager';import './index.css';import App from './App';/*** 将ReactDOM渲染入口封装成函数,并初始化SPA Editor需要的相关属性* 可以看到依赖了App组件(真正的入口)*/function render(model) {ReactDOM.render((<App cqChildren={ model[Constants.CHILDREN_PROP] }cqItems={ model[Constants.ITEMS_PROP] }cqItemsOrder={ model[Constants.ITEMS_ORDER_PROP] }cqPath={ ModelManager.rootPath }locationPathname={ window.location.pathname }/>),document.getElementById('root'));}/*初始化ModelManager*/ModelManager.initialize({ path: process.env.REACT_APP_PAGE_MODEL_PATH }).then(render);

3)修改react-app/src/App.js,相当于首页,也是应用程序的入口

// src/App.jsimport React from 'react';import { Page, withModel, EditorContext, Utils } from '@adobe/cq-react-editable-components';import './App.css';//注意这里CSS样式的引入在示例源码中没有,实际测试发现在Editor模式下出现页面向下无限滚动,就是样式导致的,建议注释掉。最后还需要清除Chrome缓存!/*** This component is the application entry point* 这个组件就是应用程序入口* Page继承了react库的Component,因此可被React识别成组件* render()函数中,this.childComponents和this.childPages将\n* 自动导入React Components,这些组件由JSON Model驱动*/class App extends Page {render() {return (<p className="App"><header className="App-header"><h1>Welcome to AEM + React</h1></header>{ this.childComponents }{ this.childPages }</p>);}}export default withModel(App);

4)开始创建Page组件(React),在src下创建,目录结构如下:

/react-app/src/components/pagePage.jsPage.css

Page.js

/*** Page.js* - WKND specific implementation of Page* - Maps to wknd-events/components/structure/page*/import {Page, MapTo, withComponentMappingContext } from "@adobe/cq-react-editable-components";require('./Page.css');/*** 此组件是React Component的一个变体,将映射"structure/page"的resource type* 目前除了添加特定的css样式外没有做其他的功能更改* 在这个例子中,通过MapTo函数实现了AEM组件和React组件的映射* 映射resourceType:wknd-events/components/structure/page的AEM组件 -> 此React组件*/class WkndPage extends Page {get containerProps() {let attrs = super.containerProps;attrs.className = (attrs.className || '') + ' WkndPage ' + (this.props.cssClassNames || '');return attrs}}MapTo('wknd-events/components/structure/page')(withComponentMappingContext(WkndPage));

Page.css

/* Center and max-width the content */.WkndPage {max-width: 1200px;margin: 0 auto;padding: 12px;padding: 0;float: unset !important;}

5)在 /react-app/src/components 下创建:MappedComponents.js

/*** Dedicated file to include all React components that map to an AEM component* 导入所有和AEM映射的React组件的专用JS文件*/require('./page/Page');

6)更新 /react-app/src/index.js ,导入MappedComponents

// src/index.js...import App from './App';//include Mapped Components+ import "./components/MappedComponents";...

7)编译安装,测试访问:http://localhost:4502/content/wknd-events/react/home.html

审查元素,可以看到自己组件里的样式:

5.3.3 Text Component

自定义的React组件,映射了AEM的Text组件

1)创建如下结构的Text React组件,目录结构

/react-app/src/components/textText.jsText.css

2)Text.css暂时为空,Text.js代码如下:

/*** Text.js* Maps to wknd-events/components/content/text*/import React, {Component} from 'react';import {MapTo} from '@adobe/cq-react-editable-components';/*** Default Edit configuration for the Text component that interact with the Core Text component and sub-types* Text组件的默认的Edit配置,此配置与AEM的Core Text Component和sub-types交互* @type EditConfig* @type {{isEmpty: (function(*=): boolean), emptyLabel: string}}*/const TextEditConfig = {emptyLabel: 'Text',isEmpty: function(props) {return !props || !props.text || props.text.trim().length < 1;}};/*** Text React component* 作为普通的组件,仅需继承React的Component即可*/class Text extends Component {get richTextContent() {return <p dangerouslySetInnerHTML={{__html: this.props.text}}/>;}get textContent() {return <p>{this.props.text}</p>;}render() {return this.props.richText ? this.richTextContent : this.textContent;}}MapTo('wknd-events/components/content/text')(Text, TextEditConfig);

3)更新 react-app/src/components/MappedComponents.js ,加入新的Text组件依赖

/*** Dedicated file to include all React components that map to an AEM component* 导入所有和AEM映射的React组件的专用JS文件*/require('./page/Page');require('./text/Text');

4)安装部署,模块顺序注意一定要是:先react-app,后ui.apps

mvn -PautoInstallPackage -Padobe-public clean install

5)测试访问:http://localhost:4502/editor.html/content/wknd-events/react/home.html,此时的Text组件是一个空组件,可以进行编辑(过程中出现了点小问题,清除浏览器缓存即可),实际测试结果:

6)测试访问当前Page的JSON数据(Sling Model Exporter导出)

访问:http://localhost:4502/content/wknd-events/react/home.model.json

可以看到整体的页面结构,这就是SPA Editor进行逐层分析并做映射的数据源,具体JSON可自己分析,其中找到当前的Text组件的JSON数据如下:

此使可以重写结合步骤2中的Text.js的代码进行分析,可以看到:MapTo函数(来自@adobe/cq-react-editable-components)通过JSON字段 :type 进行组件映射,并且能够将JSON中的其它字段通过 this.props.xxx 进行访问,此组件中就如:

this.props.text === "文本文本文本"this.props.richText === true

5.3.4 Image Component

这一节以编写Image的React组件为例

1)创建如下结构的Image组件,目录结构

/react-app/src/components/imageImage.jsImage.css

2)Image.css暂时为空,Image.js代码如下:

/*** Image.js* Maps to wknd-events/components/content/image*/import React, {Component} from 'react';import {MapTo} from '@adobe/cq-react-editable-components';/*** Default Edit configuration for the Image component that interact with the Core Image component and sub-types* Image 组件的默认的Edit配置,此配置与AEM的Core Image Component和sub-types交互* @type EditConfig* @type {{isEmpty: (function(*=): boolean), emptyLabel: string}}*/const ImageEditConfig = {emptyLabel: 'Image',isEmpty: function(props) {return !props || !props.src || props.src.trim().length < 1;}};/*** Image React component* 作为普通的组件,仅需继承React的Component即可*/class Image extends Component {get content() {return <img src={this.props.src} alt={this.props.alt}title={this.props.displayPopupTitle && this.props.title}/>}render() {return (<p className="Image">{this.content}</p>);}}MapTo('wknd-events/components/content/image')(Image, ImageEditConfig);

3)更新 react-app/src/components/MappedComponents.js ,加入新的Image组件依赖

/*** Dedicated file to include all React components that map to an AEM component* 导入所有和AEM映射的React组件的专用JS文件*/require('./page/Page');require('./text/Text');require('./image/Image');

4)安装部署

  • 访问页面测试:
  • http://localhost:4502/editor.html/content/wknd-events/react/home.html

    同样的,会看到默认为空的Image组件,可以对它进行各种编辑,Image组件支持拖拽。

  • 查看页面JSON:
  • http://localhost:4502/content/wknd-events/react/home.model.json

    找到当前Component的JSON如下:

    "image": {"alt": "Rain","src": "/content/wknd-events/react/home/_jcr_content/root/responsivegrid/image.coreimg.jpeg/1591154158036/wknd-events.jpeg","srcUriTemplate": "/content/wknd-events/react/home/_jcr_content/root/responsivegrid/image.coreimg{.width}.jpeg/1591154158036/wknd-events.jpeg","areas": [],"uuid": "b1160ef2-2c65-4de8-8b5d-491e2be3ef56","widths": [],"lazyEnabled": false,"link": "/content/wknd-events/react.html",":type": "wknd-events/components/content/image"}

    和Text组件类似,Sling Model Exporter导出的JSON中的所有字段都能够被React的Image组件使用。

    下篇教程将会开始CSS样式的添加,并向前端开发圈子看齐~

    5.3.5 HierarchyPage Sling Model

    这一小节是额外内容,主要分析了官方的示例项目中提供的Sling Model:HierarchyPage。

    HierarchyPageImpl为我们提供了在单次请求中能够获取多AEM Pages的content的能力,也就是通过一个JSON的导出所有相关的content内容。

    1)在core子模块中,接口com.adobe.aem.guides.wkndevents.core.models.HierarchyPage代码片段如下:

    package com.adobe.aem.guides.wkndevents.core.models;import com.adobe.cq.export.json.ContainerExporter;import com.adobe.cq.export.json.hierarchy.HierarchyNodeExporter;import com.fasterxml.jackson.annotation.JsonIgnore;import com.fasterxml.jackson.annotation.JsonProperty;public interface HierarchyPage extends HierarchyNodeExporter, ContainerExporter {...}

    它继承了2个接口:

    • ContainerExporter:定义了容器组件的JSON,如:Page、Responsive Grid、Parsys
    • HierarchyNodeExporter:定义了层次节点的JSON,如:Root Page和它的Child Pages

    2)接着分析其实现类:com.adobe.aem.guides.wkndevents.core.models.impl.HierarchyPageImpl

    官方的说明:在示例项目中,HierarchyPageImpl被单独拷贝在项目中使用。不久后HierarchyPageImpl将通过Core Components库提供。开发者仍可以自行扩展该接口,但不再需要负责维护这个接口的实现了。请确保备份和更新。

    ...@Model(adaptables = SlingHttpServletRequest.class, adapters = {HierarchyPage.class, ContainerExporter.class}, resourceType = HierarchyPageImpl.RESOURCE_TYPE)@Exporter(name = ExporterConstants.SLING_MODEL_EXPORTER_NAME, extensions = ExporterConstants.SLING_MODEL_EXTENSION)public class HierarchyPageImpl implements HierarchyPage {/*** Resource type of associated with the current implementation*/protected static final String RESOURCE_TYPE = "wknd-events/components/structure/page";...}

    上面代码片段中可以看到,HierarchyPageImpl被注册成"wknd-events/components/structure/page"资源类型的Sling Model Exporter。如果需要在自己的项目中需要自定义的接口实现,就需要修改 RESOURCE_TYPE 指向自定义项目的 page component,也就是基础的page原型组件(AEM的)。

    最后再稍微介绍一下实现类中的几个重要方法:getRootModel() 和 getRootPage() 将返回对应的根节点,方法中有三个fields说明如下:

    /*** Is the current model to be considered as a model root* 帮助识别程序的rootPage。rootPage被用作app的运行入口,它还集成了所有的child pages*/private static final String PR_IS_ROOT = "isRoot";/*** Depth of the tree of pages* 标识在层次结构中收集子页面(child pages)的深度*/private static final String STRUCTURE_DEPTH_PN = "structureDepth";/*** List of Regexp patterns to filter the exported tree of pages* 正则表达式,用于忽略或排除不需要被自动收集(collect)的页面*/private static final String STRUCTURE_PATTERNS_PN = "structurePatterns";

    3)看完实现类,接着我们来看下如何查看和修改上一步中提到的字段的值。

    在AEM的Lite中,找到editable template的policy节点:

    /conf/wknd-events/settings/wcm/policies/wknd-events/components/structure/app/default

    这里我通过JSON获取此节点的值,浏览器访问:

    http://localhost:4502/conf/wknd-events/settings/wcm/policies/wknd-events/components/structure/app/default.json

    结果如下:

    {"jcr:primaryType": "nt:unstructured","jcr:title": "SPA Page","isRoot": true,"structurePatterns": "(react/)(?:(?!blog)(/)?)","jcr:description": "Default policy of the page","sling:resourceType": "wcm/core/components/policy/policy","structureDepth": "2"}

    官方的提示: 目前没有UI界面修改这些字段,只能在Lite中手动修改或在ui.content子模块修改xml文件。完善的功能将在未来推出。

    4)示例中的页面分析

    示例中的根页面react.html:http://localhost:4502/content/wknd-events/react.html,是基于 wknd-events-app-template 创建的,通过添加后缀 .model.json 访问此页面的JSON:

    http://localhost:4502/content/wknd-events/react.model.json

    可以查看当前页和其子页面home.html的content内容。

    5.4 Front End Development

    https://helpx.adobe.com/experience-manager/kt/sites/using/getting-started-spa-wknd-tutorial-develop/react/chapter-2.html

    5.4.1 说明

    这一章关注于前端开发(游离于AEM外)。前端开发者能够修改JS和CSS,并且能立即在浏览器上看到效果,这个过程中不需要完整的编译(development build)。当前流行的前端工具:Webpack development server、SASS、Styleguidist已被示例项目集成,加速前端开发。

    主要内容:

    • Sass的使用
    • 为了实现前端的独立开发,需要有获取Sling Model的JSON数据的能力,两种方式:
      • 为create-react-app设置AEM服务的代理
      • 通过本地Mock JSON数据文件(这里没有用到MockJS技术,不太推荐)
    • 添加Header组件
    • 为Image组件和Text组件添加样式
    • 在React工程中集成Responsive Grid,AEM Authoring页面后可以同步效果
    • Styleguidist的使用,自动生成Image和Text的Markdown文档

    5.4.2 安装Sass

    对于React组件,需要保证模块化的独立性,因此它推荐尽量避免复用具有相同class name的CSS样式(组件间)。示例项目将引入Sass的几个实用功能实现样式复用:variables、mixins。此项目还会遵循: SUIT CSS naming conventions. (SUIT是BEM表示法(块元素修饰符)的一种变体,用于创建一致的CSS规则)。

    1)安装Sass

    # 进入react-app目录cd <src>/aem-guides-wknd-events/react-app# 安装node-sassnpm install node-sass --save# 装完后就能在项目中看.scss文件了# 更多有关adding a Sass stylesheet with a React project的帮助:https://github.com/facebook/create-react-app/blob/master/packages/react-scripts/template/README.md#adding-a-sass-stylesheet

    2)创建以下目录结构和文件,用于存放scss共享样式:

    /react-app/src/components+ /styles+ _shared.scss+ _variables.scss

    3)_variables.scss内容:

    //variables for WKND Events//Typography$em-base: 20px;$base-font-size: 1rem;$small-font-size: 1.4rem;$lead-font-size: 2rem;$title-font-size: 5.2rem;$h1-font-size: 3rem;$h2-font-size: 2.5rem;$h3-font-size: 2rem;$h4-font-size: 1.5rem;$h5-font-size: 1.3rem;$h6-font-size: 1rem;$base-line-height: 1.5;$heading-line-height: 1.3;$lead-line-height: 1.7;$font-serif: 'Asar', serif;$font-sans: 'Source Sans Pro', sans-serif;$font-weight-light: 300;$font-weight-normal: 400;$font-weight-semi-bold: 600;$font-weight-bold: 700;//Colors$color-white: #ffffff;$color-black: #080808;$color-yellow: #FFEA08;$color-gray: #808080;$color-dark-gray: #707070;//Functional Colors$color-primary: $color-yellow;$color-secondary: $color-gray;$color-text: $color-gray;//Layout$max-width: 1200px;$header-height: 80px;$header-height-big: 100px;// Spacing$gutter-padding: 12px;// Mobile Breakpoints$mobile-screen: 160px;$small-screen: 767px;$medium-screen: 992px;

    4)shared.scss内容:

    @import './_variables';//Mixins@mixin media($types...) {@each $type in $types {@if $type == tablet {@media only screen and (min-width: $small-screen + 1) and (max-width: $medium-screen) {@content;}}@if $type == desktop {@media only screen and (min-width: $medium-screen + 1) {@content;}}@if $type == mobile {@media only screen and (min-width: $mobile-screen + 1) and (max-width: $small-screen) {@content;}}}}@mixin content-area () {max-width: $max-width;margin: 0 auto;padding: $gutter-padding;}@mixin component-padding() {padding: 0 $gutter-padding !important;}@mixin drop-shadow () {box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19);}

    5.4.3 通过代理获取JSON

    这小结主要讲了如何通过代理的方式获取AEM Server的JSON数据,实现在create-react-app手脚架上实时开发的目的。在create-react-app脚手架自带的服务器(localhost:3000)上做实时的前端开发时,可以通过这种代理的方式,获取来自AEM content的JSON Model和images等数据源。

    有关Create React App脚手架中的Proxying的更多信息,可以参考:Proxying API Requests in Development

    前提条件:

    • 一个运行在:http://localhost:4502/ 的AEM Server实例
    • create-react-app
    • 在react-app工程下打开编辑器

    过程:

    1)对于示例项目,在前面的章节中,我们已经通过create-react-app脚手架创建项目了,因此这里能够直接配置proxy功能。修改react-app/package.json,添加代理配置:

    // package.json..."scripts": {"start": "react-scripts start","build": "react-scripts build && clientlib --verbose","test": "react-scripts test","eject": "react-scripts eject"},"proxy": "http://localhost:4502",...

    2)在/react-app根目录下创建文件:.env.development

    # Configure Proxy end point,定义了Page Model的JSON数据源地址REACT_APP_PAGE_MODEL_PATH=/content/wknd-events/react.model.json

    .env.development 是一种环境变量的配置文件,它在node应用以开发模式(development mode)运行时加载。更多信息可以参考: environment variables can be found here 。

    其实在前面的章节中,文件 src/index.js 中已经使用到了这个环境变量:

    // src/index.js.../*初始化ModelManager*/ModelManager.initialize({ path: process.env.REACT_APP_PAGE_MODEL_PATH }).then(render);

    3)启动creat-react-app脚手架服务器(http://localhost:3000),控制台输入:

    # 进入react-app目录cd <src>/aem-guides-wknd-events/react-app# 启动服务npm run start # 可以直接:npm start

    4)登录AEM Server(http://localhost:4502),然后访问react服务:

    http://localhost:3000/content/wknd-events/react/home.html

    如果没出错误的话,你将在create-react-app服务中看到和AEM Server中一样的页面。

    **注意:**你必须提前登录AEM Server,否则代理将无法访问,导致页面为空白。

    在create-react-app中设置Proxy可能会出现跨域问题,如果你遇到了如下的问题,可以参考: AEM CORS configuration 。

    Fetch API cannot load http://localhost:4502/content.... No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://localhost:3000' is therefore not allowed access. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.

    5)将 /src/index.css 更名为 index.scss,然后更新内容:

    /* src/index.scss */@import './styles/shared';/* Google Font import */@import url('https://fonts.googleapis.com/css?family=Asar|Source+Sans+Pro:400,600,700');body {//font-weight: $normal;background-color: $color-white;font-family: $font-sans;margin: 0;padding: 0;font-weight: $font-weight-light;font-size: $em-base;text-align: left;color: $color-black;line-height: 1.5;line-height: 1.6;letter-spacing: 0.3px;}h1, h2, h3, h4 {font-family: $font-sans;}h1 {font-size: $h1-font-size;}h2 {font-size: $h2-font-size;}h3 {font-size: $h3-font-size;}h4 {font-size: $h4-font-size;}h5 {font-size: $h5-font-size;}h6 {font-size: $h6-font-size;}p {color: $color-text;font-family: $font-serif;}ul {list-style-position: inside;}// abstracts/overridesol, ul {padding-left: 0;margin-bottom: 0;}hr {height: 2px;//background-color: fade($dusty-gray, (.3*100));border: 0 none;margin: 0 auto;max-width: $max-width;}*:focus {outline: none;}textarea:focus, input:focus{outline: none;}body {overflow-x: hidden;}img {vertical-align: middle;border-style: none;width: 100%;}

    6)修改 /src/index.js 中的样式导入

    // src/index.js...- import './index.css';+ import './index.scss';...

    7)重新访问测试:http://localhost:3000/content/wknd-events/react/home.html

    这里可以在index.scss中为h1类样式添加color属性,然后查看浏览器动态更新:

    h1 {font-size: $h1-font-size;color: $color-yellow;}

    5.4.4 通过Mock获取JSON

    和上一小节中代理方式的目的一致,这一小节将通过使用静态的JSON文件来模拟JSON数据。通过这种方式,react-app工程将不依赖AEM Server实例。同时,此方法还能让前端开发者实时更新JSON数据、方便功能测试、模拟新的JSON相应实体,完全不依赖后端开发者。

    1)首先需要获取一份Sling Model Exporter导出的JSON数据,第一次获取是需要启动并登录AEM Server实例的。在示例项目中,访问下面的链接获取页面(react.html)的完整JSON,然后保存它。

    http://localhost:4502/content/wknd-events/react.model.json,这里给出我的示例(复制粘贴即可):

    {"title": "React App",":type": "wknd-events/components/structure/app",":itemsOrder": [],":items": {},":path": "/content/wknd-events/react",":hierarchyType": "page",":children": {"/content/wknd-events/react/home": {"title": "Home",":type": "wknd-events/components/structure/page",":itemsOrder": ["root"],":items": {"root": {"columnCount": 12,"allowedComponents": {"applicable": false,"components": [{"path": "/conf/wknd-events/settings/wcm/templates/wknd-events-page-template/structure/jcr:content/root/wcm/foundation/components/responsivegrid","title": "Layout Container"},{"path": "/conf/wknd-events/settings/wcm/templates/wknd-events-page-template/structure/jcr:content/root/wknd-events/components/content/image","title": "Image"},{"path": "/conf/wknd-events/settings/wcm/templates/wknd-events-page-template/structure/jcr:content/root/wknd-events/components/content/list","title": "List"},{"path": "/conf/wknd-events/settings/wcm/templates/wknd-events-page-template/structure/jcr:content/root/wknd-events/components/content/text","title": "Text"}]},"columnClassNames": {"responsivegrid": "aem-GridColumn aem-GridColumn--default--12"},"gridClassNames": "aem-Grid aem-Grid--12 aem-Grid--default--12",":itemsOrder": ["responsivegrid"],":items": {"responsivegrid": {"columnCount": 12,"allowedComponents": {"applicable": false,"components": [{"path": "/content/wknd-events/react/home/jcr:content/root/responsivegrid/wcm/foundation/components/responsivegrid","title": "Layout Container"},{"path": "/content/wknd-events/react/home/jcr:content/root/responsivegrid/wknd-events/components/content/image","title": "Image"},{"path": "/content/wknd-events/react/home/jcr:content/root/responsivegrid/wknd-events/components/content/list","title": "List"},{"path": "/content/wknd-events/react/home/jcr:content/root/responsivegrid/wknd-events/components/content/text","title": "Text"}]},"columnClassNames": {"image": "aem-GridColumn aem-GridColumn--default--12","text": "aem-GridColumn aem-GridColumn--default--12"},"gridClassNames": "aem-Grid aem-Grid--12 aem-Grid--default--12",":itemsOrder": ["text","image"],":items": {"text": {"text": "<p>Rain<b>&nbsp;forecast</b></p>\n<ol>\n<li>not a nici day</li>\n<li>ohh</li>\n</ol>\n","richText": true,":type": "wknd-events/components/content/text"},"image": {"alt": "Rain","src": "/content/wknd-events/react/home/_jcr_content/root/responsivegrid/image.coreimg.jpeg/1591154158036/wknd-events.jpeg","srcUriTemplate": "/content/wknd-events/react/home/_jcr_content/root/responsivegrid/image.coreimg{.width}.jpeg/1591154158036/wknd-events.jpeg","areas": [],"uuid": "b1160ef2-2c65-4de8-8b5d-491e2be3ef56","widths": [],"lazyEnabled": false,"link": "/content/wknd-events/react.html",":type": "wknd-events/components/content/image"}},":type": "wcm/foundation/components/responsivegrid"}},":type": "wcm/foundation/components/responsivegrid"}},":path": "/content/wknd-events/react/home",":hierarchyType": "page"}}}

    2)进入react-app工程,在目录 /react-app/public 下创建文件: mock.model.json ,复制前面内容

    /react-app/publicfavicon.icoindex.htmlmanifest.json+ mock.model.json/src...

    3)继续在public下创建目录 images ,存放图片静态文件,目录结构如下:

    PS:图片可以去这个网站获取 Unsplash.com

    /react-app/publicfavicon.icoindex.htmlmanifest.jsonmock.model.json+ /images+ mock-image.jpg/src...

    4)修改 mock.model.json 文件中的图片路径,搜索: wknd-events/components/content/image

    "image": {...- "src": "旧的图片地址",+ "src": "/images/mock-image.jpeg""srcUriTemplate": "...",...":type": "wknd-events/components/content/image"}

    5)更新 react-app/.env.development 环境变量文件,添加Mock的JSON路径:

    # Configure Proxy end point,定义了Page Model的JSON数据源地址# REACT_APP_PAGE_MODEL_PATH=/content/wknd-events/react.model.json# Request the JSON from Mock JSON,制定了本地的静态JSON数据源,PS:public目录下的文件发布后将在根路径下REACT_APP_PAGE_MODEL_PATH=mock.model.json

    6)重启 create-react-app 脚手架工程

    # 先ctrl+c结束进程,然后重新启动npm run start

    测试访问:http://localhost:3000/ 或 http://localhost:3000/content/wknd-events/react/home.html (有关链接这里有个疑问,为什么都可以?但测试过来只要是3000端口的任意路径都是能访问的,也就是说,目前暂时还没有将React的路由功能放进去)

    然后尝试修改 mock.model.json 内容,查看页面变化

    7)最后我补充一点,通过Mock JSON文件这种方式,并没有关闭Proxy代理,因为在查找JSON文件时,先在public目录下获取到了文件,因此不会再通过代理访问AEM Server。

    5.4.5 Header组件

    这一小节运用前面的知识创建Header组件。

    1)在 /react-app/src/components 下创建如下的目录结构和文件:

    /react-app/src/components+/header+Header.js+Header.scss

    Header.js

    // src/components/header/Header.jsimport React, {Component} from 'react';import './Header.scss';export default class Header extends Component {render() {return (<header className="Header"><p className="Header-wrapper"><h1 className="Header-title">WKND<span className="Header-title--inverse">_</span></h1></p></header>);}}

    Header.scss

    @import '../../styles/shared';.Header {background-color: $color-primary;height: $header-height;width: 100%;position: fixed;top: 0;z-index: 99;@include media(tablet,desktop) {height: $header-height-big;}&-wrapper {@include content-area();display: flex;justify-content: space-between;}&-title {font-family: 'Helvetica';font-size: 20px;float: left;padding-left: $gutter-padding;@include media(tablet,desktop) {font-size: 24px;}}&-title--inverse {color: $color-white;}}

    2)更新 react-app/src/App.js 文件,将Header组件包含进去

    ...+ import Header from './components/header/Header';class App extends Page {render() {return (<p className="App"><Header/> {/*添加Header组件*/}<header className="App-header"><h1>Welcome to AEM + React</h1></header>{this.childComponents}{this.childPages}</p>);}}...

    3)更新 react-app/src/index.scss ,添加header相关的样式

    /* index.scss */body {//font-weight: $normal;background-color: $color-white;font-family: $font-sans;margin: 0;padding: 0;font-weight: $font-weight-light;font-size: $em-base;text-align: left;color: $color-black;line-height: 1.5;line-height: 1.6;letter-spacing: 0.3px;+ padding-top: $header-height-big;+ @include media(mobile, tablet) {+ padding-top: $header-height;+ }}

    4)查看浏览器效果

    5.4.6 更新Image组件

    为第三章中的Image Component添加caption标题

    1)修改 react-app/src/components/image/Image.js

    ...+ import './Image.scss'; //也可以 require('./Image.scss');...class Image extends Component {+ get caption() {+ if(this.props.title && this.props.title.length > 0) {+ return <span className="Image-caption">{this.props.title}</span>;+ }+ return null;+ }get content() {return <img src={this.props.src} alt={this.props.alt}title={this.props.displayPopupTitle && this.props.title}/>}render() {return (<p className="Image">{this.content}+ {this.caption}</p>);}}...

    2)修改/创建文件 react-app/src/components/image/Image.scss

    @import '../../styles/shared';.Image {@include component-padding();&-image {margin: 2rem 0;width: 100%;border: 0;font: inherit;padding: 0;vertical-align: baseline;}&-caption {color: $color-white;background-color: $color-black;height: 3em;position: relative;padding: 20px 10px;top: -10px;@include drop-shadow();@include media(tablet) {padding: 25px 15px;top: -14px;}@include media(desktop) {padding: 30px 20px;top: -16px;}}}

    3)查看页面效果,发现没有caption

    4)修改 mock.model.json 文件,为image组件添加title属性

    "image": {+ "title": "This is a caption.",...":type": "wknd-events/components/content/image"}

    5)查看页面效果,caption出来了

    5.4.7 更新Text组件

    为前面的Text Component添加样式

    1)更新 react-app/src/components/text/Text.js

    ...+ import './Text.scss';//或者 require('./Text.scss')...class Text extends Component {...render() {+ let innercontent = this.props.richText ? this.richTextContent : this.textContent;+ return (<p className="Text">+ {innercontent}+ </p>)- //return this.props.richText ? this.richTextContent : this.textContent;}}...

    2)新增/修改 react-app/src/components/text/Text.scss

    @import '../../styles/shared';.Text {@include component-padding();}

    5.4.8 集成Responsive Grid

    原先在AEM的Editor.html模式下,有一个Layout Mode,这个Mode下我们可以动态的修改组件大小。SPA Editor 框架为我们引入了这个能力,我们只需要集成AEM的Responsive Grid到我们的React框架即可使用。

    starter 示例项目中,有一个Responsive Grid专用的client library已经被引入,位于 ui.apps 子模块。你可以通过下面路径查看:

    /aem-guides-wknd-events/ui.apps/src/main/content/jcr_root/apps/wknd-events/clientlibs/responsive-grid

    这个client library有一个category属性:wknd-events.grid ,并且包含了名为 grid.less 的样式文件,这个样式文件为Layout Mode提供了基础样式,请确保在接下来的React APP中,此CSS样式文件被正确加载。

    1)打开 /react-app/clientlib.config.js (clientlib插件配置文件),添加dependencies属性:

    module.exports = {...libs: {name: "react-app",allowProxy: true,categories: ["wknd-events.react"],serializationFormat: "xml",jsProcessor: ["min:gcc"],+ dependencies:["wknd-events.grid"],//添加样式库依赖assets: {js: ["build/static/**/*.js"],css: ["build/static/**/*.css"]}}};

    2)重新编译React工程,然后将AEM项目打包部署;接着访问测试:

    http://localhost:4502/editor.html/content/wknd-events/react/home.html

    现在你能够在Editor模式下通过toolbar修改组件的大小了,如下图所示:

    修改Text组件大小:

    3)现在可以进行自由的创作了,这里我照着原教程简单的Authoring了此页面,请自由发挥

    4)最后,为了确保在React工程中能够正常使用Responsive Grid CSS,做以下修改:

  • 找到 react-app/public/index.html ,引用AEM的responsive-grid.css
  • <head>+ <link rel="stylesheet" href="/etc.clientlibs/wknd-events/clientlibs/responsive-grid.css" type="text/css">...</head>

    **PS:**这里使用了 /etc.clientlibs 前缀获取clientlibs的资源文件,是一种固定用法,详细的介绍可自行查找官方文档有关于clientlibs篇章内容。

  • 修改 react-app/.env.development 文件,将JSON的获取方式从静态文件改回代理模式
  • # Configure Proxy end point,定义了Page Model的JSON数据源地址REACT_APP_PAGE_MODEL_PATH=/content/wknd-events/react.model.json# Request the JSON from Mock JSON,制定了本地的静态JSON数据源,PS:public目录下的文件发布后将在根路径下# REACT_APP_PAGE_MODEL_PATH=mock.model.json

    5)重启React工程 npm start ,访问测试:http://localhost:3000/content/wknd-events/react/home.html,此时你能看到经AEM的Authoring后的Responsive Grid相关的效果了。

    **提示:**如果你想完全的前端本地开发,不依赖AEM,你应该将AEM的responsive grid CSS文件内容复制出来,粘贴到React工程的 react-app/public/grid.css 文件中,并修改 react-app/public/index.html 文件中css的依赖路径。

    5.4.9 集成Styleguidist

    开发SPA组件的一种流行方法是单独开发它们。 这使开发人员可以跟踪组件可能处于的各种状态。有许多方便开发的工具如: Styleguidist 和 Storybook ,示例项目中将会使用Styleguidist,因为它将样式指南和文档组合到一个工具中。

    PS: 其实就是一个文档编写工具,通常来说文档和代码是分离的,代码更新了文档还需要修改, React Styleguidist 就能做到,可以参考:使用 React Styleguidist 编写文档 。

    1)安装Styleguidist

    # 进入react-app模块cd <src>/aem-guides-wknd-events/react-app# 安装Styleguidistnpm install react-styleguidist --save

    2)打开 react-app/package.json 添加Styleguidist相关的scripts脚本

    "scripts": {"start": "react-scripts start","build": "react-scripts build && clientlib --verbose","test": "react-scripts test","eject": "react-scripts eject",+ "styleguide": "styleguidist server",+ "styleguide:build": "styleguidist build"},

    3)在 react-app/ 根目录下创建文件: styleguide.config.js

    const path = require('path')module.exports = {components: 'src/components/**/[A-Z]*.js',assetsDir: 'public/images',require: [path.join(__dirname, 'src/index.scss')],ignore: ['src/components/**/Page.js', 'src/components/**/MappedComponents.js', 'src/components/**/Header.js', '**/__tests__/**', '**/*.test.{js,jsx,ts,tsx}', '**/*.spec.{js,jsx,ts,tsx}', '**/*.d.ts']}

    此配置文件描述:

    • components:扫描被export的组件,支持正则
    • asseDir:静态资源文件路径
    • require:引入文档需要的样式文件,__dirnam 表示被执行js文件的绝对路径,参考
    • ignore:忽略的组件

    4)导出Image组件,修改 react-app/src/components/image/Image.js

    /*** Image React component* 作为普通的组件,仅需继承React的Component即可*/export default class Image extends Component {...

    5)创建markdown文档: react-app/src/components/image/Image.md

    Image:​```js<Image alt="Alternative Text here"src="mock-image.jpeg"/>​```Image with a caption:​```js<Image alt="Alternative Text here" title="This is a caption" src="mock-image.jpeg"/>​```

    未启动Styleguidist server,此时md显示如下:

    6)按照前面的步骤修改Text组件,Text.md参考:

    Text:​```js<Text richText="false" text="Hello world!"/>​```RichText:​```js<Text richText="true" text="<p>Rain<b>&nbsp;forecast</b> Mock JSON</p>"/>​```

    7)启动 Styleguidist server:

    npm run styleguide# 运行成功后如下You can now view your style guide in the browser:Local: http://localhost:6060/On your network: http://10.2.20.118:6060/

    成功后截图:

    大功告成,下一章将会讲述有关导航栏和Router相关的知识,十分重要!

    5.5 Navigation and Routing

    https://helpx.adobe.com/experience-manager/kt/sites/using/getting-started-spa-wknd-tutorial-develop/react/chapter-3.html

    5.5.1 说明

    这一章是目前官方的最后一篇,主要介绍了AEM SPA中路由概念和使用方式。

    新技术点:

    • React Router
    • Font Awesome Icons

    主要内容:

    • AEM SPA中的路由介绍和简要实现原理
    • 安装React Router
    • 文章列表组件(导航文章)
    • 完善Header组件(添加了路由功能,导航回首页的button)
    • 使用Font Awesome Icons(图标工具)
    • React中重定向的使用

    示例代码下载地址

    5.5.2 SPA的路由方式

    在AEM SPA Editor中使用Navigation(导航栏)的**方式就是将不同的SPA页面视图和AEM的特定页面做映射。这种方式使得管理应用程序的多个模块变得简单,并且能够让内容制作者(content author)编辑独立的页面视图。更多有关路由的介绍,可移步 [4.11 SPA Model Routing](#4.11 SPA Model Routing)

    每个AEM的Page表示在SPA框架(React)中,都会被 <Router> 标签包裹,标签中还需要AEM Page的路径,如下面伪代码描述的:

    // React JSX 伪代码,路由示例render(){return (<BrowserRouter><App><Route path="/content/wknd-events/react/home"><WkndPage cqPath="/content/wknd-events/react/home" /> </Route><Route path="/content/wknd-events/react/home/first-article"><WkndPage cqPath="/content/wknd-events/react/home/first-article" /> </Route><Route path="/content/wknd-events/react/home/second-article"><WkndPage cqPath="/content/wknd-events/react/home/second-article" /> </Route></App></BrowserRouter>);}

    在第一章中我们已经讨论过了,React App是通过AEM的JSON Model驱动的。JSON Model通过一个叫 HierarchyPage 的Sling Model,将多个AEM Pages的内容(content)包含在了一个请求中。这种方式允许React App在初始化页面时,直接将几乎所有的数据内容加载,并且在用户后续的浏览中,应当最小化服务端(server-side)的子请求。有关 HierarchyPage的介绍和实现 可参考前文 [5.3.5](#5.3.5 HierarchyPage Sling Model) 。

    以下假代码是一段AEM导出的JSON示例,注意这个JSON结构和JSX中的结构能十分完整的映射:

    {":type": "wknd-events/components/structure/app",":itemsOrder": [],":items": {},":hierarchyType": "page",":path": "/content/wknd-events/react",":children": {"/content/wknd-events/react/home": {},"/content/wknd-events/react/home/first-article": {},"/content/wknd-events/react/home/second-article": {}},"title": "React App"}

    5.5.3 安装React Router

    React Router 为React框架提供了一系列的导航组件,提供了对页面视图的管理功能。React Router分为三个子模块:

    • react-router
    • react-router-dom
    • react-router-native

    示例项目中,由于需要发布到Web平台,因此将使用 react-router-dom ,更多信息请参考: React Router for the Web

    1)安装 react-router 和 react-router-dom,在react-app工程下打开命令行:

    # 进入react-appcd react-app# 安装react-routernpm install react-router --save# 安装react-router-domnpm install react-router-dom

    2)在react-app下创建 utils 工具类文件夹,然后添加工具类 RouteHelper.js ,目录结构如下:

    /react-app/src/utilsRouteHelper.js

    RoutHelper.js(力所能及的注释了)

    /*** Helper that facilitate the use of the {@link Route} component* 对 {@link Route} 组件的改进工具类*/import React, {Component} from 'react';import {Route} from 'react-router-dom';import { withRouter } from 'react-router';/*** Returns a composite component where a {@link Route} component wraps the provided component* 将返回(传入组件)经由 {@link Route} 组件装饰后的组件** @param {React.Component} WrappedComponent - React component to be wrapped;需要被装饰的React组件* @param {string} [extension=html] - extension used to identify a route amongst the tree of resource URLs;为路径添加后缀(默认为.html)* @returns {CompositeRoute}*/export const withRoute = (WrappedComponent, extension) => {return class CompositeRoute extends Component {render() {//获取传入组件的cqPath属性值let routePath = this.props.cqPath;//当为空时默认返回原组件,还会携带上包装器的相关属性if (!routePath) {return <WrappedComponent {...this.props}/>;}//判断路径后缀,设置默认值extension = extension || 'html';// 最终的路径组成: Context path + route path + extensionreturn <Route key={ routePath }path={ '(.*)' + routePath + '.' + extension }render={//绑定了一个渲染函数,好像用了React的向上提升?具体有点忘了(routeProps) => {return <WrappedComponent {...this.props} {...routeProps}/>;}}/>}}};/*** ScrollToTop component will scroll the window on every navigation.* wrapped in in `withRouter` to have access to router's props.* 此组件主要作用是:在每次点击导航栏按钮后将页面滚动至顶部* 这个组件将被withRouter装饰(注意,不是自定义的withRoute,它来自react-router库),能够访问路由的属性*/class ScrollToTop extends Component {//React生命周期函数,在组件更新时触发componentDidUpdate(prevProps) {//当location地址发生变化时if (this.props.location !== prevProps.location) {window.scrollTo(0, 0)}}render() {return this.props.children}}export default withRouter(ScrollToTop);

    说明: withRoute(第一个组件包装方法)是可复用的组件,能包装任何React组件。这里主要将用于包装Page组件以提供页面路由功能。

    程序中的Header组件是一个固定组件,当我们导航到不同页面时,都希望将页面滚动到顶部,上面的ScrollToTop组件提供了这个功能,更详细的信息可以参考: scroll restoration and React Router

    3)更新 react-app/src/index.js ,主要将 App 组件用 BrowserRouter 和 ScrollToTop 装饰:

    ...+ import {BrowserRouter} from 'react-router-dom';+ import ScrollToTop from './utils/RouteHelper';...function render(model) {ReactDOM.render((+ <BrowserRouter>+ <ScrollToTop><App cqChildren={ model[Constants.CHILDREN_PROP] }cqItems={ model[Constants.ITEMS_PROP] }cqItemsOrder={ model[Constants.ITEMS_ORDER_PROP] }cqPath={ ModelManager.rootPath }locationPathname={ window.location.pathname }/>+ </ScrollToTop>+ </BrowserRouter>),document.getElementById('root'));}...

    说明: BrowserRouter 是由react-router-dom提供的,通过HTML5的history API同步App UI界面和URL,这样可以轻松的深度链接到程序的特定页面视图。

    4)更新 Page组件react-app/src/components/page/Page.js ,使用 RouteHelper 中的自定义包装器 withRoute 包装

    ...+ import {withRoute} from '../../utils/RouteHelper';...class WkndPage extends Page {...}- //MapTo('wknd-events/components/structure/page')(withComponentMappingContext(WkndPage));+ MapTo('wknd-events/components/structure/page')(withComponentMappingContext(withRoute(WkndPage)));

    概括说明: AEM的Resource wknd-events/components/structure/page 表示一个AEM Page对象,它将被SPA Editor映射成React组件 WkndPage 。通过 withRoute 包装器将所有page包装成 Route ,从而能被导航。

    5.5.4 List Component

    这一小节将实现一个List React组件,它能够显示链接列表。与此List React组件映射的AEM组件( AEM List component )来自AEM Core Components。

    List组件的JSON模型示例如下:

    "list": {"dateFormatString": "yyyy-MM-dd","items": [{"url": "/content/wknd-events/react/home/first-article.html","path": "/content/wknd-events/react/home/first-article","description": null,"title": "First Article","lastModified": 1539529744910 //时间戳},{"url": "/content/wknd-events/react/home/second-article.html","path": "/content/wknd-events/react/home/second-article","description": null,"title": "Second Article","lastModified": 1539532397436}],"showDescription": false,"showModificationDate": false,"linkItems": false,":type": "wknd-events/components/content/list"}

    1)创建 List 组件,在 react-app/src/components 下创建如下目录和文件:

    /react-app/src/components/listList.jsList.scss

    2)编写 List.js (力所能及的注释了)

    import React, {Component} from 'react';import {MapTo} from '@adobe/cq-react-editable-components';import {Link} from "react-router-dom";import './List.scss';/*** 1 编写EditConfig,作为占位符(placeholder),并给出组件为空时的字符串显示* @type {{isEmpty: (function(*=): boolean), emptyLabel: string}}*/const ListEditConfig = {emptyLabel: 'List',isEmpty: function (props) {return !props || !props.items || props.items.length < 1;}};/*** 2 编写ListItem组件,用于渲染li和link,将作为List组件的模块* ListItem renders the inpidual items in the list* ListItem组件需要传递以下属性:* - title* - url* - path* - date* 注意:{@link Link} 组件来自react-router-dom,不是标准的锚标记,<Link>标签的显示与传统<a>很像\n* 但是<Link>标签是通过React Router进行导航的,它不会刷新页面*/class ListItem extends Component {get date() {if (!this.props.date) {return null;}let date = new Date(this.props.date);//这里时区我改成了中文,参考 https://juejin.im/post/5ac7079f5188255c637b3233 原先是:en-USreturn date.toLocaleDateString('zh');}render() {if (!this.props.path || !this.props.title || !this.props.url) {return null;}return (<li className="ListItem" key={this.props.path}><Link className="ListItem-link" to={this.props.url}>{this.props.title}<span className="ListItem-date">{this.date}</span></Link></li>);}}/*** 3 编写List组件,在里面通过遍历集合数据,并将属性传递给ListItem组件渲染* 此组件需要一个array集合数据:items,将从JSON Model中获取* export dafault作为默认组件导出,主要用于styleguide* 最后需要映射AEM组件:wknd-events/components/content/list* List renders the list contents and maps wknd-events/components/content/list*/export default class List extends Component {render() {return (<p className="List"><ul className="List-wrapper">{this.props.items && this.props.items.map((listItem, index) => {return <ListItem path={listItem.path} url={listItem.url}title={listItem.title} date={listItem.lastModified}/>})}</ul></p>);}}MapTo("wknd-events/components/content/list")(List, ListEditConfig);

    3)编写 List.scss

    @import '../../styles/shared';.List {@include component-padding();}.ListItem {list-style: none;float: left;width: 100%;margin-bottom: 1em;font-size: $lead-font-size;padding: 4px;color: #0045ff;&:hover {background-color: #ededed;}&-link {text-decoration: none;}&-date {width: 100%;float: left;color: $color-secondary;font-size: $base-font-size;}}

    4)编写Styleguidist的markdown文档,创建 react-app/src/components/list/List.md ,在这个文档中我们为List组件模拟了数据:

    **说明:**由于使用了react-router的 <Link> 标签,我们需要模拟 <BrowserRouter> 和 <Route> 来装饰List组件。

    List Component:​```jsconst {Route} = require('react-router-dom');const {BrowserRouter} = require('react-router-dom');let items = [{url: "#",path: "item1",title: "First Article",lastModified: 1539529744910},{url: "#",path: "item2",title: "Second Article",lastModified: 1539532397436}];<BrowserRouter><Route key="list-example" path="sample"><List items={items} /></Route></BrowserRouter>​```

    5)运行styleguide服务测试:

    npm run styleguide

    遇到问题:List Component不显示 ,经排查,最终发现是 <Route> 组件的使用问题。React文档

    //原先是这样的,注意<Route>中path的属性,找不到,因此不渲染<BrowserRouter><Route key="list-example" path="sample"><List items={items} /></Route></BrowserRouter>//测试使用“#”也不行,使用“/”可以<Route key="list-example" path="/">

    成功后的效果图:

    6)将 List 组件注册到 react-app/src/components/MappedComponents.js

    require('./page/Page');require('./text/Text');require('./image/Image');+ require('./list/List');

    7)编译react-app项目,然后安装部署AEM

    8)打开 AEM Sites Console ,在 /content/wknd-events/react/home 下创建两个子页面: First ArticleSecond Article (first-article 和 second-article),使用 WKND Event Page template

    9)打开 http://localhost:4502/editor.html/content/wknd-events/react/home.html 页面,添加List Component,配置如下内容:

    10)在Preview视图下测试跳转链接,此时你应该能够跳转进子页面,子页面里的组件也是能在Editor模式下编辑的。

    接着访问非Editor环境: http://localhost:4502/content/wknd-events/react/home.html

    尝试点击导航列表,可以发现页面不会刷新,浏览器URL将被更新,浏览器的退后按钮也能工作。在导航时,可以通过审查network发现除了页面第一次初始化加载之后不会有任何的请求流量。

    目前我们还没有办法从子页面返回Home页(不通过浏览器的后退按钮),下一小结将对Header组件添加一个动态的返回按钮。

    5.5.5 更新Header组件

    这一小节将为Header组件添加一个后退按钮,和前面的List组件类似,这里也将使用React Router提供的组件实现。

    1)更新 react-app/src/components/header/Header.js ,完整代码如下,具体内容见注释:

    // src/components/header/Header.jsimport React, {Component} from 'react';import './Header.scss';/*** 1.0 react router依赖*/+ import {Link} from "react-router-dom";+ import {withRouter} from 'react-router';/*** 2.0 移除export default,原先代码:export default class Header extends Component {...}* 2.1 添加新的getter方法:homeLink(),这个方法中将会根据location URL判断当前route是否是HomePage* 如果不是HomePage,将会生成一个返回HomePage后退Link* 2.2 更新render()函数,添加方法调用homeLink* 2.3 最后在js最底部export出由withRouter函数装饰后的Header组件,确保Header组件能够访问location的props*/+ class Header extends Component {+ get homeLink() {let currLocation;currLocation = this.props.location.pathname;currLocation = currLocation.substr(0, currLocation.length - 5);if (this.props.navigationRoot && currLocation !== this.props.navigationRoot) {return (<Link className="Header-action" to={this.props.navigationRoot + ".html"}>Back</Link>);}return null;}render() {return (<header className="Header"><p className="Header-wrapper"><h1 className="Header-title">WKND<span className="Header-title--inverse">_</span></h1>+ <p className="Header-tools">+ {this.homeLink}+ </p></p></header>);}}+ export default withRouter(Header);

    2)修改 react-app/src/App.js ,为Header组件传递navigationRoot属性:

    return (<p className="App">- <Header/> {/*添加Header组件*/}+ <Header navigationRoot="/content/wknd-events/react/home"/>...{this.childComponents}{this.childPages}</p>);

    3)确保环境变量文件 react-app/.env.development 中Model获取方式是通过代理:

    # Configure Proxy end point, Request the JSON from AEM, 定义了Page Model的JSON数据源地址REACT_APP_PAGE_MODEL_PATH=/content/wknd-events/react.model.json

    4)重启 react-app 工程:npm start ,确保启动并登录了AEM Server,然后访问测试:

    http://localhost:3000/content/wknd-events/react/home.html

    此时你点击List组件,跳转到子页面后将会在Header组件上自动生成一个back按钮,如图:

    下面将会为这个Back按钮添加一些样式

    5.5.6 添加Font Awesome图标

    Font Awesome 是一款流行的icon合集,它为React提供了一套 官方的组件 ,可以十分简单方便地集成进React应用,示例项目中也将引入一小部分icons。

    1)安装Font Awesome,官方介绍文档: Font Awesome

    # 在react-app工程下打开终端cd <src>/aem-guides-wknd-events/react-app# 安装Font Awesomenpm install @fortawesome/fontawesome-svg-core --savenpm install @fortawesome/free-solid-svg-icons --savenpm i @fortawesome/react-fontawesome --save

    2)编写 Icons.js 工具类:react-app/src/utils/Icons.js ,添加一些需要使用到的Icons:

    import { library } from '@fortawesome/fontawesome-svg-core';import { faCheckSquare, faChevronLeft, faSearch, faHeadphonesAlt, faMusic, faCamera, faFutbol, faPaintBrush, faTheaterMasks} from '@fortawesome/free-solid-svg-icons';library.add(faCheckSquare, faChevronLeft, faSearch, faHeadphonesAlt, faMusic, faCamera, faFutbol, faPaintBrush, faTheaterMasks);

    3)修改 react-app/src/components/header/Header.js 组件,添加 ”chevron-left“ icon:

    + import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';+ require('../../utils/Icons');//顺序一定要在import后...class Header extends Component {get homeLink() {...if (this.props.navigationRoot && currLocation !== this.props.navigationRoot) {return (<Link className="Header-action" to={this.props.navigationRoot + ".html"}>- Back+ <FontAwesomeIcon icon="chevron-left" /></Link>);}return null;}...}

    4)修改 react-app/src/components/header/Header.scss ,添加样式(下列全部):

    @import '../../styles/shared';$icon-size-lg: 46px;$icon-size-md: 40px;$icon-size-sm: 32px;.Header {...&-tools {padding-top: 8px;padding-right: $gutter-padding;}&-action {background: $color-white;border-radius: 100%;width: $icon-size-sm;height: $icon-size-sm;font-size: 18px;color: $color-black;text-align: center;align-content: center;float: left;margin-right: 1.5rem;&:last-child {margin-right: 0;}.svg-inline--fa {position: relative;top: 2.5px;right: 1px;}@include media(desktop) {width: $icon-size-lg;height: $icon-size-lg;font-size: 26px;}@include media(tablet) {width: $icon-size-md;height: $icon-size-md;font-size: 22px;}}}

    5)重启 react-app ,测试,此时Back按钮应该成为了图标:

    5.5.7 重定向到首页

    这是本章的最后一小节,这里将更新 index.js ,也就是应用的入口JS文件。通过添加 Redirect 组件(提供自react-router),实现访问 react.html 页面时自动重定向到 home.html

    1)更新 react-app/src/index.js ,导入react-router的 Redirect he Route组件:

    import { Redirect, Route } from 'react-router';

    2)更新render函数,添加重定向规则(从react.html -> home.html):

    function render(model) {ReactDOM.render((<BrowserRouter><ScrollToTop>+ <Route path="/content/wknd-events/react.html" render={() => (+ <Redirect to="/content/wknd-events/react/home.html"/>+ )}/><App cqChildren={model[Constants.CHILDREN_PROP]}cqItems={model[Constants.ITEMS_PROP]}cqItemsOrder={model[Constants.ITEMS_ORDER_PROP]}cqPath={ModelManager.rootPath}locationPathname={window.location.pathname}/></ScrollToTop></BrowserRouter>),document.getElementById('root'));}

    3)重启 react-app ,访问:http://localhost:3000/content/wknd-events/react.html ,此时将会自动重定向到 home.html

    4)将项目编译部署到AEM Server,测试访问:http://localhost:4502/editor.html/content/wknd-events/react.html

  • 此时你能够导航到List组件中的子页面,Header组件的back按钮也将正常显示样式
  • 注意在AEM Editor模式下,浏览器的url不能够正确反射回来,需要在非Editor下测试访问:http://localhost:4502/content/wknd-events/react.html ,可以看到重定向和HTML5的History API都能够正常工作了
  • 需要做网站?需要网络推广?欢迎咨询客户经理 13272073477