Angular.js 示例应用程序






4.96/5 (121投票s)
一个使用 Angular/Rx for JavaScript/Web Sockets/jQuery 的示例应用程序
目录
引言
这是我很久以来写的第一篇文章,原因有很多,但我不会用这些来烦大家。总之,我休息了一段时间后写了这篇文章。那么它有什么用,这篇文章是关于什么的呢?
我决定花点时间多了解一下我们 Google 朋友开发的一个流行 web MVC 框架,它叫做 Angular.js,这是一个 JavaScript MVC 框架,这对我来说有点脱离了我通常受 XAML 影响的世界。但是,尝试不同的东西,以便了解如何在不同的语言/环境中完成工作(我的老导师 Fredrik Bornander (AKA Swede) 告诉我的),所以,我决定尝试一下 Angular.js。
本文将讨论 Angular.js 背后的一些基本思想,然后重点介绍我为本文创建的演示应用程序的细节。
在我们进入实际文章之前,我将简要地(暂时不会太技术性,尽管我知道你们会想看到,而且它会来的,别担心)用简单的术语谈谈演示应用程序的作用,这样您下载后就知道如何操作它了。
演示应用程序概述
所附的演示应用程序分为两部分
出版社
这是一个标准的 WPF 项目,因此会生成一个可运行的 EXE 文件。我不会在这篇文章中花太多时间谈论发布器,因为它不是文章的重要部分,它只是一个演示 Angular.js 网站中内容的工具。总之,发布器允许用户点击图像,当用户点击图像时,会使用 Web Sockets(稍后会详细介绍)向 Angular.js 网站发送一条消息。简而言之,这就是发布器的全部功能。
网站
Angular.js 网站才是乐趣所在(至少在我看来)。Angular.js 网站主要执行以下任务:
- 在根页面上,Angular.js 将监听通过 WPF 发布器通过 Web Socket 发送的消息,然后使用 Reactive Extensions for JavaScript 内部广播给任何感兴趣的人,在本文中,这主要是指根页面。
根页面将为收到的每条允许的消息显示一个图像磁贴。感谢 jQuery UI 的支持,图像磁贴可以调整大小和拖动。用户可以选择将图像磁贴保存到他们的收藏夹,这将导致有关图像磁贴的所有信息保存到 HTML 5 本地存储中。此信息包括大小、位置等,因此当用户返回根页面时,他们保存的收藏夹应该与之前完全一样。用户还可以决定从根页面中删除收藏夹中的图像磁贴。
- 用户还可以选择导航到收藏夹页面,该页面将显示其 HTML 5 本地存储持久化收藏夹的一些缩略图。可以点击这些缩略图以显示一个相当标准的
ColorBox
(Lightbox
等类型)jQuery 插件。
- 用户还可以选择查看静态“关于”页面,我只是为了有足够的路由来在演示 Angular.js 中的路由时使其更有价值而添加了此页面。
所以,用通俗易懂的话来说,就是这样了,这张图片可能有助于巩固我刚才所说的内容,所谓一图胜千言。
这是本文演示代码的两个部分正确运行时应有的样子
重要提示
您应该确保遵循以下步骤才能成功运行演示代码。
- 如果您发现Publisher.Wpf项目的一些引用找不到,我已将它们包含在“Lib”文件夹中,您可以从那里重新引用它们。
- 您应该确保 Publisher.Wpf 项目首先运行,并且它显示所有图像。这可以通过将 Publisher.Wpf 项目构建为 EXE,然后简单地在文件系统中找到 EXE 并双击它来运行(或者使用 Visual Studio 在 DEBUG 模式下运行实例)。
- 然后您应该运行 Angular 网站,确保在 Visual Studio 中将 Index.html 设置为起始页,然后使用 Visual Studio 运行 Angular 网站。
Angular.Js 简介
在本节中,我将讨论使用 Angular.js 的一些基础知识。本节将结合我自己的话以及直接从 Angular.js 网站摘录的文本。我不会涵盖 Angular.js 的所有内容,因为那更像是一本书,我没有那么多时间。但是,我将涵盖一些基本的 Angular.js 构建块,因此如果您阅读本文并认为“嗯……这个 Angular 的东西引起了我的兴趣,我可以在哪里了解更多?”,当然是通过点击超链接。
应用程序
每个 Angular.js 应用程序都迟早需要在 HTML 中使用 Angular.js ng-app
绑定,或者通过执行与声明性 HTML 绑定相同工作的代码。此代码本质上是启动 Angular.js,并让它知道应用程序正在哪个上下文中运行。例如,您可能拥有以下代码:
<div id="outer">
<div id="inner" ng-app="myApp">
<p>{{name}}</p>
</div>
<p>This is not using the angular app, as it is not within the Angular apps scope</p>
</div>
可以看出,我们可以告诉 HTML 的特定部分充当 Angular.js 应用程序。在这个例子中,这意味着 id="inner"
的 div
将具有 HTML 的一部分(尽管没有什么能阻止你将实际的 Body
标签作为 Angular 应用程序)被认为是 Angular.js 应用程序,因此将完全访问 Angular.js 应用程序功能(我们将在下面讨论)。
而 id="outer"
的 div
将不被视为 Angular.js 应用程序的一部分,因此将无法访问任何 Angular.js 应用程序功能(我们将在下面讨论)。
服务
Angular.js 中的服务与 WinForms/WPF 或 Silverlight 中的服务大致相同。它们是提供应用程序中可能使用的功能的辅助类。在像 C# 这样的强类型语言中,我们通常会让这些服务实现特定的接口,并通过构造函数或属性注入将它们注入到我们的应用程序代码中。然后我们就可以在测试中提供这些服务的模拟/替身,或者在底层系统发生变化时提供替代版本(例如从 Mongo DB 切换到 Raven DB 存储)。
虽然 Angular.js 不支持接口(尽管你可以用 TypeScript
来实现),但它确实支持将真实/模拟服务注入到其代码中。事实上,我会说 Angular.js 的主要优点之一就是它开箱即用地支持 IOC。
模块
大多数应用程序都有一个主方法,用于实例化、连接和启动应用程序。Angular 应用程序没有主方法。相反,模块声明性地指定了应用程序应该如何启动。这种方法有几个优点:
- 这个过程更具声明性,更容易理解。
- 在单元测试中,无需加载所有模块,这有助于编写单元测试。
- 可以在场景测试中加载额外的模块,这可以覆盖一些配置并帮助端到端测试应用程序。
- 第三方代码可以打包为可重用模块。
- 模块可以以任意/并行顺序加载(由于模块执行的延迟特性)。
http://docs.angularjs.org/guide/module
推荐的方法是实际拆分你的模块,这样你可能有一个这样的结构
- 一个服务模块
- 一个指令模块
- 一个过滤器模块
- 应用程序模块
这就是您可能定义 Angular.js 模块的方式,提示是使用了 Angular.js 提供的“module
”函数。
angular.module('xmpl.service', []).
value('greeter', {
salutation: 'Hello',
localize: function(localization) {
this.salutation = localization.salutation;
},
greet: function(name) {
return this.salutation + ' ' + name + '!';
}
}).
value('user', {
load: function(name) {
this.name = name;
}
});
依赖注入
Angular.js 的构建考虑了依赖注入(IOC),因此,许多基础设施可以替换为模拟版本,或者控制器可以使用模拟服务进行测试。本文不涉及如何做到这一点,也不涉及如何测试 Angular.js 应用程序。如果您想了解这些,请访问 Angular.js 文档,或者购买一本书。抱歉!
在前面的模块示例之后,这是一个可能接受一些依赖项的模块的样子,在本例中,一个我们刚刚在上面定义的模块,我们使用了“greeter
”和“user
”这两个值,它们都是“xmpl.service
”模块中可用的函数。这个模块可以提供“xmpl.service
”模块的模拟版本。
angular.module('xmpl', ['xmpl.service']).
run(function(greeter, user) {
// This is effectively part of the main method initialization code
greeter.localize({
salutation: 'Bonjour'
});
user.load('World');
})
路由
Angular.js 主要是一个单页面应用程序框架,因此它具有可以响应特定路由请求而应用的视图模板的概念。
Angular.js 中的路由与 ASP MVC 甚至 node.js 等中的路由并没有太大不同。
路由是通过使用预构建服务 $routeProvider 来实现的,该服务作为 Angular.js 的一部分免费提供。它允许用户使用一个非常简单的 API 来配置他们的路由,该 API 归结为以下两个函数:
when(path, route)
- 其中
route
对象具有以下属性控制器 (controller)
template
templateUrl
resolve
redirectTo
reloadOnSearch
- 其中
otherwise(params)
这是一个小例子
$routeProvider
.when('/products', {
templateUrl: 'views/products.html',
controller: 'ProductsCtrl'
})
.when('/about', {
templateUrl: 'views/about.html'
})
.otherwise({ redirectTo: '/products' });;
视图
关于视图没什么好说的。我们可能都遇到过 HTML。这就是视图包含的内容。唯一的区别是 Angular.js 视图将包含额外的(非标准 HTML)绑定,允许视图模板显示来自 Angular.js 作用域对象的数据。作用域对象通常来自控制器(尽管它不限于来自控制器,它可以通过继承或通过指令创建)。
这里有一个视图的小例子,请注意其中使用的绑定,例如 ng-model
和 ng-repeat
的使用,以及 Angular.js 预构建过滤器的一些使用,即 filter
和 orderBy
(请注意,本文不讨论过滤器)。
Search: <input ng-model="query">
Sort by:
<select ng-model="orderProp">
<option value="name">Alphabetical</option>
<option value="age">Newest</option>
</select>
<ul class="phones">
<li ng-repeat="phone in phones | filter:query | orderBy:orderProp">
{{phone.name}}
<p>{{phone.snippet}}</p>
</li>
</ul>
控制器
控制器用于定义视图的作用域。作用域可以看作是视图可能使用的变量和函数,例如通过使用 ng-click
绑定。这是与我们刚刚看到的视图模板配套的控制器代码。
function PhoneListCtrl($scope) {
$scope.phones = [
{"name": "Nexus S",
"snippet": "Fast just got faster with Nexus S.",
"age": 0},
{"name": "Motorola XOOM™ with Wi-Fi",
"snippet": "The Next, Next Generation tablet.",
"age": 1},
{"name": "MOTOROLA XOOM™",
"snippet": "The Next, Next Generation tablet.",
"age": 2}
];
$scope.orderProp = 'age';
可以看出,控制器定义了以下两个作用域属性
phones
:一个 JSON 数组orderProp
:一个单一的string
值
范围
Scope 是连接视图和控制器定义的 Scope 对象属性/函数的纽带。如果您曾经使用过基于 XAML 的技术,例如 WPF/Silverlight/WinRT,您可以将 Scope 视为 DataContext
。事实上,有相当多的 UI 框架具有类似 Scope 的概念。XAML 技术有 DataContext
,通常是 ViewModel
,而另一个流行的 MVVM JavaScript 库 Knockout.js 也有 Scope 的概念,以及分层 Scope,可以通过 HTML 中的绑定使用各种预构建关键字访问。
Angular.js 也支持嵌套/分层作用域,这有时会让人感到有些困惑。我个人发现,使用 Angular.js 及其作用域的最佳方法之一是安装 Batarang Chrome 插件,它提供了一种很好的方式,允许您使用作用域检查器(有点像 WPF 的 Snoop 或 Silverlight 的 SilverlightSpy)深入研究作用域。
这张图可能有助于巩固视图-控制器-作用域的概念。
指令
Angular.js 采用了一个相当新颖的概念,称为指令。指令是很巧妙的,它实际上允许你创建额外的属性,甚至新的 DOM 片段。这一切都通过对指令应用某些约束来控制,这样你可能希望声明某个指令只能用作属性,或者只能用作元素。你可以把指令看作是自定义控件。
指令也遵循常规的 Angular.js 规则,即它们支持依赖注入,并且它们也具有作用域感知能力。
我在撰写本文时遇到的最好的信息之一是 Bernardo Castilho 的这篇文章: https://codeproject.org.cn/Articles/607873/Extending-HTML-with-AngularJS-Directives,我强烈建议您阅读它,这是一篇非常出色的文章,读完后您会完全理解指令。
总之,Angular.js 的基础知识到此结束,现在我们来看看实际的演示应用程序代码演练。
发布者
正如我之前所说,发布器是一个 WPF 应用程序(因此它是一个可运行的 EXE),这不是本文的重点。关于发布器的主要观点是
- 它使用出色的 Fleck WebSocket 库与 Angular.js 网站进行通信。
- 它具有 Win8 风格的全景,因此您可以使用鼠标滚动。
这是 WPF 发布器运行时的样子
现在谈谈重要部分,Web Socket 代码。
public class WebSocketInvoker : IWebSocketInvoker
{
List<IWebSocketConnection> allSockets = new List<IWebSocketConnection>();
WebSocketServer server = new WebSocketServer("ws://:8181");
public WebSocketInvoker()
{
FleckLog.Level = LogLevel.Debug;
server.Start(socket =>
{
socket.OnOpen = () =>
{
Console.WriteLine("Open!");
allSockets.Add(socket);
};
socket.OnClose = () =>
{
Console.WriteLine("Close!");
allSockets.Remove(socket);
};
socket.OnMessage = Console.WriteLine;
});
}
public void SendNewMessage(string jsonMessage)
{
foreach (var socket in allSockets)
{
socket.Send(jsonMessage);
}
}
}
那部分就这些了,很酷吧(多亏了 Fleck WebSocket 库)。现在我意识到你们中的一些人可能会想,*为什么你没有使用 SignalR*,嗯,我本来可以用,但这会是一篇完全不同的文章,这篇文章我想纯粹专注于 Web 客户端方面,所以选择了使用原始 Web Socket,而 Fleck WebSocket 库完美符合要求。
在这段代码中,当用户点击一张图片时,会调用 SendNewMessage
,点击的图片名称将通过 WebSocket 发送到 Angular.js 网站。Angular.js 网站有所有可能图片的副本,因为我不想涉及复杂的文件 POST 操作,而且显然 Web 服务器不能显示本地文件(这在我看来(以及其他人看来)会是一个安全风险),所以我为了这个演示应用程序/文章的目的,选择了发布器和 Angular.js 网站都了解的共享文件。
Angular.js 网站
本节将讨论所附演示代码 Angular.js 网站的细节。希望,如果您已经读到这里,您在看到一些代码时会开始理解我上面提到的一些内容。
Require.js 用法
在我开始研究 Angular.js 之前,我一直在研究使用 Require.js,它是一个 JavaScript 模块加载框架,允许您指定依赖项和首选的模块加载顺序。我在另一篇文章中写过这个,您可以在这里阅读:使用 Require.Js 的模块化 Javascript
我在这篇文章中进一步完善了这一点(在 Angular.js O'Reilly 书附带的源代码的帮助下: https://github.com/shyamseshadri/angularjs-book)
所以,如果您想了解更多关于这个主题的信息,可以在那里阅读更多内容,但是让我们继续看看所附 Angular.js 网站的 Require.js 元素是什么样子的。
它从 Angular.js 主页面 (Index.html
) 中的这类代码开始
请参见 index.html
<html>
.....
<script data-main="scripts/main"
src="scripts/vendor/require.js"></script>
</html>
这是标准的 Require.js 代码,它告诉 Require.js 应该运行哪个主引导代码文件。可以看出这是 scripts/main
,所以我们现在来看看吧。
请参见 scripts\main.js
// the app/scripts/main.js file, which defines our RequireJS config
require.config({
paths: {
angular: 'vendor/angular.min',
jqueryUI: 'vendor/jquery-ui',
jqueryColorbox: 'vendor/jquery-colorbox',
jquery: 'vendor/jquery',
domReady: 'vendor/domReady',
reactive: 'vendor/rx'
},
shim: {
angular: {
deps: ['jquery', 'jqueryUI', 'jqueryColorbox'],
exports: 'angular'
},
jqueryUI: {
deps: ['jquery']
},
jqueryColorbox: {
deps: ['jquery']
}
}
});
require([
'angular',
'app',
'domReady',
'reactive',
'services/liveUpdatesService',
'services/imageService',
'services/localStorageService',
'controllers/rootController',
'controllers/favsController',
'directives/ngbkFocus',
'directives/draggable',
'directives/resizable',
'directives/tooltip',
'directives/colorbox'
// Any individual controller, service, directive or filter file
// that you add will need to be pulled in here.
],
function (angular, app, domReady) {
…….
…….
…….
…….
}
);
这里有很多事情发生。然而,它归结为三个部分
- 我们使用 JavaScript 文件路径配置 Require.js。
- 我们通过使用 Require.js 的 shim 来配置 Require.js 的首选加载顺序。shim 本质上是为库设置依赖关系。
- 然后我们使用 Require.js [Require] 告诉 Angular.js 应用程序我们希望满足哪些依赖关系。
我还使用 Require.js 来满足演示应用程序的控制器要求。一个示例如下:
define(['controllers/controllers',
'services/imageService',
'services/utilitiesService',
'services/localStorageService'],
function (controllers) {
controllers.controller('FavsCtrl',
['$window',
'$scope',
'ImageService',
'UtilitiesService',
'LocalStorageService',
function (
$window,
$scope,
ImageService,
UtilitiesService,
LocalStorageService) {
......
......
......
......
......
}]);
});
主应用程序设置
请参见 scripts\main.js
现在您已经看到了主要的引导代码(主要由 Require.js 配置组成),让我们快速看一下实际的 Angular.js 引导部分。
这正是我们在这篇文章开头讨论过的部分,你知道,正是这部分使得所附代码成为一个“Angular.js”应用程序。
该部分如下
function (angular, app, domReady) {
'use strict';
app.config(['$routeProvider',
function ($routeProvider) {
....
....
....
}]);
domReady(function () {
angular.bootstrap(document, ['MyApp']);
// The following is required if you want AngularJS Scenario tests to work
$('html').addClass('ng-app: MyApp');
});
}
这个引导过程做两件事
- 它设置了可用的有效路由,我们接下来将讨论这些路由。
- 它依赖于一个特殊的 Angular.js 插件,名为“
DomReady
”,其工作方式与 jQuery 及其ready()
事件类似。DOM 准备好后,“HTML
”元素将被赋予属性,使其充当 Angular.js 应用程序。
还有“MyApp
”模块从何而来的问题。谁在它在这里启动之前创建了它?
答案是它存在于自己的文件“app.js”中,如下所示
请参见 scripts\app.js
// The app/scripts/app.js file, which defines our AngularJS app
define(['angular', 'controllers/controllers',
'services/services', 'filters/filters', 'directives/directives'], function (angular) {
return angular.module('MyApp', ['controllers', 'services', 'filters', 'directives']);
});
路由
请参见 scripts\main.js
对于演示应用程序,有三个有效路由:Root/Favs/About。每个路由都使用标准的 Angular.js $routeProvider
服务进行配置,所有设置代码都在启动文件 main.js 中完成。
unction ($routeProvider) {
$routeProvider
.when('/', {
templateUrl: 'views/root.html',
controller: 'RootCtrl'
})
.when('/favs', {
templateUrl: 'views/favs.html',
controller: 'FavsCtrl'
})
.when('/about', {
templateUrl: 'views/about.html'
}).otherwise({ redirectTo: '/' });;
}
我认为从上面可以看出,有三个路由,并且它们是如何配置的,所以我就不多说了。
根页面
这是我编写的演示网站中最复杂的页面,因为它汇集了许多不同的东西。
那么,这个页面到底有什么作用?
这个想法是有一个名为“LiveUpdatesService
”的服务,它监听 WPF 发布器推送数据的 Web Socket 客户端端。LiveUpdatesService
使用 JavaScript 的响应式扩展来提供一个可订阅的数据流。
根页面将订阅此已发布流,每次看到新条目时,它都会添加一个新的 jQuery UI 可拖动/可调整大小的 UI 元素,前提是尚未显示具有相同图像名称的元素。
它还允许用户将图像保存到 HTML 5 本地存储,并从本地存储中删除它们。如果本地存储中已经存在项目,则其详细信息将用作根页面的初始启动状态。我不得不说这看起来很酷,因为所有信息都持久化了,因此它会记住大小、位置、ZIndex,所以它会完全按照您保存时的样子返回。
所以,总的来说,这就是根页面的作用。
这是根页面的样子
所以这就是它的样子,想看一些代码吗?
LiveUpdatesService
这个服务负责监听来自发布者 Web Socket 的传入数据,并通过 JavaScript 的响应式扩展 Subject
对象推送新收到的 Web Socket 数据。这是该服务的代码:
define(['services/services'],
function (services) {
services.factory('LiveUpdatesService', ['$window',
function (win) {
var subject = new Rx.Subject();
if ("WebSocket" in window) {
// create a new websocket and connect
var ws = new WebSocket('ws://:8181/publisher', 'my-protocol');
// when data is coming from the server, this metod is called
ws.onmessage = function (evt) {
subject.onNext(evt.data);
};
// when the connection is established, this method is called
ws.onopen = function () {
win.alert('Websocket connection opened');
};
//// when the connection is closed, this method is called
ws.onclose = function () {
subject.onError('Websocket connection closed,
perhaps you need to restart the Publisher, and refresh web site');
};
}
return {
publishEvent: function (value) {
subject.onNext(value);
},
eventsStream: function () {
return subject.asObservable();
}
};
}]);
});
这是使用响应式扩展 Subject
数据流的根控制器代码,我们首先检查是否之前见过同名项目,如果见过则简单地向用户显示一条消息(注意我们不直接使用 window,而是使用 $window
angular 服务(这可能更容易模拟))。
如果我们以前没有见过该图像名称,则会使用 ImageService
创建一个新项目,并将其随机放置。
LiveUpdatesService.eventsStream().subscribe(
function (data) {
if ($location.path() == '/') {
var idx = $scope.imageitems.propertyBasedIndexOf('name', data);
if (idx >= 0) {
$window.alert('An item with that name has already been added');
} else {
var randomLeft = UtilitiesService.getRandomInt(10, 600);
var randomTop = UtilitiesService.getRandomInt(10, 400);
var randomWidth = UtilitiesService.getRandomInt(100, 300);
var randomHeight = UtilitiesService.getRandomInt(100, 300);
$scope.imageitems.push(ImageService.createImageItem(
data, randomLeft, randomTop, randomWidth, randomHeight, false));
$scope.$apply();
}
}
},
function (error) {
$window.alert(error);
});
LocalStorageService
此服务负责从 HTML 5 本地存储中持久化/获取数据项。我认为这些代码非常自解释,所以就到此为止。
define(['services/services'],
function (services) {
services.factory('LocalStorageService', [
function () {
return {
isSupported: function () {
try {
return 'localStorage' in window && window['localStorage'] !== null;
} catch (e) {
return false;
}
},
save: function (key, value) {
localStorage[key] = JSON.stringify(value);
},
fetch: function (key) {
return localStorage[key];
},
parse: function(value) {
return JSON.parse(value);
},
clear: function (key) {
localStorage.removeItem(key);
}
};
}]);
});
ImageService
此服务仅协助为 Root 页面创建 ImageItem
对象,为 Favs 页面创建 FavItem
对象。
function ImageItem(name, left, top, width, height, isFavourite) {
var self = this;
self.name = name;
self.left = left;
self.top = top;
self.width = width;
self.height = height;
self.isFavourite = isFavourite;
self.styleProps = function () {
return {
left: self.left + 'px',
top: self.top + 'px',
width: self.width + 'px',
height: self.height + 'px',
position: 'absolute'
};
};
return self;
};
function FavImageItem(name) {
var self = this;
self.name = name;
return self;
};
define(['services/services'],
function (services) {
services.factory('ImageService', [
function () {
return {
createImageItem: function (name, left, top, width, height, isFavourite) {
return new ImageItem(name, left, top, width, height, isFavourite);
},
createFavImageItem: function (name) {
return new FavImageItem(name);
}
};
}]);
});
值得注意的一点是 Angular.js 中如何实现动态 CSS。要实现对象更改时更新的动态 CSS,您需要提供一个要调用的函数,这就是您在 styleProps()
函数中看到的。
self.styleProps = function () {
return {
left: self.left + 'px',
top: self.top + 'px',
width: self.width + 'px',
height: self.height + 'px',
position: 'absolute'
};
};
使用此标记如下,这意味着每当 JSON 对象进行更新时,CSS 也会更新,HTML 也会反映这一点。这并不容易发现,因此请务必仔细阅读此部分几次,巩固知识,牢牢记住。
ng-style="imageitems[$index].styleProps()"
UtilitiesService
此服务提供以下功能
- 向数组添加一个
propertyBasedIndexOf()
,允许在数组中搜索特定项属性,并返回索引。 getRandomInt()
:用于获取随机 x/y 点,以便在新图像项目首次显示时放置它们。delayedAlert()
:在一定延迟时间后显示警报。
这是代码
define(['services/services'],
function (services) {
services.factory('UtilitiesService', [
function () {
var initialised = false;
return {
addArrayHelperMethods: function () {
if (!initialised) {
initialised = true;
Array.prototype.propertyBasedIndexOf =
function arrayObjectIndexOf(property, value) {
for (var i = 0, len = this.length; i < len; i++) {
if (this[i][property] === value) return i;
}
return -1;
};
}
},
getRandomInt: function (min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
},
delayedAlert: function(message) {
setTimeout(function () {
$window.alert(message);
}, 1000);
}
};
}]);
});
Draggable 指令
为了实现拖动,我之前就知道必须使用 jQuery UI 库,但在使用 Angular.js 时,有一种明确的 Angular.js 方式,而用更改 DOM 的 jQuery 代码污染控制器代码绝对不是 Angular.js 方式。那么,这给我们留下了什么选择呢?嗯,这正是 Angular.js 指令的用武之地,它们旨在替换和增强 DOM,这是指令最擅长的。
因此,任何时候您需要直接修改 DOM(而不是通过作用域更改),您都应该考虑使用 Angular.js 指令。
综上所述,创建一个小的 jQuery UI Angular.js 指令,其实很简单,可以在标记中的 <table....draggable resizable>....</table>
表格标签中看到。
这是 draggable
指令的代码,您可以看到它仅限于属性使用,并且只是将工作委托给实际的 jQuery UI(它在 Require.js 配置中被引用为一个要求,因此我们知道它已加载成功,否则 Angular.js 将无法启动,因为它在 Require.js 配置中将 jQuery UI 作为依赖项)。
这里值得一提的是,一旦拖动完成,我希望通知控制器新的位置值,以便它们可以在样式中反映出来。由于定位是在 Angular.js 控制器作用域之外完成的(因为它通过内置的 jQuery UI 代码完成),我们需要让 draggable
指令更新控制器作用域,以便它知道外部有东西改变了它的变量。幸运的是,jQuery UI 可拖动小部件提供了一个很好的回调函数,我们可以利用它,然后告诉 Angular.js 控制器的作用域有东西改变了,这是通过用于此目的的 Angular.js $scope.apply()
完成的。
define(['directives/directives'], function (directives) {
directives.directive('draggable', ['$rootScope', function ($rootScope) {
return {
restrict: 'A',
//may need the model to be passed in here so we can apply changes
//to its left/top positions
link: function (scope, element, attrs) {
element.draggable(
{
stop: function (event, ui) {
scope.$apply(function() {
scope.updatePosition(
scope.imageitem.name,
{
left: ui.position.left,
top: ui.position.top
}
);
});
}
});
}
};
}]);
});
这是当指令调用控制器的作用域时被调用的控制器代码
// NOTE: $scope.$apply is called by the draggable directive
$scope.updatePosition = function (name, pos) {
var idx = $scope.imageitems.propertyBasedIndexOf('name', name);
var foundItem = $scope.imageitems[idx];
foundItem.left = pos.left;
foundItem.top = pos.top;
};
Resizable 指令
可调整大小指令的工作方式与可拖动指令非常相似,它是另一个基于 jQuery UI 的 Angular.js 指令。这是它的代码
define(['directives/directives'], function (directives) {
directives.directive('resizable', ['$rootScope', function ($rootScope) {
return {
restrict: 'A',
//may need the model to be passed in here so we can apply changes
//to its left/top positions
link: function (scope, element, attrs) {
element.resizable(
{
maxHeight: 200,
minHeight: 100,
//aspectRatio: 16 / 9,
stop: function (event, ui) {
scope.$apply(function () {
scope.updateScale(
scope.imageitem.name,
{
top: ui.position.top,
left: ui.position.left
},
{
width: ui.size.width,
height: ui.size.height
}
);
});
}
});
}
};
}]);
});
和以前一样,由于我们正在改变(UI 元素的比例)Angular.js 控制器不知情的东西,我们需要让指令更新控制器作用域,这是相关的代码。
// NOTE: $scope.$apply is called by the resizable directive
$scope.updateScale = function (name, pos, size) {
var idx = $scope.imageitems.propertyBasedIndexOf('name', name);
var foundItem = $scope.imageitems[idx];
foundItem.left = pos.left;
foundItem.top = pos.top;
foundItem.width = size.width;
foundItem.height = size.height;
};
Root Controller
根控制器的一些内部结构已经介绍过,所以我会将我们已经介绍过的函数中的内部代码删除,这样就只剩下这些控制器代码了。
define(['controllers/controllers',
'services/liveUpdatesService',
'services/utilitiesService',
'services/imageService',
'services/localStorageService'],
function (controllers) {
controllers.controller('RootCtrl',
['$window',
'$scope',
'$location',
'LiveUpdatesService',
'UtilitiesService',
'ImageService',
'LocalStorageService',
function (
$window,
$scope,
$location,
LiveUpdatesService,
UtilitiesService,
ImageService,
LocalStorageService) {
$scope.imageitems = [];
$scope.imageItemsStorageKey = 'imageItemsKey';
//load existing items from local storage which looks cool,
//as they show up in their persisted
//positions again...Cool
if (LocalStorageService.isSupported()) {
var currentFavs = LocalStorageService.fetch($scope.imageItemsStorageKey);
if (currentFavs != undefined) {
currentFavs = JSON.parse(currentFavs);
for (var i = 0; i < currentFavs.length; i++) {
var favItem = currentFavs[i];
$scope.imageitems.push(ImageService.createImageItem(
favItem.name, favItem.left, favItem.top,
favItem.width, favItem.height, true));
}
}
}
UtilitiesService.addArrayHelperMethods();
LiveUpdatesService.eventsStream().subscribe(
.....
.....
.....
);
$scope.addToFavourites = function (index) {
if (!LocalStorageService.isSupported()) {
$window.alert('Local storage is not supported by your browser,
so saving favourites isn\'t possible');
} else {
var currentStoredFavsForAdd = LocalStorageService.fetch
($scope.imageItemsStorageKey);
if (currentStoredFavsForAdd == undefined) {
currentStoredFavsForAdd = [];
} else {
currentStoredFavsForAdd = JSON.parse(currentStoredFavsForAdd);
}
var scopeImageItem = $scope.imageitems[index];
var favsIdx = currentStoredFavsForAdd.propertyBasedIndexOf
('name', scopeImageItem.name);
if (favsIdx >= 0) {
$window.alert('An item with that name is already in your favourites.');
return;
}
$scope.imageitems[index].isFavourite = true;
currentStoredFavsForAdd.push(scopeImageItem);
LocalStorageService.save($scope.imageItemsStorageKey, currentStoredFavsForAdd);
$window.alert('Saved to favourites');
}
};
$scope.removeFromFavourites = function (index) {
if (!LocalStorageService.isSupported()) {
$window.alert('Local storage is not supported by your browser,
so removing from favourites isn\'t possible');
} else {
var currentStoredFavsForRemoval = LocalStorageService.fetch
($scope.imageItemsStorageKey);
if (currentStoredFavsForRemoval == undefined) {
return;
} else {
currentStoredFavsForRemoval = JSON.parse(currentStoredFavsForRemoval);
}
var scopeImageItem = $scope.imageitems[index];
var favsIdx = currentStoredFavsForRemoval.propertyBasedIndexOf
('name', scopeImageItem.name);
$scope.imageitems.splice(index, 1);
if (favsIdx >= 0) {
currentStoredFavsForRemoval.splice(favsIdx, 1);
LocalStorageService.save
($scope.imageItemsStorageKey, currentStoredFavsForRemoval);
}
$window.alert('Item removed from favourites');
}
};
// NOTE: $scope.$apply is called by the draggable directive
$scope.updatePosition = function (name, pos) {
.....
.....
.....
};
// NOTE: $scope.$apply is called by the resizable directive
$scope.updateScale = function (name, pos, size) {
.....
.....
.....
};
}]);
});
可以看出,根控制器的大部分代码已经介绍过了,那么还有什么要讨论的呢?
本质上,剩余的代码执行以下操作:
- 允许用户通过图像磁贴 UI 中的按钮调用
addToFavourites
函数(将其添加到 HTML 5 本地存储)。 - 允许用户通过图像磁贴 UI 中的按钮调用
removeFromFavourites
函数(将其从 UI 和 HTML 5 本地存储中删除)。 - 当页面首次渲染时,将从 HTML 5 本地存储中读取所有项目及其持久化状态,这将导致所有持久化的收藏项目完全按照用户将它们保存到本地存储时的样子出现。
Root View
视图是最简单的部分,因为大部分实际工作已经由各种服务和控制器完成。这是根视图的标记
<div ng-repeat="imageitem in imageitems">
<table draggable resizable class="imageHolder, imageDropShadow"
ng-style="imageitems[$index].styleProps()">
<tr>
<td class="imageHeader"> {{imageitem.name}}</td>
</tr>
<tr>
<td align="center">
<img ng-src="https://codeproject.org.cn/app/images/{{imageitem.name}}"
class="imageCell" />
</td>
</tr>
<tr>
<td align="center">
<img src="https://codeproject.org.cn/app/images/favIcon.png"
width="25%" class="favIcon"
tooltip href="" title="Save To Favourites"
ng-click="addToFavourites($index)" />
<img src="https://codeproject.org.cn/app/images/favDelete.png"
width="25%" class="favIcon"
title="Remove From Favourites" tooltip
ng-click="removeFromFavourites($index)"
ng-show="imageitems[$index].isFavourite" />
</td>
</tr>
</table>
</div>
收藏夹页面
尽管不如根页面及其控制器复杂,但收藏夹是第二复杂的页面,因此可能需要一些解释,然后我们才能深入研究其代码。
那么,这个页面到底有什么作用?
这个想法是,有一组(可以为空)图像数据,这些数据存储在 HTML 5 本地存储中的特定键下。当请求收藏夹视图时,将检查这些 HTML 5 本地存储的数据,并为找到的所有项目渲染一个小缩略图。用户还可以单击任何缩略图以启动一个 ColorBox jQuery 插件。
这是收藏夹页面在本地存储中保存了一些项目时的样子
这是您点击其中一个缩略图时的样子
那么这个页面是如何工作的呢?其中一个难点是你们都可能认为微不足道的事情。在本地存储中,我们有一个一维 JSON 字符串化数组,我想把它转换成一个二维表格布局,以便与 Angular.js 的 ng-repeat
绑定一起使用。
Favs Controller
我们先看看控制器。
define(['controllers/controllers',
'services/imageService',
'services/utilitiesService',
'services/localStorageService'],
function (controllers) {
controllers.controller('FavsCtrl',
['$window',
'$scope',
'ImageService',
'UtilitiesService',
'LocalStorageService',
function (
$window,
$scope,
ImageService,
UtilitiesService,
LocalStorageService) {
$scope.imageItemsStorageKey = 'imageItemsKey';
$scope.favImageItems = [];
$scope.columnCount = 5;
$scope.favText = '';
$scope.shouldAlert = false;
$scope.tableItems = [];
while ($scope.tableItems.push([]) < $scope.columnCount);
if (!LocalStorageService.isSupported()) {
$scope.favText = 'Local storage is not supported by your browser,
so viewing favourites isn\'t possible';
$scope.shouldAlert = true;
} else {
var currentStoredFavs = LocalStorageService.fetch($scope.imageItemsStorageKey);
var currentFavs = [];
if (currentStoredFavs != undefined) {
currentFavs = JSON.parse(currentStoredFavs);
}
if (currentFavs.length == 0) {
$scope.favText = 'There are no favourites stored at the moment';
$scope.shouldAlert = true;
} else {
var maxRows = Math.ceil(currentFavs.length / $scope.columnCount);
$scope.favText = 'These are your currently stored favourites.
You can click on the images to see them a bit larger';
if (currentFavs.length < $scope.columnCount) {
$scope.tableItems[0] = [];
for (var i = 0; i < currentFavs.length; i++) {
$scope.tableItems[0].push(ImageService.createFavImageItem
(currentFavs[i].name));
}
} else {
var originalIndexCounter = 0;
for (var r = 0; r < maxRows; r++) {
for (var c = 0; c < $scope.columnCount; c++) {
if (originalIndexCounter < currentFavs.length) {
$scope.tableItems[r][c] =
ImageService.createFavImageItem
(currentFavs[originalIndexCounter].name);
originalIndexCounter++;
}
}
}
}
}
if ($scope.shouldAlert) {
UtilitiesService.delayedAlert($scope.favText);
}
}
}]);
});
可以看出,这里大部分工作是从 HTML 5 本地存储获取数据,并将其从 string
表示转换为 JSON 一维数组,然后转换为可用于标记中绑定的二维结构。
还有一些其他值得注意的事情:
- 我们使用 Angular.js 的
$window
而不是“window
”,以便$window
服务可以被模拟替换。 - 我们使用了我们之前看到的
LocalStorageService
- 我们使用了我们之前看到的
UtilitiesService
- 我们使用了我们之前看到的
ImageService
Favs View
控制器完成了所有繁重的工作,视图标记非常小。
<div class="infoPageContainer">
<h2>Favourites</h2>
<p>{{favText}}</p>
<table id="favsTable">
<tr ng-repeat="row in tableItems">
<td ng-repeat="cell in row">
<a colorbox title="{{cell.name}}"
ng-href="https://codeproject.org.cn/app/images/{{cell.name}}">
<img ng-src="https://codeproject.org.cn/app/images/
{{cell.name}}" class="favSmallIcon" />
</a>
</td>
</tr>
</table>
</div>
注意那个漂亮的嵌套 ng-repeat
,一旦你的作用域中有正确的结构可以迭代,在 Angular.js 中实现表格布局就是这么简单。
ColorBox 指令
谜题的最后一块是如何将这些项目制作成 jQuery ColorBox。根据我们现在所知,我们应该能够意识到答案在于使用另一个指令。
是的,你猜对了,一个 colorbox
指令,可以在标记中的 <a....colorbox>....</a>
锚标签中看到。
这是 colorbox
指令的代码,您可以看到它仅限于属性使用,并且只是将工作委托给实际的 jQuery ColorBox
(它在 Require.js 配置中被引用为一个要求,因此我们知道它已加载成功,否则 Angular.js 将无法启动)。
define(['directives/directives'], function (directives) {
directives.directive('colorbox', ['$rootScope', function ($rootScope) {
return {
restrict: 'A',
//may need the model to be passed in here
//so we can apply changes to its left/top positions
link: function (scope, element, attrs) {
$(element).colorbox({ rel: 'group3', transition:
"elastic", width: "50%", height: "50%" });
}
};
}]);
});
关于页面
关于页面只是静态文本,所以这里没什么特别的。我添加这个页面只是为了让演示应用程序中有足够的路由,使其功能更全面,我想。为了完整起见,这里是关于页面的截图。
就这些
总之,这就是我目前想说的全部,我希望您喜欢这篇文章并从中有所收获。我知道我自己也受益匪浅。从很多方面来说,写这篇文章都很棘手,因为它对我来说有一些新概念,但我对最终结果非常满意。如果您喜欢您所阅读/看到的,并且愿意留下投票/评论,那将非常酷。
总之,再见,直到下一篇文章,希望不会像这篇一样来得这么晚。