【问题标题】:Where Should Model State Be Stored In Angular.js模型状态应该存储在 Angular.js 中的什么位置
【发布时间】:2013-05-12 13:16:05
【问题描述】:

我发现 Angular 对模型的使用令人困惑。 Angular 似乎采取了一种方法,即模型可以是你喜欢的任何东西——即Angular 不包含显式模型类,您可以使用原生 JavaScript 对象作为模型。

在我见过的几乎每个 Angular 示例中,模型实际上都是一个对象,要么是手动创建的,要么是通过资源从 API 调用返回的。因为我看过的几乎每个 Angular 示例都很简单,通常存储在控制器中 $scope 上的模型数据以及与模型相关的任何状态(例如选择)也存储在控制器中的 $scope 上。这适用于简单的应用程序/示例,但是当应用程序变得更复杂时,这似乎过于简单化了。例如,如果上下文发生变化,存储在控制器中的模型状态就有可能变成上下文并丢失;存储selectedGalleryselectedPhoto 的控制器只能存储全局selectedImage,而不是每个画廊的selectedPhoto。在这种情况下,为每个画廊使用一个控制器可能会解决这个问题,但从 UI 的角度来看,这似乎是一种浪费,而且可能是不合适和不必要的。

Angular 对模型的定义似乎更接近于我认为的 VO/DTO,它是在服务器和客户端之间传递的哑对象。我的直觉是将这样一个对象包装在我认为的模型中——一个维护与 DTO/VO 相关的状态(例如选择)的类,根据需要提供修改器来操作 DTO/VO,并通知其余的应用对基础数据的更改。显然,Angular 的绑定很好地处理了最后一部分,但我仍然看到前两个职责的强大用例。

但是我还没有真正看到在我看过的示例中使用这种模式,但我也没有看到我认为可扩展的替代方案。 Angular 似乎通过强制使用单例来隐含地不鼓励使用服务作为模型(我知道有一些方法可以解决这个问题,但它们似乎没有被广泛使用或批准)。

那么我应该如何保持模型数据的状态?

[编辑]this question 中的第二个答案很有趣,与我目前使用的很接近。

【问题讨论】:

  • 对于每种模型类型的一项服务,您不喜欢什么? galleryService 可以存储一系列画廊。
  • @MarkRajcok 我对单例服务没有任何问题。在很多情况下,它们就是您所需要的,并且在您描述的情况下,效果会很好。但是如果每个画廊都有一组照片,每个照片都需要维护状态呢?
  • 我想我可能在这里过度简化和即时设计......我将拥有三个模型对象:1)照片对象,2)画廊对象(其中一个属性是照片对象数组),3)galleryCollection 对象(其中一个属性是图库对象数组)。 (galleryCollection 可能不是一个单独的对象——它可能只是galleryService 本身的一部分。)方法和属性可以存在于所有三个对象上。在我看来,每张照片和画廊都是一个单独的对象,它们只是由服务分组/管理/访问。模型可以在服务之外定义。
  • 我同意@MarkRajcok(通常是这样)。最干净、最简单的方法是使用他所描述的服务。这极大地简化了测试并使每个服务更具可扩展性和可重用性。我认为重要的是不要将服务视为返回模型 object 而是返回模型 API。该 API 是您在控制器中用来访问一个或一组模型对象的 API。因此,gallery 服务可以使用熟悉的方法(获取、更新、删除等),同时在内部管理状态并使用单独的记录方法返回对象,例如 $resource
  • 对于其他想知道的人:VO = Value Object, DTO = Data Transfer Object

标签: javascript model angularjs state


【解决方案1】:

状态(和模型)存储在 $scope

$scope 是 Angular 的数据存储对象。它类似于数据库。 $scope 本身不是模型,但你可以将模型存储在 $scope 中。

每个 $scope 都有一个父 $scope,一直到 $rootScope 形成一个松散地反映您的 DOM 的树结构。当您调用需要新 $scope 的指令时,例如 ng-controller,将创建一个新的 $scope 对象并将其添加到树中。

$scope 对象使用原型继承连接。这意味着,如果您在树中的较高级别添加模型,则所有较低级别都可以使用该模型。这是一个非常强大的功能,它使 $scope 层次结构对模板作者几乎是透明的。

