Salesforce LWC学习(二十三) Lightning Message Service 浅谈

本篇参考:

https://trailhead.salesforce.com/content/learn/superbadges/superbadge_lwc_specialist

https://developer.salesforce.com/docs/component-library/documentation/en/lwc/lwc.use_message_channel

https://developer.salesforce.com/docs/component-library/bundle/lightning-message-service/documentation

讲这个以前先以一个例子作为展开。lwc的 superbadge中有一个功能为 左侧 Gallery列表中点击一个图片,在右侧 details会展示这个船的详细信息。

 以往我们可能想着,简单,将这两部分组成到同一个父组件中,Gallery中的某个item点击以后,传递一个事件到父,父进行handler处理以后,将record id 传递给右侧的组件,右侧组件这个reRender一下就搞定了。

先说一下上面的分析能不能实现? 能,而且肯定能。因为事件的传播以后,父组件是肯定可以监听到,监听到处理到,变量绑定到其他的子可以实现。那这样好不好呢?

老实地说,好与不好不清楚,因为项目上更关注三点:

1. 稳定性

2. 性能

3. 可扩展性

这种方式这三点应该都没有太大的问题,问题在于当需求变动以后,父 组件里的逻辑将可能会越来越多。父组件我们的初衷可能是套个壳子,让他们有亲戚关系进行简单的信息交换,结果爸爸需求可能越来越多最后可能承受不能承受之重。

有没有其他的方式去实现即使两个组件没有关系,但是也可以做到信息之间的传递呢?今天的 Lightning Message Service便可以实现这个需求。

一. Lightning Message Service

Lightning Message Service用于在 VF Page, Aura Component, lwc之间进行跨DOM 通讯。可以在单一的 lightning page或者是多个page之间进行通讯。操作的步骤为发布订阅原则。听到发布订阅,大家可能想到 Streaming API 或者是 Platform Event, salesforce针对不同的通讯场景有多种的广播订阅模型进行选择,页面之间的跨DOM通讯使用 Lightning Message Service。值得注意的是,在 spring 20的时候这个功能还是一个 beta版本,在现在的 summer20已经是一个正式的功能,所以可以放心使用。 Lightning Message Service的特别的细节的介绍以及limitation还请参看上面的链接,接下来讲一下具体的使用步骤。

1. 创建 Message Channel

我们在vs code项目的目录中查看是否有messageChannels这个目录,如果不包含就手动创建一下。新建一个以messageChannel-meta.xml 结尾的文件即可。篇中demo创建的是 BoatMessageChannel.messageChannel-meta.xml。进行相关的信息填充以后保存到环境即可。如果曾经有创建过,需要从sandbox或者developer环境导入下来,执行 sfdx force:source:retrieve -m LightningMessageChannel即可retrieve下来。

 那这个xml应该如何写呢?这个时候就需要看 salesforce的 metadata api关于 LightningMessageChannel的介绍:https://developer.salesforce.com/docs/atlas.en-us.api_meta.meta/api_meta/meta_lightningmessagechannel.htm

lightning message channel包括以下的几部分构成:

  • description:lightning message channel的描述信息;
  • isExposed:标签指定当前的 lightning message channel是否暴露出来,类似我们做 lwc的metadata xml的配置;
  • masterLabel: lightning message channel的label名称,这个属性是一个必填字段;
  • lightningMessageFields:这个是 lightning message channel的核心属性,通过这个负载字段用来声明广播订阅接收的变量信息。针对这个属性有两个子属性。description用来描述 lightningMessageField的描述信息,fieldName用来描述当前 lightningMessageField要传播的字段的api name。

下面的xml是BoatMessageChannel的全的信息,以便更好的了解 lightning message channel。sample中我们声明了一个fieldName为recordId的名称是 BoatMessageChannel的 lightning message channel信息。如果需要传递多个变量,只需要多写几个 <lightningMessageFields>即可。

<?xml version="1.0" encoding="UTF-8"?>
<LightningMessageChannel xmlns="http://soap.sforce.com/2006/04/metadata">
    <description>This is a sample Lightning Message Channel for the Lightning Web Components Superbadge.</description>
    <isExposed>true</isExposed>
    <lightningMessageFields>
        <description>This is the record Id that changed</description>
        <fieldName>recordId</fieldName>
    </lightningMessageFields>
    <masterLabel>BoatMessageChannel</masterLabel>
