17、Ember.js 自定义组件编写与测试实践

Ember.js 自定义组件编写与测试实践

1. 树菜单组件的数据模型

在开始编写树菜单的代码之前,需要了解组件所使用的底层数据模型。以下是树菜单的数据模型代码:

Montric.MainMenuModel = DS.Model.extend({
    name: DS.attr('string'),
    nodeType: DS.attr('string'),
    parent: DS.belongsTo('mainMenu'),
    children: DS.hasMany('mainMenu'),
    chart: DS.belongsTo('chart'),
    isSelected: false,
    isExpanded: false,
    hasChildren: function() {
        return this.get('children').get('length') > 0;
    }.property('children'),
    isLeaf: function() {
        return this.get('children').get('length') == 0;
    }.property('children')
});

这里使用 Ember Data 模型对象作为发送到树菜单组件的数据模型。服务器通过 parent children 属性设置节点之间的链接,Ember Data 确保从服务器加载数据后,模型对象按预期连接。同时,定义了 hasChildren isLeaf 两个辅助属性,用于简化组件内的模板。树菜单组件由 tree-menu tree-menu-item 两个子组件构成,先实现多选功能,后续再扩展支持单选。

2. 定义树菜单组件

树菜单组件由单个模板 components/tree-menu.hbs 组成,其作用是渲染每个顶级节点,模板代码如下:

