Skip to content

调测

如何保证一个软件的正确性、稳定性和可靠性呢?除了需要开发人员为软件编写优秀的代码外,测试人员的测试工作也必不可少。
从测试成为一个独立岗位至今,测试领域已经发展出了一系列的技术和概念,按测试方法分类有黑盒测试、白盒测试,按测试策略分类有单元测试、集成测试等。

Electron作为一个桌面GUI框架,专门为测试人员提供了测试支持工具,比如接下来我们要讲到的Spectron。
除了此类专用工具外,Electron开发人员也可以在渲染进程中使用Node.js领域的测试框架,比如Mocha。

有些问题非常难以定位,比如性能问题、网络问题等。
测试人员如果能定位到问题的根源,那对于开发人员来说将是巨大的帮助,Electron在这方面亦有支持

测试

单元测试

开发者开发一个业务足够复杂的应用,势必会对应用内的业务进行一定程度的抽象、封装和隔离,以保证代码的可复用性、可维护性。
每个抽象出的模块或组件都会暴露出自己的接口和数据属性等供其他模块或组件使用。
测试人员(或兼有测试职责的开发人员)会为这些组件或模块撰写单元测试代码,以保证这些模块或组件正常可用。

Node.js生态里有很多流行的测试框架帮助测试人员完成这项工作,Mocha就是其中之一,它功能强大且简便易用,深受测试人员喜爱。接下来也将使用它演示如何完成接口自动化测试的工作。

首先我们需要安装Mocha。因为测试框架不需要随业务代码一起打包发布,所以安装指令使用了--dev参数。

1
> yarn add mocha --dev

安装完成后在package.json中scripts配置节增加Mocha的自动化测试指令:

1
"test": "mocha"

待我们测试用例代码编写完成后,只要在命令行运行yarn test,即可通过Mocha自动执行测试用例代码。

运行yarn test指令时,Mocha框架会默认执行项目根目录下test文件夹内的所有JavaScript文件。
因此我们在项目根目录下新建一个test文件夹,在此文件内创建一个test.js文件备用。

在撰写测试用例代码前,我们先在项目src目录下创建一个 justForTest.js 文件。
此文件为被测试的接口组件,在该文件内编写代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
module.exports = {
    getSomeThingFromWeb() {
        return new Promise(resolve => {
            let url = 'https:// www.cnblogs.com/aggsite/AggStats'
            let request = require('request');
            request(url, (err, response, body) => {
                resolve(body);
            })
        });
    }
}

这是我们专门为演示如何撰写测试用例代码而创建的一个普通JavaScript模块。
它只有一个方法,此方法请求一个网络地址,把请求到的内容封装到一个Promise对象里返回给被调用方。

接着我们在 test/test.js 文件里撰写测试用例代码,如下所示:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
let assert = require('assert');
describe('待测试的模块1', () => {
    describe('方法1', () => {
        it('此方法应该返回一个ul字符串', async () => {
            let justForTest = require('../src/justForTest');
            let result = await justForTest.getSomeThingFromWeb();
            assert(result.startsWith('<ul>'));
        });
    });
});

代码中引入的assert模块是Node.js内置的模块,该模块提供了一组简单的断言测试方法,可用于测试变量和函数是否正常运行。
比如assert方法验证传入的参数是否为真,assert.strictEqual方法验证传入的前两个参数是否严格相等。

describe是Mocha框架提供的方法,注意这里我们并没有引入任何Mocha框架的模块,但describe和后面讲到的it方法都能直接使用,这是因为我们启动测试的命令是通过Mocha执行的,它为我们建立了拥有这些方法变量的环境。

describe方法是测试集定义函数,可以在控制台输出一个方便测试人员辨识的测试集名称,可以嵌套多层。
一般情况下测试人员会使用describe方法以从一级模块到二级模块再到组件这样的层级顺序逐层显示测试用例的执行逻辑。

it方法是测试用例的定义函数,在此函数的回调函数中执行测试用例代码。
我们的单元测试代码执行逻辑是:加载待测试模块,执行待测试模块的方法,验证返回值是否符合预期。