</LightningMessageChannel>

2. 定义Lightning Message Service的作用域

广播订阅机制一个另外的重要的事情就是作用域的问题,即哪种情况订阅者可以订阅到广播源发送的消息。是整个应用级别,还是某些active区域。如果我们在lwc组件间进行广播订阅时,一定要写上@wire(MessageContext)去让scope特性可用。下图为订阅的scope的模型。salesforce默认的订阅模型的scope范围是active的,如果我们希望订阅范围扩大,需要lwc component头部引入APPLICATION_SCOPE,这个是在 ‘lightning/messageService’中。项目中没有要求指定哪种scope,如果有要求即使在Hidden的tab中也可以接收到相关的订阅消息并进行什么处理,可以设置成整个应用级别,篇中demo设置的即应用级别。

 3. 广播一个message Channel

https://developer.salesforce.com/docs/component-library/documentation/en/lwc/lwc.reference_salesforce_modules

我们在广播或者订阅以前都需要先引入我们创建的 message channel,使用 @salesforce/messageChannel进行引用,如果是包里的内容,需要添加namespace信息,如果不是包里的,直接使用channelReference即可。channelReference需要以 __c结尾,这个是强制的要求。

import channelName from '@salesforce/messageChannel/namespace__channelReference';

所以我们篇中的demo中的 messageChannel名称为BoatMessageChannel,所以引入信息如下,其中 BOATMC的名字任意起,messageChannel以__c结尾。

import BOATMC from '@salesforce/messageChannel/BoatMessageChannel__c';

引入以后我们进行发布操作,lightning/messageService包含了 publish方法,我们在发布以前也需要在头部以前引入,因为lwc需要强制使用 MessageContext让scope可用,这里也一并引入 MessageContext

import { publish, MessageContext } from 'lightning/messageService';

上面的准备工作完成以后,只需要调用publish方法即可实现发布。publish有以下的几个参数需要传递。

  • messageContext: messageContext,这里默认填写我们使用wire方法获取声明的变量即可。
  • messageChannel:头部声明的 messageChannel的引用变量;
  • message:一个序列化的JSON object信息,根据messageChannel的字段设置去填充这个字段即可。

下面有一个简单的publish代码块进行更好的了解。如果我们在 BOATMC中声明了两个变量,一个是 recordId,一个是recordData,则我们的 publish方法包含这三部分即可。

@wire(MessageContext)
    messageContext;
          
    handleClick() {
        const message = {
            recordId: '001xx000003NGSFAA4',
            recordData: {accountName: 'Burlington Textiles Corp of America'}
        };
        publish(this.messageContext, BOATMC, message);
    }

4. 订阅 messageChannel

广播源广播出去一个以后,订阅的component如何订阅呢?如果小伙伴先仔细查看上面的连接以后发现在 lightning/messageService里面同样封装了一个用来订阅的方法:subscribe,如果想要取消订阅,只需要调用unsubscribe()即可。

同样的操作,第一个步骤,需要先引入 MessageContext,这里不做重复的描述。直接描述一下 subscribe方法,里面有4个参数。

  • messageContext:描述同上;
  • messageChannel:描述同上;
  • listener:一个函数用来当发布以后处理message用;
  • subscriberOptions:这个是一个可选操作,当我们指定从APPLICATION级别接收消息情况下,设置成{scope: APPLICATION_SCOPE},如果APPLICATION级别,头部需要从lightning/messageService引入APPLICATION_SCOPE。

APPLICATION_SCOPE级别的sample,用于当订阅以后,调用 handleMessage去处理具体订阅逻辑, message.fieldName即可取出相关的值,比如message.recordId即可以取出 发布时 recordId这个变量对应的值。

this.subscription = subscribe(
        this.messageContext, 
        BOATMC, 
        (message) => {
            this.handleMessage(message);
        }, 
        { scope: APPLICATION_SCOPE }
);

active级别的sample:只需要将最后一个参数 scope信息删除即可。

this.subscription = subscribe(
        this.messageContext, 
        BOATMC, 
        (message) => {
            this.handleMessage(message);
        }
);

unsubscription这里不在介绍,看一下上面的文档以及参数介绍大家便可以进行正常的学习。

二. 代码实现

我们在第一部分已经介绍了 Lightning Message Service的基础知识以及方法的使用,下面的内容通过demo可以更好的去学习以及理解。

