Aura Refresh View的LWC平替——getRecordNotifyChange

本文得到了 ChatGPT 提供的专业建议和技术支持,特此致谢。
该文章由Notion AI辅助完成。

随着Lightning Web Components(LWC)的兴起,越来越多的开发者开始使用LWC来构建自己的Lightning组件。在这个过程中,我们可能会遇到一些需要替换旧版Aura组件的场景。其中,Aura的Refresh View是一个比较常见的用例。在LWC中,可以使用getRecordNotifyChange方法来实现这个功能。

让我们来回顾一下Aura的Refresh View

在Aura中,我们可以使用force:refreshView事件来实现类似Aura Refresh View的功能。下面是一个示例代码:

({
    handleRecordChange: function(component, event, helper) {
        $A.get('e.force:refreshView').fire();
    }
})

在上面的代码中,当记录发生变化时,会触发force:refreshView事件,从而实现页面的自动刷新。
需要注意的是,force:refreshView事件只能在Lightning Experience和Salesforce移动应用中使用,而不能在Classic中使用。如果需要在Classic中实现类似的功能,可以考虑使用force:refreshViewAction事件。

如何使用getRecordNotifyChange

在LWC中,我们可以使用getRecordNotifyChange方法来实现类似的功能。该方法可以监听记录变化并触发页面刷新。它的基本用法如下:

import { LightningElement, api, wire } from 'lwc';
import { getRecordNotifyChange } from 'lightning/uiRecordApi';

export default class MyComponent extends LightningElement {
    @api recordId;

    handleRecordChange(event) {
        getRecordNotifyChange([this.recordId]);
    }
}

在上面的例子中,我们使用了@wire装饰器来监听记录的变化,当记录发生变化时,会调用handleRecordChange方法。该方法会调用getRecordNotifyChange方法来触发页面刷新。

需要注意的是,getRecordNotifyChange方法只能在LWC中使用,而不能在Aura组件中使用。如果我们需要在Aura组件中实现类似的功能,可以考虑使用force:refreshView事件来刷新页面。

在LWC中如何使用ExcelJS-番外篇之关于regeneratorRuntime

本文得到了 ChatGPT 提供的专业建议和技术支持,特此致谢。

在上一篇文章《在LWC中如何使用ExcelJS》中介绍了如何在LWC中引入并使用ExcelJS。但是挖了一个坑,就是为什么在LoadScript之前要添加如下两句


            var regeneratorRuntime = undefined;
            window.regeneratorRuntime = regeneratorRuntime;

首先,ExcelJS由于要大量操作与渲染数据,为了避免页面经常卡死,所以使用了regenerator-runtime库以便更容易的进行异步操作。
就是说ExcelJS依赖regenerator-runtime库。

然后,ExcelJS在加载时会判断当前运行上下文中使用有变量regeneratorRuntime存在。如果没有找到该变量,ExcelJS会认为当前运行环境默认支持regenertor-runtime。不过LWC运行环境并不支持regenertor-runtime所以导致报错。

但是,如果我们手动的定义一个全局变量regeneratorRuntime,则相当于告诉ExcelJS,不要尝试使用运行环境的regenertor-runtime,去使用全局变量定义的regenertor-runtime。

但是又由于我们定义的regenertor-runtime为undefined,则迫使ExcelJS放弃全局变量定义的regenertor-runtime,转而使用自己准备的regenertor-runtime以保证自己可以成功加载与运行。