现在我们在控制台执行yarn test,看一下单元测试结果:

1
2
3
4
5
6
$ mocha
    待测试的模块1
        方法1
            √ 此方法应该返回一个ul字符串 (188ms)
    1 passing (197ms)
Done in 0.53s.

结果符合我们的预期,测试通过。假设 result.startsWith('<ul>') 结果为false,那么控制台将得到如下输出:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
$ mocha
    待测试的模块1
        方法1
            1) 此方法应该返回一个ul字符串
    0 passing (215ms)
    1 failing
    1) 待测试的模块1
            方法1
                  此方法应该返回一个ul字符串:
            AssertionError [ERR_ASSERTION]: The expression evaluated to a falsy value:
    assert(result.startsWith('<ul1>'))
            + expected - actual
            -false
            +true
            at Context.it (test\test.js:7:13)
            at process._tickCallback (internal/process/next_tick.js:68:7)

上面输出的信息足够清晰,测试人员能很直观地看出哪个测试用例没有通过。

如果你撰写的模块方法不是以Promise方式返回结果的,而是在回调方法内返回结果的,这种情况Mocha也有很好的支持,代码如下所示:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
it('此方法应该返回一个ul字符串', done => {
    let justForTest = require('../src/justForTest');
    justForTest.getSomeThingFromWeb2((result, err) => {
        if (err) {
            done(err);
            return;
        }
        assert(result.startsWith('<ul>'));
        done();
    });
})

我们为it方法的回调函数传入了done参数,这个参数其实是一个方法,在被测模块的回调方法执行完后调用done(),即可证明此接口测试通过,
一旦被测模块有异常报出(assert方法没有得到预期结果也会抛出异常),此测试用例会显示未通过。

如果你的被测接口回调函数只有一个error参数,无需验证测试结果,即可简写为如下代码:

1
justForTest.getSomeThingFromWeb3(done);