1. 创建 Message Channel,这里步骤同上面,创建了一个BoatMessageChannel 的 Message Channel,设置了一个 recordId的变量

2. 广播操作

BoatDataService.cls:这里对代码进行了删减,只保留了这次demo用到的方法,通过 getBoats获取船的数据列表信息3.

public with sharing class BoatDataService {

    public static final String LENGTH_TYPE = 'Length'; 
    public static final String PRICE_TYPE = 'Price'; 
    public static final String TYPE_TYPE = 'Type'; 
    @AuraEnabled(cacheable=true)
    public static List<Boat__c> getBoats(String boatTypeId) {
        // Without an explicit boatTypeId, the full list is desired
        String query = 'SELECT '
                     + 'Name, Description__c, Geolocation__Latitude__s, '
                     + 'Geolocation__Longitude__s, Picture__c, Contact__r.Name, '
                     + 'BoatType__c, BoatType__r.Name, Length__c, Price__c '
                     + 'FROM Boat__c';
        if (String.isNotBlank(boatTypeId)) {
            query += ' WHERE BoatType__c = :boatTypeId';
        }
        query += ' WITH SECURITY_ENFORCED ';
        return Database.query(query);
    }

    @AuraEnabled(cacheable=true)
    public static List<Boat__c> getSimilarBoats(Id boatId, String similarBy) {
        List<Boat__c> similarBoats = new List<Boat__c>();
        List<Boat__c> parentBoat = [SELECT Id, Length__c, Price__c, BoatType__c, BoatType__r.Name
                                    FROM Boat__c
                                    WHERE Id = :boatId 
                                    WITH SECURITY_ENFORCED];
        if (parentBoat.isEmpty()) {
            return similarBoats;
        }
        if (similarBy == LENGTH_TYPE) {
            similarBoats = [
                SELECT Id, Contact__r.Name, Name, BoatType__c, BoatType__r.Name, Length__c, Picture__c, Price__c, Year_Built__c
                FROM Boat__c
                WHERE Id != :parentBoat.get(0).Id
                AND (Length__c >= :parentBoat.get(0).Length__c / 1.2)
                AND (Length__c <= :parentBoat.get(0).Length__c * 1.2)
                WITH SECURITY_ENFORCED
                ORDER BY Length__c, Price__c, Year_Built__c
            ];
        } else if (similarBy == PRICE_TYPE) {
            similarBoats = [
                SELECT Id, Contact__r.Name, Name, BoatType__c, BoatType__r.Name, Length__c, Picture__c, Price__c, Year_Built__c
                FROM Boat__c
                WHERE Id != :parentBoat.get(0).Id
                AND (Price__c >= :parentBoat.get(0).Price__c / 1.2)
                AND (Price__c <= :parentBoat.get(0).Price__c * 1.2)
                WITH SECURITY_ENFORCED
                ORDER BY Price__c, Length__c, Year_Built__c
            ];
        } else if (similarBy == TYPE_TYPE) {
            similarBoats = [
                SELECT Id, Contact__r.Name, Name, BoatType__c, BoatType__r.Name, Length__c, Picture__c, Price__c, Year_Built__c
                FROM Boat__c
                WHERE Id != :parentBoat.get(0).Id
                AND (BoatType__c = :parentBoat.get(0).BoatType__c)
                WITH SECURITY_ENFORCED
                ORDER BY Price__c, Length__c, Year_Built__c
            ];
        }
        return similarBoats;
    }

}

boatSearchResults.html:使用 layout方式展示一些数据

<template>
    <lightning-tabset variant="scoped">
        <lightning-tab label="Gallery">
            <template if:true={boats.data}>
                <div class="slds-scrollable_y">
                    <lightning-layout horizontal-align="center" multiple-rows>
                        <template for:each={boats.data} for:item="boat">
                            <lightning-layout-item key={boat.Id} padding="around-small" size="12" small-device-size="6"
                                medium-device-size="4" large-device-size="3">
                                <c-boat-tile boat={boat} selected-boat-id={selectedBoatId}
                                    onboatselect={updateSelectedTile}></c-boat-tile>
                            </lightning-layout-item>
                        </template>
                    </lightning-layout>
                </div>
            </template>
        </lightning-tab>
    </lightning-tabset>
</template>

boatSearchResults.js:这里我们可以看到,首先头部引入了 MessageChannel 以及使用了 MessageContext以及 publish方法,下面的方法中使用了 publish方法去进行了广播操作

