自定义组件

本教程涵盖自定义AEM Byline组件的端到端创建,该组件显示对话框中创作的内容,并探讨如何开发Sling模型以封装可填充组件HTL的业务逻辑。

前提条件

查看设置本地开发环境所需的工具和说明。

入门项目

注意

如果您成功完成了上一章,则可以重复使用该项目并跳过签出起始项目的步骤。

查看本教程构建的基行代码:

  1. 查看GitHub中的tutorial/custom-component-start分支

    $ cd aem-guides-wknd
    $ git checkout tutorial/custom-component-start
    
  2. 使用您的Maven技能将代码库部署到本地AEM实例:

    $ mvn clean install -PautoInstallSinglePackage
    
    注意

    如果使用AEM 6.5或6.4,请将classic配置文件附加到任何Maven命令。

    $ mvn clean install -PautoInstallSinglePackage -Pclassic
    

您始终可以在GitHub上查看完成的代码,或通过切换到分支tutorial/custom-component-solution在本地签出代码。

目标

  1. 了解如何构建自定义AEM组件
  2. 了解如何使用Sling模型封装业务逻辑
  3. 了解如何从HTL脚本中使用Sling模型

将构建的内容

在WKND教程的这一部分中,将创建一个署名组件,用于显示有关文章参与者的创作信息。

署名组件示例

署名组件

署名组件的实施包括收集署名内容的对话框和检索署名的自定义Sling模型:

  • 名称
  • 图像
  • 职业

创建署名组件

首先,创建署名组件节点结构并定义一个对话框。 这表示AEM中的组件,并通过组件在JCR中的位置隐式定义组件的资源类型。

该对话框公开了内容作者可以提供的界面。 对于此实施,将利用AEM WCM核心组件的​Image​组件来创作和渲染署名图像,因此它将设置为我们组件的sling:resourceSuperType

创建组件定义

  1. 在​ui.apps​模块中,导航到/apps/wknd/components并创建一个名为byline的新文件夹。

  2. byline文件夹下,添加一个名为.content.xml的新文件

    对话框创建节点

  3. 使用以下内容填充.content.xml文件:

    <?xml version="1.0" encoding="UTF-8"?>
        <jcr:root xmlns:sling="http://sling.apache.org/jcr/sling/1.0" xmlns:cq="http://www.day.com/jcr/cq/1.0" xmlns:jcr="http://www.jcp.org/jcr/1.0"
        jcr:primaryType="cq:Component"
        jcr:title="Byline"
        jcr:description="Displays a contributor's byline."
        componentGroup="WKND Sites Project - Content"
        sling:resourceSuperType="core/wcm/components/image/v2/image"/>
    

    上述XML文件提供了组件的定义,包括标题、描述和组。 sling:resourceSuperType指向core/wcm/components/image/v2/image,即核心图像组件

创建HTL脚本

  1. byline文件夹下,添加一个新文件byline.html,该文件负责组件的HTML显示。 使用与文件夹相同的方式命名文件很重要,因为它将成为Sling用于呈现此资源类型的默认脚本。

  2. 将以下代码添加到byline.html中。

    <!--/* byline.html */-->
    <div data-sly-use.placeholderTemplate="core/wcm/components/commons/v1/templates.html">
    </div>
    <sly data-sly-call="${placeholderTemplate.placeholder @ isEmpty=true}"></sly>
    

byline.html 稍后 将重新查看,在创建Sling模型后。HTL文件的当前状态允许组件在拖放到页面上时以空状态在AEM Sites的页面编辑器中显示。

创建对话框定义

