全书目录

第四十二篇:Ember.js 完全指南 —— 约定秩序城区宇宙

第四十二篇:Ember.js 完全指南 —— 约定秩序城区宇宙

26 分钟 11,457 字 第 807 / 962 个阅读单元

如果说有些前端框架像给你一块地、一批钢筋、再加一本“你想怎么盖都行”的施工手册,那 Ember.js 就完全不是这条路子。

Ember 更像一座已经有完整城市总规的“约定秩序城区”。

这座城区里:

  • 路怎么修,先看总路网。
  • 楼怎么盖,先看地块编号。
  • 市政大厅、档案馆、公交系统、审批流程,默认都给你配好。
  • 你当然可以改造,但默认方案本身就是为了让一座城市能稳定运转很多年。

这就是 Ember 存在的原因。

它不是为了让你“随便拼一个页面”而生的。它是为了让一个大型、长期、多人协作、业务流程复杂的 Web 应用,从第一天起就像一座按规划建成的城市,而不是一片越长越乱的野生棚户区。

截至 2026-03-28,Ember 官方 stable channel 给出的稳定安装版本是 Ember 6.11.0,对应的 Ember Data 5.8.0Ember 6.8 已在 2026-01-09 被提升为 LTS。这个时间点下,Ember 的主路径已经非常明确:

  • 新项目默认走 Embroider + Vite
  • 新生成的组件和路由模板默认走 严格模式 template tag,也就是 .gjs / .gts
  • 主流组件写法是 Glimmer 组件 + @tracked + 原生 class
  • 旧时代的 Component.extendthis.get()、observer、经典组件、到处依赖全局解析器的写法,依然会在老项目里看到,但不该再当成主线心智

所以你读这一章,要先接受 Ember 的第一条城市法则:

Ember 不想让你先发明城市规则,再开始建城;它直接把规则、街道、门牌、审批系统一起交给你。


#一、为什么 Ember.js 会存在:因为“自由”在大项目里经常变成“失序”

前端世界有一个常见误区:大家以为“灵活”天然比“约束”高级。

小项目里这话常常成立。可一旦项目变成真正的业务城市,事情就变了。

想象你在盖一座“市民政务城区”:

  • 有许可证审批大厅
  • 有居民档案
  • 有跨部门流程
  • 有登录态、通知、搜索、筛选
  • 有不同街区之间的跳转
  • 有几十个开发者轮班施工
  • 这城要运行五年、八年、十年

这时候你最怕的不是“不够灵活”,你最怕的是:

  • A 队修路一套标准
  • B 队挂门牌一套标准
  • C 队修排水又是一套标准
  • D 队接档案馆再来一套标准

最后整座城市不是没有能力,而是每个区都能跑,但全城协作成本高得离谱

Ember 的回答非常直接:

  • 路由我给你内建
  • 工程骨架我给你脚手架
  • 数据层我给你官方路线
  • 测试我默认配
  • 升级路径我尽量铺平
  • 文件结构我不让你瞎摆

所以 Ember 官方一直强调的,是“productive”“battle-tested”“ambitious”。

翻成人话就是:

它不是最爱秀肌肉的新玩具,而是那种给大型组织长期交付用的城市级施工方案。


#二、核心心智模型:Ember 不是“组件框架”,它是“一座按总规运行的城市”

学 Ember,不能只用“组件”那一层去理解。那样你会一直觉得它“怎么这么重”。

你得用整座城市去看它。

在“约定秩序城区”里,最核心的五个角色分别是:

  • Router 路由器:城市总路网和交通调度中心,决定用户此刻走到哪条街
  • Route 路由对象:每个街区的区政府,负责在进入该区域前把数据、状态、上下文准备好
  • Template / Component 模板与组件:楼体图纸和标准化建筑模块,负责把街区长出来
  • Service 服务:常驻市政机关,跨区域共享状态与能力
  • Store / Model 数据层:城市总档案馆,负责记录、缓存、同步全城的业务实体

如果你只把 Ember 看成“另一个写组件的方案”,你就会误解它。

Ember 真正的心智,不是“页面 = 组件树”,而是:

URL 决定你走进哪片城区,Route 负责调档案,Template 和 Component 负责按图施工,Service 提供跨区公共能力,Store 作为城市总档案馆维持全局一致性。