以下流程图由ChatGPT生成(请点击View Source查看)

                                 +----------------------+
                                 |                      |
                                 |   Load ExcelJS into   |
                                 |   Lightning Web      |
                                 |   Component (LWC)    |
                                 |                      |
                                 +----------+-----------+
                                            |
                                  +---------+---------+
                                  |                   |
                                  |   ExcelJS checks   |
                                  |   for              |
                                  |   regeneratorRuntime|
                                  |                   |
                                  +---------+---------+
                                            |
                            +---------------+---------------+
                            |                               |
                      +-----v-----+                 +---------+---------+
                      |           |                 |                   |
                      | regeneratorRuntime found    |    ExcelJS uses   |
                      |           |                 |native JS generator|
                      +-----+-----+                 +---------+---------+
                            |                                   |
                    +-------+-------+                           |
                    |               |                           |
        +-----------v---+   +-------v---+               +-------v------+
        |               |   |           |               |              |
        | Define        |   | Define    |               |ExcelJS reports|
        | regenerator-  |   | regenerator-             |      error    |
        | Runtime as    |   | Runtime as               |               |
        | undefined     |   | function()               |               |
        |               |   | {return;};               |               |
        +-------+-------+   +-------------+             +-------+------+
                |                               +-----------------+
                |                               |                 |
                |                               | ExcelJS executes |
                |                               | successfully    |
                |                               |                 |
                +-------------------------------+-----------------+

在LWC中如何使用ExcelJS

1. Exceljs是什么?

Exceljs作为开源免费功能强大的JS库,受到了广大开发人员的好评。开源地址为https://github.com/exceljs/exceljs

通过这个lib我们可以使用Javascript轻松的生成/操纵/读取Excel文件。

1.1 如何获得Exceljs文件

官方渠道没有提供现成的CDN或者直接可下载的js lib文件。根据官方文档,需要使用npm install命令来获得exceljs文件。

npm install exceljs

执行完该命令后,会在当前目录下生成node js project,所以建议先新建一个文件夹,然后命令行切换到改文件夹之后再执行此命令。

然后进入下列位置:
XXXXXX(当前文件夹名称)-> node_modules -> exceljs -> dist
拷贝出exceljs.min.js与exceljs.min.js.map文件。

新建文件夹exceljslib,并将上述两个文件放入。

压缩该文件夹为exceljslib.zip备用。

1.2 当然,如果你手懒的话我可以提供打包好的版本

点击下载(如果信任我的话):exceljslib.zip

2. Salesforce端

2.1 将exceljslib.zip上传至static resouce:

Setup-> Custom Code->Static Resouces->New->Name:exceljs->File:exceljslib.zip

2.2 LWC中引用ExcelJS

在环境中创建LWC【TestExcelJS】,根据需要,参考下面代码,结合官方文档填充操纵excel部分代码。

import { LightningElement, api } from 'lwc';
// 加载Static Resource必须先引入loadScript
import { loadScript } from 'lightning/platformResourceLoader';
// 引入staticResource并且命名为exceljs,这里喜欢的话叫它zhangsan也可以
import exceljs from '@salesforce/resourceUrl/exceljs';

export default class TestExcelJS extends LightningElement {
    @api async download() {
        try{
            // 不加入下面两行引用exceljs会报错,具体原理以后会专门开一篇文章讲解
            var regeneratorRuntime = undefined;
            window.regeneratorRuntime = regeneratorRuntime;
            // loadScript路径规则=> / import的staticResource名 / zip包名 / 文件夹名 / js文件名
            await loadScript(this, exceljs + '/exceljslib/exceljs.min.js');
            // 创建ExcelJS实例
            const workbook = new ExcelJS.Workbook();
            // 此处参照官方文档对excel进行操作
            // ......
            // 比如添加一个sheet页
            // const sheet1 = workbook.addWorksheet("Sheet1");
            // 之后用前端页面或者后台查询的数据填充该sheet页等。

            // 下载生成的workbook
            const buffer = await workbook.xlsx.writeBuffer();
            const blob = new Blob([buffer], { type: 'application/octet-stream' });
            const link = document.createElement('a');
            link.href = window.URL.createObjectURL(blob);
            link.download = 'MyData.xlsx';
            link.click();  
        } catch (error) {
            console.error(error);
        }
    }
}

在LWC的HTML中随便创建一个可点击的button

<template>
    <button onclick={download}><b>Click to Export!</b></button>
</template>

点击后,就可以得到想要的Excel文件。

关于Trigger中的Return

某天,旁边的同事自顾自地嘀咕起来:“如果Trigger里也能用Return该多好啊。。。。”。
我一听这话忍不了啊,使劲儿拍了下他的桌子,“能啊!谁说不能的!”