接下来,为署名组件定义一个对话框,其中包含以下字段:

  • 名称:参与者名称的文本字段。
  • 图像:参考投稿人的个人简介图片。
  • 职业:贡献者的职业列表。职业应按字母顺序升序(a到z)排序。
  1. byline文件夹下,创建一个名为_cq_dialog的新文件夹。

  2. byline/_cq_dialog下添加一个名为.content.xml的新文件。 这是对话框的XML定义。 添加以下XML:

    <?xml version="1.0" encoding="UTF-8"?>
    <jcr:root xmlns:sling="http://sling.apache.org/jcr/sling/1.0" xmlns:cq="http://www.day.com/jcr/cq/1.0" xmlns:jcr="http://www.jcp.org/jcr/1.0" xmlns:nt="http://www.jcp.org/jcr/nt/1.0"
            jcr:primaryType="nt:unstructured"
            jcr:title="Byline"
            sling:resourceType="cq/gui/components/authoring/dialog">
        <content
                jcr:primaryType="nt:unstructured"
                sling:resourceType="granite/ui/components/coral/foundation/container">
            <items jcr:primaryType="nt:unstructured">
                <tabs
                        jcr:primaryType="nt:unstructured"
                        sling:resourceType="granite/ui/components/coral/foundation/tabs"
                        maximized="{Boolean}false">
                    <items jcr:primaryType="nt:unstructured">
                        <asset
                                jcr:primaryType="nt:unstructured"
                                sling:hideResource="{Boolean}false"/>
                        <metadata
                                jcr:primaryType="nt:unstructured"
                                sling:hideResource="{Boolean}true"/>
                        <properties
                                jcr:primaryType="nt:unstructured"
                                jcr:title="Properties"
                                sling:resourceType="granite/ui/components/coral/foundation/container"
                                margin="{Boolean}true">
                            <items jcr:primaryType="nt:unstructured">
                                <columns
                                        jcr:primaryType="nt:unstructured"
                                        sling:resourceType="granite/ui/components/coral/foundation/fixedcolumns"
                                        margin="{Boolean}true">
                                    <items jcr:primaryType="nt:unstructured">
                                        <column
                                                jcr:primaryType="nt:unstructured"
                                                sling:resourceType="granite/ui/components/coral/foundation/container">
                                            <items jcr:primaryType="nt:unstructured">
                                                <name
                                                        jcr:primaryType="nt:unstructured"
                                                        sling:resourceType="granite/ui/components/coral/foundation/form/textfield"
                                                        emptyText="Enter the contributor's name to display."
                                                        fieldDescription="The contributor's name to display."
                                                        fieldLabel="Name"
                                                        name="./name"
                                                        required="{Boolean}true"/>
                                                <occupations
                                                        jcr:primaryType="nt:unstructured"
                                                        sling:resourceType="granite/ui/components/coral/foundation/form/multifield"
                                                        fieldDescription="A list of the contributor's occupations."
                                                        fieldLabel="Occupations"
                                                        required="{Boolean}false">
                                                    <field
                                                            jcr:primaryType="nt:unstructured"
                                                            sling:resourceType="granite/ui/components/coral/foundation/form/textfield"
                                                            emptyText="Enter an occupation"
                                                            name="./occupations"/>
                                                </occupations>
                                            </items>
                                        </column>
                                    </items>
                                </columns>
                            </items>
                        </properties>
                    </items>
                </tabs>
            </items>
        </content>
    </jcr:root>
    

    这些对话框节点定义使用Sling资源合并器来控制从sling:resourceSuperType组件继承的对话框选项卡,在本例中为​核心组件的图像组件

    署名的已完成对话框

创建策略对话框

按照与创建对话框相同的方法,创建策略对话框(以前称为设计对话框)以隐藏从核心组件的图像组件继承的策略配置中的不需要字段。

  1. byline文件夹下,创建一个名为_cq_design_dialog的新文件夹。

  2. byline/_cq_design_dialog下面创建一个名为.content.xml的新文件。 使用以下方法更新文件:使用以下XML。 最简单的方法是打开.content.xml并将下面的XML复制/粘贴到其中。

    <?xml version="1.0" encoding="UTF-8"?>
    <jcr:root xmlns:sling="http://sling.apache.org/jcr/sling/1.0" xmlns:granite="http://www.adobe.com/jcr/granite/1.0" xmlns:cq="http://www.day.com/jcr/cq/1.0" xmlns:jcr="http://www.jcp.org/jcr/1.0" xmlns:nt="http://www.jcp.org/jcr/nt/1.0"
        jcr:primaryType="nt:unstructured"
        jcr:title="Byline"
        sling:resourceType="cq/gui/components/authoring/dialog">
        <content
                jcr:primaryType="nt:unstructured">
            <items jcr:primaryType="nt:unstructured">
                <tabs
                        jcr:primaryType="nt:unstructured">
                    <items jcr:primaryType="nt:unstructured">
                        <properties
                                jcr:primaryType="nt:unstructured">
                            <items jcr:primaryType="nt:unstructured">
                                <content
                                        jcr:primaryType="nt:unstructured">
                                    <items jcr:primaryType="nt:unstructured">
                                        <decorative
                                                jcr:primaryType="nt:unstructured"
                                                sling:hideResource="{Boolean}true"/>
                                        <altValueFromDAM
                                                jcr:primaryType="nt:unstructured"
                                                sling:hideResource="{Boolean}true"/>
                                        <titleValueFromDAM
                                                jcr:primaryType="nt:unstructured"
                                                sling:hideResource="{Boolean}true"/>
                                        <displayCaptionPopup
                                                jcr:primaryType="nt:unstructured"
                                                sling:hideResource="{Boolean}true"/>
                                        <disableUuidTracking
                                                jcr:primaryType="nt:unstructured"
                                                sling:hideResource="{Boolean}true"/>
                                    </items>
                                </content>
                            </items>
                        </properties>
                        <features
                                jcr:primaryType="nt:unstructured">
                            <items jcr:primaryType="nt:unstructured">
                                <content
                                        jcr:primaryType="nt:unstructured">
                                    <items jcr:primaryType="nt:unstructured">
                                        <accordion
                                                jcr:primaryType="nt:unstructured">
                                            <items jcr:primaryType="nt:unstructured">
                                                <orientation
                                                        jcr:primaryType="nt:unstructured"
                                                        sling:hideResource="{Boolean}true"/>
                                                <crop
                                                        jcr:primaryType="nt:unstructured"
                                                        sling:hideResource="{Boolean}true"/>
                                            </items>
                                        </accordion>
                                    </items>
                                </content>
                            </items>
                        </features>
                    </items>
                </tabs>
            </items>
        </content>
    </jcr:root>
    

    前面​策略对话框 XML的基础是从核心组件图像组件获取的。

    与对话框配置中一样,Sling资源合并器用于隐藏从sling:resourceSuperType继承的不相关字段,如sling:hideResource="{Boolean}true"属性的节点定义所示。