import { LightningElement, wire, api, track } from 'lwc';
import getBoats from '@salesforce/apex/BoatDataService.getBoats';
import { publish, MessageContext } from 'lightning/messageService';
import BoatMC from '@salesforce/messageChannel/BoatMessageChannel__c';

export default class BoatSearchResults extends LightningElement {
    boatTypeId = '';
    @track boats;
    @track draftValues = [];
    selectedBoatId = '';
    isLoading = false;
    error = undefined;
    wiredBoatsResult;

    @wire(MessageContext) messageContext;

    columns = [
        { label: 'Name', fieldName: 'Name', type: 'text', editable: 'true'  },
        { label: 'Length', fieldName: 'Length__c', type: 'number', editable: 'true' },
        { label: 'Price', fieldName: 'Price__c', type: 'currency', editable: 'true' },
        { label: 'Description', fieldName: 'Description__c', type: 'text', editable: 'true' }
    ];

    @api
    searchBoats(boatTypeId) {
        this.isLoading = true;
        this.notifyLoading(this.isLoading);
        this.boatTypeId = boatTypeId;
    }

    @wire(getBoats, { boatTypeId: '$boatTypeId' })
    wiredBoats(result) {
        this.boats = result;
        if (result.error) {
            this.error = result.error;
            this.boats = undefined;
        }
        this.isLoading = false;
        this.notifyLoading(this.isLoading);
    }

    updateSelectedTile(event) {
        this.selectedBoatId = event.detail.boatId;
        this.sendMessageService(this.selectedBoatId);
    }

    notifyLoading(isLoading) {
        if (isLoading) {
            this.dispatchEvent(new CustomEvent('loading'));
        } else {
            this.dispatchEvent(CustomEvent('doneloading'));
        }
    }

     sendMessageService(boatId) { 
        publish(this.messageContext, BoatMC, { recordId : boatId });
    }
}

boatTile.html:展示gallery中的每一个item的UI

<template>
    <div onclick={selectBoat} class={tileClass}>
        <div style={backgroundStyle} class="tile"></div>
        <div class="lower-third">
            <h1 class="slds-truncate slds-text-heading_medium">{boat.Name}</h1>
            <h2 class="slds-truncate slds-text-heading_small">{boat.Contact__r.Name}</h2>
            <div class="slds-text-body_small">
                Price: <lightning-formatted-number maximum-fraction-digits="2" format-style="currency" currency-code="USD" value={boat.Price__c}> </lightning-formatted-number>
            </div>
            <div class="slds-text-body_small"> Length: {boat.Length__c} </div>
            <div class="slds-text-body_small"> Type: {boat.BoatType__r.Name} </div>
        </div>
    </div>
</template>

boatTile.js:item点击以后调度事件,boatSearchResults这个父组件 handle事件,从而实现了广播的发布

import { LightningElement, api} from "lwc";
const TILE_WRAPPER_SELECTED_CLASS = "tile-wrapper selected";
const TILE_WRAPPER_UNSELECTED_CLASS = "tile-wrapper";
export default class BoatTile extends LightningElement {
    @api boat;
    @api selectedBoatId;
    get backgroundStyle() {
        return `background-image:url(${this.boat.Picture__c})`;
    }
    get tileClass() {
        return this.selectedBoatId == this.boat.Id ? TILE_WRAPPER_SELECTED_CLASS : TILE_WRAPPER_UNSELECTED_CLASS;
    }
    selectBoat() {
        this.selectedBoatId = !this.selectedBoatId;
        const boatselect = new CustomEvent("boatselect", {
            detail: {
                boatId: this.boat.Id
            }
        });
        this.dispatchEvent(boatselect);
    }
}

3. 消息订阅

boatDetailTabs.html:用来展示发布过来的指定的记录的详细信息

