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
- 5.2 Project Setup
- 5.3 Editable Components
- 5.3.1 说明
- 5.3.2 SPA Editor SDK的安装和集成
- 5.3.3 Text Component
- 5.3.4 Image Component
- 5.3.5 HierarchyPage Sling Model
- 5.4 Front End Development
- 5.4.1 说明
- 5.4.2 安装Sass
- 5.4.3 通过代理获取JSON
- 5.4.4 通过Mock获取JSON
- 5.4.5 Header组件
- 5.4.6 更新Image组件
- 5.4.7 更新Text组件
- 5.4.8 集成Responsive Grid
- 5.4.9 集成Styleguidist
- 5.5 Navigation and Routing
5 AEM SPA React完整教程
文档:
- Getting Started with React and AEM SPA Editor
- GitHub: WKND Events SPA Editor Project 每章教程都有对应Github的代码
5.1 Overview
https://helpx.adobe.com/experience-manager/kt/sites/using/getting-started-spa-wknd-tutorial-develop/react.html
技能要求:
- Node.js
- npm
- webpack
- Scss - todo
- React Styleguidist - todo
环境要求:
- Java 1.8
- AEM 6.5 or AEM 6.4 + SP2
- Apache Maven (3.3.9 or newer)
- Node.js v10+
- npm 6+
开发工具:
- IntelliJ with AEM IDE Tooling - 这是那个用不起来的工具。。
- Visual Studio - AEM Sync
Starter项目下载(建议从这里开始,每一章进行实操练习):
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,详见下图:
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运行配置如下:
注意:由于npm中版本的不同,因此react-app编译时会有问题(有个依赖过期了),导致无法进行完整流程的编译和部署,目前此问题已修复,若未修复则需要单独编译完再打包。各个子模块的编译顺序:
- react-app
- core
- apps
- content
注意: IDEA的运行配置设置中需要**Profile:adobe-public(用于解决运行时依赖问题),如下图:
5.2.4 集成React App到Page
其实就是将SPA导出的Webpack工程集成到AEM的structure/page页面模板中
1)修改headerlibs:apps/wknd-events/components/structure/page/customheaderlibs.html
主要是meta和css设置,将导入到页首,具体的分析可以参考前文章节[4.9 SPA Page Component](#4.9 SPA Page Component)
<!--/*Custom Headerlibs for React Site*/-->
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
<!--/*指定JSON传输*/-->
<meta property="cq:datatype" data-sly-test="${wcmmode.edit || wcmmode.preview}" content="JSON" />
<!--/*通知SPA Editor是否是edit模式*/-->
<meta property="cq:wcmmode" data-sly-test="${wcmmode.edit}" content="edit" />
<!--/*通知SPA Editor是否是preview模式*/-->
<meta property="cq:wcmmode" data-sly-test="${wcmmode.preview}" content="preview" />
<!--/*引入自定义的slingmodel,其中获取rootUrl是示例写的方法*/-->
<meta property="cq:pagemodel_root_url"
data-sly-use.page="com.adobe.aem.guides.wkndevents.core.models.HierarchyPage"
content="${page.rootUrl}" />
<!--/*引入react-app相关的css*/-->
<sly data-sly-use.clientlib="/libs/granite/sightly/templates/clientlib.html" />
<sly data-sly-call="${clientlib.css @ categories='wknd-events.react'}" />
<!--/*原内容备份*/-->
<!--/*<sly data-sly-use.clientLib="/libs/granite/sightly/templates/clientlib.html"
data-sly-call="${clientlib.css @ categories='wknd-events.base'}"/>
<sly data-sly-resource="${'contexthub' @ resourceType='granite/contexthub/components/contexthub'}"/>*/-->
2)修改footerlibs:apps/wknd-events/components/structure/page/customfooterlibs.html
主要是js依赖设置,将导入到页尾
<!--/*Custom footer React libs*/-->
<sly data-sly-use.clientLib="${'/libs/granite/sightly/templates/clientlib.html'}"></sly>
<!--/*开发环境判断,是否调用pagemodel的messaging,这个库就是SPA Editor发送改变时的消息通道*/-->
<sly data-sly-test="${wcmmode.edit || wcmmode.preview}"
data-sly-call="${clientLib.js @ categories='cq.authoring.pagemodel.messaging'}"></sly>
<!--/*引入react-app相关的js,这里包括了react相关的js依赖,因为是通过webpack打包的*/-->
<sly data-sly-call="${clientLib.js @ categories='wknd-events.react'}"></sly>
<!--/*原内容备份*/-->
<!--/*
<sly data-sly-use.clientlib="/libs/granite/sightly/templates/clientlib.html"/>
<sly data-sly-call="${clientlib.js @ categories='wknd-events.base'}"/>*/-->
3)创建React入口页面body.html
在apps/wknd-events/components/structure/page下创建:body.html
<!--/*
- body.html
- includes 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个)是否齐全
- @adobe/cq-spa-component-mapping - 映射AEM Components和SPA Components,此模块不依赖特定的SPA框架
- @adobe/cq-spa-page-model-manager - 提供API,来管理用于组成SPA(页面)的AEM Pages的Model表示,此模块不依赖特定的SPA框架
- @adobe/cq-react-editable-components - 提供通用的React helpers和Components支持AEM authoring;此模块还封装了cq-spa-page-model-manager和cq-spa-component-mapping以支持React框架
2)现在可以开始将AEM SPA editor JS SDK集成进React中了。首先是通过JSON Model(来自AEM)初始化App,修改:react-app/src/index.js,就是react的入口JS
import React from 'react';
import ReactDOM from 'react-dom';
import { ModelManager, Constants } from '@adobe/cq-spa-page-model-manager';
import './index.css';
import App from './App';
/**
* 将ReactDOM渲染入口封装成函数,并初始化SPA Editor需要的相关属性
* 可以看到依赖了App组件(真正的入口)
*/
function render(model) {
ReactDOM.render(
(<App cqChildren={ model[Constants.CHILDREN_PROP] }
cqItems={ model[Constants.ITEMS_PROP] }
cqItemsOrder={ model[Constants.ITEMS_ORDER_PROP] }
cqPath={ ModelManager.rootPath }
locationPathname={ window.location.pathname }/>),
document.getElementById('root'));
}
/*初始化ModelManager*/
ModelManager.initialize({ path: process.env.REACT_APP_PAGE_MODEL_PATH }).then(render);
3)修改react-app/src/App.js,相当于首页,也是应用程序的入口
// src/App.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
审查元素,可以看到自己组件里的样式:
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组件是一个空组件,可以进行编辑(过程中出现了点小问题,清除浏览器缓存即可),实际测试结果:
6)测试访问当前Page的JSON数据(Sling Model Exporter导出)
访问:http://localhost:4502/content/wknd-events/react/home.model.json
可以看到整体的页面结构,这就是SPA Editor进行逐层分析并做映射的数据源,具体JSON可自己分析,其中找到当前的Text组件的JSON数据如下:
此使可以重写结合步骤2中的Text.js的代码进行分析,可以看到:MapTo函数(来自@adobe/cq-react-editable-components)通过JSON字段 :type
进行组件映射,并且能够将JSON中的其它字段通过 this.props.xxx
进行访问,此组件中就如:
this.props.text === "文本文本文本"
this.props.richText === true
5.3.4 Image Component
这一节以编写Image的React组件为例
1)创建如下结构的Image组件,目录结构
/react-app
/src
/components
/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)安装部署
- 访问页面测试:
http://localhost:4502/editor.html/content/wknd-events/react/home.html
同样的,会看到默认为空的Image组件,可以对它进行各种编辑,Image组件支持拖拽。
- 查看页面JSON:
http://localhost:4502/content/wknd-events/react/home.model.json
找到当前Component的JSON如下:
"image": {
"alt": "Rain",
"src": "/content/wknd-events/react/home/_jcr_content/root/responsivegrid/image.coreimg.jpeg/1591154158036/wknd-events.jpeg",
"srcUriTemplate": "/content/wknd-events/react/home/_jcr_content/root/responsivegrid/image.coreimg{.width}.jpeg/1591154158036/wknd-events.jpeg",
"areas": [],
"uuid": "b1160ef2-2c65-4de8-8b5d-491e2be3ef56",
"widths": [],
"lazyEnabled": false,
"link": "/content/wknd-events/react.html",
":type": "wknd-events/components/content/image"
}
和Text组件类似,Sling Model Exporter导出的JSON中的所有字段都能够被React的Image组件使用。
下篇教程将会开始CSS样式的添加,并向前端开发圈子看齐~
5.3.5 HierarchyPage Sling Model
这一小节是额外内容,主要分析了官方的示例项目中提供的Sling Model:HierarchyPage。
HierarchyPageImpl为我们提供了在单次请求中能够获取多AEM Pages的content的能力,也就是通过一个JSON的导出所有相关的content内容。
1)在core子模块中,接口com.adobe.aem.guides.wkndevents.core.models.HierarchyPage代码片段如下:
package com.adobe.aem.guides.wkndevents.core.models;
import com.adobe.cq.export.json.ContainerExporter;
import com.adobe.cq.export.json.hierarchy.HierarchyNodeExporter;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
public interface HierarchyPage extends HierarchyNodeExporter, ContainerExporter {...}
它继承了2个接口:
- ContainerExporter:定义了容器组件的JSON,如:Page、Responsive Grid、Parsys
- HierarchyNodeExporter:定义了层次节点的JSON,如:Root Page和它的Child Pages
2)接着分析其实现类:com.adobe.aem.guides.wkndevents.core.models.impl.HierarchyPageImpl
官方的说明:在示例项目中,HierarchyPageImpl被单独拷贝在项目中使用。不久后HierarchyPageImpl将通过Core Components库提供。开发者仍可以自行扩展该接口,但不再需要负责维护这个接口的实现了。请确保备份和更新。
...
@Model(adaptables = SlingHttpServletRequest.class, adapters = {HierarchyPage.class, ContainerExporter.class}, resourceType = HierarchyPageImpl.RESOURCE_TYPE)
@Exporter(name = ExporterConstants.SLING_MODEL_EXPORTER_NAME, extensions = ExporterConstants.SLING_MODEL_EXTENSION)
public class HierarchyPageImpl implements HierarchyPage {
/**
* Resource type of associated with the current implementation
*/
protected static final String RESOURCE_TYPE = "wknd-events/components/structure/page";
...
}
上面代码片段中可以看到,HierarchyPageImpl被注册成"wknd-events/components/structure/page"资源类型的Sling Model Exporter。如果需要在自己的项目中需要自定义的接口实现,就需要修改 RESOURCE_TYPE 指向自定义项目的 page component,也就是基础的page原型组件(AEM的)。
最后再稍微介绍一下实现类中的几个重要方法:getRootModel()
和 getRootPage()
将返回对应的根节点,方法中有三个fields说明如下:
/**
* Is the current model to be considered as a model root
* 帮助识别程序的rootPage。rootPage被用作app的运行入口,它还集成了所有的child pages
*/
private static final String PR_IS_ROOT = "isRoot";
/**
* Depth of the tree of pages
* 标识在层次结构中收集子页面(child pages)的深度
*/
private static final String STRUCTURE_DEPTH_PN = "structureDepth";
/**
* List of Regexp patterns to filter the exported tree of pages
* 正则表达式,用于忽略或排除不需要被自动收集(collect)的页面
*/
private static final String STRUCTURE_PATTERNS_PN = "structurePatterns";
3)看完实现类,接着我们来看下如何查看和修改上一步中提到的字段的值。
在AEM的Lite中,找到editable template的policy节点:
/conf/wknd-events/settings/wcm/policies/wknd-events/components/structure/app/default
这里我通过JSON获取此节点的值,浏览器访问:
http://localhost:4502/conf/wknd-events/settings/wcm/policies/wknd-events/components/structure/app/default.json
结果如下:
{
"jcr:primaryType": "nt:unstructured",
"jcr:title": "SPA Page",
"isRoot": true,
"structurePatterns": "(react/)(?:(?!blog)(/)?)",
"jcr:description": "Default policy of the page",
"sling:resourceType": "wcm/core/components/policy/policy",
"structureDepth": "2"
}
官方的提示: 目前没有UI界面修改这些字段,只能在Lite中手动修改或在ui.content子模块修改xml文件。完善的功能将在未来推出。
4)示例中的页面分析
示例中的根页面react.html:http://localhost:4502/content/wknd-events/react.html,是基于 wknd-events-app-template 创建的,通过添加后缀 .model.json
访问此页面的JSON:
http://localhost:4502/content/wknd-events/react.model.json
可以查看当前页和其子页面home.html的content内容。
5.4 Front End Development
https://helpx.adobe.com/experience-manager/kt/sites/using/getting-started-spa-wknd-tutorial-develop/react/chapter-2.html
5.4.1 说明
这一章关注于前端开发(游离于AEM外)。前端开发者能够修改JS和CSS,并且能立即在浏览器上看到效果,这个过程中不需要完整的编译(development build)。当前流行的前端工具:Webpack development server、SASS、Styleguidist已被示例项目集成,加速前端开发。
主要内容:
- Sass的使用
- 为了实现前端的独立开发,需要有获取Sling Model的JSON数据的能力,两种方式:
- 为create-react-app设置AEM服务的代理
- 通过本地Mock JSON数据文件(这里没有用到MockJS技术,不太推荐)
- 添加Header组件
- 为Image组件和Text组件添加样式
- 在React工程中集成Responsive Grid,AEM Authoring页面后可以同步效果
- Styleguidist的使用,自动生成Image和Text的Markdown文档
5.4.2 安装Sass
对于React组件,需要保证模块化的独立性,因此它推荐尽量避免复用具有相同class name的CSS样式(组件间)。示例项目将引入Sass的几个实用功能实现样式复用:variables、mixins。此项目还会遵循: SUIT CSS naming conventions. (SUIT是BEM表示法(块元素修饰符)的一种变体,用于创建一致的CSS规则)。
1)安装Sass
# 进入react-app目录
cd <src>/aem-guides-wknd-events/react-app
# 安装node-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> 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)查看浏览器效果
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
4)修改 mock.model.json 文件,为image组件添加title属性
"image": {
+ "title": "This is a caption.",
...
":type": "wknd-events/components/content/image"
}
5)查看页面效果,caption出来了
5.4.7 更新Text组件
为前面的Text Component添加样式
1)更新 react-app/src/components/text/Text.js
...
+ import './Text.scss';//或者 require('./Text.scss')
...
class Text extends Component {
...
render() {
+ let innercontent = this.props.richText ? this.richTextContent : this.textContent;
+ return (<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修改组件的大小了,如下图所示:
修改Text组件大小:
3)现在可以进行*的创作了,这里我照着原教程简单的Authoring了此页面,请*发挥
4)最后,为了确保在React工程中能够正常使用Responsive Grid CSS,做以下修改:
- 找到 react-app/public/index.html ,引用AEM的responsive-grid.css
<head>
+ <link rel="stylesheet" href="/etc.clientlibs/wknd-events/clientlibs/responsive-grid.css" type="text/css">
...
</head>
**PS:**这里使用了 /etc.clientlibs
前缀获取clientlibs的资源文件,是一种固定用法,详细的介绍可自行查找官方文档有关于clientlibs篇章内容。
- 修改 react-app/.env.development 文件,将JSON的获取方式从静态文件改回代理模式
# Configure Proxy end point,定义了Page Model的JSON数据源地址
REACT_APP_PAGE_MODEL_PATH=/content/wknd-events/react.model.json
# Request the JSON from Mock JSON,制定了本地的静态JSON数据源,PS:public目录下的文件发布后将在根路径下
# REACT_APP_PAGE_MODEL_PATH=mock.model.json
5)重启React工程 npm start
,访问测试:http://localhost:3000/content/wknd-events/react/home.html,此时你能看到经AEM的Authoring后的Responsive Grid相关的效果了。
**提示:**如果你想完全的前端本地开发,不依赖AEM,你应该将AEM的responsive grid CSS文件内容复制出来,粘贴到React工程的 react-app/public/grid.css 文件中,并修改 react-app/public/index.html 文件中css的依赖路径。
5.4.9 集成Styleguidist
开发SPA组件的一种流行方法是单独开发它们。 这使开发人员可以跟踪组件可能处于的各种状态。有许多方便开发的工具如: Styleguidist 和 Storybook ,示例项目中将会使用Styleguidist,因为它将样式指南和文档组合到一个工具中。
PS: 其实就是一个文档编写工具,通常来说文档和代码是分离的,代码更新了文档还需要修改, React Styleguidist 就能做到,可以参考:使用 React Styleguidist 编写文档 。
1)安装Styleguidist
# 进入react-app模块
cd <src>/aem-guides-wknd-events/react-app
# 安装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显示如下:
6)按照前面的步骤修改Text组件,Text.md参考:
Text:
```js
<Text richText="false" text="Hello world!"/>
```
RichText:
```js
<Text richText="true" text="<p>Rain<b> forecast</b> Mock JSON</p>"/>
```
7)启动 Styleguidist server:
npm run styleguide
# 运行成功后如下
You can now view your style guide in the browser:
Local: http://localhost:6060/
On your network: http://10.2.20.118:6060/
成功后截图:
大功告成,下一章将会讲述有关导航栏和Router相关的知识,十分重要!
5.5 Navigation and Routing
https://helpx.adobe.com/experience-manager/kt/sites/using/getting-started-spa-wknd-tutorial-develop/react/chapter-3.html
5.5.1 说明
这一章是目前官方的最后一篇,主要介绍了AEM SPA中路由概念和使用方式。
新技术点:
- React Router
- Font Awesome Icons
主要内容:
- AEM SPA中的路由介绍和简要实现原理
- 安装React Router
- 文章列表组件(导航文章)
- 完善Header组件(添加了路由功能,导航回首页的button)
- 使用Font Awesome Icons(图标工具)
- React中重定向的使用
5.5.2 SPA的路由方式
在AEM SPA Editor中使用Navigation(导航栏)的最佳方式就是将不同的SPA页面视图和AEM的特定页面做映射。这种方式使得管理应用程序的多个模块变得简单,并且能够让内容制作者(content author)编辑独立的页面视图。更多有关路由的介绍,可移步 [4.11 SPA Model Routing](#4.11 SPA Model Routing)
每个AEM的Page表示在SPA框架(React)中,都会被 <Router>
标签包裹,标签中还需要AEM Page的路径,如下面伪代码描述的:
// React JSX 伪代码,路由示例
render(){
return (
<BrowserRouter>
<App>
<Route path="/content/wknd-events/react/home">
<WkndPage cqPath="/content/wknd-events/react/home" />
</Route>
<Route path="/content/wknd-events/react/home/first-article">
<WkndPage cqPath="/content/wknd-events/react/home/first-article" />
</Route>
<Route path="/content/wknd-events/react/home/second-article">
<WkndPage cqPath="/content/wknd-events/react/home/second-article" />
</Route>
</App>
</BrowserRouter>
);
}
在第一章中我们已经讨论过了,React App是通过AEM的JSON Model驱动的。JSON Model通过一个叫 HierarchyPage
的Sling Model,将多个AEM Pages的内容(content)包含在了一个请求中。这种方式允许React App在初始化页面时,直接将几乎所有的数据内容加载,并且在用户后续的浏览中,应当最小化服务端(server-side)的子请求。有关 HierarchyPage的介绍和实现 可参考前文 [5.3.5](#5.3.5 HierarchyPage Sling Model) 。
以下假代码是一段AEM导出的JSON示例,注意这个JSON结构和JSX中的结构能十分完整的映射:
{
":type": "wknd-events/components/structure/app",
":itemsOrder": [],
":items": {},
":hierarchyType": "page",
":path": "/content/wknd-events/react",
":children": {
"/content/wknd-events/react/home": {},
"/content/wknd-events/react/home/first-article": {},
"/content/wknd-events/react/home/second-article": {}
},
"title": "React App"
}
5.5.3 安装React Router
React Router 为React框架提供了一系列的导航组件,提供了对页面视图的管理功能。React Router分为三个子模块:
- react-router
- react-router-dom
- react-router-native
示例项目中,由于需要发布到Web平台,因此将使用 react-router-dom ,更多信息请参考: React Router for the Web
1)安装 react-router 和 react-router-dom,在react-app工程下打开命令行:
# 进入react-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="/">
成功后的效果图:
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 Article 和 Second Article (first-article 和 second-article),使用 WKND Event Page template
9)打开 http://localhost:4502/editor.html/content/wknd-events/react/home.html 页面,添加List Component,配置如下内容:
10)在Preview视图下测试跳转链接,此时你应该能够跳转进子页面,子页面里的组件也是能在Editor模式下编辑的。
接着访问非Editor环境: http://localhost:4502/content/wknd-events/react/home.html
尝试点击导航列表,可以发现页面不会刷新,浏览器URL将被更新,浏览器的退后按钮也能工作。在导航时,可以通过审查network发现除了页面第一次初始化加载之后不会有任何的请求流量。
目前我们还没有办法从子页面返回Home页(不通过浏览器的后退按钮),下一小结将对Header组件添加一个动态的返回按钮。
5.5.5 更新Header组件
这一小节将为Header组件添加一个后退按钮,和前面的List组件类似,这里也将使用React Router提供的组件实现。
1)更新 react-app/src/components/header/Header.js ,完整代码如下,具体内容见注释:
// src/components/header/Header.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按钮,如图:
下面将会为这个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按钮应该成为了图标:
5.5.7 重定向到首页
这是本章的最后一小节,这里将更新 index.js ,也就是应用的入口JS文件。通过添加 Redirect 组件(提供自react-router),实现访问 react.html 页面时自动重定向到 home.html 。
1)更新 react-app/src/index.js ,导入react-router的 Redirect he Route组件:
import { Redirect, Route } from 'react-router';
2)更新render函数,添加重定向规则(从react.html -> home.html):
function render(model) {
ReactDOM.render(
(
<BrowserRouter>
<ScrollToTop>
+ <Route path="/content/wknd-events/react.html" render={() => (
+ <Redirect to="/content/wknd-events/react/home.html"/>
+ )}/>
<App cqChildren={model[Constants.CHILDREN_PROP]}
cqItems={model[Constants.ITEMS_PROP]}
cqItemsOrder={model[Constants.ITEMS_ORDER_PROP]}
cqPath={ModelManager.rootPath}
locationPathname={window.location.pathname}/>
</ScrollToTop>
</BrowserRouter>
),
document.getElementById('root')
);
}
3)重启 react-app ,访问:http://localhost:3000/content/wknd-events/react.html ,此时将会自动重定向到 home.html
4)将项目编译部署到AEM Server,测试访问:http://localhost:4502/editor.html/content/wknd-events/react.html
- 此时你能够导航到List组件中的子页面,Header组件的back按钮也将正常显示样式
- 注意在AEM Editor模式下,浏览器的url不能够正确反射回来,需要在非Editor下测试访问:http://localhost:4502/content/wknd-events/react.html ,可以看到重定向和HTML5的History API都能够正常工作了