-
Notifications
You must be signed in to change notification settings - Fork 0
Tech Note: ServiceScope API
We received some questions about the ServiceScope API, which is SPFx's implementation of the service locator pattern.
In this document, a "service" is a TypeScript object that will be discovered indirectly by the objects that want to consume it (a.k.a. "inversion of control"). Typically the service implements an interface contract that could have alternative implementations (e.g. mock implementations for unit tests), however this is not required. The main purpose of ServiceScope is to allow the service to be constructed/configured in one place, and then passed down to a consumer in other place, without affecting the intermediary objects that it must pass through.
If your service can have multiple implementations, you would typically start by defining a interface contract like this:
/**
* This interface allows unit tests to simulate the system clock.
*/
export interface ITimeProvider {
/**
* Returns the current date/time.
*/
getDate(): Date;
/**
* Returns a DOMHighResTimeStamp timing measurement, as defined by the
* standard performance.now() API.
*/
getTimestamp(): number;
}
Then you create the default implementation:
/**
* This is the default implementation of ITimeProvider that simply
* calls the real browser APIs.
*/
export default class TimeProvider implements ITimeProvider {
constructor(serviceScope: ServiceScope) {
// (this constructor is currently unused, but it is required by the
// ServiceKey.create() contract)
}
public getDate(): Date {
return new Date();
}
public getTimestamp(): number {
return performance.now();
}
}
Lastly, you define a service "key", which will be used to register the service with ServiceScope:
export const dateProviderServiceKey: ServiceKey<ITimeProvider>
= ServiceKey.create<ITimeProvider>('sp-client-base:TimeProvider', TimeProvider);
Note that the key provides a default implementation. Currently, the default implementation is required.
Consider these alternatives to using ServiceScope:
-
Explicit dependencies: If your DataFetcher class requires an HttpClient object and
-
Service Contexts:
ServiceScope provides a formalized way for components to register and consume dependencies ("services"), and to enable different implementations to be registered in different scopes. This improves modularity by decoupling components from their dependencies in an extensible way.
Wuppose that various components need access to an IPageManager instance. We could simply make the PageManager a singleton (i.e. global variable), but this will not work e.g. if we need to create a pop-up dialog that requires a second PageManager instance. A better solution would be to add the PageManager as a constructor parameter for each component that requires it, however then we immediately face the problem that any code that calls these constructors also needs a PageManager parameter. In an application with many such dependencies, business logic that ties together many subsystems would eventually pick up a constructor parameter for every possible dependency, which is unwieldy. A natural solution would be to move all the dependencies into a class with name like "ApplicationContext", and then pass this around as our constructor parameter. This enables the PageManager to be passed to classes that need it without cluttering the intermediary classes that don't. However, it still has a design problem that "ApplicationContext" has hard-coded dependencies on many unrelated things. A more flexible approach is to make it a dictionary that can look up items for consumers/providers who know the right lookup key (i.e. ServiceKey). This is the popular "service locator" design pattern, familiar from the SPContext API in classic SharePoint.
ServiceScope takes this idea a step further in two important ways: First, it provides a scoping mechanism so that e.g. if we had two different pages, they could each consume a unique PageManager instance while still sharing other common dependencies. Secondly, it allows for a ServiceKey to provide a default implementation of the dependency. This is important for API stability in our modular client-side environment: For example, suppose that version 2.0 of our application introduced a new IDiagnosticTracing interface that a version 2.0 component will expect to consume. If the version 2.0 component gets loaded by an older 1.0 application, it would fail. We could fix this by requiring each consumer to check for any missing dependencies and handle that case, but it would require a lot of checks. A better solution is to ensure that a default implementation always exists, perhaps just a trivial behavior, so that components don't have to worry about it.
Usage: ServiceScope instances are created by calling either ServiceScope.startNewRoot() or ServiceScope.startNewChild(). They are initially in an "unfinished" state, during which provide() can be called to register service keys, but consume() is forbidden. After ServiceScope.finish() is called, consume() is allowed and provide() is now forbidden. These semantics ensure that ServiceScope.consume() always returns the same result for the same key, and does not depend on order of initialization. It also allows us to support circular dependencies without worrying about infinite loops, even when working with external components that were implemented by third parties. To avoid mistakes, it's best to always call consume() inside a callback from serviceScope.whenFinished().
- SharePoint Framework
- Release Notes
- Getting Started
- Getting familiar with the tools and libraries
- Setup your developer tenant
- Setup your machine
- Tutorial 1: HelloWorld WebPart
- Tutorial 2: HelloWorld, Talking to SharePoint
- Tutorial 3: HelloWorld, Serving in SharePoint Pages
- Tutorial 4: HelloWorld, Deploy to CDN
- Tutorial 5: jQueryUI Accordion WebPart
- Tutorial 6: Using Office UI Fabric Components
- Basics
- Tech Notes