AngularJS 的角度 - 第二部分






4.93/5 (7投票s)
第二部分 – 扩展 AngularJS 使其更简单、
第 2 部分 – 扩展 AngularJS 以使其更简单、更动态
在上一篇文章中,我们讨论了每个开发人员在使用 Angular 实现时都必须面对的一些样板代码和问题,我们还通过将这些通用工作抽象到基础服务和控制器中来克服这些限制。这种方法的主要好处是,我们可以将应用程序的核心功能实现在基础层,以便开发人员可以随时随地轻松使用。
到目前为止,我们实现的关键优势如下:
- 开发人员无需显式注入任何依赖项
- 可以使用
ServiceLocator
模式实现将通用服务提供给任何需要的地方。这将减少在整个应用程序中定义依赖项所需的时间和精力。 - 由于核心逻辑位于基类中,因此开发人员将被强制遵循应用程序标准,而不是以自己的方式实现。
到本文结束时,我们将实现:
- 按需从服务器加载服务(延迟加载)
- 简化组件开发,类似于我们在上一部分中为控制器和服务所做的工作
如果我试图解释每一个设计元素的必要性,这将会是一次漫长的谈话,因此我将把讨论保持在要点上。否则,您可以直接下载源代码并开始探索它,以更深入地了解它,甚至可以进一步扩展它,使其能够满足您的设计需求。
按需从服务器加载服务(延迟加载)
通常,小型应用程序不太需要担心此功能,您可以确保所有服务都在页面加载时加载。如果您的应用程序很大,并且注入了许多服务,那么拥有此实现是完全有意义的。
在上一篇文章中,我们一起实现了 ServiceLocator
,它充当了应用程序所有需求的 >>= 11>>。此方法将帮助我们轻松地从服务器延迟加载任何服务。
以下是应用延迟加载 Service1
文件(来自服务器)在我们的先前工作中实现服务的示例。
class Service1 extends BaseService {
GetItems(): angular.IPromise<string[]> {
return this.Http().get("/api/products");
}
}
class ServiceLocator extends BaseService {
Service1(): Service1 {
return this.Injector().get<service1>("Service1");
}
}
class HomeController extends BaseController<ihomescope>{
Init(): void {
this.Scope.Items = ["I", "was", "loaded", "synchronously"];
this.Services().Service1().GetItems()
.then((result) => {
this.Scope.Items = result.data;
});
}
}
有很多方法可以从服务器加载文件,这里我将使用 ocLazyLoad
库。让我们向 BaseService
添加 Load
方法,该方法将负责从服务器加载服务。此 Load
方法将接收服务名称(也指示服务文件名),并指示 ocLazyLoad
库从位置 /apps/services 加载它。您可以根据您的应用程序目录结构使用路径。
protected Load(service: string): ng.IPromise<any> {
var lazy = this.Injector().get("$ocLazyLoad") as oc.ILazyLoad;
return lazy.load("/apps/services/" + service + ".js")
.catch((reason) => {
throw service + " " + reason.description;
});
}
如果您注意到从服务器加载文件是一个异步过程,这意味着我们不能再执行 Service1().GetItems()
调用了,因为在访问 Service1()
方法时,我们首先必须从服务器加载文件,然后开发人员才能在他们的实现中访问服务。
解决方法是,我们现在必须获取调用者的操作,并在 Promise 完成后执行它。让我们添加一个辅助方法 Invoke
,它将调用我们的 Load
方法,并处理 Promise 以执行操作。
protected Invoke<t, u="">(service: string, action: (service: T) => ng.IPromise<u> | U):
ng.IPromise<u> | U {
return this.Load(service)
.then<u>((): ng.IPromise<u> | U => {
return action(this.Injector().get<t>(service));
});
}
将上面显示的 BaseService
、ServiceLocator
和 HomeController
代码整合在一起,最终看起来是这样的:
abstract class BaseService {
static $inject = ["$http", "$rootScope", "$injector"];
private _http: angular.IHttpService;
private _rootScope: angular.IRootScopeService;
private _injector: angular.auto.IInjectorService;
constructor(http: angular.IHttpService, rootScope: angular.IRootScopeService,
injector: angular.auto.IInjectorService) {
this._http = http;
this._rootScope = rootScope;
this._injector = injector;
this.Init.bind(this);
this.Init();
}
Init(): void { }
Http(): angular.IHttpService {
return this._http;
}
RootScope(): angular.IRootScopeService {
return this._rootScope;
}
protected Injector(): angular.auto.IInjectorService {
return this._injector;
}
protected Load(service: string): ng.IPromise<any> {
var lazy = this.Injector().get("$ocLazyLoad") as oc.ILazyLoad;
return lazy.load("/apps/services/" + service + ".js")
.catch((reason) => {
throw service + " " + reason.description;
});
}
protected Invoke<t, u="">(service: string,
action: (service: T) => ng.IPromise<u> | U): ng.IPromise<u> | U {
return this.Load(service)
.then<u>((): ng.IPromise<u> | U => {
return action(this.Injector().get<t>(service));
});
}
}
class HomeController extends BaseController<ihomescope>{
Init(): void {
this.Scope.Items = ["I", "was", "loaded", "synchronously"];
this.Services().Service1((service) => {
service.GetItems()
.then((result) => {
this.Scope.Items = result;
})
});
}
}
现在您可以看到,在不进行大量代码更改的情况下,我们实现了按需加载服务文件。
TypeScript 装饰器
在我们转向组件的下一个主题之前,我想添加一些小东西使其更有趣。由于组件的实现方式与我们对服务和控制器所做的有所不同,组件是两者的混合体,并且具有更多功能。
这就是 TypeScript 装饰器功能将帮助我们简化组件定义并最大程度地减少相关编码工作的地方。我之所以要谈论 TypeScript Decorator
,是因为它为 JavaScript 生成和运行时对象创建增加了更多价值。我建议您在继续阅读之前先了解装饰器的工作原理。
既然您知道它的作用,人们就可以轻松地想到它在 AngularJS 中的用法。我们之前并没有真正讨论过如何在 Angular 中注册任何对象以便与框架协同工作。
class HomeController extends BaseController<ihomescope>{
. . .
}
angular.module("portal").controller("HomeController", HomeController);
让我们也创建一个装饰器来处理这个问题,这样开发人员就不必担心在 Angular 中注册它了。
function Service(name: string) {
return (target: Function) => {
angular.module("portal").service(name, target);
};
}
function Controller(name: string) {
return (target: Function) => {
angular.module("portal").controller(name, target);
};
}
现在,这个装饰器可以用于我们想要与我们的 Angular 模块注册的任何服务和控制器。
@Service("Service1")
class Service1 extends BaseService {
GetItems(): angular.IPromise<string[]> {
return this.Http().get("/api/products");
}
}
@Service("ServiceLocator")
class ServiceLocator extends BaseService {
Service1<t>(action: (service: Service1) => ng.IPromise<t> | T): ng.IPromise<t> | T {
return this.Invoke("Service1", action);
}
}
@Controller("HomeController")
class HomeController extends BaseController<ihomescope>{
. . .
}
就这样;令人惊讶的是,装饰器将负责在运行时注册您的函数。基本上,我们已经很大程度上将 Angular 依赖项与开发人员隔离开来了。
简化组件 / 指令
让我们来谈谈 AngularJS 中的组件(和指令),看看我们可以对其哪些方面进行改进,就像我们为控制器和服务所做的那样。下面是一个关于如何正常定义组件的简单片段。
interface IMyComponentScope extends IScope {
Items: string[];
}
angular.module('portal').directive("MyComponent", function () {
controller.$inject = ["$scope", "Service1"];
var controller = function ($scope: IMyComponentScope, Service1: Service1) {
$scope.Items = ["I", "was", "loaded", "synchronously"];
Service1.GetItems()
.then((result) => {
$scope.Items = ["I", "was", "loaded", "asynchronously"];
});
}
return {
templateUrl: "/apps/components/MyComponent/MyComponent.html",
controller: controller,
};
});
如您所见,组件包括控制器和其他指令选项,这些选项决定了组件的功能。组件不一定需要控制器,但作为标准,我们将在设计框架中将其设为默认值。
那么,让我们首先创建定义我们的组件的类,该组件也将充当控制器。
abstract class BaseComponent<t extends="" iscope=""> {
static $inject = ["$scope", "ServiceLocator"];
private serviceLocator: ServiceLocator;
public Scope: T;
constructor(scope: T, services: ServiceLocator) {
this.Scope = scope;
this.serviceLocator = services;
this.Init.bind(this);
this.Init();
}
abstract Init(): void;
Services(): ServiceLocator {
return this.serviceLocator;
}
Parent(): IScope {
return this.Scope["$parent"] as IScope;
}
}
这很好,我们有一个类可以充当控制器,但如果您注意到,组件的设置方式并非如此。Angular 期望一个函数来返回组件的定义。我们不能使用上述方法将其注册为组件,就像我们对服务和控制器所做的那样。
如果您真的比较指令和组件,它们的设置方式非常相似,唯一的区别是组件是指令的轻量级且简单的形式。但我会考虑仅使用指令方法来实现我们的设计,以支持早期版本的 Angular,而且无论如何,我们的最终结果将比原始的组件设置建议要简单得多。
以下是您可以为指令定义的定义:
interface IDirective {
compile?: IDirectiveCompileFn;
controller?: any;
controllerAs?: string;
link?: IDirectiveLinkFn | IDirectivePrePost;
multiElement?: boolean;
name?: string;
priority?: number;
require?: string | string[] | {[controller: string]: string};
restrict?: string;
scope?: boolean | Object;
template?: string | Function;
templateNamespace?: string;
templateUrl?: string | Function;
terminal?: boolean;
transclude?: boolean | string | {[slot: string]: string};
}
让我们创建接口并在此处包含这些选项:
interface ICompoenent {
compile?: ng.IDirectiveCompileFn;
multiElement?: boolean;
priority?: number;
require?: string | string[] | { [controller: string]: string };
scope?: boolean | Object;
terminal?: boolean;
transclude?: boolean | string | { [slot: string]: string };
template?: string | Function;
}
我没有包含某些属性,因为那些属性将是自动化的,而有些则永远不需要。您可以根据需要添加或扩展它。
让我们定义装饰器,以便按照 Angular 所需的定义来注册组件。
function Component(name: string, options: ICompoenent) {
return (target: Function) => {
var directiveOptions = options as ng.IDirective;
directiveOptions.controller = target;
directiveOptions.scope = (directiveOptions.scope || {});
directiveOptions.templateUrl =
(directiveOptions.templateUrl || "/apps/components/" + name + "/" + name + ".html");
if (target.prototype.Link)
directiveOptions.link = target.prototype.Link;
angular.module('portal').directive(name, function () { return directiveOptions; });
};
}
在这里,我为所有组件的模板 URL 使用了预设路径。我不喜欢在代码中定义 HTML 模板,而是倾向于在原地放置一个专用的模板供 Angular 查找。不过,您可以根据需要更改此行为。
让我们将所有这些放在一起,看看我们最终的组件实现是什么样的:
var option: ICompoenent = {
transclude: true
};
@Component("MyComponent", option)
class MyComponent extends BaseComponent<IMyComponentScope>{
Init(): void {
this.Scope.Items = ["I", "was", "loaded", "synchronously"];
this.Services().Service1((service) => {
return service.GetItems()
.then((result) => {
this.Scope.Items = ["I", "was", "loaded", "asynchronously"];
});
});
}
}
就这样。现在我们在组件中也获得了与控制器和服务相同的优势。
到目前为止,我们实现了什么?
- 装饰器,负责注册您的控制器、服务和组件
- 构造函数中无需提供依赖项
- 所有依赖项始终可通过基础实现供开发人员在整个应用程序中使用
- 开发人员无需担心从服务器加载服务,如果我们尚未加载,我们的框架将负责加载它。
项目结构
现在我们的基本框架设计已经准备好,我们需要正确地构建我们的项目。
我通常遵循两种常见的结构:
- 默认的 MVC.NET 样式
app \controller \module1 \module1controller.ts \views \module1 \module1view.html \services \module1 \module1service.ts \components
- 模块化结构(Uncle Bob 的咆哮式架构)
app \module \controller \modulecontroller.ts \views \moduleview.html \moduleservice.ts \components
在本文提供的最终源代码中,我使用了第二种,即模块化结构。
文件名约定
最后,由于我们的基类和装饰器在猜测要自动加载哪个视图或服务,因此我们需要在应用程序中有明确的文件命名标准。
控制器文件名
我通常喜欢用操作来命名控制器,例如:modulename-action.controller.ts
如您所见,模块名和操作由连字符分隔,通过查看该文件是做什么用的,可以轻松理解。
视图文件名
视图名称遵循类似的约定:modulename-action.html
服务文件名
我通常每个模块只有一个服务文件,但您可以根据需要扩展此行为。 modulename.service.ts
将所有这些放在一个示例中,我们的项目结构是这样的:
app
\employee
\controller
\emlpoyee-view.controller.ts
\views
\emlpoyee-view.html
\employee.service.ts
\components
让我们看看我们最终的工作示例是什么样的:
namespace Portal.Employee {
export interface IEmployeeViewScope extends Core.IScope {
GridOptions: uiGrid.IGridOptionsOf<employee.employeeviewmodel>;
}
@Core.Controllers.Controller("Portal.Employee.EmployeeViewController")
export class EmployeeViewController extends Core.Controllers.BaseController<iemployeeviewscope> {
Init(): void {
this.Scope.GridOptions = {};
this.Scope.GridOptions.data = [];
this.Scope.GridOptions.columnDefs = [
{ field: 'name' },
{ field: 'gender' },
{ field: 'company', enableSorting: false }
];
this.Scope.GridOptions.onRegisterApi = (gridApi) => {
this.LoadData();
};
}
LoadData(): void {
this.Services()
.EmployeeService((service) => {
return service.GetAll()
.then((result) => {
this.Scope.GridOptions.data = result;
});
});
}
}
}
namespace Portal.Employee {
export class EmployeeViewModel {
name: string;
gender: string;
company: string;
}
@Core.Services.Service("Portal.Employee.EmployeeService")
export class EmployeeService extends Core.Services.BaseService {
GetAll(): Core.IPromise<array<employeeviewmodel>> {
return this.Http()
.get("https://cdn.rawgit.com/angular-ui/ui-grid.info/gh-pages/data/100.json")
.then((result) => {
return result.data;
});
}
}
}
我将在下一篇文章中介绍的一些主题已包含在代码中。继续探索其中包含的功能,例如:
- 更好的路由实现
- 从路由注册本身定义页面标题(未包含在代码中)
- 按需延迟加载控制器文件(未包含在代码中)
- Angular 2 支持。实现无需代码更改,这是此方法的另一个好处,开发人员无需担心是 Angular 1.5 还是 2 在幕后工作。
如果您觉得这篇文章有用,请分享和评论。如果您有其他关于此方法想要探讨的问题或任何建议,请发表评论。