【问题标题】:AngularJS controllers, design pattern for a DRY codeAngularJS 控制器,DRY 代码的设计模式
【发布时间】:2015-11-25 14:13:25
【问题描述】:

为了描述这个问题,我创建了一个完整的示例。我的实际应用程序比演示的还要大,每个控制器操作的服务和指令更多。这会导致更多的代码重复。我试着放一些代码 cmets 来澄清一下, PLUNKERhttp://plnkr.co/edit/781Phn?p=preview

重复部分

routerApp.controller('page1Ctrl', function(pageFactory) {
  var vm = this;

  // page dependent
  vm.name = 'theOne';
  vm.service = 'oneService';
  vm.seriesLabels = ['One1', 'Two1', 'Three1'];

  // these variables are declared in all pages
  // directive variables,
  vm.date = {
    date: new Date(),
    dateOptions: {
      formatYear: 'yy',
      startingDay: 1
    },
    format: 'dd-MMMM-yyyy',
    opened: false
  };

  vm.open = function($event) {
    vm.date.opened = true;
  };

  // dataservice
  vm.data = []; // the structure can be different but still similar enough
  vm.update = function() {
      vm.data = pageFactory.get(vm.service);
    }

  //default call
  vm.update();   
})

基本上我将所有我能做的逻辑都移到了工厂和指令中。但是现在在每个使用特定指令的控制器中,我需要一个字段来保存指令正在修改的值。它的设置。后来我需要类似的字段来保存来自dataservice的数据,调用本身(方法)也是一样的。

这会导致大量重复。


从图形上看,当前示例如下所示:

虽然我认为正确的设计应该看起来更像这样:


我试图在这里找到一些解决方案,但似乎都没有得到证实。我发现了什么:

  1. AngularJS DRY controller structure,建议我通过 $scope 或 vm 并用额外的方法和字段装饰它。但许多消息来源说这是肮脏的解决方案。
  2. What's the recommended way to extend AngularJS controllers? 使用 angular.extend,但是在使用 controller as 语法时会出现问题。
  3. 然后我也找到了答案(在上面的链接中):

您不扩展控制器。如果它们执行相同的基本功能,则需要将这些功能转移到服务中。该服务可以注入到您的控制器中。

