Ember Helper助手 #

一、Helper概述 #

Helper是Ember中用于模板数据转换的函数。它们接收参数,处理后返回结果,非常适合用于:

  • 数据格式化
  • 数据转换
  • 条件判断
  • 计算逻辑

1.1 生成Helper #

bash
ember generate helper format-date

生成文件:

text
app/helpers/format-date.js
tests/unit/helpers/format-date-test.js

二、基本Helper #

2.1 简单Helper #

javascript
// app/helpers/format-date.js
import { helper } from '@ember/component/helper';

export default helper(function formatDate([date]) {
  if (!date) return '';

  const d = new Date(date);
  return d.toLocaleDateString('zh-CN', {
    year: 'numeric',
    month: 'long',
    day: 'numeric',
  });
});
handlebars
{{! 使用Helper}}
<p>发布于:{{format-date @post.createdAt}}</p>

2.2 带参数的Helper #

javascript
// app/helpers/format-date.js
import { helper } from '@ember/component/helper';

export default helper(function formatDate([date], { format }) {
  if (!date) return '';

  const d = new Date(date);

  switch (format) {
    case 'short':
      return d.toLocaleDateString();
    case 'long':
      return d.toLocaleDateString('zh-CN', {
        year: 'numeric',
        month: 'long',
        day: 'numeric',
        weekday: 'long',
      });
    case 'relative':
      return getRelativeTime(d);
    default:
      return d.toLocaleDateString();
  }
});

function getRelativeTime(date) {
  const now = new Date();
  const diff = now - date;
  const seconds = Math.floor(diff / 1000);
  const minutes = Math.floor(seconds / 60);
  const hours = Math.floor(minutes / 60);
  const days = Math.floor(hours / 24);

  if (days > 0) return `${days}天前`;
  if (hours > 0) return `${hours}小时前`;
  if (minutes > 0) return `${minutes}分钟前`;
  return '刚刚';
}
handlebars
{{! 默认格式}}
{{format-date @post.createdAt}}

{{! 短格式}}
{{format-date @post.createdAt format="short"}}

{{! 长格式}}
{{format-date @post.createdAt format="long"}}

{{! 相对时间}}
{{format-date @post.createdAt format="relative"}}

三、常用Helper示例 #

3.1 字符串处理 #

javascript
// app/helpers/truncate.js
import { helper } from '@ember/component/helper';

export default helper(function truncate([text], { length = 100, suffix = '...' }) {
  if (!text) return '';
  if (text.length <= length) return text;
  return text.substring(0, length) + suffix;
});
handlebars
<p>{{truncate @post.body length=200}}</p>

3.2 数字格式化 #

javascript
// app/helpers/format-number.js
import { helper } from '@ember/component/helper';

export default helper(function formatNumber([number], { locale = 'zh-CN', style = 'decimal' }) {
  if (number === null || number === undefined) return '';

  return new Intl.NumberFormat(locale, { style }).format(number);
});
handlebars
{{! 普通数字}}
{{format-number 1234567}}

{{! 货币格式}}
{{format-number @price style="currency"}}

3.3 货币格式化 #

javascript
// app/helpers/format-currency.js
import { helper } from '@ember/component/helper';

export default helper(function formatCurrency([amount], { currency = 'CNY', locale = 'zh-CN' }) {
  if (amount === null || amount === undefined) return '';

  return new Intl.NumberFormat(locale, {
    style: 'currency',
    currency,
  }).format(amount);
});
handlebars
{{! 人民币}}
{{format-currency @price}}

{{! 美元}}
{{format-currency @price currency="USD"}}

3.4 条件Helper #

javascript
// app/helpers/eq.js
import { helper } from '@ember/component/helper';