这套秩序很重要,因为它解决了大项目最难的那类问题:

  • 页面数据到底在哪里取
  • 共享状态到底放哪
  • 一个业务实体到底谁算权威副本
  • 路由切换和异步加载怎么统一处理
  • 团队里新人一进来,怎么凭目录和命名就找到地方

你会发现,Ember 的很多“限制感”,其实都是在防止城市失序。


#三、CLI 与项目结构:Ember CLI 就是城建总包和门牌发放局

Ember 的上手入口不是“先装十个库再自己决定怎么拼”,而是先用 CLI 把城市骨架立起来。

典型命令大致是这样:

bash
npm install -g ember-cli
ember new permit-city --lang en --strict
cd permit-city
npm start

在 2025 年后这条线尤其重要,因为新蓝图已经默认走:

  • Vite 作为开发构建体验
  • Embroider 作为现代化打包架构
  • 严格模式模板
  • .gjs / .gts 作为主要作者体验

也就是说,今天的新城区不是旧时代那种“很多魔法藏在地基下面”的 Ember 了,而是更接近现代 JS 工具链的显式城市。

你可以把 CLI 理解成“城建总包”:

  • ember new:新建整座城
  • ember generate route permits:新开一个街区,并自动补门牌、街道登记、基础图纸
  • ember generate component permit-card:造一个标准建筑模块
  • ember generate service session:新设一个常驻部门
  • ember generate model permit:在总档案馆里登记一个新实体类型

这正是 Ember 的味道:不是让你空手摆文件,而是让工具按城市法则替你把骨架搭好。

一个现代 Ember 项目,你最该先认识的通常是这些地块:

text
permit-city/
├── app/
│   ├── components/         # 标准建筑模块
│   ├── models/             # 档案实体定义
│   ├── routes/             # 各街区的区政府逻辑
│   ├── services/           # 常驻市政机关
│   ├── templates/          # 路由模板 / 应用外壳
│   └── router.js           # 总路网
├── config/                 # 城市运行配置
├── public/                 # 公开静态资产
├── tests/                  # 验收与巡检系统
├── babel.config.cjs        # 现代构建配置
├── package.json
└── .ember-cli

这里最有 Ember 味道的一点是:

目录名不是摆设,它们就是城市分区本身。

在别的生态里,文件夹有时只是“大家约定俗成”。在 Ember 里,文件夹经常就是系统认路的一部分。


#四、路由:先修主干道,再谈盖楼

Ember 对路由的态度很“老派 Web”,但恰恰因此很强。

它相信:URL 不是附属品,而是整座城市的主干道。

你先进哪条街、街区之间怎么嵌套、进城前要调哪些档案、加载失败怎么兜底,这些不该零散塞进组件,而应由路由系统统一治理。

一个典型的总路网长这样:

js
// app/router.js
import EmberRouter from '@embroider/router';
import config from 'permit-city/config/environment';

export default class Router extends EmberRouter {
  location = config.locationType;
  rootURL = config.rootURL;
}

Router.map(function () {
  this.route('permits', function () {
    this.route('permit', { path: '/:permit_id' });
  });

  this.route('citizens');
});

这张路网图的意思是:

  • /permits 是许可证大厅
  • /permits/123 是 123 号许可证的详情街区
  • /citizens 是居民档案区

在 Ember 里,定义一条路,往往就等于同时决定了几件事:

  • URL 长什么样
  • 对应 route 文件放哪
  • 对应 template 文件放哪
  • 嵌套路由渲染到谁的 {{outlet}}
  • 相关测试从哪起步

这就是“门牌号式开发”。

如果你生成一个 route:

bash
ember generate route permits

Ember 会顺手帮你把区政府办公室、楼体图纸、基础验收文件一起建出来。它不是在问你“你喜欢哪种项目结构”,它直接说:“这座城区就按这一套编号法走。”

而且这套路由不是单纯 URL 匹配器,它还是数据与渲染的调度器


#五、Route:每个街区都有一个“区政府”,负责先把事准备好

很多初学者学 Ember 的第一个拐点,就在这里。

在 React 生态里,你很容易把“数据获取”理解成组件自己的事。
在 Ember 的城市里,页面级数据更像区政府提前调档,而不是楼里某个房间自己跑去档案馆翻卷宗。

看一个典型例子:

js
// app/routes/permits.js
import Route from '@ember/routing/route';
import { service } from '@ember/service';

export default class PermitsRoute extends Route {
  @service store;

  model() {
    return this.store.findAll('permit');
  }
}

这里的 model(),就是这个街区的“入区准备流程”。

用户走进 /permits 这条街时:

  1. Router 发现用户要去 permits
  2. PermitsRoutemodel() 自动执行
  3. Route 去总档案馆 storepermit 全量记录
  4. 数据准备好后,再把模板真正渲染出来

这件事非常 Ember。

因为它的逻辑是:

先决定你去哪,再决定这个地方需要什么数据,然后才开始盖楼。

而不是“先把楼盖出来,楼里组件再各自出去拉材料”。

如果是动态街区:

js
// app/routes/permits/permit.js
import Route from '@ember/routing/route';
import { service } from '@ember/service';

export default class PermitsPermitRoute extends Route {
  @service store;

  model(params) {
    return this.store.findRecord('permit', params.permit_id);
  }
}

这里 params.permit_id 就像街区门牌号,Route 拿着门牌去总档案馆调对应卷宗。

Route 的强大,不只在于取数据,还在于它天然对异步友好:

  • 返回普通值,可以
  • 返回 Promise,也可以
  • Promise 没 resolve 之前,路由切换会等待
  • 还能配合 loading / error 子状态做区级兜底

这就是 Ember 在大型应用里让人舒服的地方:异步不是散在楼里的电线,而是铺在市政主干道里的统一管网。


#六、模板:不是 JSX 宇宙,而是“按 HTML 施工”的城市图纸

Ember 模板的一个关键气质是:它非常强调 HTML 本身。

你可以把模板理解成建筑图纸。
这份图纸不是“随手拼字符串”,也不是“什么 JS 都能塞进去”,而是强调结构清楚、来源明确、可读性高。

在现代 Ember 里,一个路由级外壳通常会写在 app/templates/application.gjs

js
// app/templates/application.gjs
import { LinkTo } from '@ember/routing';

<template>
  <header class="city-shell">
    <h1>约定秩序城区</h1>

    <nav>
      <LinkTo @route="permits">许可证大厅</LinkTo>
      <LinkTo @route="citizens">居民档案区</LinkTo>
    </nav>
  </header>

  <main>
    {{outlet}}
  </main>
</template>

这里有两个 Ember 味道很重的点。

第一,<LinkTo> 是官方路线内的导航组件。
第二,{{outlet}} 是一个“街区预留渲染口”。

你可以把 application.gjs 看成城市总入口大厅。
所有子街区的楼体,都会被灌进这个大厅里的 {{outlet}}

如果是 permits 路由模板:

js
// app/templates/permits.gjs
import PermitCard from 'permit-city/components/permit-card';