{{#each node in rootNodes}}
    {{tree-menu-node node=node}}
{{/each}}

该模板的实现简单易懂,目前无需为该组件实现组件类,因为没有触发任何操作,Ember.js 提供的默认实现已足够。

3. 定义树菜单项目和树菜单节点组件

树菜单项目组件相对复杂,需要支持渲染正确的展开三角图标,并设置所代表节点的 isExpanded isSelected 属性。其模板代码如下:

{{#if node.hasChildren}}
    {{#if node.isExpanded}}
        <span class="downarrow" {{action "toggleExpanded"}}></span>
    {{else}}
        <span class="rightarrow" {{action "toggleExpanded"}}></span>
    {{/if}}
    <span {{action "toggleExpanded"}}>{{node.name}}</span>
{{else}}
    {{view Ember.Checkbox checkedBinding="node.isSelected"}}
    <span {{action "toggleSelected"}}>{{node.name}}</span>
{{/if}}
{{#if node.isExpanded}}
    {{#each child in node.children}}
        <div style="margin-left: 22px;">
            {{tree-menu-node node=child}}
        </div>
    {{/each}}
{{/if}}

该模板会触发 toggleExpanded toggleSelected 两个操作,因此需要重写默认的 Montric.TreeMenuNodeComponent 来捕获这些操作。如果节点有子节点,会渲染展开三角图标和节点名称,两者都可点击并触发 toggleExpanded 操作;如果节点没有子节点,会渲染复选框和节点名称,两者都可点击并触发 toggleSelected 操作。当 isExpanded 属性为 true 时,会递归渲染子节点。

以下是 Montric.TreeMenuNodeComponent 的类定义:

Montric.TreeMenuNodeComponent = Ember.Component.extend({
    classNames: ['pointer'],
    actions: {
        toggleExpanded: function() {
            this.toggleProperty('node.isExpanded');
        },
        toggleSelected: function() {
            this.toggleProperty('node.isSelected');
        }
    }
});

toggleExpanded 函数用于切换节点的 isExpanded 属性, toggleSelected 函数用于切换节点的 isSelected 属性。

4. 支持单选功能

为支持单选,需要添加一个标志 allowMultipleSelections 来告知组件是允许多选还是单选,并将该属性传递给 tree-menu-node 组件。同时,将选中项分配给 tree-menu 组件的 selectedNode 属性,并将该属性传递给每个 tree-menu-node 组件。更新后的树菜单组件模板如下:

{{#each node in rootNodes}}
    {{tree-menu-node node=node
      allowMultipleSelections=allowMultipleSelections action="selectNode"
      selectedNode=selectedNode}}
{{/each}}

更新后的 tree-menu-node 组件模板如下:

{{#if node.hasChildren}}
    {{#if node.isExpanded}}
        <span class="downarrow" {{action "toggleExpanded"}}></span>
    {{else}}
        <span class="rightarrow" {{action "toggleExpanded"}}></span>
    {{/if}}
    <span {{action "toggleExpanded"}}>{{node.name}}</span>
{{else}}
    {{#if allowMultipleSelections}}
        {{view Ember.Checkbox checkedBinding="node.isSelected"}}
        <span {{action "toggleSelected"}}>{{node.name}}</span>
    {{else}}
        <span {{action "selectNode" node}}
          {{bind-attr class=isSelected}}>{{node.name}}</span>
    {{/if}}
{{/if}}
{{#if node.isExpanded}}
    {{#each child in node.children}}
        <div style="margin-left: 22px;">
            {{tree-menu-node node=child
              action="selectNode" selectedNode=selectedNode}}
        </div>
    {{/each}}
{{/if}}

allowMultipleSelections false 时,点击叶节点会触发 selectNode 操作。如果当前叶节点是选中节点,会添加 CSS 类 is-selected 来标记为蓝色。

更新后的 Montric.TreeMenuNodeComponent 如下:

Montric.TreeMenuNodeComponent = Ember.Component.extend({
    classNames: ['pointer'],
    actions: {
        toggleExpanded: function() {
            this.toggleProperty('node.isExpanded');
        },
        toggleSelected: function() {
            this.toggleProperty('node.isSelected');
        },
        selectNode: function(node) {
            this.sendAction('action', node);
        }
    },
    isSelected: function() {
        return this.get('selectedNode') === this.get('node.id');
    }.property('selectedNode', 'node.id')
});

新的 Montric.TreeMenuComponent 如下:

Montric.TreeMenuComponent = Ember.Component.extend({
    classNames: ['selectableList'],
    actions: {
        selectNode: function(node) {
            this.set('selectedNode', node.get('id'));
        }
    }
});

最后,将更新后的树菜单组件添加到 alert.hbs 模板中,指定单选并将选中节点映射到当前选中的 Alert 模型的 alertSource 属性:

{{tree-menu rootNodes=controllers.admin.rootNodes
  allowMultipleSelections=false selectedNode=alertSource}}
5. 测试 Ember.js 应用

JavaScript 应用的测试领域仍在不断发展,有多种测试方式,包括单元测试、集成测试、性能测试、回归测试、黑盒测试和持续集成等。这里主要介绍使用 QUnit 和 PhantomJS 进行单元测试。

5.1 QUnit 简介

QUnit 是一个用于编写应用单元测试的框架,被 jQuery、jQuery UI 和 jQuery Mobile 等使用,Ember.js 框架的单元测试大多也用 QUnit 编写。由于在持续集成环境中运行需要浏览器的测试较为困难,使用 PhantomJS 可以在无头模式下执行测试,便于在 CI 环境中设置测试。

5.2 编写第一个 QUnit 测试

首先,从 QUnit 官网 下载 QUnit JavaScript 文件和 QUnit CSS 文件,创建一个名为 firstTest.html 的 HTML 文件,内容如下:

<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN"
   "http://www.w3.org/TR/html4/strict.dtd">
<html lang="en">
<head>
    <title>First Test</title>
    <link rel="stylesheet" href="qunit-1.11.0.css" type="text/css" charset="utf-8">
    <script src="qunit-1.11.0.js" type="text/javascript" charset="utf-8"></script>
</head>
<body bgcolor="#ffffff">
    <div id="qunit" style="z-index: 100;"></div>
    <script type="text/javascript" charset="utf-8" src="firstTest.js"></script>
</body>
</html>

该 HTML 页面用于设置测试,需要包含 QUnit CSS 和 JavaScript 文件,并创建一个 div 元素用于显示测试结果。所有要通过该 HTML 文件运行的测试都需在 HTML 页面的 body 标签内指定。

接着,创建 firstTest.js 文件,编写一个测试来验证整数 1 是否等于字符串 "1" ,代码如下:

test("Test that QUnit is working as expected", function() {
    ok( 1 == "1", "QUnit Test Passed!" );
});

每个测试通过 QUnit 的 test() 函数指定,第一个参数是测试名称,第二个参数是包含测试断言的回调函数。将 firstTest.html 文件拖到浏览器中即可执行测试。

以下是树菜单组件的结构关系图:

graph LR
    A[tree-menu component] --> B[tree-menu-item component]
    A --> C[tree-menu-item component]
    A --> D[tree-menu-item component]
    B --> E[tree-menu-item component]
    B --> F[tree-menu-item component]
    C --> G[tree-menu-item component]
    D --> H[tree-menu-item component]

测试 Ember.js 应用的流程如下:
1. 下载 QUnit 和 PhantomJS。
2. 创建 firstTest.html 文件并配置 QUnit。
3. 编写 firstTest.js 测试文件。
4. 在浏览器中执行测试。

Ember.js 自定义组件编写与测试实践

6. 使用 PhantomJS 执行测试

PhantomJS 是一个无界面的浏览器,可用于在无头模式下执行测试,这对于持续集成环境非常有用。在使用 PhantomJS 执行 QUnit 测试之前,需要确保 PhantomJS 已安装在系统中,可从 PhantomJS 官网 下载安装。

为了让 PhantomJS 能够执行 QUnit 测试,需要创建一个 PhantomJS 脚本。以下是一个简单的 PhantomJS 脚本示例 run-qunit.js

var page = require('webpage').create();
var system = require('system');

if (system.args.length !== 2) {
    console.log('Usage: run-qunit.js URL');
    phantom.exit(1);
}

var url = system.args[1];

page.open(url, function(status) {
    if (status !== 'success') {
        console.log('Unable to access network');
        phantom.exit(1);
    } else {
        page.onConsoleMessage = function(msg) {
            console.log(msg);
        };
        page.evaluate(function() {
            QUnit.done(function(results) {
                console.log('Tests completed in ' + results.runtime + 'ms, ' + results.total + ' tests run, ' + results.passed + ' passed, ' + results.failed + ' failed.');
                if (results.failed > 0) {
                    phantom.exit(1);
                } else {
                    phantom.exit(0);
                }
            });
        });
    }
});

这个脚本接受一个 URL 作为参数,打开该 URL 并执行其中的 QUnit 测试。测试完成后,会输出测试结果,并根据失败的测试数量决定退出状态码。

要使用这个脚本执行之前创建的 firstTest.html 测试,可以在命令行中运行以下命令:

phantomjs run-qunit.js file:///path/to/your/firstTest.html

其中 /path/to/your/firstTest.html firstTest.html 文件的实际路径。

7. 集成测试

除了单元测试,集成测试也是确保应用功能正常的重要手段。集成测试主要测试多个组件或模块之间的交互是否正常。

在 Ember.js 中进行集成测试时,可以使用 QUnit 结合 Ember 的测试助手。以下是一个简单的集成测试示例,假设要测试树菜单组件的单选功能:

module('Integration - Tree Menu Single Selection', {
    setup: function() {
        // 初始化测试环境
        Ember.run(function() {
            // 创建必要的模型和控制器
        });
    },
    teardown: function() {
        // 清理测试环境
    }
});

test('Single selection works correctly', function(assert) {
    // 渲染树菜单组件
    var treeMenu = Ember.View.create({
        templateName: 'components/tree-menu',
        rootNodes: [/* 根节点数据 */],
        allowMultipleSelections: false,
        selectedNode: null
    });
    treeMenu.appendTo('#ember-testing');

    // 模拟点击一个叶节点
    Ember.run(function() {
        var leafNode = treeMenu.$('.leaf-node').first();
        leafNode.click();
    });

    // 验证选中节点是否正确
    assert.equal(treeMenu.get('selectedNode'), 'expected-node-id', 'Selected node is correct');

    // 清理测试视图
    treeMenu.destroy();
});

在这个示例中,首先创建一个测试模块,在 setup 函数中初始化测试环境,在 teardown 函数中清理环境。然后编写一个测试用例,渲染树菜单组件,模拟点击一个叶节点,并验证选中节点是否正确。

8. 性能测试与 Ember Instrumentation

性能测试用于评估应用的性能指标,如响应时间、吞吐量等。Ember Instrumentation 是 Ember.js 提供的一个工具,可用于快速测量应用的性能。

以下是一个使用 Ember Instrumentation 进行性能测试的示例:

Ember.Instrumentation.subscribe('render', {
    before: function(name, timestamp, payload) {
        // 记录渲染开始时间
        payload.startTime = timestamp;
    },
    after: function(name, timestamp, payload) {
        // 计算渲染时间
        var renderTime = timestamp - payload.startTime;
        console.log('Render time for ' + name + ': ' + renderTime + 'ms');
    }
});

在这个示例中,订阅了 render 事件,在渲染开始时记录时间,渲染结束时计算渲染时间并输出。

9. 回归测试与持续集成

回归测试用于确保在对应用进行修改后,之前的功能仍然正常工作。持续集成(CI)是一种软件开发实践,通过频繁地将代码集成到共享仓库中,并自动运行测试,及时发现和解决问题。

可以使用工具如 Jenkins、GitLab CI/CD 等实现持续集成。以下是一个简单的 GitLab CI/CD 配置示例 .gitlab-ci.yml

stages:
  - test

test:
  stage: test
  image: node:latest
  script:
    - npm install
    - npm run test

在这个配置中,定义了一个 test 阶段,使用 Node.js 镜像,安装依赖并运行测试脚本。

10. 总结

通过以上步骤,我们学习了如何编写 Ember.js 自定义树菜单组件,包括实现多选和单选功能。同时,介绍了使用 QUnit 和 PhantomJS 进行单元测试,以及如何进行集成测试、性能测试和回归测试。持续集成是保证应用质量的重要手段,可以通过工具实现自动化测试。

以下是 Ember.js 应用测试的方法总结表格:
| 测试类型 | 工具 | 说明 |
| ---- | ---- | ---- |
| 单元测试 | QUnit、PhantomJS | 测试单个组件或函数的功能 |
| 集成测试 | QUnit、Ember 测试助手 | 测试多个组件或模块之间的交互 |
| 性能测试 | Ember Instrumentation | 测量应用的性能指标 |
| 回归测试 | QUnit、持续集成工具 | 确保修改后之前的功能仍然正常 |

Ember.js 提供了丰富的工具和功能,通过合理运用这些工具,可以有效地编写和测试高质量的应用。

以下是 Ember.js 应用测试的整体流程图:

graph LR
    A[开始] --> B[下载 QUnit 和 PhantomJS]
    B --> C[创建测试文件]
    C --> D[编写单元测试]
    D --> E[使用 PhantomJS 执行测试]
    E --> F{测试通过?}
    F -- 是 --> G[进行集成测试]
    F -- 否 --> D
    G --> H[性能测试]
    H --> I[回归测试]
    I --> J[持续集成]
    J --> K[结束]
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值