约定优于配置 #

一、什么是约定优于配置 #

“约定优于配置”(Convention over Configuration)是Ember.js的核心设计理念,源自Ruby on Rails框架。它的核心思想是:

  • 框架提供合理的默认约定
  • 开发者遵循约定即可快速开发
  • 只在必要时才进行配置
  • 减少决策,提高效率

1.1 与其他框架对比 #

框架 理念 特点
Ember.js 约定优于配置 开箱即用,决策少
React 配置优先 灵活,需选择方案
Vue.js 渐进式 可选约定,灵活配置
Angular 约定+配置 有约定但可配置

二、Ember的核心约定 #

2.1 文件命名约定 #

Ember使用一致的文件命名规则:

text
类型          文件名              类名
─────────────────────────────────────────────
Route         posts.js           PostsRoute
Controller    posts.js           PostsController
Component     user-card.js       UserCardComponent
Model         post.js            PostModel
Service       session.js         SessionService
Helper        format-date.js     formatDate
Modifier      autofocus.js       autofocus

2.2 目录结构约定 #

text
app/
├── routes/           # 路由文件
│   └── posts.js
├── controllers/      # 控制器文件
│   └── posts.js
├── templates/        # 模板文件
│   └── posts.hbs
├── components/       # 组件文件
│   ├── user-card.js
│   └── user-card.hbs
├── models/           # 模型文件
│   └── post.js
└── services/         # 服务文件
    └── session.js

2.3 URL与路由约定 #

javascript
// app/router.js
Router.map(function () {
  this.route('posts');
  this.route('posts', function () {
    this.route('new');
    this.route('edit', { path: '/:post_id/edit' });
  });
});
路由配置 URL 模板文件
this.route('posts') /posts templates/posts.hbs
this.route('new') (嵌套) /posts/new templates/posts/new.hbs
this.route('edit', {path: '/:id/edit'}) /posts/123/edit templates/posts/edit.hbs

三、自动解析机制 #

3.1 路由自动解析 #

当用户访问 /posts 时,Ember自动:

text
1. 查找 app/routes/posts.js
2. 实例化 PostsRoute
3. 调用 model() 加载数据
4. 查找 app/controllers/posts.js
5. 查找 app/templates/posts.hbs
6. 渲染模板

3.2 组件自动解析 #

使用 <UserCard /> 组件时,Ember自动查找:

text
app/components/user-card.js
app/components/user-card.hbs

或

app/components/user-card/index.js
app/components/user-card/index.hbs

3.3 模型自动解析 #

在路由中使用 this.store.findRecord('post', 1) 时:

text
Ember 自动查找:
app/models/post.js
app/adapters/post.js (或 application.js)
app/serializers/post.js (或 application.js)

四、减少配置的场景 #

4.1 路由配置 #

其他框架可能需要:

javascript
// 需要手动配置路由、控制器、模板的关联
const routes = [
  {
    path: '/posts',
    component: PostsComponent,
    controller: PostsController,
    template: 'posts.html',
  },
];

Ember只需:

javascript
// 只需定义路由
Router.map(function () {
  this.route('posts');
});
// 其他都由约定自动处理

4.2 组件注册 #

其他框架可能需要:

javascript
// 需要手动注册组件
import UserCard from './components/UserCard.vue';
app.component('UserCard', UserCard);

Ember自动处理:

handlebars
{{! 直接使用,无需注册}}
<UserCard @user={{@currentUser}} />

4.3 服务注入 #

其他框架可能需要:

javascript
// 需要手动配置依赖注入
import { Container } from 'some-di-library';
const container = new Container();
container.register('session', SessionService);

Ember自动处理:

javascript
// 自动注入,无需配置
import { inject as service } from '@ember/service';

export default class MyComponent extends Component {
  @service session;
}

五、约定的好处 #

5.1 提高开发效率 #

bash
# 一条命令生成完整功能
ember generate resource post

# 自动创建:
# - app/models/post.js
# - app/routes/posts.js
# - app/templates/posts.hbs
# - tests/...

5.2 代码一致性 #

javascript
// 所有项目结构一致,新成员快速上手
// 项目A
app/routes/posts.js
app/controllers/posts.js
app/templates/posts.hbs

// 项目B(相同结构)
app/routes/posts.js
app/controllers/posts.js
app/templates/posts.hbs

5.3 减少决策 #