export default helper(function eq([a, b]) {
  return a === b;
});
handlebars
{{#if (eq @user.role "admin")}}
  <p>管理员</p>
{{/if}}

3.5 逻辑Helper #

javascript
// app/helpers/and.js
import { helper } from '@ember/component/helper';

export default helper(function and([...args]) {
  return args.every(Boolean);
});

// app/helpers/or.js
import { helper } from '@ember/component/helper';

export default helper or([...args]) {
  return args.some(Boolean);
});

// app/helpers/not.js
import { helper } from '@ember/component/helper';

export default helper(function not([value]) {
  return !value;
});
handlebars
{{#if (and @user.isLoggedIn @user.hasPermission)}}
  <p>可以访问</p>
{{/if}}

{{#if (or @user.isAdmin @user.isEditor)}}
  <p>管理权限</p>
{{/if}}

{{#if (not @user.isBanned)}}
  <p>正常用户</p>
{{/if}}

3.6 数学Helper #

javascript
// app/helpers/math.js
import { helper } from '@ember/component/helper';

export const add = helper(([a, b]) => a + b);
export const sub = helper(([a, b]) => a - b);
export const mul = helper(([a, b]) => a * b);
export const div = helper(([a, b]) => a / b);
export const mod = helper(([a, b]) => a % b);
handlebars
<p>总计:{{add @price @tax}}</p>
<p>折扣:{{mul @price 0.9}}</p>

四、响应式Helper #

4.1 Class-based Helper #

当需要响应数据变化时,使用Class-based Helper:

javascript
// app/helpers/time-ago.js
import Helper from '@ember/component/helper';
import { tracked } from '@glimmer/tracking';

export default class TimeAgoHelper extends Helper {
  @tracked interval;

  compute([date]) {
    this.clearInterval();

    this.interval = setInterval(() => {
      this.recompute();
    }, 60000);

    return this.formatTimeAgo(date);
  }

  formatTimeAgo(date) {
    const now = new Date();
    const diff = now - new Date(date);
    const minutes = Math.floor(diff / 60000);

    if (minutes < 1) return '刚刚';
    if (minutes < 60) return `${minutes}分钟前`;

    const hours = Math.floor(minutes / 60);
    if (hours < 24) return `${hours}小时前`;

    const days = Math.floor(hours / 24);
    return `${days}天前`;
  }

  clearInterval() {
    if (this.interval) {
      clearInterval(this.interval);
    }
  }

  willDestroy() {
    this.clearInterval();
    super.willDestroy();
  }
}
handlebars
<span>{{time-ago @comment.createdAt}}</span>

4.2 带服务的Helper #

javascript
// app/helpers/current-user.js
import Helper from '@ember/component/helper';
import { inject as service } from '@ember/service';

export default class CurrentUserHelper extends Helper {
  @service session;

  compute([property]) {
    const user = this.session.currentUser;
    if (!user) return null;
    return user[property];
  }
}
handlebars
<p>欢迎,{{current-user "name"}}</p>

五、Helper组合 #

5.1 嵌套使用 #

handlebars
{{! Helper嵌套}}
<p>{{truncate (format-date @post.createdAt format="long") length=20}}</p>

{{! 多层嵌套}}
<p>{{format-currency (mul @price 0.9)}}</p>

5.2 与条件组合 #

handlebars
{{#if (gt (format-number @stock) 0)}}
  <span class="in-stock">有货</span>
{{else}}
  <span class="out-of-stock">缺货</span>
{{/if}}

六、Helper最佳实践 #

6.1 纯函数原则 #

javascript
// 好的做法 - 纯函数
export default helper(function upper([str]) {
  return str?.toUpperCase() ?? '';
});

// 避免 - 副作用
export default helper(function log([message]) {
  console.log(message); // 副作用
  return message;
});

6.2 参数验证 #

javascript
export default helper(function formatDate([date], options) {
  if (!date) return '';
  if (!(date instanceof Date)) {
    date = new Date(date);
  }
  if (isNaN(date.getTime())) {
    return '无效日期';
  }
  return date.toLocaleDateString();
});

6.3 默认值处理 #

javascript
export default helper(function truncate([text], { length = 100, suffix = '...' }) {
  if (!text) return '';
  if (text.length <= length) return text;
  return text.substring(0, length) + suffix;
});

七、测试Helper #

7.1 单元测试 #

javascript
// tests/unit/helpers/format-date-test.js
import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';
import formatDate from 'my-app/helpers/format-date';

module('Unit | Helper | format-date', function (hooks) {
  setupTest(hooks);

  test('it formats date correctly', function (assert) {
    const date = new Date('2024-01-15');
    const result = formatDate([date]);

    assert.ok(result.includes('2024'));
  });

  test('it handles empty values', function (assert) {
    assert.strictEqual(formatDate([null]), '');
    assert.strictEqual(formatDate([undefined]), '');
  });

  test('it supports different formats', function (assert) {
    const date = new Date('2024-01-15');

    const shortResult = formatDate([date], { format: 'short' });
    assert.ok(shortResult);

    const longResult = formatDate([date], { format: 'long' });
    assert.ok(longResult.includes('2024'));
  });
});

7.2 集成测试 #

javascript
// tests/integration/helpers/format-date-test.js
import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { render } from '@ember/test-helpers';
import { hbs } from 'ember-cli-htmlbars';

module('Integration | Helper | format-date', function (hooks) {
  setupRenderingTest(hooks);

  test('it renders formatted date', async function (assert) {
    this.set('date', new Date('2024-01-15'));

    await render(hbs`{{format-date this.date}}`);

    assert.dom(this.element).hasText(/2024/);
  });
});

八、常用Helper集合 #

8.1 字符串Helper #

javascript
// app/helpers/upper.js
export default helper(([str]) => str?.toUpperCase() ?? '');

// app/helpers/lower.js
export default helper(([str]) => str?.toLowerCase() ?? '');

// app/helpers/capitalize.js
export default helper(([str]) => {
  if (!str) return '';
  return str.charAt(0).toUpperCase() + str.slice(1);
});

8.2 数组Helper #

javascript
// app/helpers/includes.js
export default helper(([array, value]) => {
  return Array.isArray(array) && array.includes(value);
});

// app/helpers/first.js
export default helper(([array]) => array?.[0]);

// app/helpers/last.js
export default helper(([array]) => array?.[array.length - 1]);

8.3 对象Helper #

javascript
// app/helpers/pick.js
export default helper(([object, key]) => object?.[key]);

// app/helpers/keys.js
export default helper(([object]) => Object.keys(object || {}));

// app/helpers/values.js
export default helper(([object]) => Object.values(object || {}));

九、总结 #

Helper是Ember模板中强大的数据处理工具:

类型 用途 示例
简单Helper 数据转换 {{format-date date}}
带参数Helper 可配置转换 {{format-date date format="long"}}
Class-based 响应式更新 {{time-ago timestamp}}

合理使用Helper可以让模板更加简洁,逻辑更加清晰。

最后更新:2026-03-28