即使我这样做了,仍然有很多重复。或者它只是必须的方式?像 John Papa sais (http://www.johnpapa.net/angular-app-structuring-guidelines/):

尽量保持 DRY(不要重复自己)或 T-DRY

您是否遇到过类似的问题?有哪些选择?

【问题讨论】:

  • 听起来这是一个“他们在多大程度上不同”的问题。这是一个非常主观的问题——如果没有具体的细节,我认为站在一边是不对的。但是,当您最了解您的应用程序时,将其移至服务中感觉不对,那么它可能就是不正确的做法。但是,如果您提供一些代码,也许其他成员可以对此有所了解。
  • 我给出了更多真实数据的例子,但我认为它不会有帮助。它是 - 如前所述,只是某些指令或服务使用的大多数变量。但问题是我仍然需要为每个使用类似控制器的视图重新定义它们。
  • John Papa 还写道 _“保持干燥很重要,但如果它在 LIFT 中牺牲其他人则并不重要”。考虑将控制器拆分为更小的控制器和/甚至指令。组件越小,可重复使用的机会就越高。
  • @zeroflagL 但现在这就是问题所在。我确实尽可能地拆分它,但现在我有例如 6-10 个视图使用相同的指令,这需要声明相同的字段。例如需要日期字段和日期格式的日期选择器。它不能在不破坏 LIFT 的情况下组合成更大的指令,但现在每个视图都必须一遍又一遍地声明相同的字段。以完全相同的方式。就我而言,它实际上是反复复制的 2-3 个指令的组合。那么有什么方法可以让这个更干吗?
  • 我通过使用允许父子状态的 ui-router 解决了这个问题。我将有一个父控制器,每个子控制器都会调用它。

标签: javascript angularjs design-patterns dry angularjs-controller


【解决方案1】:

从整体设计的角度来看,我认为装饰控制器和扩展控制器之间没有太大区别。最后,这些都是混合的一种形式,而不是继承。所以它真的归结为你最舒服的工作。一项重大的设计决策不仅在于如何将功能传递给所有控制器,还在于如何将功能也传递给 3 个控制器中的 2 个。

工厂装饰师

正如您所提到的,一种方法是将您的 $scope 或 vm 传递给工厂,该工厂用额外的方法和字段装饰您的控制器。我不认为这是一个肮脏的解决方案,但我可以理解为什么有些人想要将工厂从他们的 $scope 中分离出来,以便分离他们代码的关注点。如果您需要在 2 out of 3 场景中添加额外的功能,您可以传入额外的工厂。我做了一个plunker example of this

dataservice.js

routerApp.factory('pageFactory', function() {

    return {
      setup: setup
    }

    function setup(vm, name, service, seriesLabels) {
      // page dependent
      vm.name = name;
      vm.service = service;
      vm.seriesLabels = seriesLabels;

      // these variables are declared in all pages
      // directive variables,
      vm.date = {
        date: moment().startOf('month').valueOf(),
        dateOptions: {
          formatYear: 'yy',
          startingDay: 1
        },
        format: 'dd-MMMM-yyyy',
        opened: false
      };

      vm.open = function($event) {
        vm.date.opened = true;
      };

      // dataservice
      vm.data = []; // the structure can be different but still similar enough
      vm.update = function() {
        vm.data = get(vm.service);
      }

      //default call
      vm.update();
    }

});

page1.js

routerApp.controller('page1Ctrl', function(pageFactory) {
    var vm = this;
    pageFactory.setup(vm, 'theOne', 'oneService', ['One1', 'Two1', 'Three1']);
})

扩展控制器

您提到的另一个解决方案是扩展控制器。这可以通过创建一个超级控制器来实现,您可以将其混入正在使用的控制器中。如果您需要为特定控制器添加额外的功能,您可以混合使用具有特定功能的其他超级控制器。这是plunker example

父页面

routerApp.controller('parentPageCtrl', function(vm, pageFactory) {

    setup()

    function setup() {

      // these variables are declared in all pages
      // directive variables,
      vm.date = {
        date: moment().startOf('month').valueOf(),
        dateOptions: {
          formatYear: 'yy',
          startingDay: 1
        },
        format: 'dd-MMMM-yyyy',
        opened: false
      };

      vm.open = function($event) {
        vm.date.opened = true;
      };

      // dataservice
      vm.data = []; // the structure can be different but still similar enough
      vm.update = function() {
        vm.data = pageFactory.get(vm.service);
      }

      //default call
      vm.update();
    }

})

page1.js

routerApp.controller('page1Ctrl', function($controller) {
    var vm = this;
    // page dependent
    vm.name = 'theOne';
    vm.service = 'oneService';
    vm.seriesLabels = ['One1', 'Two1', 'Three1'];
    angular.extend(this, $controller('parentPageCtrl', {vm: vm}));
})

嵌套状态 UI 路由器

由于你使用的是ui-router,你也可以通过嵌套状态来达到类似的效果。对此的一个警告是 $scope 不会从父控制器传递到子控制器。因此,您必须在 $rootScope 中添加重复的代码。当我想要传递整个程序的功能时,我会使用它,例如测试我们是否在手机上的功能,它不依赖于任何控制器。这是plunker example

【讨论】:

  • 也许这只是观点上的不同,但对我来说最吸引人的解决方案仍然是extending controller,老实说,我真的很惊讶你用controller as 语法实现了这一点!我认为(并阅读)它不起作用。即使指令解决方案适合这里,我也会选择您的解决方案,因为它更灵活。非常感谢!
  • 对于您的情况,这很有意义!我个人在我的应用程序中使用了这些解决方案的组合。
  • 扩展控制器解决方案非常棒……非常感谢!如果可以的话,我会投票两次
【解决方案2】:

您可以通过使用指令来减少大量样板文件。我创建了一个简单的控制器来替换您的所有控制器。您只需通过属性传入特定于页面的数据,它们就会绑定到您的范围。

routerApp.directive('pageDir', function() {
  return {
    restrict: 'E',
    scope: {},
    controller: function(pageFactory) {
      vm = this;
      vm.date = {
        date: moment().startOf('month').valueOf(),
        dateOptions: {
          formatYear: 'yy',
          startingDay: 1
        },
        format: 'dd-MMMM-yyyy',
        opened: false
      };

      vm.open = function($event) {
        vm.date.opened = true;
      };

      // dataservice
      vm.data = []; // the structure can be different but still similar enough
      vm.update = function() {
        vm.data = pageFactory.get(vm.service);
      };

      vm.update();
    },
    controllerAs: 'vm',
    bindToController: {
      name: '@',
      service: '@',
      seriesLabels: '='
    },
    templateUrl: 'page.html',
    replace: true
  }
});

如您所见,它与您的控制器没有太大区别。不同之处在于要使用它们,您将使用路由的 template 属性中的指令来初始化它。像这样:

    .state('state1', {
        url: '/state1',
        template: '<page-dir ' +
          'name="theOne" ' +
          'service="oneService" ' +
          'series-labels="[\'One1\', \'Two1\', \'Three1\']"' +
          '></page-dir>'
    })

差不多就是这样。我叉了你的 Plunk 来演示。 http://plnkr.co/edit/NEqXeD?p=preview

编辑:忘记添加您也可以根据需要设置指令样式。删除冗余代码时忘记将其添加到 Plunk。

【讨论】:

  • 样式只是为了区分视图所以不用担心。我也很喜欢使用指令。我可以想象的唯一困难部分是您必须更改 >inside
  • 这是真的。我没有考虑到这一点。不过,有解决方案。您可以在模板中创建点以进行扩展。也许设置一个标志,例如&lt;page-dir button="button.html"&gt;&lt;/page-dir&gt;,然后您可以在模板中检查vm.button,并在特定位置检查ng-include 该文件。不过,在这种情况下,对您的代码进行推理可能会变得更加困难。
【解决方案3】:

我无法在评论中回复,但我会在这里做什么:

我将有一个 ConfigFactory 持有页面相关变量的映射:

{
  theOne:{
      name: 'theOne',
      service: 'oneService',
      seriesLabels: ['One1', 'Two1', 'Three1']
  },
  ...
}

然后我将拥有一个带有 newInstance() 方法的 LogicFactory,以便在我每次需要时获取合适的对象。 logicFactory 将获取所有控制器之间共享的数据/方法。 对于这个 LogicFactory,我将给出视图特定的数据。并且视图必须绑定到这个工厂。

为了检索特定于视图的数据,我将在路由器中传递我的配置映射的键。

假设路由器给你#current=theOne,我会在控制器中做:

var specificData = ServiceConfig.get($location.search().current);
this.logic = LogicFactory.newInstance(specificData);

希望对你有帮助

我修改了你的例子,结果如下:http://plnkr.co/edit/ORzbSka8YXZUV6JNtexk?p=preview

编辑:就是这么说,您可以从为您提供特定视图数据的远程服务器加载特定配置

【讨论】:

  • 您能用示例 plunker 介绍一下吗?
  • 我正在考虑使用一个具有不同设置的控制器,但我从未在网上找到此类配置的示例。然而,它的工作可能不会那么糟糕。谢谢!
  • 一个控制器不是强制性的,我只是修改它来清理代码。您可以根据需要拥有具有自己逻辑的多控制器
  • 坦率地说,我还没有以呈现的方式实现我以前的组件。我发现它作为一种解决方案非常有趣,但我不确定我是否会在我的项目中使用它。不过,感谢分享,我希望其他人也可以从中学习。
【解决方案4】:

我遇到了与您描述的完全相同的问题。我非常支持保持干燥。当我开始使用 Angular 时,没有规定或推荐的方法来做到这一点,所以我只是在进行过程中重构了我的代码。与许多事情一样,我不认为他们是做这些事情的正确或错误的方式,所以使用任何你觉得舒服的方法。所以下面是我最终使用的,它对我很有帮助。

在我的应用程序中,我通常有三种类型的页面:

  1. 列表页 - 特定资源的表格列表。你可以 搜索/过滤/排序您的数据。
  2. 表单页面 - 创建或编辑资源。
  3. 显示页面 - 资源/数据的详细仅查看显示页面。

我发现 (1) 和 (2) 中通常有很多重复的代码,我并不是指应该提取到服务中的功能。因此,为了解决这个问题,我使用了以下继承层次结构:

  1. 列出页面

    • 基本列表控制器
      • loadNotification()
      • 搜索()
      • 高级搜索()
      • 等等……
    • 资源ListController
      • 任何资源特定的东西
  2. 表单页面

    • 基本表单控制器
      • setServerErrors()
      • clearServerErrors()
      • 诸如警告用户在保存表单之前离开此页面之类的内容,以及任何其他常规功能。
    • 抽象表单控制器
      • 保存()
      • processUpdateSuccess()
      • processCreateSuccess()
      • processServerErrors()
      • 设置任何其他共享选项
    • 资源表单控制器
      • 任何资源特定的东西

要启用此功能,您需要一些约定。对于表单页面,我通常每个资源只有一个视图模板。使用路由器resolve 功能,我传入一个变量以指示表单是用于创建还是编辑目的,然后我将其发布到我的vm。然后可以在您的AbstractFormController 中使用它来调用保存或更新您的数据服务。

为了实现控制器继承,我使用 Angulars $injector.invoke 函数传入 this 作为实例。由于$injector.invoke 是 Angulars DI 基础架构的一部分,因此它工作得很好,因为它可以处理基本控制器类所需的任何依赖项,并且我可以根据需要提供任何特定的实例变量。

这是一个关于如何实现的小sn-p:

Common.BaseFormController = function (dependencies....) {
    var self = this;
    this.setServerErrors = function () {
    };
    /* .... */
};

Common.BaseFormController['$inject'] = [dependencies....];

Common.AbstractFormController = function ($injector, other dependencies....) {
    $scope.vm = {};
    var vm = $scope.vm;
    $injector.invoke(Common.BaseFormController, this, { $scope: $scope, $log: $log, $window: $window, alertService: alertService, any other variables.... });
   /* ...... */
}

Common.AbstractFormController['$inject'] = ['$injector', other dependencies....];

CustomerFormController = function ($injector, other dependencies....) {
    $injector.invoke(Common.AbstractFormController, this, {
            $scope: $scope,
            $log: $log,
            $window: $window,
            /* other services and local variable to be injected .... */
        });

    var vm = $scope.vm;
    /* resource specific controller stuff */
}

CustomerFormController['$inject'] = ['$injector', other dependencies....];

为了更进一步,我发现通过我的数据访问服务实现大量减少了重复代码。对于数据层约定为王。我发现如果你在你的服务器 API 上保持一个通用的约定,你可以用一个基本的工厂/存储库/类或任何你想调用的东西来走很长的路。我在 AngularJs 中实现这一点的方法是使用一个 AngularJs 工厂,它返回一个基本存储库类,即工厂返回一个带有原型定义而不是对象实例的 javascript 类函数,我称之为 abstractRepository。然后对于每个资源,我为该特定资源创建一个具体的存储库,该资源在原型上继承自 abstractRepository,因此我从 abstractRepository 继承所有共享/基本功能,并将任何资源特定功能定义到具体存储库。

我认为一个例子会更清楚。让我们假设您的服务器 API 使用以下 URL 约定(我不是最纯粹的 REST,所以我们将把约定留给您想要实现的任何内容):

GET  -> /{resource}?listQueryString     // Return resource list
GET  -> /{resource}/{id}                // Return single resource
GET  -> /{resource}/{id}/{resource}view // Return display representation of resource
PUT  -> /{resource}/{id}                // Update existing resource
POST -> /{resource}/                    // Create new resource
etc.

我个人使用 Restangular,因此以下示例基于它,但您应该能够轻松地将其调整为 $http 或 $resource 或您正在使用的任何库。

AbstractRepository

app.factory('abstractRepository', [function () {

    function abstractRepository(restangular, route) {
        this.restangular = restangular;
        this.route = route;
    }

    abstractRepository.prototype = {
        getList: function (params) {
            return this.restangular.all(this.route).getList(params);
        },
        get: function (id) {
            return this.restangular.one(this.route, id).get();
        },
        getView: function (id) {
            return this.restangular.one(this.route, id).one(this.route + 'view').get();
        },
        update: function (updatedResource) {
            return updatedResource.put();
        },
        create: function (newResource) {
            return this.restangular.all(this.route).post(newResource);
        }
        // etc.
    };

    abstractRepository.extend = function (repository) {
        repository.prototype = Object.create(abstractRepository.prototype);
        repository.prototype.constructor = repository;
    };

    return abstractRepository;
}]);

具体的仓库,我们以客户为例:

app.factory('customerRepository', ['Restangular', 'abstractRepository', function (restangular, abstractRepository) {

    function customerRepository() {
        abstractRepository.call(this, restangular, 'customers');
    }

    abstractRepository.extend(customerRepository);
    return new customerRepository();
}]);

所以现在我们有了数据服务的通用方法,可以在 Form 和 List 控制器基类中轻松使用。

【讨论】:

  • 感谢您的出色贡献。这是对未来应用程序规划的一个很好的建议。但是它不是我现在可以使用的东西。由于@jjbskir 是第一个提供工作扩展示例的人(实际上是两种方式),我选择了他的赏金答案。不过,我可能会在下一个项目中使用您的解决方案:)!干杯。
