Ember.js项目结构 #

一、项目根目录 #

一个典型的Ember项目根目录结构如下:

text
my-ember-app/
├── app/                    # 应用核心代码
├── config/                 # 配置文件
├── tests/                  # 测试文件
├── public/                 # 静态资源
├── vendor/                 # 第三方库
├── node_modules/           # npm依赖
├── .ember-cli              # Ember CLI配置
├── .eslintrc.js            # ESLint配置
├── .template-lintrc.js     # 模板检查配置
├── .prettierrc.js          # Prettier配置
├── .watchmanconfig         # Watchman配置
├── ember-cli-build.js      # 构建配置
├── package.json            # 项目依赖
├── testem.js               # 测试运行器配置
└── README.md               # 项目说明

二、app目录详解 #

app/ 目录是应用的核心,包含所有业务代码:

text
app/
├── app.js                  # 应用入口
├── index.html              # HTML入口文件
├── router.js               # 路由配置
├── adapters/               # 数据适配器
│   └── application.js
├── components/             # 组件
│   ├── my-component.hbs
│   └── my-component.js
├── controllers/            # 控制器
│   └── posts.js
├── helpers/                # 模板助手
│   └── format-date.js
├── models/                 # 数据模型
│   └── post.js
├── modifiers/              # 模板修饰符
│   └── autofocus.js
├── routes/                 # 路由
│   ├── application.js
│   └── posts.js
├── serializers/            # 序列化器
│   └── post.js
├── services/               # 服务
│   └── auth.js
├── styles/                 # 样式文件
│   └── app.css
├── templates/              # 模板
│   ├── application.hbs
│   └── posts.hbs
└── transforms/             # 数据转换器
    └── date.js

2.1 app.js #

应用入口文件,定义应用的基本配置:

javascript
import Application from '@ember/application';
import Resolver from 'ember-resolver';
import loadInitializers from 'ember-load-initializers';
import config from './config/environment';

export default class App extends Application {
  modulePrefix = config.modulePrefix;
  podModulePrefix = config.podModulePrefix;
  Resolver = Resolver;
}

loadInitializers(App, config.modulePrefix);

2.2 router.js #

路由配置文件,定义应用的URL结构:

javascript
import EmberRouter from '@ember/routing/router';
import config from 'my-app/config/environment';

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

Router.map(function () {
  this.route('home', { path: '/' });
  this.route('about');
  this.route('posts', function () {
    this.route('new');
    this.route('edit', { path: '/:post_id/edit' });
  });
});

2.3 index.html #

HTML入口文件,Ember应用挂载点:

html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>MyApp</title>
    <meta name="description" content="" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    {{content-for "head"}}
    <link integrity="" rel="stylesheet" href="{{rootURL}}assets/vendor.css" />
    <link integrity="" rel="stylesheet" href="{{rootURL}}assets/my-app.css" />
    {{content-for "head-footer"}}
  </head>
  <body>
    {{content-for "body"}}
    <script src="{{rootURL}}assets/vendor.js"></script>
    <script src="{{rootURL}}assets/my-app.js"></script>
    {{content-for "body-footer"}}
  </body>
</html>

三、核心目录详解 #

3.1 components/ #

组件是Ember应用的核心构建块:

text
components/
├── user-profile/
│   ├── index.hbs           # 组件模板
│   ├── index.js            # 组件逻辑
│   └── styles.css          # 组件样式(可选)
├── todo-item.hbs
├── todo-item.js
└── nav-bar.gjs             # 单文件组件格式

组件示例:

javascript
// app/components/user-profile.js
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';

export default class UserProfileComponent extends Component {
  @tracked isExpanded = false;