控制器初始化 $scope

控制器的目的是初始化$scope。同一个控制器可以在页面的不同部分初始化许多 $scope 对象。控制器被实例化,设置 $scope 对象然后退出。您可以使用同一个控制器在页面的不同部分初始化许多 $scope。

对于您的图片库,您将拥有一个 imageGallery 控制器,然后您将使用 ng-controller 指令将其应用于您希望成为图库的 DOM 的每个部分。页面的该部分将获得它自己的 $scope,您将使用它来存储 selectedPhoto 属性。

原型作用域

$scope 使用普通的旧原型继承从其父级继承一直到 $rootScope,因此您可以将对象存储在层次结构中有意义的任何位置。你会得到一棵与你当前的 DOM 大致相关的 $scope 对象树。如果您的 DOM 发生变化,则会根据需要为您创建新的 $scope 对象。

$scope 只是一个普通的 JavaScript 对象。创建多个 $scope 对象并不比创建一个包含多个 currentImage 对象的数组更浪费。这是组织代码的明智方式。

通过这种方式,Angular 消除了我们在 JavaScript 中经常遇到的“我在哪里存储我的数据”的老问题。这是我们从 Angular 获得的真正巨大的生产力提升之一。

有全局数据(例如,用户 ID)?将其存储在 $rootScope 上。获取本地数据(例如,在有多个画廊实例的画廊中的 currentImage)?将其存储在属于该画廊的 $scope 对象中。

$scope 会在模板的正确部分自动提供给您。

Angular 模型很薄

来自我们强调胖模型和瘦控制器的 Rails 背景,我发现 Angular 的“几乎没有”模型令人惊讶。事实上,在模型中放置大量业务逻辑通常会导致问题,正如我们有时在 Rails 中看到的那样,如果您不小心,它会不断增长,直到变得无法维护。

角度模型只是一个 JavaScript 对象或原语。

任何对象都可以是模型。模型通常在控制器中使用 JSON 定义,或者从服务器 AJAX 中定义。模型可能是一个 JSON 对象,也可能只是一个字符串、数组,甚至是一个数字。

当然,如果您愿意,没有什么可以阻止您向模型添加额外的函数并将它们存储在 JSON 对象中,但这将移植到一个并不真正适合 Angular 的范例。

Angular 对象通常是数据存储库,而不是函数。

前端的模型不是真实模型

当然,您持有的客户模型不是真正的模型。您的实际模型,您的单一事实来源存在于服务器上。我们使用 API 对其进行同步,但如果两者之间存在冲突,则数据库中的模型显然是最终的胜利者。

这为您提供折扣代码等内容的隐私。您在前端找到的模型是真实模型的公共属性的同步版本,它是远程的。

业务逻辑可以存在于服务中。

假设您想编写一个方法来对您的模型执行某些操作、同步它或验证它。在其他框架中,您可能很想用一种方法来扩展您的模型。在 Angular 中,您更有可能编写服务。

服务是单例对象。像任何其他 JavaScript 对象一样,您可以将函数或数据放入其中。 Angular 附带了一堆内置服务,例如 $http。您可以构建自己的,并使用依赖注入自动将它们提供给您的控制器。

服务可能包含与 RESTful API 通信、验证数据或您可能需要执行的任何其他工作的方法。

服务不是模型

当然,您不应该将服务用作模型。将它们用作可以做事的对象。有时他们会对你的模型做一些事情。这是一种不同的思维方式,但可行。

【讨论】:

  • 谢谢。所有的好点。但是,您没有解决问题的症结所在。您是否建议所有状态都应存储在服务器上的“真实”模型中?因为这有效地否定了富客户端的主要好处之一——速度。我会争辩说,在许多情况下,客户端模型与服务器端模型一样“真实”,尽管它显然取决于架构。在我的问题中,我专门解决了仅与客户端相关的状态 - 选择状态 - 而不是需要持久保存到服务器的更永久的状态。
  • 是的,你当然是对的,状态应该在客户端,前端任务应该在客户端。我只是觉得讨论这个问题很有用。
  • 谈论它绝对好。我认为这仍然是一个未解决的问题。状态是怪物潜伏的地方,没有人愿意与之打交道。
  • 状态当然可以通过 Angular 使用 $scope 为我们完美解决。
  • 不错的帖子,这是我试图在我的应用程序中遵循的范例。
