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
讲这个以前先以一个例子作为展开。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
我们在广播或者订阅以前都需要先引入我们创建的 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这里不做过多的讲解,自行查看官方文档。篇中有错误地方欢迎指出,有不懂欢迎留言。
Salesforce LWC学习(二十三) Lightning Message Service 浅谈的更多相关文章
- Salesforce LWC学习(二十四) Array.sort 浅谈
本篇参考:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Array/sort sal ...
- Salesforce LWC学习(二十一) Error浅谈
本篇参考:https://developer.salesforce.com/docs/component-library/documentation/en/lwc/data_error https:/ ...
- Salesforce LWC学习(二十七) File Upload
本篇参考: https://developer.salesforce.com/docs/component-library/bundle/lightning-file-upload/documenta ...
- Salesforce LWC学习(二十九) getRecordNotifyChange(LDS拓展增强篇)
本篇参考: https://developer.salesforce.com/docs/component-library/documentation/en/lwc/data_ui_api https ...
- Salesforce LWC学习(二十六) 简单知识总结篇三
首先本篇感谢长源edward老哥的大力帮助. 背景:我们在前端开发的时候,经常会用到输入框,并且对这个输入框设置 required或者其他的验证,当不满足条件时使用自定义的UI或者使用标准的 inpu ...
- Salesforce LWC学习(二) helloWorld程序在VSCode中的实现
上一篇我们简单的描述了一下Salesforce DX的配置以及CLI的简单功能使用,此篇主要简单描述一下LWC如何实现helloWorld以及LWC开发时应该注意的一些规范. 做国内项目的同学直观的感 ...
- Salesforce LWC学习(二十二) 简单知识总结篇二
本篇参看: https://developer.salesforce.com/docs/component-library/documentation/en/lwc/lwc.reactivity_fi ...
- Salesforce LWC学习(二十五) Jest Test
本篇参看: https://trailhead.salesforce.com/content/learn/modules/test-lightning-web-components https://j ...
- Salesforce LWC学习(二十八) 复制内容到系统剪贴板(clipboard)
本篇参考: https://developer.mozilla.org/zh-CN/docs/Mozilla/Add-ons/WebExtensions/Interact_with_the_clipb ...
随机推荐
- 灰帽黑客 基本的Linux漏洞攻击
有两个重要的寄存器负责处理堆栈:基址指针(EBP)和栈指针(ESP),EBP指向当前进程的当前栈帧的底部,ESP则总是指向栈顶 当调用函数的时候,会导致程序流跳转.在汇编代码调用函数时,将发生以下三件 ...
- “随手记”开发记录day07
今天完成了关于我们页面中的相关信息,由于之前没有做过这个东西,只想着用一个view解决 可是发现我们整的太简单了,还是太年轻,最后想出来要跟java代码一起解决这个问题, 效果
- three.js 着色器材质之纹理
今天郭先生说一说如何在three.js着色器中添加纹理,先看看今天要完成的效果,在线案例请点击博客原文. 这里我们分别引入三个纹理,分别是地球的表面纹理,对应的海拔灰度图,和云朵的纹理.使用表面纹理还 ...
- 2020-04-14:mysql原子性和持久性怎么保证
1.Mysql怎么保证一致性的? OK,这个问题分为两个层面来说. 从数据库层面,数据库通过原子性.隔离性.持久性来保证一致性.也就是说ACID四大特性之中,C(一致性)是目的,A(原子性).I(隔离 ...
- Python多进程队列间传递对象
前言 在python 需要在队列中传递对象, 会出现进程不能正常退出的情况. 其原因是因为 在父进程 向子进程传入的Queue对象不对, Queue对象正常是子进程之间的信息传递, 而当我在父进程 创 ...
- 笔试算法稳了,GitHub 50k Star《labuladong的算法小抄》
秋招算法有救了!!! 前不久在 GitHub 出现了一个手把手带你刷 LeetCode 的项目:fucking-algorithm. 该项目此前在 GitHub 开源后,连续多次霸榜 GitHub T ...
- JavaScript设计模式之单例模式【惰性单例】
在提高开发水平,往中高级前端工程师中,利用设计模式是必不可少的一条道路.掌握设计模式的思想远远比硬套重要,因为设计模式是一种思想,不局限于开发语言.但实际上由于语言的特性不同,往往在实现的时候会有不少 ...
- 基于vue的实时视频流开发
背景:多个实时视频的介入 技术:hls.js的流媒体,支持格式已m3u8为主 解决了什么:多个实时视频长时间播放会有卡顿的情况 具体代码实现: import Hls from 'hls.js' pla ...
- 走正确的路 - IT业没有护城河 - 机器翻译新锐Deepl
最近发生了一件很令我震惊的事情:新的一个机器翻译网站出现了 - www.deepl.com (DeepL 或许会成为你今年首选的翻译工具) 机器翻译早就是红海市场了.我就不从1954年IBM发布俄翻英 ...
- 第7章 Spark SQL 的运行原理(了解)
第7章 Spark SQL 的运行原理(了解) 7.1 Spark SQL运行架构 Spark SQL对SQL语句的处理和关系型数据库类似,即词法/语法解析.绑定.优化.执行.Spark SQL会先将 ...