决策 其他框架 Ember
目录结构 需要设计 约定好
文件命名 需要决定 约定好
路由配置 需要配置 约定好
状态管理 需要选择 内置
数据层 需要选择 内置

5.4 易于维护 #

javascript
// 看到URL就知道文件位置
// URL: /posts/123/edit
// 路由: app/routes/posts/edit.js
// 模板: app/templates/posts/edit.hbs
// 模型: app/models/post.js

六、何时需要配置 #

虽然Ember强调约定,但配置仍然灵活:

6.1 自定义适配器 #

javascript
// app/adapters/application.js
import RESTAdapter from '@ember-data/adapter/rest';

export default class ApplicationAdapter extends RESTAdapter {
  namespace = 'api/v1';
  host = 'https://api.example.com';

  buildURL(...args) {
    return super.buildURL(...args) + '.json';
  }
}

6.2 自定义序列化器 #

javascript
// app/serializers/application.js
import RESTSerializer from '@ember-data/serializer/rest';

export default class ApplicationSerializer extends RESTSerializer {
  normalizeResponse(store, primaryModelClass, payload) {
    return super.normalizeResponse(...arguments);
  }
}

6.3 自定义路由行为 #

javascript
// app/routes/posts.js
import Route from '@ember/routing/route';

export default class PostsRoute extends Route {
  // 自定义模型加载
  async model() {
    const response = await fetch('/custom-api/posts');
    return response.json();
  }

  // 自定义渲染
  renderTemplate() {
    this.render('posts/list');
  }
}

七、CLI与约定 #

Ember CLI是约定的最佳实践工具:

7.1 代码生成 #

bash
# 生成路由
ember generate route about
# 创建: app/routes/about.js
# 创建: app/templates/about.hbs
# 更新: app/router.js

# 生成组件
ember generate component user-card
# 创建: app/components/user-card.js
# 创建: app/components/user-card.hbs
# 创建: tests/integration/components/user-card-test.js

# 生成模型
ember generate model post title:string body:text
# 创建: app/models/post.js
# 创建: tests/unit/models/post-test.js

# 生成完整资源
ember generate resource comment
# 创建: 路由、模板、模型、测试

7.2 蓝图自定义 #

可以自定义生成模板:

javascript
// blueprints/component/files/app/components/__name__.js
import Component from '@glimmer/component';

export default class <%= classifiedModuleName %>Component extends Component {
  // 自定义模板内容
}

八、约定最佳实践 #

8.1 遵循命名规范 #

javascript
// 好的命名
app/routes/user-profile.js      // UserProfileRoute
app/components/user-avatar.js   // UserAvatarComponent
app/models/blog-post.js         // BlogPostModel

// 避免的命名
app/routes/userProfile.js       // 不推荐驼峰
app/components/useravatar.js    // 不推荐无分隔
app/models/blog_post.js         // 不推荐下划线

8.2 保持结构一致 #

text
// 推荐的结构
app/
├── routes/
│   ├── posts/
│   │   ├── index.js
│   │   ├── new.js
│   │   └── edit.js
│   └── posts.js
├── templates/
│   ├── posts/
│   │   ├── index.hbs
│   │   ├── new.hbs
│   │   └── edit.hbs
│   └── posts.hbs

8.3 利用CLI生成 #

bash
// 使用CLI而不是手动创建
ember generate route posts/new    # 正确
// 手动创建文件                    # 不推荐

九、约定与灵活性 #

Ember的约定并不限制灵活性:

9.1 可以覆盖约定 #

javascript
// 自定义模板渲染位置
export default class PostsRoute extends Route {
  renderTemplate() {
    this.render('posts/custom-template');
  }
}

// 自定义控制器
export default class PostsRoute extends Route {
  setupController(controller, model) {
    super.setupController(controller, model);
    // 自定义逻辑
  }
}

9.2 可以扩展约定 #

javascript
// 扩展基础适配器
import ApplicationAdapter from './application';

export default class PostAdapter extends ApplicationAdapter {
  pathForType() {
    return 'blog-posts';
  }
}

十、总结 #

"约定优于配置"带来的好处:

好处 说明
效率 减少决策,快速开发
一致性 团队代码风格统一
可维护 结构清晰,易于理解
可扩展 约定可覆盖,灵活配置

掌握Ember的约定是高效开发的关键。遵循约定,让框架为你工作,而不是你为框架工作。

最后更新:2026-03-28