【解决方案2】:

首先,我们不要忘记 Angular 是一个基于 Web 的框架,如果你只在一个对象中“保持你的状态”,它将无法在用户点击浏览器刷新时存活下来。因此,弄清楚如何在基于 Web 的应用程序中保持模型数据的状态意味着弄清楚如何将其持久化,以便您的代码能够在浏览器环境中运行。

Angular 使您可以非常轻松地使用以下方法来持久化您的状态:

  1. 对 RESTful $resource 的调用
  2. 代表模型实例的 URL

在您的简单示例中,selectedGalleryselectedPhoto 等用户操作的存储可以使用以下 URL 表示:

// List of galleries
.../gallery

// List of photos in a gallery
.../gallery/23

// A specific photo
.../gallery/23/photo/2

URL 至关重要,因为它允许您的用户使用backforward 按钮浏览浏览器历史记录。如果您希望与应用程序的其他部分共享此状态,Web 应用程序为您提供了丰富的方法,使用 cookie/localStorage、隐藏的框架/字段甚至将其存储在您的服务器中。

一旦您定义了如何持久化应用程序的不同状态的策略,就应该更容易决定是使用.service 提供的单例对象还是通过.factory 提供的实例来访问这些持久化信息。

【讨论】:

  • 这很有意义,我完全同意应用程序状态需要通过路径/查询字符串来跟踪,但是这肯定是应用程序状态的投影,而不是存储它的方式。 URL 栏是模型和视图的奇怪组合,但它最终反映的是内部应用程序状态或提示此状态的变化。其余应用程序本身仍然需要在内部跟踪此状态的方法,并且在应用程序的参与者可以共享访问权限的服务/模型上这样做对我来说似乎是正确的解决方案。
  • 另外 .service 和 .factory 都返回单例服务(这非常违反直觉)。
  • @marcoseu,很有趣......虽然你可以做到,对吗?现在依赖注入与该工厂的工作方式非常不同 - 即,由于您返回的是函数(对象)而不是对象(即 {}),因此您必须使用 new 来创建实例。这意味着您不能使用正常的依赖注入语法与其他实体共享该实例。我认为这会让其他可能使用您的代码的 Angular 开发人员感到困惑。
  • @MarkRajcok 我有意返回一个新的'able 函数,以便new 关键字将指示此服务不与其他实体共享。例如,我有一个围绕 ui.bootstrap.alert 的包装器,我在控制器中实例化传递 $scope。当我稍后需要显示警报时,我只需调用alert.success("Ok")。也就是说,我同意调用者何时应该使用 new 尚不清楚,但服务的用户应该了解如何使用服务。
  • @MarkRajcok 我确实意识到new 的使用令人困惑,但它提醒我不要共享我调用的服务。但是,如果仅关注语法方面,您也可以返回一个对象,即:在 .factory 中调用 new。我更新了 plunker 来说明如何做到这一点。
【解决方案3】:

Angular 对如何存储所谓的“模型对象”没有意见。 Angular 控制器$scope 仅作为“视图模型”存在,用于管理您的 UI。我建议在您的代码中将这两个概念分开。

如果您想要 Angular 范围更改通知 ($watch) 的精妙之处,您可以根据需要使用范围对象来存储模型数据 (var myScope = $rootScope.$new())。只是不要使用你的 UI 绑定到的同一个范围对象。

我建议为此编写自定义服务。所以数据流是这样的:

AJAX --> 自定义服务 --> 模型作用域对象 --> 控制器 --> UI 作用域对象 --> DOM

或者这个:

AJAX --> 自定义服务 --> 普通的旧 JavaScript 对象 --> 控制器 --> UI 范围对象 --> DOM

【讨论】:

  • 我已经将这些概念分开了,这正是我问这个问题的原因。处理模型状态的标准 Angular 方式似乎是存储在这个视图模型中,这正是让我感到不舒服的地方。创建一个新范围对我来说并不是一个好的解决方案。感觉就像滥用范围。
猜你喜欢
  • 2021-07-28
  • 2017-02-23
  • 2010-12-18
  • 1970-01-01
  • 2020-06-27
  • 2021-06-08
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多