部署代码

  1. 使用您的Maven技能将更新的代码库部署到本地AEM实例:

    $ cd aem-guides-wknd
    $ mvn clean install -PautoInstallSinglePackage
    

将组件添加到页面

为了保持AEM组件开发的简单性和重点,我们会将处于当前状态的Byline组件添加到文章页面,以验证cq:Component节点定义是否已部署和正确,AEM可识别新组件定义,并且组件的对话框可用于创作。

将图像添加到AEM Assets

首先,将头部拍摄示例上传到AEM Assets,以用于填充署名组件中的图像。

  1. 导航到AEM Assets的LA Skateparks文件夹:http://localhost:4502/assets.html/content/dam/wknd/en/magazine/la-skateparks

  2. 将​stacey-roswells.jpg​的头部照片上传到文件夹。

    已上传头部照片

创作组件

接下来,将署名组件添加到AEM中的页面。 由于我们通过ui.apps/src/main/content/jcr_root/apps/wknd/components/byline/.content.xml定义将署名组件添加到​WKND站点项目 — 内容​组件组,因此它可自动供任何​容器​使用,其​Policy​允许​WKND站点项目 — 内容​组件组,文章页面的布局容器就是该组件组。

  1. 导航到LA Skatepark文章:http://localhost:4502/editor.html/content/wknd/us/en/magazine/guide-la-skateparks.html

  2. 从左侧边栏中,将​署名组件​拖放到已打开文章页面布局容器的​底部​上。

    将署名组件添加到页面

  3. 确保左侧边栏已打开​且可见,并且已选择​资产查找器​

    打开资产查找器

  4. 选择​Byline组件占位符 ,这又显示操作栏并点按​扳手​图标以打开对话框。

    组件操作栏

  5. 打开对话框,并且第一个选项卡(资产)处于活动状态,打开左侧边栏,然后从资产查找器中,将图像拖到图像拖放区域。 搜索“stacey”以查找WKND ui.content包中提供的Stacey Roswells生物图片。

    将图像添加到对话框

  6. 添加图像后,单击​属性​选项卡以输入​名称​和​职业

    进入职业时,请按​反向字母顺序​输入,这样我们在Sling模型中实施的按字母顺序排列的业务逻辑就显而易见了。

    点按右下方的​Done​按钮以保存更改。

    填充署名组件属性

    AEM作者通过对话框配置和创作组件。 在开发署名组件时,将包含用于收集数据的对话框,但尚未添加呈现创作内容的逻辑。 因此,只显示占位符。

  7. 保存对话框后,导航到CRXDE Lite并查看组件内容在AEM页面下的署名组件内容节点上的存储方式。

    在“LA Skate Parks”页面下方找到Byline组件内容节点,即/content/wknd/us/en/magazine/guide-la-skateparks/jcr:content/root/container/container/byline

    请注意,属性名称nameoccupationsfileReference存储在​byline节点​上。

    此外,请注意节点的sling:resourceType设置为wknd/components/content/byline,这是将此内容节点绑定到Byline组件实现的内容。

    CRXDE中的署名属性

创建署名Sling模型

接下来,我们将创建一个Sling模型以充当数据模型,并存储Byline组件的业务逻辑。

Sling模型是注释驱动的Java“POJO”(纯旧Java对象),它有助于将数据从JCR映射到Java变量,并在AEM上下文中进行开发时提供许多其他细节。

查看Maven依赖项

署名Sling模型将依赖于AEM提供的多个Java API。 这些API通过core模块POM文件中列出的dependencies提供。 已为AEM as a Cloud Service构建本教程所用的项目。 但是,它的独特之处在于,它向后兼容AEM 6.5/6.4。因此,它同时包含Cloud Service和AEM 6.x的依赖项。

  1. <src>/aem-guides-wknd/core/pom.xml下打开pom.xml文件。

  2. 查找aem-sdk-api - AEM的依赖项,仅作为Cloud Service

    <dependency>
        <groupId>com.adobe.aem</groupId>
        <artifactId>aem-sdk-api</artifactId>
    </dependency>
    

    aem-sdk-api包含由AEM公开的所有公共Java API。 默认情况下,在构建此项目时使用aem-sdk-api。 该版本将在位于aem-guides-wknd/pom.xml项目根的父反应器pom中维护。

  3. 查找uber-jar - AEM 6.5/6.4的依赖项仅

    ...
        <dependency>
            <groupId>com.adobe.aem</groupId>
            <artifactId>uber-jar</artifactId>
            <classifier>apis</classifier>
        </dependency>
    ...
    

    只有在调用classic配置文件(即mvn clean install -PautoInstallSinglePackage -Pclassic)时,才会包含uber-jar。 同样,这是此项目特有的。 在从AEM项目原型生成的真实项目中,如果指定的AEM版本为6.5或6.4,则默认为uber-jar

    uber-jar包含由AEM 6.x公开的所有公共Java API。该版本在位于项目aem-guides-wknd/pom.xml根的父反应器pom中进行维护。

  4. 查找core.wcm.components.core的依赖项:

     <!-- Core Component Dependency -->
        <dependency>
            <groupId>com.adobe.cq</groupId>
            <artifactId>core.wcm.components.core</artifactId>
        </dependency>
    

    这是AEM核心组件公开的所有公共Java API。 AEM核心组件是在AEM外部维护的项目,因此具有单独的发行周期。 因此,它是需要单独包含的依赖项,​包含在uber-jaraem-sdk-api中。

    与uber-jar一样,此依赖项的版本在位于aem-guides-wknd/pom.xml的父反应器pom文件中进行维护。

    在本教程的后面,我们将使用核心组件图像类在署名组件中显示图像。 要构建和编译我们的Sling模型,必须具有核心组件依赖关系。