Trigger中使用Return规则遵从代码块作用域规则。
举个例子,

(注:Return;皆加在Debug Log之前)
如果在位置0写Return;
那么整个Trigger都会被Return跳过去,Debug Log中没有任何输出。
如果在位置1写Return;
那么第一个handler中的内容都会被跳过,Debug Log的输出结果为0,4,5,6。
如果在位置2写Return;
那么第一个handler的第一个方法的内容都会被跳过,Debug Log的输出结果为0,1,3,4,5,6。
同理,如果在位置4写Return;
那么第二个handler被完整跳过,Debug Log的输出结果为0,1,2,3。
通俗的讲,就是从方法级别开始,只影响到花括号范围。

那么有同学肯定会问。虽然代码规范一般不允许一个sObject建多个Trigger。那么万一就是有两个Trigger呢。
然后在其中一个Trigger中使用Return,会影响另一个Trigger执行吗?

答案是:不会。

道理是一样的,把Trigger降级一层,外面再加一层运行环境,那么每个Trigger里的Return就如同Handler里的Return,也只能影响自己而已。

// Update
自从某次Release之后,如果Return; 语句后面还有可执行的语句,保存时会报错,说有unreachable的代码。
如果想做实验,需要使用如下写法

if(true) {
    return;
}

利用Gitlab CI将代码部署至FTP服务器

作为Github的拥趸,最初这篇文章的标题是《利用Github Travis CI将代码部署至FTP服务器》。本来盘算的很好,利用这篇文章,一来是记录一下自己解决的问题,二来是宣传一下Github是多好用。
众所周知,Github等一众在线同性交友代码托管平台随着开发方法论的不断进化,已经演变成了CI/CD平台。哪家平台要是说自己不支持CI/CD,都不好意思出来打招呼。而Gitbub原来并没有自己的CI/CD功能,Travis CI充当了这一角色。最近,Github也推出了自己的Gitbub Actions来弥补这一块空缺,目前仍然在beta阶段。我也申请了测试资格,但目前仍然没有轮到我。

作为个人立场,很喜欢使用Github来托管代码。以前免费账号只能托管公开仓库,而收费账户又舍不得银子,无奈只能把私有仓库分散在Bitbucket和Gitlab上。
后来大家都知道了,Github被微软收购之后,免费账号也可以托管私有仓库了。我就琢磨着把所有repo都集中到Github上吧,结果猜怎么着?Travis CI免费版仍然只能用在公开仓库上。只好作罢。

当然,这次放弃Github+Travis CI组合的原因之中,Repo必须公开是其中一方面。另一方面是在使用Travis CI时遇到了无法解决的问题。

俗话说,先有问题才有的工具,而不是先有的工具而后产生的问题。首先说这次要解决的问题是什么。
其实这个问题很简单。平时在写文章或者写插件的过程中会遇到一些不懂的web技术,然后就需要做一些小demo或者简单的概念验证放到我自己的服务器上。由于有多设备切换开发的需求,任何代码都要放在远程git上做版本管理与同步。这样的话,就会出现一个稍许繁琐的操作————改完代码之后,要先commit到本地仓库,然后再push到远程仓库,然后将本地文件通过FTP工具上传到服务器,之后在浏览器打开网站进行测试,如果有问题则循环本套操作。

如果在我将代码提交到远程仓库之后,能够自动部署到FTP上该多好啊~!
很久很久以前,在SFDX还没发布的那个年代,我曾经利用GitHub+Travis CI做过一个Salesforce StandardSetController的小Demo的自动部署。每次提交完改动,就能自动部署到指定环境。我觉得部署到FTP怎么也得比部署到Salesforce Org还要简单吧!

说干就干,由于对Travis CI还算熟悉,立马查了一下文档,自信满满的写下了如下的yml文件。

# .travis.yml
language: node_js
script:
- echo "skipping tests"
after_success:
- curl --ftp-create-dirs
       -T index.html
       ftp://${FTPUSER}:${FTPPASSWORD}@${FTPENDPOINT}

