From 4308b37d27d4b209549d6c9cfaf3daddbd03946d Mon Sep 17 00:00:00 2001 From: Dan Gebhardt Date: Mon, 28 Sep 2015 10:22:52 -0400 Subject: [PATCH] Introduce "Application Concerns" section. Replaces the "Services and Initializers" section and expands its content to include pages explaining Applications and Instances, as well as Dependency Injection. All the examples and text have been reviewed and updated for clarity and compatibility with Ember 2.1. One caveat is that the Initializers page references the `instance-initializer` generator, which is still a WIP at the moment. I'll try to get that into CLI ASAP. --- data/pages.yml | 14 +- .../applications-and-instances.md | 18 ++ source/applications/dependency-injection.md | 208 ++++++++++++++++++ source/applications/initializers.md | 100 +++++++++ .../services.md | 30 ++- source/routing/redirection.md | 2 +- source/services-and-initializers/container.md | 95 -------- .../services-and-initializers/initializers.md | 80 ------- 8 files changed, 358 insertions(+), 189 deletions(-) create mode 100644 source/applications/applications-and-instances.md create mode 100644 source/applications/dependency-injection.md create mode 100644 source/applications/initializers.md rename source/{services-and-initializers => applications}/services.md (71%) delete mode 100644 source/services-and-initializers/container.md delete mode 100644 source/services-and-initializers/initializers.md diff --git a/data/pages.yml b/data/pages.yml index 5ddffafef..2eb943ead 100644 --- a/data/pages.yml +++ b/data/pages.yml @@ -129,15 +129,17 @@ - title: "Customizing Serializers" url: "customizing-serializers" -- title: "Services and Initializers" - url: 'services-and-initializers' +- title: "Application Concerns" + url: 'applications' pages: - - title: "Services" - url: "services" + - title: "Applications and Instances" + url: "applications-and-instances" + - title: "Dependency Injection" + url: "dependency-injection" - title: "Initializers" url: "initializers" - - title: "The Container" - url: "container" + - title: "Services" + url: "services" - title: "Testing" url: 'testing' diff --git a/source/applications/applications-and-instances.md b/source/applications/applications-and-instances.md new file mode 100644 index 000000000..70587bba3 --- /dev/null +++ b/source/applications/applications-and-instances.md @@ -0,0 +1,18 @@ +Every Ember application is represented by a class that extends +`Ember.Application`. This class is used to declare and configure the many +objects that make up your app. + +As your application boots, it creates an `Ember.ApplicationInstance` that is +used to manage its stateful aspects. This instance acts as a container for the +objects instantiated for your app. + +Essentially, the `Application` *defines your application* while the +`ApplicationInstance` *manages its state*. + +This separation of concerns not only clarifies the architecture of your app, it +can also improve its efficiency. This is particularly true when your app needs +to be booted repeatedly during testing and / or server-rendering (e.g. via +[FastBoot](https://github.com/tildeio/ember-cli-fastboot)). The configuration of +a single `Application` can be done once and shared among multiple stateful +`ApplicationInstance` instances. These instances can be discarded once they're +no longer needed (e.g. when a test run or FastBoot request has finished). diff --git a/source/applications/dependency-injection.md b/source/applications/dependency-injection.md new file mode 100644 index 000000000..ec8e2aa13 --- /dev/null +++ b/source/applications/dependency-injection.md @@ -0,0 +1,208 @@ +Ember applications utilize the [dependency +injection](https://en.wikipedia.org/wiki/Dependency_injection) ("DI") design +pattern to declare and instantiate classes of objects and dependencies between +them. Applications and application instances each serve a role in Ember's DI +implementation. + +An `Ember.Application` serves as a "registry" for dependency declarations. +Factories (i.e. classes) are registered with an application, as well as rules +about "injecting" dependencies that are applied when objects are instantiated. + +An `Ember.ApplicationInstance` serves as a "container" for objects that are +instantiated from registered factories. Application instances provide a means to +"look up" (i.e. instantiate and / or retrieve) objects. + +> _Note: Although an `Application` serves as the primary registry for an app, +each `ApplicationInstance` can also serve as a registry. Instance-level +registrations are useful for providing instance-level customizations, such as +A/B testing of a feature._ + +## Factory Registrations + +A factory can represent any part of your application, like a _route_, +_template_, or custom class. Every factory is registered with a particular key. +For example, the index template is registered with the key `template:index`, and +the application route is registered with the key `route:application`. + +Registration keys have two segments split by a colon (`:`). The first segment is +the framework factory type, and the second is the name of the particular +factory. Hence, the `index` template has the key `template:index`. Ember has +several built-in factory types, such as `service`, `route`, `template`, and +`component`. + +You can create your own factory type by simply registering a factory with the +new type. For example, to create a `user` type, you'd simply register your +factory with `application.register('user:user-to-register')`. + +Factory registrations must be performed either in application or application +instance initializers (with the former being much more common). + +For example, an application initializer could register a `Logger` factory with +the key `logger:main`: + +```app/initializers/logger.js +export function initialize(application) { + var Logger = Ember.Object.extend({ + log(m) { + console.log(m); + } + }); + + application.register('logger:main', Logger); +} + +export default { + name: 'logger', + initialize: initialize +}; +``` + +### Registering Already Instantiated Objects + +By default, Ember will attempt to instantiate a registered factory when it is +looked up. When registering an already instantiated object instead of a class, +use the `instantiate: false` option to avoid attempts to re-instantiate it +during lookups. + +In the following example, the `logger` is a plain JavaScript object that should +be returned "as is" when it's looked up: + +```app/initializers/logger.js +export function initialize(application) { + var logger = { + log(m) { + console.log(m); + } + }; + + application.register('logger:main', logger, { instantiate: false }); +} + +export default { + name: 'logger', + initialize: initialize +}; +``` + +### Registering Singletons vs. Non-Singletons + +By default, registrations are treated as "singletons". This simply means that +an instance will be created when it is first looked up, and this same instance +will be cached and returned from subsequent lookups. + +When you want fresh objects to be created for every lookup, register your +factories as non-singletons using the `singleton: false` option. + +In the following example, the `Message` class is registered as a non-singleton: + +```app/initializers/logger.js +export function initialize(application) { + var Message = Ember.Object.extend({ + text: '' + }); + + application.register('notification:message', Message, { singleton: false }); +} + +export default { + name: 'logger', + initialize: initialize +}; +``` + +## Factory Injections + +Once a factory is registered, it can be "injected" where it is needed. + +Factories can be injected into whole "types" of factories with *type +injections*. For example: + +```app/initializers/logger.js +export function initialize(application) { + var Logger = Ember.Object.extend({ + log(m) { + console.log(m); + } + }); + + application.register('logger:main', Logger); + application.inject('route', 'logger', 'logger:main'); +} + +export default { + name: 'logger', + initialize: initialize +}; +``` + +As a result of this type injection, all factories of the type `route` will be +instantiated with the property `logger` injected. The value of `logger` will +come from the factory named `logger:main`. + +Routes in this example application can now access the injected logger: + +```app/routes/index.js +export default Ember.Route.extend({ + activate() { + // The logger property is injected into all routes + this.get('logger').log('Entered the index route!'); + } +}); +``` + +Injections can also be made on a specific factory by using its full key: + +```js +application.inject('route:index', 'logger', 'logger:main'); +``` + +In this case, the logger will only be injected on the index route. + +Injections can be made onto any class that requires instantiation. This includes +all of Ember's major framework classes, such as components, helpers, routes, and +the router. + +### Ad Hoc Injections + +Dependency injections can also be declared directly on Ember classes using +`Ember.inject`. Currently, `Ember.inject` supports injecting controllers (via +`Ember.inject.controller`) and services (via `Ember.inject.service`). + +The following code injects the `shopping-cart` service on the `cart-contents` +component as the property `cart`: + +```app/components/cart-contents.js +export default Ember.Component.extend({ + cart: Ember.inject.service('shopping-cart') +}); +``` + +If you'd like to inject a service with the same name as the property, simply +leave off the service name (the dasherized version of the name will be used): + +```app/components/cart-contents.js +export default Ember.Component.extend({ + shoppingCart: Ember.inject.service() +}); +``` + +## Factory Lookups + +The vast majority of Ember registrations and lookups are performed implicitly. + +In the rare cases in which you want to perform an explicit lookup of a +registered factory, you can do so on an application instance in its associated +instance initializer. For example: + +```app/instance-initializers/logger.js +export function initialize(applicationInstance) { + var logger = applicationInstance.lookup('logger:main'); + + logger.log('Hello from the instance initializer!'); +} + +export default { + name: 'logger', + initialize: initialize +}; +``` diff --git a/source/applications/initializers.md b/source/applications/initializers.md new file mode 100644 index 000000000..eccfc8243 --- /dev/null +++ b/source/applications/initializers.md @@ -0,0 +1,100 @@ +Initializers provide an opportunity to configure your application as it boots. + +There are two types of initializers: application initializers and application +instance initializers. + +Application initializers are run as your application boots, and provide the +primary means to configure [dependency injections](./dependency-injection) in +your application. + +Application instance initializers are run as an application instance is loaded. +They provide a way to configure the initial state of your application, as well +as to set up dependency injections that are local to the application instance +(e.g. A/B testing confurations). + +Initializers can perform any configuration operations, as long as they are +synchronous. The user experience is degraded by attempting to block the booting +of your application with asynchronous operations. + +> _Note: Any asynchronous loading conditions (e.g. user authorization) should be +placed in your application route's hooks, which allows for DOM interaction while +waiting for conditions to resolve._ + +## Application Initializers + +Application initializers can be created with Ember CLI's `initializer` +generator: + +```bash +ember generate initializer shopping-cart +``` + +Let's customize the `shopping-cart` initializer to inject a `cart` property into +all the routes in your application: + +```app/initializers/shopping-cart.js +export function initialize(application) { + application.inject('route', 'cart', 'service:shopping-cart'); +}; + +export default { + name: 'shopping-cart', + initialize: initialize +}; +``` + +## Application Instance Initializers + +Application instance initializers can be created with Ember CLI's +`instance-initializer` generator: + +```bash +ember generate instance-initializer logger +``` + +Let's add some simple logging to indicate that the instance has booted: + +```app/instance-initializers/logger.js +export function initialize(applicationInstance) { + var logger = applicationInstance.lookup('logger:main'); + logger.log('Hello from the instance initializer!'); +} + +export default { + name: 'logger', + initialize: initialize +}; +``` + +## Specifying Initializer Order + +If you'd like to control the order in which initializers run, you can use the +`before` and/or `after` options: + +```app/initializers/config-reader.js +export function initialize(application) { + // ... your code ... +}; + +export default { + name: 'configReader', + before: 'websocketInit', + initialize: initialize +}; +``` + +```app/initializers/websocket-init.js +export function initialize(application) { + // ... your code ... +}; + +export default { + name: 'websocketInit', + after: 'configReader', + initialize: initialize +}; +``` + +Note that ordering only applies to initializers of the same type (i.e. +application or application instance). Application initializers will always run +before application instance initializers. diff --git a/source/services-and-initializers/services.md b/source/applications/services.md similarity index 71% rename from source/services-and-initializers/services.md rename to source/applications/services.md index fcad9f36f..3670eeb0c 100644 --- a/source/services-and-initializers/services.md +++ b/source/applications/services.md @@ -13,18 +13,31 @@ Example uses of services include: ### Defining Services -To define a service, extend the `Ember.Service` base class: +Services can be generated using Ember CLI's `service` generator. For example, +the following command will create the `ShoppingCart` service: + +```bash +ember generate service shopping-cart +``` + +Services must extend the `Ember.Service` base class: ```app/services/shopping-cart.js export default Ember.Service.extend({ }); ``` -Like any Ember object, a service can have properties and methods of its own. +Like any Ember object, a service is initialized and can have properties and +methods of its own. ```app/services/shopping-cart.js export default Ember.Service.extend({ - items: [], + items: null, + + init() { + this._super(...arguments); + this.set('items', []); + }, add(item) { this.get('items').pushObject(item); @@ -42,7 +55,7 @@ export default Ember.Service.extend({ ### Accessing Services -To access a service, inject it with `Ember.inject`: +To access a service, inject it either in an initializer or with `Ember.inject`: ```app/components/cart-contents.js export default Ember.Component.extend({ @@ -50,7 +63,8 @@ export default Ember.Component.extend({ }); ``` -This injects the shopping cart service into the component and makes it available as the `cart` property. +This injects the shopping cart service into the component and makes it available +as the `cart` property. You can then access properties and methods on the service: @@ -77,9 +91,11 @@ export default Ember.Component.extend({ ``` -The injected property is lazy; the service will not be instantiated until the property is explicitly called. It will then persist until the application exits. +The injected property is lazy; the service will not be instantiated until the +property is explicitly called. It will then persist until the application exits. -If no argument is provided to `service()`, Ember will use the dasherized version of the property name: +If no argument is provided to `service()`, Ember will use the dasherized version +of the property name: ```app/components/cart-contents.js export default Ember.Component.extend({ diff --git a/source/routing/redirection.md b/source/routing/redirection.md index 044c8bada..4e6bb5927 100644 --- a/source/routing/redirection.md +++ b/source/routing/redirection.md @@ -26,7 +26,7 @@ export default Ember.Route.extend({ ``` If you need to examine some application state to figure out where to redirect, -you might use a [service](../../services-and-initializers/services). +you might use a [service](../../applications/services). ## Transitioning After the Model is Known diff --git a/source/services-and-initializers/container.md b/source/services-and-initializers/container.md deleted file mode 100644 index 61ff6a3b9..000000000 --- a/source/services-and-initializers/container.md +++ /dev/null @@ -1,95 +0,0 @@ -When an Ember application starts running, it will create and use a single -instance of the `Ember.Container` object. This container object is responsible -for managing factories and the dependencies between them. At the level of the -container, a factory can be any part of the framework, like _route_ or -_template_. For example, the index template is a factory with the name -`template:index`, and the application route is a factory with the name -`route:application`. The container understands how to use these factories -(Are they singleton? Should they be instantiated?) and manages their -dependencies. - -Factory names have two parts segmented by a `:`. The first segment is the -framework factory type, and the second is the name of the factory requested. -Hence, the `index` template would be named `template:index`. Ember has several -built-in factory types, and you can also create your own by simply naming your -factory appropriately. For example, to create a `user` type, you'd simply -register your factory with `application.register('user:user-to-register')`. - -If the container does not already have a requested factory, it uses a -resolver to discover that factory. For example, the resolver is responsible for -mapping the name of `component:show-posts` to the JavaScript module -located in the filesystem at `app/components/show-posts.js`. After -optionally adding dependencies to the requested factory, that factory is -cached and returned. - -Ember's container should be viewed as an implementation detail, and is not -part of the supported public API. Instead, you should use initializers to -register factories on the container: - -```app/initializers/logger.js -export function initialize(container, application) { - var Logger = Ember.Object.extend({ - log(m) { - console.log(m); - } - }); - - application.register('logger:main', Logger); -} -``` - -The `register` function adds the factory (`logger`) into the container. It adds -it with the full name of `logger:main`. - -By default, Ember will instantiate the object being injected. If you'd prefer -not to have it instantiated (such as when working with a plain JavaScript -object), you can tell Ember not to instantiate it: - -```app/initializers/logger.js -export function initialize(container, application) { - var logger = { - log(m) { - console.log(m); - } - }; - - application.register('logger:main', logger, { instantiate: false }); -} -``` - -Once a factory is registered, it can be injected: - -```app/initializers/logger.js -export function initialize(container, application) { - var Logger = Ember.Object.extend({ - log(m) { - console.log(m); - } - }); - - application.register('logger:main', logger, { instantiate: false }); - application.inject('route', 'logger', 'logger:main'); -} -``` - -This is an example of a *type injection*. Onto all factories of the type -`route`, the property, `logger` will be injected with the factory named -`logger:main`. Routes in this example application can now access the logger: - -```app/routes/index.js -export default Ember.Route.extend({ - activate() { - // The logger property is injected into all routes - this.logger.log('Entered the index route!'); - } -}); -``` - -Injections can also be made on a specific factory by using its full name: - -```js -application.inject('route:index', 'logger', 'logger:main'); -``` - -Injections can be made onto all of Ember's major framework classes including -components, routes, and the router. diff --git a/source/services-and-initializers/initializers.md b/source/services-and-initializers/initializers.md deleted file mode 100644 index 84ba84d28..000000000 --- a/source/services-and-initializers/initializers.md +++ /dev/null @@ -1,80 +0,0 @@ -Sometimes you'll want to have certain things happen as your application boots, -like starting services that should be available before the rest of your -application loads. For example, Ember Data uses this to give routes access to -the shared store. - -This can be accomplished with an initializer. For example, say you have a -service that sets up a shopping cart, and you want to inject that cart into all -the routes in your application. You could do something like this: - -```app/initializers/shopping-cart.js -export default { - name: 'shopping-cart', - initialize: function (container, application) { - application.inject('route', 'cart', 'service:shopping-cart'); - } -}; -``` - -The `shopping-cart` service will be injected as the `cart` property into every -`route` in your application, which they can access via -`this.get('shopping-cart')`. - -You can also inject a service into a specific class: - -```app/initializers/shopping-cart.js -application.inject('route:checkout', 'cart', 'service:shopping-cart'); -``` - -## Specifying Initializer Order - -If the order in which initializers load is important, you can use the `before` -and/or `after` options: - -```app/initializers/config-reader.js -export default { - name: "configReader", - before: "websocketInit", - - initialize: function(container, application) { - ... your code ... - } -}; -``` - - -```app/initializers/websocket-init.js -export default { - name: "websocketInit", - after: "configReader", - - initialize: function(container, application) { - ... your code ... - } -}; -``` - -## Pausing the Boot Process - -If you need to make sure that the rest of your application doesn't load until -an initializer has finished loading, you can use the `deferReadiness` and -`advanceReadiness` methods, which will wait until all of the promises -are resolved until continuing. - -For example, if you wanted to keep your application from booting until the -current user was set, you could do something like this: - -```app/initializers/current-user.js -export default { - name: "currentUserLoader", - after: "store", - - initialize: function(container, application) { - application.deferReadiness(); - - container.lookup('store:main').find('user', 'current').then(function(user) { - application.inject('route', 'currentUser', 'user:current'); - application.advanceReadiness(); - }); - } -};