署名界面

为署名创建公共Java接口。 Byline.java 定义驱动HTL脚本所需的公 byline.html 共方法。

  1. core/src/main/java/com/adobe/aem/guides/wknd/core/models下的aem-guides-wknd.core模块中,创建一个名为Byline.java的新文件

    创建署名界面

  2. 使用以下方法更新Byline.java:

    package com.adobe.aem.guides.wknd.core.models;
    
    import java.util.List;
    
    /**
    * Represents the Byline AEM Component for the WKND Site project.
    **/
    public interface Byline {
        /***
        * @return a string to display as the name.
        */
        String getName();
    
        /***
        * Occupations are to be sorted alphabetically in a descending order.
        *
        * @return a list of occupations.
        */
        List<String> getOccupations();
    
        /***
        * @return a boolean if the component has enough content to display.
        */
        boolean isEmpty();
    }
    

    前两种方法显示Byline组件​name​和​schrips​的值。

    isEmpty()方法用于确定组件是否包含要渲染的内容或组件是否正在等待配置。

    请注意,图像没有方法;我们将查看为什么这是以后的

  3. 包含公共Java类(本例中为Sling模型)的Java包必须使用包的package-info.java文件进行版本控制。

由于WKND源的Java包com.adobe.aem.guides.wknd.core.models声明为2.0.0版本,并且我们添加的是不中断的公共接口和方法,因此必须将该版本增加到2.1.0。 在core/src/main/java/com/adobe/aem/guides/wknd/core/models/package-info.java打开文件,并将@Version("2.0.0")更新为@Version("2.1.0")

&quot;&#39;
@Version(&quot;2.1.0&quot;)
包com.adobe.aem.guides.wknd.core.models;

导入org.osgi.annotation.versioning.Version;
&quot;

每当对此包中的文件进行更改时,必须从语义上🔗调整包版本。 如果没有,则Maven项目的bnd-baseline-maven-plugin将检测到无效的包版本并中断构建。 幸运的是,Maven插件在失败时会报告无效的Java包版本以及该版本。 刚刚将违反Java包package-info.java中的@Version("...")声明更新为插件建议的要修复的版本。

署名实施

BylineImpl.java 是用于实现之前定义的接口 Byline.java 的Sling模型。BylineImpl.java的完整代码可在此部分的底部找到。

  1. core/src/main/java/com/adobe/aem/guides/core/models下创建一个名为impl的新文件夹。

  2. impl文件夹中,创建新文件BylineImpl.java

    署名导入文件

  3. 打开 BylineImpl.java. 指定它实现Byline接口。 使用IDE的自动完成功能或手动更新文件以包含实现Byline接口所需的方法:

    package com.adobe.aem.guides.wknd.core.models.impl;
    import java.util.List;
    import com.adobe.aem.guides.wknd.core.models.Byline;
    
    public class BylineImpl implements Byline {
    
        @Override
        public String getName() {
            // TODO Auto-generated method stub
            return null;
        }
    
        @Override
        public List<String> getOccupations() {
            // TODO Auto-generated method stub
            return null;
        }
    
        @Override
        public boolean isEmpty() {
            // TODO Auto-generated method stub
            return false;
        }
    }
    
  4. 通过使用以下类级别注释更新BylineImpl.java来添加Sling模型注释。 此@Model(..)注释可将类转换为Sling模型。

    import org.apache.sling.api.SlingHttpServletRequest;
    import org.apache.sling.models.annotations.Model;
    import org.apache.sling.models.annotations.DefaultInjectionStrategy;
    ...
    @Model(
            adaptables = {SlingHttpServletRequest.class},
            adapters = {Byline.class},
            resourceType = {BylineImpl.RESOURCE_TYPE},
            defaultInjectionStrategy = DefaultInjectionStrategy.OPTIONAL
    )
    public class BylineImpl implements Byline {
        protected static final String RESOURCE_TYPE = "wknd/components/content/byline";
        ...
    }
    

    让我们查看此注释及其参数:

实施Sling模型方法

getName()

我们将处理的第一个方法是getName(),它只返回存储到署名的JCR内容节点中属性name下的值。

为此,可使用@ValueMapValue Sling模型注释,使用请求资源的ValueMap将值注入Java字段。