语言无所谓,测试跳过,直接将文件通过ftp传送到服务器上。
结果执行之后,发现不对劲了。正常传送一个文件不说是瞬间完成,也不至于10分钟都传不完,导致被Travis强行把任务中止吧?然后去FTP上瞧了一眼,文件是创建出来了,但是大小是0KB。这肯定不对劲啊,查查吧。结果查到一篇Travis的官方博客,解释他们的NAT因为什么什么原因,导致FTP的被动模式失败之类的。总之就是ftp命令不能用了。
那行,我改用sftp命令。结果这次直接报错了,说不支持sftp命令。Execuse me? 然后搜了大半天,也没找到什么靠谱的解决方案。这就让人觉得不可思议了,对于CI工具来讲,FTP上传不比连接AWS、Heroku之类的简单多了?

正在感到弱小无助之际,我想起了一直在默默当备胎的Gitlab。
Gitlab也有自己的CI/CD工具,并且是免费,且可以用在私有Repo上的。
但是Gitlab的yml文件语法与Travis不同,只好硬着头皮速读了一下Gitlab CI的yml写法。
经过不断的研究与尝试,终于写下了如下的yml文件。(其实是从sample yml文件改造出来的)

# .gitlab-ci.yml
# This file is a template, and might need editing before it works on your project.
# Full project: https://gitlab.com/pages/plain-html
pages:
  stage: deploy
  script:
    - mkdir .public
    - cp -r * .public
    - mv .public public
    - apt-get update -qq && apt-get install -y -qq lftp
  artifacts:
    paths:
      - public
  only:
    - master
  after_script:
    - lftp -c "set ftp:ssl-allow no; open -u $USERNAME,$PASSWORD $HOST; mirror -Rev public/ ./ --ignore-time --parallel=10 --exclude-glob .git* --exclude .git/"

任务名称随意,artifact就是本次部署的全部文件归档,这里文件夹名字叫public。为了使用lftp命令,在脚本中提前安装lftp命令。
在CI/CD的setting页面提前配置好USERNAME, PASSWORD, HOST三个变量可以避免将敏感信息放到仓库文件中。
接下来在Gitlab仓库中启用CI/CD,就可以享受了。

其实同理,既然能用Gitlab CI部署代码到FTP,也能部署代码到Salesforce。之后还会写一篇Salesforce的兄弟篇。而这篇文章呢,也不想写CI/CD是什么,也不想谈论DevOps的优势,毕竟这种东西用关键字一搜索,满大街都是,感兴趣的人在看到文章头两个自然段之后早就自己去搜了。毕竟我说它千好万好,不如举一个实际的,好上手的例子让大家看到实实在在的东西来的实惠。

// 后记

也很长时间没有更新了。有人问我哪去了。其实。。。这期间发生了很多事情,也经历了很多事情。素材也是越攒越多。奇怪的是素材攒的越多越觉得没什么值得写的东西。可能,大家都在进步,对于Salesforce平台的基本认知已经都跨越了那道门槛,现在更需要的是Salesforce这款产品以外的,更关乎于计算机世界的、网络世界的基础知识,软件工程的基本概念。还有对于各个行业是如何运转的行业基本了解。

随着年龄增加,进入到了人生的不同阶段,会发现你完全属于你自己的时候越来越少。你逐渐被必须要承担的各种角色完全瓜分。以前争不到的东西现在更加的争不到,原来拥有的东西反而越丢越多。Whatever,要不然又能怎样呢,要不然选择随波逐流,要不然选择自己受的苦自己咽下去,你敢选第三条路吗?(摊手)

关于在Component中向Lightning Button传值的方法

某日,
打水途中看到某位同学眉头紧锁双手合十做沉思状,于是乎忍不住凑了过去。
哦。。。原来在写Lightning Component。隐隐约约看到密密麻麻的controller.js代码感觉不妙。
“咳。”为了避免吓到他,我轻咳了一下表示进场。
只见他先是略微抬起头,之后彻底滑落到了椅子上。
“唉,事情是这样的,” 他用沙哑的嗓音说道,“最开始他们要我做一个button,点了之后去后台更新一条数据,将其字段A__c更新为a。”
他把自己从椅子上重新支了起来,指着屏幕说,“这个很简单,我放了一个attribute来存需要更新的数据的id,然后在lightning button的onClick属性指定controller的方法,在controller的方法中cmp.get那个attribute的值,之后传给apex去更新这条数据。”
我微微点头表示赞同。“没毛病。然后呢?”