  @action
  toggle() {
    this.isExpanded = !this.isExpanded;
  }
}
handlebars
{{! app/components/user-profile.hbs}}
<div class="user-profile">
  <h2>{{@user.name}}</h2>
  <button type="button" {{on "click" this.toggle}}>
    {{if this.isExpanded "收起" "展开"}}
  </button>
  {{#if this.isExpanded}}
    <p>{{@user.email}}</p>
    <p>{{@user.bio}}</p>
  {{/if}}
</div>

3.2 routes/ #

路由负责加载数据和处理路由逻辑:

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

export default class PostsRoute extends Route {
  async model() {
    const response = await fetch('/api/posts');
    return response.json();
  }

  setupController(controller, model) {
    super.setupController(controller, model);
    controller.set('pageTitle', '所有文章');
  }
}

3.3 templates/ #

模板定义视图层:

handlebars
{{! app/templates/posts.hbs}}
<h1>{{@model.length}} 篇文章</h1>

<ul>
  {{#each @model as |post|}}
    <li>
      <LinkTo @route="posts.show" @model={{post.id}}>
        {{post.title}}
      </LinkTo>
    </li>
  {{/each}}
</ul>

{{outlet}}

3.4 models/ #

模型定义数据结构:

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

export default class PostModel extends Model {
  @attr('string') title;
  @attr('string') body;
  @attr('date') publishedAt;
  @attr('boolean') isPublished;

  @belongsTo('user') author;
  @hasMany('comment') comments;
}

3.5 controllers/ #

控制器处理模板的交互逻辑:

javascript
// app/controllers/posts.js
import Controller from '@ember/controller';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';

export default class PostsController extends Controller {
  @tracked filter = 'all';

  get filteredPosts() {
    if (this.filter === 'all') {
      return this.model;
    }
    return this.model.filter((post) => post.isPublished);
  }

  @action
  setFilter(value) {
    this.filter = value;
  }
}

3.6 services/ #

服务是可注入的单例对象:

javascript
// app/services/session.js
import Service from '@ember/service';
import { tracked } from '@glimmer/tracking';

export default class SessionService extends Service {
  @tracked isAuthenticated = false;
  @tracked currentUser = null;

  async login(credentials) {
    const response = await fetch('/api/login', {
      method: 'POST',
      body: JSON.stringify(credentials),
    });
    const user = await response.json();
    this.currentUser = user;
    this.isAuthenticated = true;
  }

  logout() {
    this.isAuthenticated = false;
    this.currentUser = null;
  }
}

3.7 helpers/ #

助手用于模板中的数据转换:

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);
  if (format === 'short') {
    return d.toLocaleDateString();
  }
  return d.toLocaleString();
});
handlebars
{{! 使用助手}}
<p>发布于:{{format-date @post.publishedAt}}</p>
<p>简短格式:{{format-date @post.publishedAt format="short"}}</p>

3.8 modifiers/ #

修饰符用于DOM操作:

javascript
// app/modifiers/autofocus.js
import { modifier } from 'ember-modifier';

export default modifier(function autofocus(element, positional, { delay = 0 }) {
  setTimeout(() => {
    element.focus();
  }, delay);
});
handlebars
{{! 使用修饰符}}
<input {{autofocus}} />
<input {{autofocus delay=100}} />

四、config目录 #

配置文件目录:

text
config/
├── environment.js          # 环境配置
├── targets.js              # 目标浏览器配置
├── optional-features.json  # 可选特性
└── ember-try.js            # 版本测试配置

4.1 environment.js #

javascript
// config/environment.js
'use strict';

module.exports = function (environment) {
  let ENV = {
    modulePrefix: 'my-app',
    environment: environment,
    rootURL: '/',
    locationType: 'history',
    EmberENV: {
      FEATURES: {},
    },
    APP: {
      API_HOST: 'https://api.example.com',
    },
  };

  if (environment === 'development') {
    ENV.APP.API_HOST = 'http://localhost:3000';
  }

  if (environment === 'test') {
    ENV.locationType = 'none';
    ENV.APP.rootElement = '#ember-testing';
    ENV.APP.autoboot = false;
  }

  if (environment === 'production') {
    // 生产环境配置
  }

  return ENV;
};

4.2 targets.js #

javascript
// config/targets.js
'use strict';

const browsers = ['last 2 Chrome versions', 'last 2 Firefox versions', 'last 2 Safari versions'];

module.exports = {
  browsers,
  node: 'current',
};

五、tests目录 #

测试文件目录:

text
tests/
├── index.html              # 测试入口
├── test-helper.js          # 测试助手
├── acceptance/             # 验收测试
│   └── login-test.js
├── integration/            # 集成测试
│   └── components/
│       └── user-profile-test.js
├── unit/                   # 单元测试
│   ├── models/
│   │   └── post-test.js
│   └── services/
│       └── session-test.js
└── helpers/                # 测试助手
    └── setup-application.js

六、public目录 #

静态资源目录:

text
public/
├── favicon.ico
├── images/
│   └── logo.png
└── robots.txt

七、Pods结构(可选) #

Ember支持Pods结构,将相关文件组织在一起:

text
app/
├── pods/
│   ├── post/
│   │   ├── route.js
│   │   ├── template.hbs
│   │   ├── controller.js
│   │   └── model.js
│   ├── components/
│   │   └── user-profile/
│   │       ├── component.js
│   │       └── template.hbs
│   └── application/
│       ├── route.js
│       └── template.hbs
└── router.js

启用Pods结构需要在 config/environment.js 中配置:

javascript
let ENV = {
  podModulePrefix: 'my-app/pods',
  // ...
};

八、文件命名约定 #

8.1 命名规则 #

类型 文件名 类名
Route posts.js PostsRoute
Controller posts.js PostsController
Component user-profile.js UserProfileComponent
Model post.js PostModel
Service session.js SessionService
Helper format-date.js format-date
Modifier autofocus.js autofocus

8.2 目录与URL对应 #

text
路由配置                          URL
─────────────────────────────────────────
this.route('posts');            /posts
this.route('posts', function() {
  this.route('new');            /posts/new
  this.route('show', {          /posts/123
    path: '/:post_id'
  });
});

九、总结 #

Ember项目结构遵循约定优于配置原则:

目录 用途
app/ 应用核心代码
config/ 配置文件
tests/ 测试文件
public/ 静态资源
vendor/ 第三方库

理解项目结构是掌握Ember开发的基础,遵循约定可以让团队协作更加高效。

最后更新:2026-03-28