【解决方案5】:

总结之前的答案:

  1. 装饰控制器:正如你所说,这是一个肮脏的解决方案;想象一下让不同的工厂装饰同一个控制器,将很难(尤其是对于其他开发人员)防止属性冲突,同样难以追踪哪个工厂添加了哪些属性。这实际上就像在 OOP 中具有多重继承,大多数现代语言出于同样的原因通过设计来防止这种情况。

  2. 使用指令:如果您的所有控制器都将具有相同的 html 视图,这可能是一个很好的解决方案,但除此之外,您必须在视图中包含相当复杂的逻辑,这可能难以调试.


我建议的方法是使用组合(而不是使用装饰器继承)。将工厂中所有重复的逻辑分开,只留下控制器中工厂的创建。

routerApp.controller('page1Ctrl', function (Page, DateConfig, DataService) {
    var vm = this;

    // page dependent
    vm.page = new Page('theOne', 'oneService', ['One1', 'Two1', 'Three1']);

    // these variables are declared in all pages
    // directive variables,
    vm.date = new DateConfig()

    // dataservice
    vm.dataService = new DataService(vm.page.service);

    //default call
    vm.dataService.update();

})

.factory('Page', function () {

    //constructor function
    var Page = function (name, service, seriesLabels) {
        this.name = name;
        this.service = service;
        this.seriesLabels = seriesLabels;
    };

    return Page;

})


.factory('DateConfig', function () {

    //constructor function
    var DateConfig = function () {
        this.date = new Date();
        this.dateOptions = {
            formatYear: 'yy',
            startingDay: 1
        };
        this.format = 'dd-MMMM-yyyy';
        this.opened = false;
        this.open = function ($event) {
            this.opened = true;
        };
    };

    return DateConfig;

})

此代码未经测试,但我只是想提供一个想法。这里的关键是将工厂中的代码分开,并将它们作为属性添加到控制器中。这样实现就不会重复(DRY),并且在控制器代码中一切都是显而易见的。

您可以通过将所有工厂包装在一个更大的工厂(外观)中来使您的控制器更小,但这可能会使它们更紧密地耦合。

【讨论】:

  • 感谢您的总结,至于赏金 - 我实际上要求提供一个工作示例,因为有时这个想法是正确的,但最终它不起作用;-)。希望我们都能从示例中学习。
  • 我的意思是这个概念有效,但您可能会在示例代码中发现一些错误
猜你喜欢
  • 1970-01-01
  • 2014-02-11
  • 2011-04-28
  • 1970-01-01
  • 2013-01-23
  • 2018-09-29
  • 2014-03-03
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多