<template>
  <section>
    <h2>许可证大厅</h2>

    {{#each @model as |permit|}}
      <PermitCard @permit={{permit}} />
    {{/each}}
  </section>
</template>

这里你能看出 Ember 模板的几个核心习惯:

  • 路由返回的数据,模板直接从 @model
  • 组件参数用 @参数名
  • 当前实例上的字段用 this.xxx
  • 控制结构像 {{#each}}{{#if}} 依然很重要
  • 严格模式下,组件、helper、modifier 更倾向显式 import,而不是全城到处靠“神秘自动识别”

这就是 Ember 模板的城市纪律:

图纸尽量像图纸,不要把钢筋、水泥、审批流全混成一团。


#七、组件:标准建筑模块,不是整座城市的总调度中心

Ember 的组件今天主流是 Glimmer 组件

你要把它理解成“标准建筑模块”。

它负责:

  • 展示局部 UI
  • 处理局部交互
  • 接收外部参数
  • 管理局部状态

不该默认负责整座街区的入区数据调度。那是 Route 的职责。

看一个典型组件:

js
// app/components/permit-card.gjs
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { on } from '@ember/modifier';

export default class PermitCardComponent extends Component {
  @tracked expanded = false;

  toggle = () => {
    this.expanded = !this.expanded;
  };

  approve = async () => {
    this.args.permit.status = 'approved';
    await this.args.permit.save();
  };

  <template>
    <article class="permit-card">
      <h3>{{@permit.title}}</h3>
      <p>状态:{{@permit.status}}</p>

      <button type="button" {{on "click" this.toggle}}>
        {{if this.expanded "收起详情" "查看详情"}}
      </button>

      {{#if this.expanded}}
        <p>{{@permit.description}}</p>
      {{/if}}

      <button type="button" {{on "click" this.approve}}>
        审批通过
      </button>
    </article>
  </template>
}

这里你会看到现代 Ember 组件的主心骨:

  • Component:Glimmer 组件基类
  • @tracked:可追踪状态
  • this.args:外部传入参数
  • {{on "click" ...}}:事件绑定 modifier
  • 模板和类写在同一个 .gjs 文件里

这套模型很像城市里的标准商铺:

  • 店铺长什么样,由模板决定
  • 店铺自己的开关灯、折叠面板、按钮交互,由组件自己管
  • 但店铺不该自己去改城市主路规划,也不该乱接全城总水管

所以写 Ember 组件最重要的一条城市纪律是:

组件负责“楼内事务”,Route 负责“街区调度”。

这条线一旦守住,项目会非常清爽。


#八、服务:常驻市政机关,跨区共享,不随街区卸载

有些能力不属于某一栋楼,也不属于某一个街区,而是整座城市运行时一直在线的。

比如:

  • 当前登录人
  • 购物车
  • WebSocket 连接
  • 通知中心
  • 地理定位
  • 第三方 API 客户端
  • 跨页面缓存状态

这些在 Ember 里通常放进 Service

你可以把 Service 看成“常驻市政机关”。

它的特点是:

  • 生命周期跟整个应用走
  • 可以被组件、路由、其他服务注入使用
  • 适合共享状态和跨区域能力

例如:

js
// app/services/session.js
import Service from '@ember/service';
import { trackedObject } from '@ember/reactive/collections';

export default class SessionService extends Service {
  currentUser = trackedObject({
    id: 'u-1',
    name: '城区审批员',
    role: 'reviewer',
  });

  get isReviewer() {
    return this.currentUser.role === 'reviewer';
  }

  signOut() {
    this.currentUser = trackedObject({
      id: null,
      name: null,
      role: null,
    });
  }
}

然后在组件里注入:

js
// app/components/city-header.gjs
import Component from '@glimmer/component';
import { service } from '@ember/service';

export default class CityHeaderComponent extends Component {
  @service session;

  <template>
    <header>
      <strong>{{this.session.currentUser.name}}</strong>

      {{#if this.session.isReviewer}}
        <span>审批权限已启用</span>
      {{/if}}
    </header>
  </template>
}

这个机制像什么?

像每栋楼都可以拨打“市政热线”,但热线背后是同一个常驻部门,不会因为你从 A 街区走到 B 街区就销毁重建。

不过,很多人也会在这里犯错:
把 Service 当成“全局大垃圾桶”。

这就像把所有业务都塞进“市政府总办公室”,最后任何事都能管,任何事都难维护。

所以 Service 的正确姿势是:

  • 共享状态放这里
  • 跨页面能力放这里
  • 页面级数据主入口仍优先放 Route
  • 不要因为偷懒,就把一切都变成全局单例

#九、数据层:Ember Data、store、model,就是城市总档案馆

Ember 的另一个标志性气质,是它对数据层也有官方叙事。

在别的生态里,数据层常常是“你爱选啥选啥”。
在 Ember 的城区里,官方默认会给你一座总档案馆,这就是 Ember Data

你可以把它拆成三个角色来理解:

  • Model:档案类型定义,说明“许可证”“居民”“工单”这些实体长什么样
  • Store:总档案馆前台,负责查询、缓存、复用、分发
  • Adapter / Serializer:档案馆和外部系统之间的翻译官

Ember Data 默认更偏向 JSON:API 这条官方公路,但它并不强迫你后端只能长成一种样子;需要时你可以自定义 adapter / serializer,或者对某些非标准接口直接用别的请求方式。

先看 Model:

js
// app/models/permit.js
import Model, { attr, belongsTo } from '@ember-data/model';

export default class PermitModel extends Model {
  @attr('string') title;
  @attr('string') status;
  @attr('string') description;
  @attr('date') submittedAt;

  @belongsTo('citizen', { async: true, inverse: null }) owner;
}

这相当于在档案馆里定义一种卷宗格式:
许可证有标题、状态、说明、提交时间,还有一个关联的申请人。

然后看 Store 怎么用:

js
// 查单条档案
this.store.findRecord('permit', 'p-42');

// 查全部档案
this.store.findAll('permit');

// 只读缓存,不发请求
this.store.peekRecord('permit', 'p-42');
this.store.peekAll('permit');

// 带条件查询
this.store.query('permit', {
  filter: { status: 'pending' }
});

再看创建与保存:

js
let permit = this.store.createRecord('permit', {
  title: '夜间施工许可证',
  status: 'draft',
  description: '申请在 23:00 后施工'
});

await permit.save();

你要特别理解 Ember Data 的“城市档案馆心智”:

Store 不是一个临时数组,不是某个页面的局部状态,而是全城业务实体的统一登记处。

它的好处非常大:

  • 同一条记录在不同页面、不同组件里能指向一致对象
  • 已经调过的档案可以缓存
  • 局部修改能更自然地在别处反映
  • 你不用每个页面都手动维持“同一人、同一工单、同一许可证”的多份副本同步

这在大型业务系统里太重要了。

因为大系统最怕的,不是“没拿到数据”,而是拿到了三份看起来像同一条、实际却不是同一份对象的数据

当然,也要说句实话:

Ember Data 很强,但也不是“所有请求都必须经过它”。
如果你有:

  • 一次性统计接口
  • 非实体化的报表接口
  • 第三方服务调用
  • 很偏门的协议格式

完全可以放进 Service 或其他请求层。

城市比喻里就是:
居民档案进总档案馆,临时访客登记表没必要也硬塞进人口数据库。


#十、约定优于配置:文件名不是装饰,是门牌号

Ember 最容易让新人又爱又恨的,就是“约定优于配置”。

但你一旦用城市去理解,就会觉得它非常合理。

如果一个街区叫 permits,那 Ember 会默认期待这些门牌:

  • app/routes/permits.js
  • app/templates/permits.gjs

如果它下面还有一个 permit 子街区:

  • app/routes/permits/permit.js
  • app/templates/permits/permit.gjs

如果你有一个组件 permit-card

  • app/components/permit-card.gjs

如果你有个服务 session

  • app/services/session.js

这就像城市门牌系统。

门牌的价值不是“好看”,而是:

  • 邮差能送到
  • 消防能定位
  • 维修队能找门
  • 新人一看就懂

Ember 的约定体系,就是在做这件事。

它在替团队消灭很多无意义选择题:

  • route 文件放哪
  • template 文件怎么配
  • 组件怎么命名
  • 模型去哪
  • 服务去哪
  • 新人第一次进项目该先看什么

当然,Ember 并不是完全不能改路。
比如路由 path 就能改:

js
Router.map(function () {
  this.route('contact', { path: '/getting-in-touch' });
});

这就像“街区内部叫市民服务中心,但对外保留旧路名”。
所以 Ember 不是“不能配置”,而是:

先给你一套能跑十年的城市标准;只有真的需要偏离时,再显式偏离。

这和“什么都能配”的差别非常大。
因为“什么都能配”在大多数团队里,最后往往会滑向“每个人都配得不一样”。


#十一、运行机制:Ember 这座城市到底怎么动起来

现在把整座城市串起来。

当用户打开一个 Ember 应用时,发生的事情,大致像这样:

text
用户访问 URL
   |
   v
Router 匹配总路网
   |
   v
进入对应 Route 链
   |
   v
执行 model() / 相关 hook
   |
   v
Route 从 Store / API / Service 准备数据
   |
   v
把数据交给 route template(@model)
   |
   v
Template 组装 Component
   |
   v
Glimmer 渲染为 DOM
   |
   v
页面可交互
   |
   v
@tracked / args / service / store 中被读取过的数据发生变化
   |
   v
Autotracking 记录依赖,Glimmer 精准更新受影响的 DOM

再画得完整一点:

text
                   约定秩序城区运行图

URL
 |
 v
+--------------------+
| Router 总路网中心  |
+--------------------+
          |
          v
+--------------------+
| Route 街区政府     |
| model() / redirect |
+--------------------+
          |
          v
+--------------------------+
| Store 城市总档案馆       |
| findRecord / findAll     |
| cache / identity map     |
+--------------------------+
          |
          v
+--------------------------+
| Adapter / Serializer     |
| 对接 REST / JSON:API 等  |
+--------------------------+
          |
          v
        API / Backend

Route 准备好的数据
          |
          v
+---------------------------+
| Template 城市图纸         |
| application.gjs           |
| permits.gjs               |
| {{outlet}}                |
+---------------------------+
          |
          v
+---------------------------+
| Component 标准建筑模块    |
| args / @tracked / on      |
+---------------------------+
          |
          v
+---------------------------+
| Service 常驻市政机关      |
| session / cart / socket   |
+---------------------------+
          |
          v
+---------------------------+
| Glimmer 渲染调度中心      |
| autotracking + DOM patch  |
+---------------------------+

这套机制里最值得你记住的是两件事。

第一件,Route 先于页面级展示
Ember 的天然节奏就是“先入区审批,再开放街区”。

第二件,Ember 的更新模型不是到处手动通知,而是依赖追踪 + 高效重渲染心智
官方在组件更新思路里强调得很明确:你应该把模板想成“根据当前输入重新求值”,至于 DOM 该怎么最小化更新,由 Glimmer 去处理。

这非常像城市数字孪生系统:

  • 哪些传感器值被这个屏幕读过
  • 哪个广告牌内容真的变了
  • 哪块指示牌不用动

Glimmer 会尽量只更新必要部分,而不是把整条街推倒重来。


#十二、典型代码片段:把一条审批街区完整串一遍

如果把 Ember 的主线压缩成一条最典型的开发路径,大概就是下面这组代码。

先定义路网:

js
// app/router.js
import EmberRouter from '@embroider/router';
import config from 'permit-city/config/environment';

export default class Router extends EmberRouter {
  location = config.locationType;
  rootURL = config.rootURL;
}

Router.map(function () {
  this.route('permits', function () {
    this.route('permit', { path: '/:permit_id' });
  });
});

再让街区先调档案:

js
// app/routes/permits.js
import Route from '@ember/routing/route';
import { service } from '@ember/service';

export default class PermitsRoute extends Route {
  @service store;

  model() {
    return this.store.findAll('permit');
  }
}

定义档案格式:

js
// app/models/permit.js
import Model, { attr } from '@ember-data/model';

export default class PermitModel extends Model {
  @attr('string') title;
  @attr('string') status;
  @attr('string') description;
}

总入口大厅:

js
// app/templates/application.gjs
import { LinkTo } from '@ember/routing';

<template>
  <header>
    <h1>市政审批城区</h1>
    <LinkTo @route="permits">进入许可证大厅</LinkTo>
  </header>

  {{outlet}}
</template>

街区图纸:

js
// app/templates/permits.gjs
import PermitCard from 'permit-city/components/permit-card';

<template>
  <section>
    <h2>许可证大厅</h2>

    {{#each @model as |permit|}}
      <PermitCard @permit={{permit}} />
    {{/each}}
  </section>
</template>

楼内交互:

js
// app/components/permit-card.gjs
import Component from '@glimmer/component';
import { on } from '@ember/modifier';

export default class PermitCardComponent extends Component {
  approve = async () => {
    this.args.permit.status = 'approved';
    await this.args.permit.save();
  };

  <template>
    <article>
      <h3>{{@permit.title}}</h3>
      <p>{{@permit.description}}</p>
      <p>当前状态:{{@permit.status}}</p>

      <button type="button" {{on "click" this.approve}}>
        审批通过
      </button>
    </article>
  </template>
}

这段代码的意思,不只是“能跑”。

它更重要的是体现了 Ember 的城市秩序:

  • 路怎么进:router.js
  • 街区先拿什么档案:routes/permits.js
  • 档案长什么样:models/permit.js
  • 城市外壳放哪:application.gjs
  • 列表街区怎么展示:permits.gjs
  • 单个楼块怎么交互:permit-card.gjs

这不是写法偏好,而是城市治理方式。


#十三、历史包袱要知道,但别把旧城区当新蓝图

Ember 是一个老牌、长期稳定演进的框架,所以你读代码、搜文章、看老项目时,一定会穿越到旧城区。

常见的旧城区痕迹有这些:

  • Route.extend({ ... })
  • Component.extend({ ... })
  • EmberObject
  • this.get('foo') / this.set('foo', value')
  • computed properties 到处飞
  • observers
  • 经典组件
  • 单独的 .hbs + .js 文件配套写法
  • 强依赖全局 resolver 自动找组件、helper、modifier

这些都不是“假 Ember”。
它们是 Ember 历史发展的真实遗产。

但你在 2026 视角下建立主线心智时,应该优先抓住今天稳定成立的主路径:

  • 原生 class
  • @tracked
  • Glimmer 组件
  • @service
  • .gjs / .gts
  • 严格模式模板
  • 更显式的 import
  • Vite + Embroider 新蓝图

城市比喻里,这就像:

旧城区的老楼还在,也能住人;但新城区的城市规范已经换代了。你学规划时,别拿老式木窗审批标准去指导今天的高层综合体。


#十四、常见误区:很多人不是没学会 Ember,而是带着别的城市地图来理解它

#误区 1:Ember 只是“另一个组件框架”

不对。
Ember 有组件层,但它真正的价值在于:路由、数据层、工程骨架、测试、升级路径、约定体系一起形成的整城秩序。

#误区 2:组件自己 fetch 一切更灵活

小块组件偶尔可以这么做。
但页面级主数据在 Ember 里更适合进 Route。否则很快就会出现一栋楼拉一份卷宗、另一栋楼再拉一份、整条街都在重复施工。

#误区 3:所有共享内容都丢 Service

Service 是市政机关,不是宇宙垃圾桶。
全局状态、长生命周期连接、跨页面共享能力适合放这里;页面数据主调度依旧应该先看 Route。

#误区 4:@tracked 只要写过一次就万事大吉

不是。
如果你改的是普通对象/数组的内部结构,可能并不会触发你预期的更新。现代 Ember 提供了 trackedArraytrackedObject 等更明确的响应式集合工具,这很值得用。

#误区 5:Ember Data 是强制宗教

也不是。
Ember Data 是官方档案馆,非常适合“实体化、可缓存、需要一致性”的业务模型。
但统计接口、一次性报表、第三方 API,完全可以走 Service 或别的请求层。

#误区 6:老文章里能跑的写法,今天就是最佳实践

这是 Ember 学习里最危险的坑。
你搜到的很多内容横跨十几年演进史。能跑,不等于该作为新项目主路径。

#误区 7:约定优于配置 = 不可定制

恰恰相反。
Ember 是“先有统一城市法”,不是“禁止改造”。
只是它要求你在偏离默认方案时,偏离得清楚、显式、可追踪。


#十五、什么场景适合 Ember:它最像“长期运营型业务城区”

如果你的项目像下面这些,Ember 往往会非常顺手:

  • 长生命周期的后台、管理台、工作流系统
  • 业务实体关系复杂,需要统一数据语义
  • 页面导航、嵌套路由、筛选、详情、编辑流很重
  • 团队人数较多,希望新人快速按目录认路
  • 很在意升级稳定性、弃坑成本、组织一致性
  • 不想每次开新项目都重新决定路由、状态、构建、测试方案

这类项目像什么?

像:

  • 政务审批平台
  • CRM / ERP / OMS
  • 内容运营后台
  • 企业级数据录入与审核系统
  • 多步骤工作流应用
  • 内部业务中台

这种城市不追求“这栋楼单独很炫”,它追求的是:

整座城十年后依然能维护。

这正是 Ember 的审美。


#十六、什么场景不太适合 Ember:它不是每块地都该用的规划局

也要讲清楚,Ember 不是万能银弹。

下面这些情况,它往往不是默认首选:

  • 以营销落地页、内容站、SEO 首屏为核心的网站
  • 超轻量的嵌入式小部件
  • 团队明确只想要 UI 库,不想接受强约束
  • 需要最大化跟随主流生态热点和招聘池
  • 服务端渲染、边缘渲染、内容分发是核心叙事的项目

为什么?

因为这些场景更像:

  • 临街快闪店
  • 宣传展馆
  • 极轻量可插拔 kiosk
  • 需要和主流 SSR / full-stack 叙事高度贴合的产品

Ember 当然不是做不了其中一些事。
但它的城市气质,天然更偏“长期运营城区”,而不是“爆款商场快装系统”。


#十七、和 React / Vue / Angular / Next 等方案怎么对比:不是谁更强,而是谁在修哪种城

这部分最容易吵架,所以一定要用城市规划视角来看,而不是用“谁更火”来看。

方案 更像什么 核心气质 你主要得到什么 你主要付出什么
Ember.js 规划完整的秩序城区 强约定、整城治理 路由、工程结构、数据层叙事、升级稳定性、一致性 生态热度较小,必须接受它的城市法
React 高质量建筑构件库 组件优先、生态超大 自由度、生态、组合能力 你要自己拼很多城市级决策
Vue 规划较柔和的新城区 渐进、平衡、易上手 较低学习曲线、较完整官方生态、模板体验顺滑 组织级强约束不如 Ember/Angular
Angular 国家级基础设施总包 官方一体化、DI、组织治理强 表单、路由、DI、工程规范、企业级一致性 体系更重,学习和工程感更强
Next.js 带服务端能力的 React 商业综合体 React full-stack、SSR/RSC/缓存/部署叙事强 服务端与前端一体化、内容站和产品站路线更成熟 仍然建立在 React 世界观上,城市法不是 Ember 那种整城约定

把它们说得更直白一点。

React 像给你一套国际通用建筑零件。
零件质量很高,配套市场巨大,但整座城市怎么规划,很多事要你自己定。

Vue 像规划比较温和的新城区。
它保留了很强的开发舒适度,也给你一套比较完整的官方路线,但不会像 Ember 那样把城市编号体系压得那么实。

Angular 像国家级基础设施总包。
它也很完整、很有纪律,甚至比 Ember 更强调官方平台感。
如果说 Ember 是“约定秩序城区”,Angular 更像“制度化工业新区”。

Next.js 则不是 Ember 的同层替代。
它更像 React 上面叠起来的商业综合体方案,特别擅长把:

  • 路由
  • 渲染策略
  • 服务端能力
  • 数据获取
  • 部署链路

整成一个 full-stack 叙事。
如果你的核心问题是“内容首屏、服务端能力、SEO、边缘部署”,Next 的自然度通常更高。
但如果你的核心问题是“一个长期运营的前端业务城区如何保持十年不乱”,Ember 的整城秩序感反而很有魅力。

所以别问“Ember 会不会被 React/Vue/Next 打败”。

更准确的问法是:

你现在要修的是一座什么样的城?

如果你要的是一座路网、门牌、档案、审批、升级制度都非常统一的业务城区,Ember 依然有极强辨识度。


#十八、真正掌握 Ember 的标志:你开始按“城区职责”分配代码

什么时候算真的入门 Ember?

不是你会写 @tracked
也不是你能背出 store.findRecord()

而是你开始本能地这么分工:

  • URL 和进入哪个区:先看 Router
  • 进入这个区前要拿什么数据:先看 Route
  • 这个街区整体怎么摆:看 route template
  • 某个局部楼块怎么交互:写 Component
  • 跨街区共享什么能力:放 Service
  • 哪些是稳定业务实体:进 Model / Store
  • 为什么文件放这里:因为这不是随意放置,而是门牌系统

一旦你能按这个秩序思考,Ember 就突然从“有点古典、有点重”的框架,变成一套非常稳的城市规划语言。

它的魅力不在于让你每次都拥有一百种施工自由。
它的魅力在于:当项目真的大起来时,它能让整座城还像一座城。


#十九、收尾总结:Ember.js 是给“长期秩序”设计的,不是给“即时炫技”设计的

用一句话收住整章:

Ember.js 的本质,是把大型 Web 应用当作一座需要长期治理的城市,而不是一堆暂时能拼起来的页面。

所以它才会执着于:

  • 路由优先
  • 文件结构约定
  • 官方数据层叙事
  • 服务式共享能力
  • 清晰的模板边界
  • 稳定升级路径
  • 框架级组织秩序

在“约定秩序城区宇宙”里:

  • router.js 是总路网
  • route 是区政府
  • template 是施工图纸
  • component 是标准建筑模块
  • service 是常驻市政机关
  • storemodel 是城市总档案馆
  • @tracked 与 Glimmer 是实时调度系统
  • CLI 和生成器是城建总包与门牌发放局

如果你能一直用这套比喻去理解 Ember,你就不会再把它看成“一个有点老的前端框架”,而会看见它真正的设计野心:

它想解决的,从来不是一个按钮怎么点亮,而是一整座前端业务城市如何长期有序运行。

#官方来源链接清单