【前言】:在实施Service Cloud Embeded Service时候,自定义Pre-Chat Form,Chat Window等需要用到Lightning Component,所以之前使用VFP + Site这一套方案实施的Live Agent需要升级为Lightning Component + Embeded Service + Site了,那么第一步需要解决的是如何将Lightning Component通过Site暴露给外部服务?经查阅资料,发现Lightning Out可以在VFP中引用Lightning Component。本篇将以Demo形式展示Lightning Out这一技术,并提供授权和非授权两种使用Lightning Out的方法。
【应用场景举例】:
1、使用Aura Component写了Trailhead排名应用,想暴露给参与者访问;
2、通过git应用托管服务,将sf资源暴露给end user,Sample;
3、之前在git上找Custom Email Composer资源时,看到有作者把演示链接放在里面,或许我们也可以通过这个技术更形象直观的让visitor了解这个代码是在干些啥。
【技术骨架】:
【Demo演练】:
目标:我们在Org1有2个Aura Component,分别用来展示10个Accounts和Contacts,我们将2个组件通过resource的形式放入同一个Dependency App里,然后通过Lightning Out JS将组件内容渲染到VFP指定的div盒子,最终通过Site将VFP暴露给外部(免登陆访问SF资源)。
效果1:dependency app通过实现ltng:allowGuestAccess接口,以Guest User Context供外部User访问
代码示例:
Lightning Component部分:
a. Component & Controller.js
<!-- DisplayTenAccountsComp.cmp -->
<aura:component controller="LightningOutCtrl">
<aura:attribute name="accList" type="Account[]" access="private"/>
<aura:attribute name="userInfo" type="String" access="private"/>
<aura:attribute name="conList" type="Contact[]" access="private"/>
<aura:handler name="init" value="{!this}" action="{!c.doInit}"/>
<div>
<p>Current UserInfo: {!v.userInfo}</p>
</div>
<br/>
<ul>
<aura:iteration items="{!v.accList}" var="acc">
<li>{!acc.Id} · {!acc.Name}</li>
</aura:iteration>
</ul>
<br/>
<ul>
<aura:iteration items="{!v.conList}" var="con">
<li>{!con.Id} · {!con.Name} · {!con.Account.Name}</li>
</aura:iteration>
</ul>
</aura:component>
<!-- DisplayTenAccountsCompController.js -->
({
doInit : function(component, event, helper) {
let action = component.get("c.getTenAccounts");
action.setCallback(this, function(response) {
const data = response.getReturnValue();
const dataSize = data.length;
console.log('dataSize: ' + dataSize);
component.set('v.accList', data);
});
$A.enqueueAction(action);
let action1 = component.get("c.getTenContacts");
action1.setCallback(this, function(response) {
const data = response.getReturnValue();
const dataSize = data.length;
console.log('dataSize: ' + dataSize);
component.set('v.conList', data);
});
$A.enqueueAction(action1);
let action2 = component.get("c.getUserInfo");
action2.setCallback(this, function(response) {
const data = response.getReturnValue();
component.set('v.userInfo', data);
});
$A.enqueueAction(action2);
}
})
<!-- DisplayTenContactsComp.cmp -->
<aura:component controller="LightningOutCtrl">
<aura:attribute name="conList" type="Contact[]" access="private"/>
<aura:handler name="init" value="{!this}" action="{!c.doInit}"/>
<ul>
<aura:iteration items="{!v.conList}" var="con">
<li>{!con.Id} · {!con.Name}</li>
</aura:iteration>
</ul>
</aura:component>
<!-- DisplayTenContactsCompController.js -->
({
doInit : function(component, event, helper) {
let action1 = component.get("c.getTenContacts");
action1.setCallback(this, function(response) {
const data = response.getReturnValue();
const dataSize = data.length;
console.log('dataSize: ' + dataSize);
component.set('v.conList', data);
});
$A.enqueueAction(action1);
}
})
b. Apex Controller
public without sharing class LightningOutCtrl {
@AuraEnabled
public static List<Account> getTenAccounts() {
return [SELECT Id, Name FROM Account LIMIT 10];
}
@AuraEnabled
public static List<Contact> getTenContacts() {
return [SELECT Id, Name, Account.Name FROM Contact LIMIT 10];
}
@AuraEnabled
public static String getUserInfo() {
return '{"UserName": '+UserInfo.getUserName()+', "Name": '+UserInfo.getName()+', "Email": '+UserInfo.getUserEmail()+'}';
// return [SELECT Id, Name, UserName, Email, EmailEncodingKey, Profile.Name, UserRole.Name FROM User WHERE Id =:UserInfo.getUserId()];
}
}
c. Dependency App(此处实现了ltng:allowGuestAccess接口用于暴露服务给外部用户,若未声明需要通过配置Connected App,然后在第三方应用通过账密获取access token访问SF资源)
<!-- ALightningOutApp.app -->
<aura:application extends="ltng:outApp" access="global" implements="ltng:allowGuestAccess">
<aura:dependency resource="c:DisplayTenAccountsComp" />
<aura:dependency resource="c:DisplayTenContactsComp" />
</aura:application>
d. VFP
<!-- Component和VFP不同源 -->
<apex:page showHeader="false" sidebar="false">
<script src="https://yoursitedomain.force.com/site/lightning/lightning.out.js"></script>
<center><apex:outputText style="font-size: 18px; color: #f0f" value="Hello, this is my Lightning Out Demo" /></center>
<div id='accList'></div>
<br/>
<div id='conList'></div>
<script>
// dependencyApp
$Lightning.use('c:ALightningOutApp', function() {
// component used in dependencyApp
$Lightning.createComponent('c:DisplayTenAccountsComp',
{},
'accList',
function() {
//do some stuff
console.log('accList has been created...');
});
// component used in dependencyApp
$Lightning.createComponent('c:DisplayTenContactsComp',
{},
'conList',
function() {
//do some stuff
console.log('conList has been created...');
});
}, 'https://yoursitedomain.force.com/site'
);
</script>
</apex:page>
<!-- Component和VFP同源 -->
<apex:page showHeader="false" sidebar="false">
<apex:includeLightning />
<center><apex:outputText style="font-size: 18px; color: #f0f" value="Hello, this is my Lightning Out Demo" /></center>
<div id='accList'></div>
<br/>
<div id='conList'></div>
<script>
$Lightning.use('c:ALightningOutApp', function() {
// component used in dependencyApp
$Lightning.createComponent('c:DisplayTenAccountsComp',
{},
'accList',
function() {
//do some stuff
console.log('accList has been created...');
});
// component used in dependencyApp
$Lightning.createComponent('c:DisplayTenContactsComp',
{},
'conList',
function() {
//do some stuff
console.log('conList has been created...');
});
});
</script>
</apex:page>
效果2:org1包含lightning组件和数据资产,org2模拟第三方应用,我们拟在org2中通过Site以org1 authToken的形式访问org1的lightning component和data
代码示例:
这里与上边通过实现ltng:allowGuestAccess接口不同之处有2点:
a. AlightningOutApp.app里边无需实现guest接口:
<!-- <aura:application extends="ltng:outApp" access="global" implements="ltng:allowGuestAccess"> -->
<aura:application extends="ltng:outApp" access="global">
<aura:dependency resource="c:DisplayTenAccountsComp" />
<aura:dependency resource="c:DisplayTenContactsComp" />
</aura:application>
b. org2里边vfp代码在引用Lightning Out JS库和使用库提供的Use方法传参有差异(注意下面代码中一定要使用org1 lightning URI,classic的URI会报错):
<apex:page showHeader="false" sidebar="false">
<!-- <apex:includeLightning/> -->
<script src="https://org1.lightning.force.com/lightning/lightning.out.js"></script>
<center><apex:outputText style="font-size: 18px; color: #f0f" value="Hello, this is my Lightning Out Demo" /></center>
<div id='accList'></div>
<br/>
<div id='conList'></div>
<script>
// dependencyApp
$Lightning.use('c:ALightningOutApp', function() {
// component used in dependencyApp
$Lightning.createComponent('c:DisplayTenAccountsComp',
{},
'accList',
function() {
//do some stuff
console.log('accList has been created...');
});
// component used in dependencyApp
$Lightning.createComponent('c:DisplayTenContactsComp',
{},
'conList',
function() {
//do some stuff
console.log('conList has been created...');
});
}, 'https://org1.lightning.force.com', 'access_token'
);
</script>
</apex:page>
【Q&A】:
Q1. 如何获取access_token?
A1. 参见Using Advanced REST Client to test REST Request - step by step
【特别注意】:
1. 如果不同源,需要在Component所在的Org1加CORS [Org2的community uri / classic uri](同源不存在跨域,不需要加);
2. 必须在site profile里启用vfp(同源时),apex和OLS(缺一不可);
官方文档说lightning out not enforce OLS and FLS, 实验后持保留意见
3. 如果查询org级别数据,apex必须使用without sharing,FLS加不加无所谓;
否则,console可能会出现服务器500错误或如下错误:
Access to XMLHttpRequest at 'https://yoursitedomain1.force.com/site1/ALightningOutApp.app?aura.format=JSON&aura.formatAdapter=LIGHTNING_OUT' from origin 'https://yoursitedomain2.force.com' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.