import org.apache.sling.models.annotations.injectorspecific.ValueMapValue;

public class BylineImpl implements Byline {
    ...
    @ValueMapValue
    private String name;

    ...
    @Override
    public String getName() {
        return name;
    }
    ...
}

由于JCR属性与Java字段共享相同的名称(两者都是“name”),因此@ValueMapValue会自动解析此关联,并将属性的值插入Java字段。

getScriptions()

要实现的下一个方法是getOccupations()。 此方法将收集JCR属性occupations中存储的所有职业,并返回这些职业的排序(按字母顺序)集合。

使用getName()中探索的相同技术,属性值可以注入到Sling模型的字段中。

在JCR属性值通过插入的Java字段occupations在Sling模型中可用后,可以在getOccupations()方法中应用排序业务逻辑。

import java.util.ArrayList;
import java.util.Collections;
  ...

public class BylineImpl implements Byline {
    ...
    @ValueMapValue
    private List<String> occupations;
    ...
    @Override
    public List<String> getOccupations() {
        if (occupations != null) {
            Collections.sort(occupations);
            return new ArrayList<String>(occupations);
        } else {
            return Collections.emptyList();
        }
    }
    ...
}
  ...

isEmpty()

最后一个公共方法是isEmpty(),它确定组件何时应考虑“创作足够”来渲染。

对于此组件,我们的业务要求是,必须填写​的所有三个字段、名称、图像和职业,才能渲染组件

import org.apache.commons.lang3.StringUtils;
  ...
public class BylineImpl implements Byline {
    ...
    @Override
    public boolean isEmpty() {
        if (StringUtils.isBlank(name)) {
            // Name is missing, but required
            return true;
        } else if (occupations == null || occupations.isEmpty()) {
            // At least one occupation is required
            return true;
        } else if (/* image is not null, logic to be determined */) {
            // A valid image is required
            return true;
        } else {
            // Everything is populated, so this component is not considered empty
            return false;
        }
    }
    ...
}

解决"形象问题"

检查名称和占用条件很琐碎(Apache Commons Lang3提供了始终方便使用的StringUtils类),但是,由于核心组件图像组件用于显示图像,因此不清楚如何验证图像​​存在。

有两种方法可以解决此问题:

检查fileReference JCR属性是否解析为资产。 ** ORC将此资源转换为核心组件图像Sling模型,并确保 getSrc() 方法不为空。