<template>
  <template if:false={wiredRecord.data}>
    <!-- lightning card for the label when wiredRecord has no data goes here  -->
      <lightning-card class= "slds-align_absolute-center no-boat-height">
          <span>{label.labelPleaseSelectABoat}</span>
      </lightning-card>
  </template>
  <template if:true={wiredRecord.data}>
     <!-- lightning card for the content when wiredRecord has data goes here  -->
     <lightning-card>
         <lightning-tabset variant="scoped">
             <lightning-tab label={label.labelDetails}>
                 <lightning-card icon-name={detailsTabIconName} title={boatName}>
                     <lightning-button slot="actions" title={boatName} label={label.labelFullDetails} onclick={navigateToRecordViewPage}></lightning-button>
                     <lightning-record-view-form density="compact"
                          record-id={boatId}
                          object-api-name="Boat__c">
                          <lightning-output-field field-name="BoatType__c" class="slds-form-element_1-col"></lightning-output-field>
                          <lightning-output-field field-name="Length__c" class="slds-form-element_1-col"></lightning-output-field>
                          <lightning-output-field field-name="Price__c" class="slds-form-element_1-col"></lightning-output-field>
                          <lightning-output-field field-name="Description__c" class="slds-form-element_1-col"></lightning-output-field>
                     </lightning-record-view-form>
                 </lightning-card>
             </lightning-tab>
             
         </lightning-tabset>
     </lightning-card>
  </template>
</template>

boatDetailsTabs.js:connectedCallback生命周期函数中进行订阅操作,订阅到boatId的值,从而 getRecord展示左侧galery选择的数据的详情信息

// Custom Labels Imports
// import labelDetails for Details
// import labelReviews for Reviews
// import labelAddReview for Add_Review
// import labelFullDetails for Full_Details
// import labelPleaseSelectABoat for Please_select_a_boat
// Boat__c Schema Imports
// import BOAT_ID_FIELD for the Boat Id
// import BOAT_NAME_FIELD for the boat Name
import { LightningElement, api,wire } from 'lwc';
import labelDetails from '@salesforce/label/c.Details';
import labelReviews from '@salesforce/label/c.Reviews';
import labelAddReview from '@salesforce/label/c.Add_Review';
import labelFullDetails from '@salesforce/label/c.Full_Details';
import labelPleaseSelectABoat from '@salesforce/label/c.Please_select_a_boat';
import BOAT_ID_FIELD from '@salesforce/schema/Boat__c.Id';
import BOAT_NAME_FIELD from '@salesforce/schema/Boat__c.Name';
import { getRecord,getFieldValue } from 'lightning/uiRecordApi';
import BOATMC from '@salesforce/messageChannel/BoatMessageChannel__c';
import { APPLICATION_SCOPE,MessageContext, subscribe } from 'lightning/messageService';
const BOAT_FIELDS = [BOAT_ID_FIELD, BOAT_NAME_FIELD];
import {NavigationMixin} from 'lightning/navigation';
export default class BoatDetailTabs extends NavigationMixin(LightningElement) {
  @api boatId;

  label = {
    labelDetails,
    labelReviews,
    labelAddReview,
    labelFullDetails,
    labelPleaseSelectABoat,
  };
  
  // Decide when to show or hide the icon
  // returns 'utility:anchor' or null
  get detailsTabIconName() {
    return this.wiredRecord && this.wiredRecord.data ? 'utility:anchor' : null;
   }
  
  // Utilize getFieldValue to extract the boat name from the record wire
  @wire(getRecord,{recordId: '$boatId', fields: BOAT_FIELDS})
  wiredRecord;

  get boatName() {
    return getFieldValue(this.wiredRecord.data, BOAT_NAME_FIELD);
   }
  
  // Private
  subscription = null;
  // Initialize messageContext for Message Service
  @wire(MessageContext)
  messageContext;
  
  // Subscribe to the message channel
  subscribeMC() {
    if(this.subscription) { return; }
    // local boatId must receive the recordId from the message
    this.subscription = subscribe(
        this.messageContext, 
        BOATMC, 
        (message) => {
            this.boatId = message.recordId;
        }, 
        { scope: APPLICATION_SCOPE }
    );
  }
  
  // Calls subscribeMC()
  connectedCallback() { 
    this.subscribeMC();
  }

}

效果展示:当点击左侧列表的图像,右侧会展示当条的具体信息。

 总结:篇中代码看上去可能有点冗余,因为superbadege中还有其他功能,所以只是做了简单的删减,想要复现这种效果可以在lwc superbadge安装一下 unmanaged package然后代码赋值粘贴可以看到效果。篇中只是简单介绍了一下lightning message service的简单实用,limitation以及unsubscription这里不做过多的讲解,自行查看官方文档。篇中有错误地方欢迎指出,有不懂欢迎留言。

posted @ 2020-09-05 11:34  zero.zhang  阅读(2066)  评论(4编辑  收藏  举报