欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

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

程序员文章站 2024-01-15 22:31:10
...

前言

这篇文章是对官方教程的整理,并实践动手排坑后做的总结,主要以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 aaa@qq.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,详见下图:

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

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 aaa@qq.com --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 aaa@qq.com --update-db
# 实际测试可行
npx aaa@qq.com --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运行配置如下:

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

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

  • react-app
  • core
  • apps
  • content

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

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

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

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

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

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

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

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

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

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

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

此使可以重写结合步骤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)查看浏览器效果

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

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

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

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

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

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

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

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修改组件的大小了,如下图所示:

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

修改Text组件大小:

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

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

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

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显示如下:

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

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/

成功后截图:

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

大功告成,下一章将会讲述有关导航栏和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="/">

成功后的效果图:

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

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

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

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

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

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

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

下面将会为这个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按钮应该成为了图标:

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

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都能够正常工作了