我们将选择​second​方法。 第一种方法可能就足够了,但在本教程中,将使用后一种方法来探索Sling模型的其他功能。

  1. 创建用于获取图像的专用方法。 此方法保留为私有,因为我们不需要在HTL本身中公开Image对象,它仅用于驱动isEmpty().

    getImage()的以下专用方法:

    import com.adobe.cq.wcm.core.components.models.Image;
    ...
    private Image getImage() {
        Image image = null;
        // Figure out how to populate the image variable!
        return image;
    }
    

    如上所述,还有两种方法可获取​图像Sling模型:

    第一个组件使用@Self注释,以自动将当前请求调整为核心组件的Image.class

    @Self
    private Image image;
    

    第二种方法使用Apache Sling ModelFactory OSGi服务,这项服务非常便于使用,可帮助我们在Java代码中创建其他类型的Sling模型。

    我们将选择第二种方法。

    注意

    在实际实施中,首选使用@Self的“One”方法,因为它是更简单、更优雅的解决方案。 在本教程中,我们将使用第二种方法,因为它要求我们探索更多方面的Sling模型,这些方面对于更复杂的组件非常有用!

    由于Sling模型是Java POJO,而不是OSGi服务,因此通常的OSGi注入批注@Reference 不能​使用,而Sling模型会提供特殊的​@OSGiService​注释,以提供类似的功能。

  2. 更新BylineImpl.java以包含OSGiService注释以插入ModelFactory:

    import org.apache.sling.models.factory.ModelFactory;
    import org.apache.sling.models.annotations.injectorspecific.OSGiService;
    ...
    public class BylineImpl implements Byline {
        ...
        @OSGiService
        private ModelFactory modelFactory;
    }
    

    提供ModelFactory后,可以使用以下方法创建核心组件图像Sling模型:

    modelFactory.getModelFromWrappedRequest(SlingHttpServletRequest request, Resource resource, java.lang.Class<T> targetClass)
    

    但是,此方法既需要请求,又需要资源,在Sling模型中均不可用。 要获取这些标注,需要使用更多Sling模型批注!

    要获取当前请求,可使用​@Self​注释将adaptable(在@Model(..)中定义为SlingHttpServletRequest.class)注入Java类字段。

  3. 添加​@Self​注释以获取​SlingHttpServletRequest请求:

    import org.apache.sling.models.annotations.injectorspecific.Self;
    ...
    @Self
    private SlingHttpServletRequest request;
    

    请记住,上面有一个选项,即使用@Self Image image插入核心组件图像Sling模型 — @Self注释尝试插入可调整的对象(在本例中为SlingHttpServletRequest),并适应注释字段类型。 由于核心组件图像Sling模型可以从SlingHttpServletRequest对象进行调整,因此它会起作用,而且代码少于我们探索性更强的方法。

    现在,我们已通过ModelFactory API插入实例化我们的图像模型所需的变量。 我们将使用Sling模型的​@PostConstruct​注释在Sling模型实例化后获取此对象。

    @PostConstruct 非常有用,且作用容量与构造函数类似,但是,在实例化类并插入所有注释的Java字段后,将调用它。其他Sling模型注释在Java类字段(变量)中添加批注时,@PostConstruct会标注void,零参数方法,通常名为init()(但可以命名为任何内容)。

  4. 添加​@PostConstruct​方法:

    import javax.annotation.PostConstruct;
    ...
    public class BylineImpl implements Byline {
        ...
        private Image image;
    
        @PostConstruct
        private void init() {
            image = modelFactory.getModelFromWrappedRequest(request,
                                                            request.getResource(),
                                                            Image.class);
        }
        ...
    }
    

    请记住,Sling模型是​NOT OSGi服务,因此维护类状态是安全的。 通常,@PostConstruct会派生并设置Sling Model类状态,供以后使用,与普通构造函数的功能类似。

    请注意,如果@PostConstruct方法引发异常,则Sling模型将不会实例化(它将为空)。

  5. 现在可以更新getImage() ,以仅返回图像对象。

    /**
        * @return the Image Sling Model of this resource, or null if the resource cannot create a valid Image Sling Model.
    */
    private Image getImage() {
        return image;
    }
    
  6. 让我们返回到isEmpty()并完成实施:

    @Override
    public boolean isEmpty() {
       final Image componentImage = getImage();
    
        if (StringUtils.isBlank(name)) {
            // Name is missing, but required
            return true;
        } else if (occupations == null || occupations.isEmpty()) {
            // At least one occupation is required
            return true;
        } else if (componentImage == null || StringUtils.isBlank(componentImage.getSrc())) {
            // A valid image is required
            return true;
        } else {
            // Everything is populated, so this component is not considered empty
            return false;
        }
    }
    

    请注意,对getImage()进行多次调用不会产生问题,因为返回初始化的image类变量,并且不会调用modelFactory.getModelFromWrappedRequest(...),这并不过于昂贵,但值得避免不必要的调用。

  7. 最终的BylineImpl.java应该如下所示:

    package com.adobe.aem.guides.wknd.core.models.impl;
    
    import java.util.ArrayList;
    import java.util.Collections;
    import java.util.List;
    import javax.annotation.PostConstruct;
    import org.apache.commons.lang3.StringUtils;
    import org.apache.sling.api.SlingHttpServletRequest;
    import org.apache.sling.models.annotations.DefaultInjectionStrategy;
    import org.apache.sling.models.annotations.Model;
    import org.apache.sling.models.annotations.injectorspecific.OSGiService;
    import org.apache.sling.models.annotations.injectorspecific.Self;
    import org.apache.sling.models.annotations.injectorspecific.ValueMapValue;
    import org.apache.sling.models.factory.ModelFactory;
    import com.adobe.aem.guides.wknd.core.models.Byline;
    import com.adobe.cq.wcm.core.components.models.Image;
    
    @Model(
            adaptables = {SlingHttpServletRequest.class},
            adapters = {Byline.class},
            resourceType = {BylineImpl.RESOURCE_TYPE},
            defaultInjectionStrategy = DefaultInjectionStrategy.OPTIONAL
    )
    public class BylineImpl implements Byline {
        protected static final String RESOURCE_TYPE = "wknd/components/content/byline";
    
        @Self
        private SlingHttpServletRequest request;
    
        @OSGiService
        private ModelFactory modelFactory;
    
        @ValueMapValue
        private String name;
    
        @ValueMapValue
        private List<String> occupations;
    
        private Image image;
    
        @PostConstruct
        private void init() {
            image = modelFactory.getModelFromWrappedRequest(request, request.getResource(), Image.class);
        }
    
        @Override
        public String getName() {
            return name;
        }
    
        @Override
        public List<String> getOccupations() {
            if (occupations != null) {
                Collections.sort(occupations);
                return new ArrayList<String>(occupations);
            } else {
                return Collections.emptyList();
            }
        }
    
        @Override
        public boolean isEmpty() {
            final Image componentImage = getImage();
    
            if (StringUtils.isBlank(name)) {
                // Name is missing, but required
                return true;
            } else if (occupations == null || occupations.isEmpty()) {
                // At least one occupation is required
                return true;
            } else if (componentImage == null || StringUtils.isBlank(componentImage.getSrc())) {
                // A valid image is required
                return true;
            } else {
                // Everything is populated, so this component is not considered empty
                return false;
            }
        }
    
        /**
        * @return the Image Sling Model of this resource, or null if the resource cannot create a valid Image Sling Model.
        */
        private Image getImage() {
            return image;
        }
    }
    

署名HTL

ui.apps模块中,打开我们在AEM组件的前面设置中创建的/apps/wknd/components/byline/byline.html

<div data-sly-use.placeholderTemplate="core/wcm/components/commons/v1/templates.html">
</div>
<sly data-sly-call="${placeholderTemplate.placeholder @ isEmpty=false}"></sly>

