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

前言

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

5 AEM SPA React完整教程

文档:

5.1 Overview

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

技能要求:

环境要求:

开发工具:

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

git clone git@github.com:Adobe-Marketing-Cloud/aem-guides-wknd-events.git
cd aem-guides-wknd-events
git 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,详见下图:

521

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.3
npm install aem-clientlib-generator --save-dev
# 或跟着教程版本1.4.1
npm install aem-clientlib-generator@1.4.1 --save-dev

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

# 尝试更新所有包,没用
npm cache clean --force
npm update
# 尝试更新部分包,没用
npm update caniuse-lite browserslist
# 通过强制更新工具:https://www.npmjs.com/package/npm-update-all,太慢了
npm install npm-update-all -g
npm-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 control
react-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.1
npm -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-app
mvn clean install 
# 通过frontend-maven-plugin插件,将自动运行配置的NPM脚本,从而编译react-app

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

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

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

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

2788

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

  • react-app
  • core
  • apps
  • content

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

7618

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

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-mapping
npm install @adobe/cq-spa-page-model-manager
npm install @adobe/cq-react-editable-components
# 安装一些其它的依赖(4个)
npm install react-fast-compare
npm install typescript --save-dev
npm install ajv --save-dev
npm install clone --save-dev
# 安装后检查package.json中的dependencies(7个)和devDependencies(4个)是否齐全

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.js
import 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 (
        <div className="App">
          <header className="App-header">
            <h1>Welcome to AEM + React</h1>
          </header>
          { this.childComponents }
          { this.childPages }
        </div>
    );
  }
}

export default withModel(App);

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

/react-app
	/src
		/components
			/page
				Page.js
				Page.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

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

6797

5.3.3 Text Component

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

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

/react-app
	/src
		/components
			/text
				Text.js
				Text.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 <div dangerouslySetInnerHTML={{__html:  this.props.text}}/>;
    }

    get textContent() {
        return <div>{this.props.text}</div>;
    }

    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组件是一个空组件,可以进行编辑(过程中出现了点小问题,清除浏览器缓存即可),实际测试结果:

9664

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

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

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

9528

此使可以重写结合步骤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
			/image
				Image.js
				Image.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 (<div className="Image">
            {this.content}
        </div>);
    }
}

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)安装部署

  1. 访问页面测试:

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

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

  1. 查看页面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-sass
npm 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/overrides

ol, 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
	/public
		favicon.ico
        index.html
        manifest.json
      + mock.model.json
	/src
		...

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

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

/react-app
	/public
		favicon.ico
        index.html
        manifest.json
        mock.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.js

import React, {Component} from 'react';
import './Header.scss';

export default class Header extends Component {

    render() {
        return (
            <header className="Header">
                <div className="Header-wrapper">
                    <h1 className="Header-title">WKND<span className="Header-title--inverse">_</span></h1>
                </div>
            </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 (
            <div className="App">
                <Header/> {/*添加Header组件*/}
                <header className="App-header">
                    <h1>Welcome to AEM + React</h1>
                </header>
                {this.childComponents}
                {this.childPages}
            </div>
        );
    }
}
...

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)查看浏览器效果

8951

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 (<div className="Image">
                {this.content}
+               {this.caption}
            </div>);
    }
}
...

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

0795

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

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

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

0754

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 (<div className="Text">
+            {innercontent}
+        </div>)
-        //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修改组件的大小了,如下图所示:

7138

修改Text组件大小:

6607

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

9305

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

  1. 找到 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篇章内容。

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

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

1)安装Styleguidist

# 进入react-app模块
cd <src>/aem-guides-wknd-events/react-app
# 安装Styleguidist
npm 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显示如下:

4091

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/

成功后截图:

5674

大功告成,下一章将会讲述有关导航栏和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-app
cd react-app
# 安装react-router
npm install react-router --save
# 安装react-router-dom
npm install react-router-dom

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

/react-app
	/src
		/utils
			RouteHelper.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 + extension
            return <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
			/list
				List.js
				List.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 individual 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-US
        return 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 (
            <div 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>
            </div>
        );
    }
}
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:
 
​```js
const {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="/">

成功后的效果图:

2308

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

6062

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

555

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.js

import 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">
                <div className="Header-wrapper">
                    <h1 className="Header-title">WKND<span className="Header-title--inverse">_</span></h1>
+                    <div className="Header-tools">
+                        {this.homeLink}
+                    </div>
                </div>
            </header>
        );
    }
}
+ export default withRouter(Header);

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

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

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按钮,如图:

5051

下面将会为这个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 Awesome
npm install @fortawesome/fontawesome-svg-core --save
npm install @fortawesome/free-solid-svg-icons --save
npm 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按钮应该成为了图标:

3088

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

  1. 此时你能够导航到List组件中的子页面,Header组件的back按钮也将正常显示样式
  2. 注意在AEM Editor模式下,浏览器的url不能够正确反射回来,需要在非Editor下测试访问:http://localhost:4502/content/wknd-events/react.html ,可以看到重定向和HTML5的History API都能够正常工作了
  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值