“然后,他们又说,要再加一个button,点这个button之后,要给这条数据的字段A__c更新为b。OK,我在cmp中加了又一个button,然后在controller里又为了新button新建了一个方法,与之前的方法不同的是,更新的值为空。”
“也不是不可以呀。“
“对,”接下来他双眼空洞的目视前方。“然后他们又让我加三个button,点了之后分别赋值b,c,d。这时候我意识到情况不对,打算在button的conlick事件把值传给controller中的方法,这样就不用每加一个button就新加一个方法了。结果……我发现……lightning button的onclick根本不能设参数,controller的方法参数也是固定的。我没办法了……”

“我明白你的意思了”。我按住了他的肩膀。

在常规前端开发中,在button的click事件中绑定的js函数传递不同的值是常见做法。如果点击button之后,处理相同,仅是需要处理的参数不同的情况应该对代码进行合理的复用。如下例。

let foo1 = function(param1) {
console.log(param1);
}
<div>
    <input type="button" name="Button" onClick="foo1('a')"/>
</div>

但是lightning button(官方文档)的onclick事件,并没有提供直接传递参数的方式。所以我们只能走曲线救国的路线。
其实很简单。
首先,在官方文档里提到,

You can retrieve the button that’s clicked by using event.getSource(). For example, to retrieve the label on the button, use event.getSource().get("v.label").

说明我们可以在controller的方法中拿到event的触发者——被点击的button,然后获得此button上的属性——比如Label。

那么就好办了,首先我们在lightning button上光明正大的设置value属性

<lightning:button label="button1" title="create" onclick="{!c.updateRecord}" value="a"/>

然后在controller的方法中,去get这个button的value属性,接下来正常执行业务逻辑就可以了。

({   
    updateRecord : function (component, event, helper) {
        let targetValue = event.getSource().get("v.value");
        // YOUR STATEMENT HERE ......
    }
})

好了,水凉了,还得重新接一杯。

关于Javascript的Proxy

我只是想开橘子罐头,你却要卖我一把瑞士军刀Plus。为了用这把瑞士军刀Plus开罐头,我需要先折叠锤子,收起剪子,掰开螺丝刀……

一个只是想吃橘子罐头的人

某天,我要写一个显示某服务实时状态的页面。
动手之前感觉这个需求还是挺简单的。不就是新建一组HTML元素,然后用javascript对其中的元素赋值。So easy。

一边哼着小曲,一边就把HTML元素整整齐齐的码好了。接来下,让我看看有几个信息来源会更新HTML的值。1,2,3,4,5,6……什么?三十个模块?OK,OK,让我一个一个写,只是体力活而已。

继续阅读“关于Javascript的Proxy”

关于window.postMessage

很久没写东西了,偶尔会对着本文编辑器发呆,不知道自己想写什么,不知道应该写什么。又觉得什么都不应该写,什么也都不值得写。能静下心坐在电脑前写点什么是越来越奢侈的事情。也许,too many mind。

我自己说的

我相信每一个接触过前端开发的程序员都被跨域问题折磨过。

那么什么是跨域问题?
经历过的人脱口而出,“跨域问题就是处于安全考虑,当前域名不允许访问另一个域名的资源”。
这样说其实并不正确。仔细回忆一下,在曾经访问过的网页中,是否包含非本域名的CSS,JS和图片?
实际上获取这些CSS,JS和图片的请求已经跨域了,可是并没有出错。