让我们来查看一下此HTL脚本迄今为止的功能:

  • placeholderTemplate指向核心组件占位符,当组件未完全配置时,将显示该占位符。 如上所述,在AEM Sites页面编辑器中,该编辑器将呈现为带有组件标题的框,具体如cq:Componentjcr:title属性中所定义。

  • data-sly-call="${placeholderTemplate.placeholder @ isEmpty=false}加载上面定义的placeholderTemplate,并将布尔值(当前硬编码为false)传递到占位符模板。 当isEmpty为true时,占位符模板会呈现灰色框,否则不会呈现任何内容。

更新署名HTL

  1. 使用以下骨架HTML结构更新​byline.html:

    <div data-sly-use.placeholderTemplate="core/wcm/components/commons/v1/templates.html"
        class="cmp-byline">
            <div class="cmp-byline__image">
                <!--/* Include the Core Components Image Component */-->
            </div>
            <h2 class="cmp-byline__name"><!--/* Include the name */--></h2>
            <p class="cmp-byline__occupations"><!--/* Include the occupations */--></p>
    </div>
    <sly data-sly-call="${placeholderTemplate.placeholder @ isEmpty=true}"></sly>
    

    请注意,CSS类遵循BEM命名约定。 虽然使用BEM约定并非强制性的,但建议使用BEM,因为它在核心组件CSS类中使用,并且通常会生成清晰易读的CSS规则。

在HTL中实例化Sling模型对象

Use block语句用于实例化HTL脚本中的Sling模型对象,并将其分配给HTL变量。

data-sly-use.byline="com.adobe.aem.guides.wknd.models.Byline" 使用由BylineImpl实施的Byline接口(com.adobe.aem.guides.wknd.models.Byline),并自适应当前的SlingHttpServletRequest,结果将逐行( data-sly-use.<variable-name>)存储在HTL变量名称中。

  1. 更新外部div以通过其公共接口引用​Byline Sling模型:

    <div data-sly-use.byline="com.adobe.aem.guides.wknd.core.models.Byline"
        data-sly-use.placeholderTemplate="core/wcm/components/commons/v1/templates.html"
        class="cmp-byline">
        ...
    </div>
    

访问Sling模型方法

HTL从JSTL中借用,并使用与缩短Java getter方法名称相同的方法。

例如,可以将调用Byline Sling模型的getName()方法缩短为byline.name,类似地,可以将其缩短为byline.emptybyline.isEmpty使用完整的方法名称(byline.getNamebyline.isEmpty)也适用。 请注意,从未使用()在HTL中调用方法(与JSTL类似)。

HTL中使用需要参数​的Java方法不能。 这是为了使HTL中的逻辑保持简单而设计的。

  1. 可以通过在Byline Sling模型或HTL中调用getName()方法,将Byline名称添加到组件中:${byline.name}

    更新h2标记:

    <h2 class="cmp-byline__name">${byline.name}</h2>
    

使用HTL表达式选项

HTL表达式 选项用作HTL中内容的修饰符,范围从日期格式转换到i18n转换。表达式还可用于连接列表或值数组,这是以逗号分隔格式显示职位所需的内容。

表达式通过HTL表达式中的@运算符添加。

  1. 若要加入“, ”职业名单,请使用以下代码:

    <p class="cmp-byline__occupations">${byline.occupations @ join=', '}</p>
    

有条件地显示占位符

AEM组件的大多数HTL脚本都利用​占位符范例​为作者​提供一个可视提示,指示组件的创作不正确,且不会显示在AEM发布​中。 推动此决策的约定是对组件的支持Sling模型实施一种方法,在本例中为:Byline.isEmpty()

isEmpty() 将在署名Sling模型上调用,结果(或者它的负值,通过运 ! 算符)将保存到名为 hasContent的HTL变量中:

  1. 更新外部div以保存名为hasContent的HTL变量:

     <div data-sly-use.byline="com.adobe.aem.guides.wknd.core.models.Byline"
          data-sly-use.placeholderTemplate="core/wcm/components/commons/v1/templates.html"
          data-sly-test.hasContent="${!byline.empty}"
          class="cmp-byline">
          ...
    </div>
    

    请注意,使用data-sly-test时,HTL test块很有意思,因为它既设置了HTL变量,又根据HTL表达式的结果是否真实,呈现/不呈现其所基于的HTML元素。 如果为“truthy”,则HTML元素会呈现,否则不会呈现。

    现在,可以重新使用此HTL变量hasContent有条件地显示/隐藏占位符。

  2. 使用以下内容更新对文件底部placeholderTemplate的条件调用:

    <sly data-sly-call="${placeholderTemplate.placeholder @ isEmpty=!hasContent}"></sly>
    

使用核心组件显示图像

byline.html的HTL脚本现在几乎已完成,并且只缺少图像。

由于我们使用sling:resourceSuperType核心组件图像组件来提供图像创作,因此我们还可以使用核心组件图像组件来渲染图像!