除Mocha外,业界优秀的单元测试框架还有AVA(https://github.com/avajs/ava)、jest(https://github.com/facebook/jest)等,用法大同小异

界面测试

上面讲解了如何用Mocha框架测试Electron应用内部模块的接口,其实一些HTTP服务也可以用Mocha测试框架来完成测试,其原理是一样的。
但如果要做界面测试,单单使用Mocha测试框架就不足以支撑了,比如界面上是不是呈现出了某个DOM元素,应用程序是不是打开了某个窗口,用户剪切板里是不是有某段文字等。

要做好界面测试工作,就需要使用专门为Electron应用设计的测试框架Spectron。
Spectron是Electron官方团队打造的测试工具,它封装了ChromeDriver和WebdriverIO这两个专门用来测试Web界面的工具。

首先在package.json中配置开发环境依赖的库:

1
2
3
4
5
"devDependencies": {
    "electron": "^6.0.0",
    "mocha": "^6.2.2",
    "spectron": "^8.0.0"
}

然后执行yarn指令,安装这些依赖库,注意:Spectron版本号^8.0.0对应Electron版本号^6.0.0,Spectron版本号^9.0.0对应Electron版本号^7.0.0,以此类推。
如果安装了错误的Spectron版本,测试用例可能无法顺利执行。

接着我们在test文件夹内创建一个test.js测试文件。首先引入必要的测试环境依赖的库,代码如下所示:

1
2
3
4
const Application = require('spectron').Application
const assert = require('assert')
const electronPath = require('electron')
const path = require('path')

Application类是Spectron库的入口,使用Spectron库进行测试要先实例化Application类型。

搭建开发环境时我们知道,electron库其实只导出了本项目下Electron的可执行文件的安装路径,所以直接把electron库的导出值赋值给了electronPath变量。

接着完成测试代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
describe('开始执行界面测试', function(){
    this.timeout(10000);
    beforeEach(function() {
        this.app = new Application({
            path: electronPath,
            args: [path.join(__dirname, '..')]
        })
        return this.app.start();
    })
    afterEach(function() {
        if (this.app && this.app.isRunning()) {
            return this.app.stop();
        }
    })
    it('测试窗口是否启动', async function() {
        let count = await this.app.client.getWindowCount();
        assert.equal(count, 1)
    })
})

上述代码中 this.timeout(10000); 设置了测试用例的超时时间,一旦某个测试用例超过此时间尚未返回结果,则报超时错误,测试用例不通过。
这是Mocha框架的一个方法。

beforeEach和afterEach是Mocha环境提供的两个钩子函数,代表着此作用域内“每个”测试用例执行前和执行后需要完成的工作。
对应的还有before和after钩子函数,代表着此作用域内“所有”测试用例执行前和执行后需要完成的工作。
我们这个作用域内只有一个测试用例,所以这两对函数都可以使用。

在启动这个测试用例之前,我们在beforeEach的回调函数内创建了Spectron的Application对象,并为此对象设置了Electron的可执行文件的路径和当前项目package.json文件的路径。
对象创建完成后,通过this.app.start()方法启动应用,这个方法返回一个Promise对象。
Mocha框架会为我们等待此方法执行成功后再启动测试用例。

在测试用例执行完成之后,我们在afterEach的回调函数中通过 this.app.stop() 释放了Spectron的测试环境。
这个方法也返回一个Promise对象。

在具体的测试用例中,我们并没有执行过多的业务,只是通过this.app.client.getWindowCount()方法获取了窗口的数量。
确认有且只有一个窗口被打开了,则认为测试通过。

运行上述代码,得到如下输出:

1
2
3
4
5
$ mocha
    开始执行界面测试
        √ 测试窗口是否启动
    1 passing (6s)
Done in 6.95s.

除了getWindowCount方法之外,还可以通过 await this.app.client.waitUntilWindowLoaded() 方法等待页面加载完毕,
通过 app.client.waitUntilTextExists 等待获取当前窗口内某个元素的内容,通过app的browserWindow属性获取当前窗口等。
更多详情请查看官方文档说明:https://github.com/electron-userland/spectron

调试

渲染进程性能问题追踪

谷歌浏览器提供了很强大的性能追踪工具。打开开发者调试工具,选中Performance标签页,如图11-1所示。

图11-1开发者调试工具性能追踪面板

图11-1 开发者调试工具性能追踪面板

这就是谷歌开发者工具内提供的性能追踪面板,点击黑色小圆点,或在调试工具处于激活状态时按下Ctrl+E快捷键,开始追踪当前页面的性能问题。

为了便于演示,我专门写了一个比较耗时的函数,代码如下:

1
2
3
4
5
start(e) {
    let a = 9;
    a = eval('for (let i = 0; i < 1000000; i++) a = a * a')
    console.log("ok");
}

我们知道JavaScript不善于做CPU密集型的任务,这显然是一个需要大量CPU计算的函数,为了防止JavaScript执行引擎对代码进行优化,我还使用了eval方法。
在执行此函数前,点击黑色小圆点,开始性能追踪。如图11-2所示。

图11-2开始性能追踪后的提示框

图11-2 开始性能追踪后的提示框

接着执行上述方法,当控制台输出ok后,点击上图的Stop按钮,追踪结果即呈现在此面板上,如图11-3所示。

图11-3性能追踪结束后的窗口示例

图11-3 性能追踪结束后的窗口示例

上图中间部分是追踪的摘要信息,用一个环形图表示,黄色区块代表着此监控时段内JavaScript执行耗时情况。
此外还有界面重排和界面重绘的耗时等,不是我们本案例关注的内容,不多做解释。

把鼠标移至 Main--http://localhost:8080 区域的黄色位置附近,向上滚动鼠标滚轮,放大此区域,如图11-4所示。

图11-4性能追踪面板局部放大

图11-4 性能追踪面板局部放大

现在我们还是不能追踪到具体哪里耗时了。鼠标点击 Event:clickFunctionCall 黄色区块,选中下面面板的 Call Tree 选项卡,依次展开Activity的调用堆栈树状结构,操作步骤如图11-5所示。

图11-5性能追踪面板方法调用树

图11-5 性能追踪面板方法调用树

至此,我们终于找到耗时最长的部分出现在Home.vue中了。点击Home.vue上的链接,跳转到源码页面,发现谷歌浏览器已经体贴地把每行的耗时时间都显示在行首了,如图11-6所示。

图11-6性能追踪代码执行情况示例

图11-6 性能追踪代码执行情况示例

这就是谷歌浏览器提供的性能追踪工具,任何Web页面都可以使用此工具分析性能,Electron窗口内的页面也不例外。

自动追踪性能问题

在渲染进程中追踪性能问题存在三个问题:

  • 它只能追踪单个渲染进程的性能问题,主进程的性能问题无法覆盖。
  • 应用中如果有多个渲染进程只能一个一个追踪,手动排查性能问题非常麻烦。
  • 你只能手动启动、手动停止,如果想精细化地观察某一个操作的性能问题,没办法精确地控制监控的开始时间和结束时间。

基于此Electron为我们提供了contentTracing模块,此模块允许开发者以编码的方式启动性能问题追踪工作和停止性能问题追踪工作。代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
(async() => {
    const { contentTracing } = require('electron');
    await contentTracing.startRecording({
        include_categories: ['*']
    });
    await new Promise(resolve => setTimeout(resolve, 6000));
    const path = await contentTracing.stopRecording()
    console.log('性能追踪日志地址:' + path);
    createWindow();
})()

上面代码运行在app的ready事件的回调函数里,通过contentTracing.startRecording方法来启动性能监控,开始监控后等待了6秒钟,再通过contentTracing.stopRecording停止监控。

startRecording方法接收一个配置参数,配置参数的include_categories属性是准备监控的项,类型为数组,支持通配符,* 号代表监控所有项。

监控完成后,Electron会把监控日志保存在指定的目录下(Windows系统下此日志保存在 C:\Users\allen\AppData\Local\Temp\ 目录下)。
日志文件内容包含很多信息,不容易读懂,我们可以使用Chrome浏览器的trace viewer工具加载并查看这个日志文件。

打开Chrome浏览器,在地址栏输入 chrome://tracing,如图11-7所示。

图11-7tracing面板

点击①处Load按钮加载contentTracing保存的日志文件,上图是已经加载过日志文件的截图。

点击②处Process按钮,可以选择追踪哪几个进程。
这也是trace viewer的强大之处,它可以分析性能日志文件中的所有进程,把所有进程的性能数据加载到同一个窗口中显示。

③面板是你选择追踪的进程,你可以看到这里显示了包括浏览器进程、渲染进程、GPU进程、网络进程。

选中④按钮,你可以左右拖动时间轴,来精细化地追踪某个时间段内的性能问题。

选中⑤按钮,向上拖拽鼠标放大时间轴,向下拖拽鼠标缩小时间轴。当时间轴放大到一定程度,你可以看到时间轴上的标注信息,如图11-8所示。

图11-8tracing面板火焰图局部

图11-8 tracing面板火焰图局部

选中一块你感兴趣的区域,底部面板会展示这块区域的详细信息,如图11-9所示。

打开右侧FrameData面板可以看到各渲染进程渲染页面的性能情况,如图11-10所示。

图11-9性能消耗详细信息

图11-9 性能消耗详细信息

图11-10FrameData面板

图11-10 FrameData面板

你也可以通过Chrome浏览器的Performance来加载这个性能日志文件,如图11-11所示。
点击①处按钮即可加载日志文件,只是以这种方法能查看到的信息要少得多。

图11-11开发者工具内加载追踪日志

图11-11 开发者工具内加载追踪日志

性能优化技巧

如果你想驾驭一个大型复杂业务的桌面GUI应用,那么在应用建设之初就应该考虑系统性能的问题,而不是等到性能问题出现了之后再考虑如何优化,因为到那时你将付出比前者多得多的成本。

有一项工作,只要你做得足够好,就可以解决掉80%以上的性能问题。这项工作就是写优秀的代码。
也就是说,一个系统中大部分性能问题,都是因为开发人员没把代码写好导致的。
正因为总有人写不好代码,所以才需要工具来排查性能问题。

引入第三方模块须谨慎:

在Web服务上安装Node.js模块,我们一般不会关注Node.js模块的体积和加载该模块带来的损耗。
但我们现在使用的是Electron,不同于Web服务,我们开发的应用是要打包发布给客户的,体积庞大的Node.js包不利于我们应用程序的分发工作。

服务于Web的Node.js模块也不关注应用的启动效率,因此它们往往会在应用启动时做大量的初始化工作,但这在Electron应用中也是不可接受的,我们往往希望首屏加载时间尽量短。

另外,服务于Web的Node.js模块也不关注内存的消耗问题,它们甚至尽可能地占用内存以提高IO吞吐率。
但Electron应用部署环境多种多样,有些客户的机器本身内存就不大,我们应该尽量节省内存。

在浏览器上使用的Node.js模块,往往需要考虑兼容不同浏览器的特性而做很多额外的工作。
而这些工作对于Electron来说也不同于浏览器,因为Electron使用基于Chrome浏览器的核心,能很好地兼容新标准,没必要做这些多余的工作。

比如我们常用的前端网络库axios,就有下面这样的代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
function isStandardBrowserEnv() {
  if (typeof navigator !== 'undefined' && 
      (navigator.product === 'ReactNative' || 
      navigator.product === 'NativeScript' || 
      navigator.product === 'NS')) {
    return false;
  }
  return (
    typeof window !== 'undefined' &&
    typeof document !== 'undefined'
  );
}

上面这段代码对用户环境做出了详尽的判断,而且在axios库中频繁地使用了此工具函数。
类似这样的代码对于Electron应用来说毫无帮助,平添负担。
jQuery也是如此(如果你在开发过程中离不开jQuery,我建议你到这个网站学习一下:http://youmightnotneedjquery.com)。

你应该仔细考察你引入的Node.js模块本身依赖了什么模块,你真的需要它们吗?是不是自己简单封装一下就可以达到同样的功能了?

尽量避免等待

在主进程启动窗口前如果使用同步方法,比如Node.js的fs.readFileSync,将会造成主进程阻塞,窗口迟迟打不开,用户体验下降。

开发者就不应该在窗口创建前处理大量的业务,而是应该在窗口创建后,页面加载完成时再处理你需要在应用程序启动之初完成的工作。
哪怕显示一个等待画面(数据加载中的画面),对于用户来说也更容易接受。

主进程窗口启动后如果使用同步方法,可能会造成主进程的IPC消息迟迟得不到处理(一直处于排队中),一些时效性要求高的操作可能会产生异常。

在渲染进程中使用同步方法可能会造成页面卡顿,比如 dialog.showSaveDialogSync,如果用户一直没有保存文件,那么该渲染进程的JavaScript执行器就会一直等下去,你用JavaScript操作Dom、执行定时任务等工作都将处于等待状态。

无论是渲染进程还是主进程,如果有大量耗时的任务需要完成,应该考虑以下问题:是否可以使用Web Worker来处理这项工作以及是否可以等系统空闲的时候再处理这项工作。
判断系统是否处于空闲状态,可以使用Electron提供的如下API

1
powerMonitor.getSystemIdleState(idleThreshold);

powerMonitor模块我们在前文已经讲解过,getSystemIdleState可以获取当前系统是否处于空闲状态,idleThreshold是一个整型参数,代表系统处于空闲状态的时长。

同样,尽量把用于显示的资源,如HTML、CSS、JavaScript文件、图片、字体等打包到你的应用中,这样客户启动你的应用时从本地加载这些资源。
如果这些资源是放在互联网上的,那么如果客户的网络状况不好,甚至根本没处在网络环境中,那么你的应用就无法为客户提供服务了。

在用Node.js开发Web服务端时,我们往往会把一大堆require方法放在JavaScript文件头部,这是因为在Web服务端环境下我们不用太担心初始化时资源消耗的问题。
Electron应用就不一样了,你应该尽可能地在必要时才使用require方法,因为require是一项昂贵的操作,占用资源较多,这一点尤其体现在Windows系统中。

尽量合并资源

在开发Web应用时,我们往往会把大文件拆分成小文件,以利用浏览器的并发下载能力,加快下载速度。
但Electron是本地应用,且我们的资源是在客户电脑本地环境下的,因此下载速度已不是问题。

你应该尽可能地把CSS和JavaScript合并到同一个文件中,以避免不必要的require和浏览器加载工作。
如果你使用webpack,这项工作只需要简单地配置一下即可完成。

开发环境调试工具

Electron官方团队为开发者提供了一个调试工具——Devtron,这个工具以浏览器插件的形式为开发者服务,安装方式如下:

先为项目安装Devtron包:

1
> yarn add devtron --dev

接着启动窗口后,在开发者工具的命令行内执行如下指令,安装插件:

1
> require('devtron').install();

安装完成后,开发者工具将会增加一个Devtron的面板,如图11-12所示。

图11-12Devtron调试工具

图11-12 Devtron调试工具

  • 选中①处Devtron标签,可以查看Devtron为开发者提供的功能。
  • ②处面板显示主进程和渲染进程引用了哪些脚本资源。
  • ③处面板显示应用程序注册了哪些Electron提供的事件,比如app的事件、窗口的事件等。
  • ④处面板显示渲染进程和主进程通信的情况,包括消息发生的顺序、消息管道的名称和消息携带的数据等。
  • ⑤处面板显示应用程序有可能存在的问题,提示开发者应该监听哪些事件。
  • ⑥处面板显示应用程序界面元素可访问性相关的问题。

生产环境调试工具

一旦应用程序打包发布,开发者就很难再使用开发者工具或者Devtron来调试了,但如果应用程序在生产环境出现问题,我们有没有办法对它进行调试呢?
又或者我们知道某个应用程序是基于Electron开发的,希望学习一下它内部的运行机制,是否有调试工具支持呢?

答案是有的,字节跳动公司的工程师开发的Debugtron(https://github.com/bytedance/debugtron)可以帮助开发者完成这项工作。
Debugtron是一个基于Electron开发的客户端桌面GUI程序,它就是为调试生产环境下的Electron应用而生的。

下载、安装后界面如图11-13所示。

图11-13Debugtron主界面

图11-13 Debugtron主界面

Debugtron启动后会自动把电脑内基于Electron开发的应用程序显示在主界面上,如上图①区域所示。
如果你发现自己电脑内的某个基于Electron开发的应用没出现在此区域内,你也可以把它拖拽到②区域,手动让Debugtron加载它。

选中你需要调试的应用,点击③区域内的调试目标,打开一个基于Chrome的调试工具,如图11-14所示。

图11-14Debugtron调试界面1

图11-14 Debugtron调试界面1

你会发现Debugtron会自动读取asar当中的源码,如①处所示。如果你调试HTML的内容,将得到如图11-15所示的内容。

图11-15Debugtron调试界面2

图11-15 Debugtron调试界面2

开发者可以在此调试工具内下断点对业务代码进行调试。

如你所见,界面源码已经全部显示在Debugtron调试工具内了。建议开发者在发布应用程序前对源码做好防护措施,避免被恶意第三方调试。

日志

业务日志

开发者开发一个业务足够复杂的应用程序时,往往会在关键业务执行时记录日志。
这些日志在排查问题、跟踪调用流程时非常有用。
因为一旦应用程序出现问题,如果没有业务日志,又不方便让用户重现问题的话,开发者就要花费大量的时间去追踪和排查。
如果有业务日志,就可以根据日志整理出一个业务处理链条,顺着这个业务处理链条就可以得出程序执行的过程,顺利定位并复现问题。

记录业务日志并不是一个技术难度很高的工作,但它比较烦琐,因为有的时候要把业务日志打印到控制台,有的时候要把业务日志输出到本地文件中。
日志还要分级别,比如error级别、warn级别、info级别等。

这些烦琐的工作对于Web服务端开发人员来说不是问题,因为他们可以用Log4j(一个Java的日志记录工具)、winston(Node.js的日志记录工具)等工具。
但Electron应用是一个桌面GUI应用,不能使用这些日志记录工具,因此Elecron社区有人专门为Electron应用开发了electron-log日志记录工具,以解决这方面的问题。
下面我们就来了解一下这个工具。为Electron项目安装electron-log模块的命令如下:

1
> yarn add electron-log

此模块在主进程和渲染进程中都可以使用,我们在主进程中加入如下代码:

1
2
3
4
5
6
7
var log = require("electron-log");
log.error("error");
log.warn("warn");
log.info("info");
log.verbose("verbose");
log.debug("debug");
log.silly("silly");

上述代码以不同的日志级别记录日志,默认情况下日志既会在控制台输出,也会保存到本地文件中,日志默认保存在 app.getPath("userData") 目录下的log.log文件中。

日志记录的内容如下:

1
2
3
4
5
6
[2019-12-02 11:17:03.635] [error] error
[2019-12-02 11:17:03.636] [warn] warn
[2019-12-02 11:17:03.636] [info] info
[2019-12-02 11:17:03.636] [verbose] verbos
[2019-12-02 11:17:03.637] [debug] debug
[2019-12-02 11:17:03.637] [silly] silly

你会发现,日志发生的时间、日志级别、日志内容都记录在其中了。

开发者可以通过 log.transports.file.levellog.transports.console.level 来分别设置日志输出目标和日志输出级别。

记录日志的信息也可以通过如下方法进行格式化:

1
log.transports.file.format = '[{y}-{m}-{d} {h}:{i}:{s}.{ms}] [{level}] {text}'

如果不方便让用户把日志文件发送给我们的测试人员,那么开发者也可以开发一个程序,在适当的时候把用户日志自动上传到Web服务器上去。

网络日志

一般情况下,我们通过渲染进程的开发者调试工具即可查看应用程序发起的网络请求数据,如图11-16所示。

图11-16开发者调试工具网络面板

图11-16 开发者调试工具网络面板

但使用这种方法有三个问题:

  • 无法监控主进程发起的网络请求。
  • 多个窗口发起的网络请求要在不同的窗口中监控,如果不同窗口发起的网络请求间存在一定的关联关系,就需要联合监控,非常麻烦。
  • 无法精确地分析某个时段内的网络请求。当一个应用内的网络请求非常频繁时,你只能手动查找某个时间点发生的请求,而且其中可能会有干扰数据(例如Wiresharq)。

为了弥补这些方面的不足,Electron为我们提供了netLog模块,允许开发人员通过编程的方式记录网络请求数据。代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
let { remote } = require("electron");
await remote.netLog.startLogging("E:\\net.log");
let ses = remote.getCurrentWebContents().session;
let xhr = new XMLHttpRequest();
xhr.open("GET", "https:// www.baidu.com");
xhr.onload = async () => {
    console.log(xhr.responseText);
    await remote.netLog.stopLogging()
};
xhr.send();

netLog模块是一个主进程模块,所以我们需要通过remote来使用它。
它的startLogging方法接收两个参数,第一个参数是日志文件记录的路径,你可以通过app.getPath方法获取当前环境下可用的路径,我这里指定了一个固定的路径。

第二个参数是一个配置对象,该对象的captureMode属性代表你想记录哪些网络数据,默认只记录请求的元数据。
你可以把它设置成includeSensitive,这样记录的数据就包括cookie和authentication相关的数据了。

接着我们发起了一个网络请求,得到响应后,使用netLog.stopLogging方法停止网络监控。

startLogging和stopLogging都返回一个Promise对象,所以它们都是异步操作(因此上面的代码应该放在一个async方法中执行)。

收集到的数据信息含量巨大,请求的地址、请求头、响应、甚至HTTPS证书的数据都在日志中有所体现,截取一部分数据如图11-17所示。

图11-17网络日志数据

图11-17 网络日志数据

崩溃报告

开发一个业务较复杂的客户端应用程序,开发者很难确保程序运行期不出任何问题。
这主要是因为客户端的环境多种多样,不同客户使用应用程序的方式也各有不同。
开发者能做的就是尽可能地收集应用崩溃时产生的异常数据,为将来解决问题保存必要的信息。

Electron内置了崩溃报告上报模块crashReporter,开发者可以利用此模块收集应用程序崩溃时的环境情况和异常数据,并让应用程序把这些数据提交到一个事先指定好的服务器上。

启动崩溃报告服务的代码如下:

1
2
3
4
5
6
7
let electron = require('electron');
electron.crashReporter.start({
    productName: 'YourName',
    companyName: 'YourCompany',
    submitURL: 'http:// localhost:8989/',
    uploadToServer: true
});

Electron官方推荐了两个用于构建崩溃报告服务的项目。
一个是Mozilia的Socorro,虽然这是一个开源项目,但Mozilia的开发人员明确说明这是他们的内部项目,没有精力支持外部用户的需求。
另外一个是mini-breakpad-server,这是Electron官方推出的崩溃报告服务,但已经有近3年没更新过了。
除此之外还有一些第三方托管的服务,这里也都不推荐使用。

虽然没有现成的工具支撑我们完成此项任务,但并没有什么大碍,我们可以自己构建一个用于接收崩溃报告的HTTP服务。
当你的应用崩溃时,Electron框架会以POST的形式发送以下数据到你指定的HTTP服务器:

  • ver:Electron的版本。
  • platform:系统环境,如:'win32'。
  • process_type:崩溃进程,如:'renderer'。
  • guid:ID,如:'5e1286fc-da97-479e-918b-6bfb0c3d1c72'。
  • _version:系统版本号,为package.json里的版本号。
  • _productName:系统名称,开发者在crashReporter对象中指定的产品名字。
  • prod:基础产品名字,一般情况下为Electron。
  • _companyName:公司名称,开发者在crashReporter对象中指定的公司名称。
  • upload_file_minidump:这是一个minidump格式的崩溃报告文件。

如果以上数据项不足以支撑你分析应用崩溃的原因,你还可以通过如下代码增加上报的内容:

1
crashReporter.addExtraParameter(key, value);

或者在启动崩溃报告服务时即设置好附加字段,代码如下:

1
2
3
4
electron.crashReporter.start({
    // ...
    extra: { 'key': 'value' }
});

为了测试崩溃报告服务是否正常可用,你可以通过如下代码,引发应用崩溃:

1
process.crash();

以下为一个简单的崩溃日志接收服务的演示代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
let http = require('http');
let inspect = require('util').inspect;
let Busboy = require('busboy');
http.createServer(function(req, res) {
    if (req.method === 'POST') {
        var busboy = new Busboy({ headers: req.headers });
        busboy.on('file', function(fieldname, file, filename, encoding, mimetype) {
            console.log('File [' + fieldname + ']: filename: ' + filename + ', mimetype: ' + mimetype);
        });
        busboy.on('field', function(fieldname, val, fieldnameTruncated, valTruncated, encoding, mimetype) {
            console.log('Field [' + fieldname + ']: value: ' + inspect(val));
        });
        busboy.on('finish', function() {
          res.end();
        });
        req.pipe(busboy);
    }
}).listen(8989);

上述代码以最简单的方式创建了一个Node.js HTTP服务,服务接收客户端请求,并把请求体中的内容格式化显示在控制台中。
Busboy是一个可以格式化含有文件的请求体的第三方库,开源地址为 https://github.com/mscdex/busboy

此服务接收到的dump文件可以使用Electron团队提供的minidump工具查看(https://github.com/electron/node-minidump)。

另外你可以通过crashReporter.getLastCrashReport()方法获得已上传的崩溃报告,通过此方法也可以适时地提醒用户:应用上次退出是内部异常导致的,管理人员会跟踪此错误。