CORS定义 点这里。严谨点说,是只有通过脚本发出的跨域请求才会被禁止,而通过标签发出的资源请求则可以顺利完成。并且进行拦截动作的,是浏览器本人。
原因显而易见————脚本运行在浏览器,而不是服务器。所以通过服务器,或者非浏览器的客户端(或者魔改版浏览器),可以随意的向任意域名发出请求。
为了绕开这个机制,各路神仙也是各显神通,开了各种各样的脑洞。

继续阅读“关于window.postMessage”

关于Stub API

Spring’17,Salesforce为开发者提供了一个强大的工具,Apex STUB API

看完官方文档提供的例子之后,感觉确实很美好,但又觉得没什么机会用。
不同于Java项目对框架和设计模式的极致追求。
我接手的Apex代码均为意识流散文诗式写法。根本没有抽象化,解耦等操作。

Stub Api是基于Java的Mocking Framework
mockito开发的。
使用Mocking Framework的前提是代码必须进行解耦,就是所谓的依赖注入(Dependency Injection)。
要求代码不能按业务直接写成流水账,而是将模块之间的强依赖解开。

比如,A类的构造强依赖与B类,如果没有B则没有A。

A a = new A();
//-----------------------------------
Class A {
    private B b;
    public A() {
        b = new B();
    }
    public String getName() {
        return b.getName();
    }
}

Class B {
   public String getName() {
       return "I'm the B";
   }
}

那么,问题来了,如果我有一个C类,返回“I’m the C”,我就要为C类新建一个A1类,在A1类中将A1与C进行强依赖。
如果需求越来越多,代码也随时越来越膨胀。
那么怎么改变这个局面呢?

B b = new B();
A a = new A(b);
-----------------------------------------------
Class A {
    private CustomObject co;
    public A(CustomObject cObj) {
        this.co = cObj;
    }
    public String getName() {
        reutrn co.Name;
    }
}

Class B extends CustomObject {
    public String getName() {
        return "I'm the B";
    }
}
abstract Class CustomObject {
    abstract public String getName();
}

这样一来,A和B的强依赖就解耦了,不再是每次new一个A,就必然得到B的内容,而是根据我传进去的B来决定A返回的内容。
如果想返回“I’m the C”,就不用再去新建一个A1类,想返回“I’m the D”,不用再去新建一个A2类。

只有依赖注入的写法才能进行方便的Mocking。

Stub Api可以做到的是,在解耦之后,为A类mock一个B类,C类。做到完美的单元测试(模块隔离)。
Whatever,用不上。至今还没见到过有控制反转思想的的Apex。
说句题外话,apex-enterprise-patterns(官方介绍文章)到现在还没推广开呢。

// 未完待续

Salesforce的StandardSetController使用Demo

先放Github地址
https://github.com/Kealthals/Salesforce-StandardSetControllerDemo.git

事情起因,是有人问我为什么StandardSetController无法保存Selected状态。
我觉得既然标准List View可以,那么使用StandardSetController应该也可以。
虽然这个Class我用的也不多,但还是动手写个一个小Demo验证了一下我的想法,虽然花了些小心思,但证明确实可行。
验证完毕,转念一想,既然已经动手了,就干脆把所有的methods都演示一遍吧。权当给自己留一个财富。

StandardController大家都比较熟悉,用来处理单条数据。模拟的是Create/Edit页面和Detail页面。
StandardSetController用的不多,因为模拟的是List View页面。标准List View页面已经很强大了,很少会遇到这种需求。
这个Demo也就是尽量做一个和标准List View类似的页面,时间有限,肯定做不到标准页面一般的好看和完美。
最低目标是将这个Class提供的method都使用一遍,除了一个不明所以的method之外,最低目标我觉得是完成了。
功能上,也提供了基本的
1. 罗列数据
2. 翻页
3. 选择数据(Selected)
4. 选择List View
5. 改变Page Size
6. 跳转指定页
7. Inline Edit(还不完美)
作为一个独立功能来讲,勉强达到了凑合着用的程度。
在功能实现的时候,又不断遇到了一些或新或旧的小问题,在大家的帮助下都找了比较好的小方案解决掉了这些小问题。
比如SFDC的autofocus,inputtext的输入类型限制等等。

继续阅读“Salesforce的StandardSetController使用Demo”