为此,我们需要包含当前的署名资源,但需使用资源类型core/wcm/components/image/v2/image强制核心组件图像组件的资源类型。 这是一种强大的组件重复使用模式。 为此,使用HTL的data-sly-resource块。

  1. div替换为cmp-byline__image类,如下所示:

    <div class="cmp-byline__image"
        data-sly-resource="${ '.' @ resourceType = 'core/wcm/components/image/v2/image' }"></div>
    

    data-sly-resource通过相对路径'.'包含当前资源,并强制将当前资源(或署名内容资源)包含在core/wcm/components/image/v2/image资源类型中。

    核心组件资源类型是直接使用的,而不是通过代理使用,因为这是脚本内的用法,永远不会保留在我们的内容中。

  2. 已完成以下byline.html:

    <!--/* byline.html */-->
    <div data-sly-use.byline="com.adobe.aem.guides.wknd.core.models.Byline" 
        data-sly-use.placeholderTemplate="core/wcm/components/commons/v1/templates.html"
        data-sly-test.hasContent="${!byline.empty}"
        class="cmp-byline">
        <div class="cmp-byline__image"
            data-sly-resource="${ '.' @ resourceType = 'core/wcm/components/image/v2/image' }">
        </div>
        <h2 class="cmp-byline__name">${byline.name}</h2>
        <p class="cmp-byline__occupations">${byline.occupations @ join=', '}</p>
    </div>
    <sly data-sly-call="${placeholderTemplate.placeholder @ isEmpty=!hasContent}"></sly>
    
  3. 将代码库部署到本地AEM实例。 由于对POM文件进行了重大更改,因此请从项目的根目录执行完整的Maven内部版本。

    $ cd aem-guides-wknd/
    $ mvn clean install -PautoInstallSinglePackage
    

    如果部署到AEM 6.5/6.4,请调用classic配置文件:

    $ mvn clean install -PautoInstallSinglePackage -Pclassic
    

查看未设置样式的署名组件

  1. 部署更新后,导航到Ultimate Guide to LA Skateparks 页面,或在章节前面添加Byline组件的任何位置。

  2. 图像名称​和​职业​现在出现,我们有一个未设置样式但正在工作的Byline组件。

    未设置署名的组件

查看Sling模型注册

AEM Web Console的“Sling模型状态”视图显示AEM中所有已注册的Sling模型。 可通过查看此列表,验证并识别署名Sling模型。

如果此列表中未显示​BylineImpl,则可能会出现Sling模型批注问题,或者Sling模型未添加到核心项目中已注册的Sling模型包(com.adobe.aem.guides.wknd.core.models)中。

已注册署名Sling模型

http://localhost:4502/system/console/status-slingmodels

署名样式

署名组件需要设置样式,以与署名组件的创意设计保持一致。 这将通过使用SCSS来实现,AEM通过​ui.frontend Maven子项目为SCSS提供支持。

添加默认样式

为署名组件添加默认样式。 在​ui.frontend​项目中,位于/src/main/webpack/components下:

  1. 创建名为_byline.scss的新文件。

    署名项目资源管理器

  2. 将署名实施CSS(写为SCSS)添加到default.scss中:

    .cmp-byline {
        $imageSize: 60px;
    
        .cmp-byline__image {
            float: left;
    
        /* This class targets a Core Component Image CSS class */
        .cmp-image__image {
            width: $imageSize;
            height: $imageSize;
            border-radius: $imageSize / 2;
            object-fit: cover;
            }
        }
    
        .cmp-byline__name {
            font-size: $font-size-medium;
            font-family: $font-family-serif;
            padding-top: 0.5rem;
            margin-left: $imageSize + 25px;
            margin-bottom: .25rem;
            margin-top:0rem;
        }
    
        .cmp-byline__occupations {
            margin-left: $imageSize + 25px;
            color: $gray;
            font-size: $font-size-xsmall;
            text-transform: uppercase;
        }
    }
    
  3. ui.frontend/src/main/webpack/site/main.scss查看main.scss:

    @import 'variables';
    @import 'wkndicons';
    @import 'base';
    @import '../components/**/*.scss';
    @import './styles/*.scss';
    

    main.scss 是模块所包含样式的主入口 ui.frontend 点。正则表达式'../components/**/*.scss'将包含components/文件夹下的所有文件。

  4. 构建完整项目并将其部署到AEM:

    $ cd aem-guides-wknd/
    $ mvn clean install -PautoInstallSinglePackage
    

    如果使用AEM 6.4/6.5,请添加-Pclassic配置文件。

    小贴士

    您可能需要清除浏览器缓存以确保不提供过时的CSS,并使用署名组件刷新页面以获得完整的样式。

拼合在一起

下面是完整创作且设置样式的署名组件在AEM页面上的样子。

已完成署名组件

恭喜!

恭喜,您刚刚使用Adobe Experience Manager从头开始创建了自定义组件!

下面的步骤

继续了解AEM组件开发,方法是探索如何为Byline Java代码编写JUnit测试,以确保所有内容都得到正确开发,并且实施的业务逻辑正确且完整。

GitHub上查看完成的代码,或在Git浏览器tutorial/custom-component-solution上的本地查看并部署代码。

  1. 克隆github.com/adobe/aem-guides-wknd存储库。
  2. 查看tutorial/custom-component-solution分支

在此页面上