164

AngularJS でサービス プロパティにバインドする方法のベスト プラクティスを探しています。

AngularJS を使用して作成されたサービスのプロパティにバインドする方法を理解するために、複数の例に取り組みました。

以下に、サービス内のプロパティにバインドする方法の 2 つの例を示します。どちらも機能します。最初の例では基本的なバインディングを使用し、2 番目の例では $scope.$watch を使用してサービス プロパティにバインドしています。

サービスのプロパティにバインドするときにこれらの例のいずれかが優先されますか、それとも私が知らない別のオプションが推奨されますか?

これらの例の前提は、サービスがそのプロパティ「lastUpdated」と「calls」を 5 秒ごとに更新する必要があるということです。サービス プロパティが更新されると、これらの変更がビューに反映されます。これらの例はどちらも正常に機能します。それを行うより良い方法があるかどうか疑問に思います。

基本的なバインディング

次のコードは、ここで表示および実行できます: http://plnkr.co/edit/d3c16z

<html>
<body ng-app="ServiceNotification" >

    <div ng-controller="TimerCtrl1" style="border-style:dotted"> 
        TimerCtrl1 <br/>
        Last Updated: {{timerData.lastUpdated}}<br/>
        Last Updated: {{timerData.calls}}<br/>
    </div>

    <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.0.5/angular.js"></script>
    <script type="text/javascript">
        var app = angular.module("ServiceNotification", []);

        function TimerCtrl1($scope, Timer) {
            $scope.timerData = Timer.data;
        };

        app.factory("Timer", function ($timeout) {
            var data = { lastUpdated: new Date(), calls: 0 };

            var updateTimer = function () {
                data.lastUpdated = new Date();
                data.calls += 1;
                console.log("updateTimer: " + data.lastUpdated);

                $timeout(updateTimer, 5000);
            };
            updateTimer();

            return {
                data: data
            };
        });
    </script>
</body>
</html>

サービス プロパティへのバインドを解決するもう 1 つの方法は、コントローラーで $scope.$watch を使用することです。

$scope.$watch

次のコードは、ここで表示および実行できます: http://plnkr.co/edit/dSBlc9

<html>
<body ng-app="ServiceNotification">
    <div style="border-style:dotted" ng-controller="TimerCtrl1">
        TimerCtrl1<br/>
        Last Updated: {{lastUpdated}}<br/>
        Last Updated: {{calls}}<br/>
    </div>

    <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.0.5/angular.js"></script>
    <script type="text/javascript">
        var app = angular.module("ServiceNotification", []);

        function TimerCtrl1($scope, Timer) {
            $scope.$watch(function () { return Timer.data.lastUpdated; },
                function (value) {
                    console.log("In $watch - lastUpdated:" + value);
                    $scope.lastUpdated = value;
                }
            );

            $scope.$watch(function () { return Timer.data.calls; },
                function (value) {
                    console.log("In $watch - calls:" + value);
                    $scope.calls = value;
                }
            );
        };

        app.factory("Timer", function ($timeout) {
            var data = { lastUpdated: new Date(), calls: 0 };

            var updateTimer = function () {
                data.lastUpdated = new Date();
                data.calls += 1;
                console.log("updateTimer: " + data.lastUpdated);

                $timeout(updateTimer, 5000);
            };
            updateTimer();

            return {
                data: data
            };
        });
    </script>
</body>
</html>

サービスで $rootscope.$broadcast を使用し、コントローラーで $root.$on を使用できることは承知していますが、$broadcast/$on を使用して作成した他の例では、最初のブロードキャストがコントローラーですが、ブロードキャストされる追加の呼び出しはコントローラーでトリガーされます。$rootscope.$broadcast の問題を解決する方法を知っている場合は、回答を提供してください。

しかし、前に述べたことをもう一度言いますと、サービス プロパティにバインドする方法のベスト プラクティスを知りたいと思います。


アップデート

この質問は、最初は 2013 年 4 月に尋ねられ、回答されました。2014 年 5 月に、Gil Birman が新しい回答を提供し、それを正解として変更しました。Gil Birmanの回答には賛成票がほとんどないため、この質問を読んでいる人が彼の回答を無視して、より多くの票を持つ他の回答を支持することを懸念しています. 最良の答えを決める前に、Gil Birman の答えを強くお勧めします。

4

10 に答える 10

100

2 番目のアプローチの長所と短所をいくつか考えてみましょう。

  • {{lastUpdated}}の代わりに0{{timerData.lastUpdated}}を使用すると、簡単に を使用でき{{timer.lastUpdated}}、より読みやすくなると思います (ただし、議論はやめましょう... この点には中立的な評価を付けているので、自分で判断してください)。

  • +1コントローラーがマークアップの一種のAPIとして機能するのは便利かもしれません.何らかの形でデータモデルの構造が変更された場合、(理論的には)htmlパーシャルに触れることなくコントローラーのAPIマッピングを更新できます.

  • -1ただし、理論は常に実践的であるとは限りません。変更が必要な場合は、通常、マークアップコントローラーのロジックを変更する必要があります。したがって、API を作成する余分な労力は、その利点を無効にします。

  • -1さらに、このアプローチはあまり DRY ではありません。

  • -1データをコードにバインドする場合は、新しい REST 呼び出しを行うためにコントローラーにng-model再パッケージ化する必要があるため、DRY はさらに少なくなります。$scope.scalar_values

  • -0.1余分なウォッチャーを作成する小さなパフォーマンス ヒットがあります。また、特定のコントローラーで監視する必要のないデータ プロパティがモデルにアタッチされている場合、ディープ ウォッチャーに追加のオーバーヘッドが発生します。

  • -1複数のコントローラーが同じデータモデルを必要とする場合は? つまり、モデルが変更されるたびに複数の API を更新する必要があります。

$scope.timerData = Timer.data;は今、非常に魅力的に聞こえ始めています... 最後のポイントをもう少し深く掘り下げてみましょう... どのようなモデル変更について話していましたか? バックエンド (サーバー) のモデル? それとも、フロントエンドでのみ作成され、存在するモデルですか? どちらの場合も、基本的にデータ マッピング APIフロントエンド サービス レイヤー(Angular ファクトリまたはサービス) に属します。(あなたの最初の例 - 私の好み - には、サービス層にそのような API がないことに注意してください。これは、それを必要としないほど単純なので問題ありません。)

結論として、すべてを分離する必要はありません。マークアップをデータ モデルから完全に分離する限り、欠点は利点を上回ります。


コントローラは、一般$scope = injectable.data.scalarに'sで散らかすべきではありません。$scope = injectable.dataむしろ、「 」、promise.then(..)「」、および$scope.complexClickAction = function() {..}「 」を散りばめるべきです。

データの分離、つまりビューのカプセル化を実現するための代替アプローチとして、モデルからビューを分離することが実際に理にかなっている唯一の場所は、ディレクティブを使用することです。ただし、その場合でも、または関数$watchで値をスカラーしないでください。時間を節約したり、コードを保守しやすくしたり、読みやすくしたりすることはできません。angularでの堅牢なテストは通常​​、結果のDOMをとにかくテストするため、テストが容易になることさえありません。むしろ、ディレクティブでは、データ APIをオブジェクト形式で要求し、によって作成された ersのみを使用することを好みます。controllerlink$watchng-bind


http://plnkr.co/edit/MVeU1GKRTN4bqA3h9Yio

<body ng-app="ServiceNotification">
    <div style="border-style:dotted" ng-controller="TimerCtrl1">
        TimerCtrl1<br/>
        Bad:<br/>
        Last Updated: {{lastUpdated}}<br/>
        Last Updated: {{calls}}<br/>
        Good:<br/>
        Last Updated: {{data.lastUpdated}}<br/>
        Last Updated: {{data.calls}}<br/>
    </div>

    <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.0.5/angular.js"></script>
    <script type="text/javascript">
        var app = angular.module("ServiceNotification", []);

        function TimerCtrl1($scope, Timer) {
            $scope.data = Timer.data;
            $scope.lastUpdated = Timer.data.lastUpdated;
            $scope.calls = Timer.data.calls;
        };

        app.factory("Timer", function ($timeout) {
            var data = { lastUpdated: new Date(), calls: 0 };

            var updateTimer = function () {
                data.lastUpdated = new Date();
                data.calls += 1;
                console.log("updateTimer: " + data.lastUpdated);

                $timeout(updateTimer, 500);
            };
            updateTimer();

            return {
                data: data
            };
        });
    </script>
</body>

更新:最終的にこの質問に戻って、どちらのアプローチも「間違っている」とは思わないことを付け加えました。Josh David Miller の答えは間違っていると最初に書きましたが、振り返ってみると、彼の指摘、特に関心の分離に関する指摘は完全に有効です。

懸念事項の分離は別として (接線的には関係がありますが)、私が考慮しなかった防御的コピーの別の理由があります。この質問は主に、サービスから直接データを読み取ることを扱います。しかし、チームの開発者が、コントローラーがデータをビューに表示する前に簡単な方法で変換する必要があると判断した場合はどうなるでしょうか? (コントローラーがデータを変換する必要があるかどうかは、別の議論です。) 最初にオブジェクトのコピーを作成しないと、同じデータを消費する別のビュー コンポーネントで無意識のうちにリグレッションが発生する可能性があります。

この質問が本当に際立たせているのは、典型的なAngularアプリケーション(そして実際にはどんなJavaScriptアプリケーションでも)のアーキテクチャ上の欠点です: 懸念の密結合とオブジェクトの可変性です。私は最近、React不変データ構造を使用したアプリケーションの設計に夢中になっています。そうすることで、次の 2 つの問題が見事に解決されます。

  1. 関心の分離: コンポーネントは props を介してすべてのデータを消費し、グローバル シングルトン (Angular サービスなど) にほとんどまたはまったく依存せず、ビュー階層でそので何が起こったかについて何も知りません。

  2. 可変性: すべての props は不変であり、無意識のうちにデータが変更されるリスクを排除します。

Angular 2.0 は、上記の 2 つの点を達成するために React から多くを借りる方向に進んでいます。

于 2014-05-21T05:40:29.430 に答える
78

私の観点から$watchは、ベストプラクティスの方法です。

実際には、例を少し単純化できます。

function TimerCtrl1($scope, Timer) {
  $scope.$watch( function () { return Timer.data; }, function (data) {
    $scope.lastUpdated = data.lastUpdated;
    $scope.calls = data.calls;
  }, true);
}

それだけです。

プロパティは同時に更新されるため、必要なウォッチは 1 つだけです。また、それらは単一のかなり小さなオブジェクトから発生するため、Timer.dataプロパティを監視するだけに変更しました。に渡された最後のパラメーター$watchは、参照が同じであることを確認するだけでなく、深い等価性をチェックするように指示します。


少しコンテキストを提供するために、サービス値をスコープに直接配置するよりもこの方法を好む理由は、懸念事項を適切に分離するためです。ビューは、動作するためにサービスについて何も知る必要はありません。コントローラーの仕事は、すべてを接着することです。その仕事は、サービスからデータを取得し、必要な方法でそれらを処理し、ビューに必要な詳細を提供することです。しかし、その仕事は、サービスをビューにそのまま渡すことではないと思います。それ以外の場合、コントローラーはそこで何をしているのですか? ifAngularJS の開発者は、テンプレート (ステートメントなど)に「ロジック」を一切含めないことを選択したときと同じ理由に従いました。

公平を期すために、ここにはおそらく複数の視点があり、他の回答を楽しみにしています.

于 2013-04-04T00:22:31.360 に答える
21

パーティーには遅れましたが、将来の Google 社員のために - 提供された回答は使用しないでください。

JavaScript には参照によってオブジェクトを渡すメカニズムがありますが、「数値、文字列など」の値の浅いコピーのみを渡します。

上記の例では、サービスの属性をバインドする代わりに、サービスをスコープに公開しないのはなぜですか?

$scope.hello = HelloService;

この単純なアプローチにより、angular は双方向バインディングと、必要なすべての魔法の機能を実行できるようになります。ウォッチャーや不要なマークアップでコントローラーをハッキングしないでください。

また、ビューが誤ってサービス属性を上書きしてしまうことが心配な場合は、 を使用definePropertyして読み取り可能、列挙可能、構成可能にするか、ゲッターとセッターを定義してください。サービスをより強固にすることで、多くの制御を得ることができます。

最後のヒント: サービスよりもコントローラーの作業に多くの時間を費やしている場合、それは間違っています :(.

あなたが提供した特定のデモコードでは、次のことをお勧めします。

 function TimerCtrl1($scope, Timer) {
   $scope.timer = Timer;
 }
///Inside view
{{ timer.time_updated }}
{{ timer.other_property }}
etc...

編集:

上で述べたように、次を使用してサービス属性の動作を制御できます。defineProperty

例:

// Lets expose a property named "propertyWithSetter" on our service
// and hook a setter function that automatically saves new value to db !
Object.defineProperty(self, 'propertyWithSetter', {
  get: function() { return self.data.variable; },
  set: function(newValue) { 
         self.data.variable = newValue; 
         // let's update the database too to reflect changes in data-model !
         self.updateDatabaseWithNewData(data);
       },
  enumerable: true,
  configurable: true
});

私たちがそうするなら、今私たちのコントローラーで

$scope.hello = HelloService;
$scope.hello.propertyWithSetter = 'NEW VALUE';

私たちのサービスは値を変更しpropertyWithSetter、新しい値を何らかの方法でデータベースに投稿します!

または、任意のアプローチを取ることもできます。

については、 MDN のドキュメントを参照してくださいdefineProperty

于 2016-04-06T03:37:07.153 に答える
12

この質問には文脈上の要素があると思います。

単にサービスからデータを取得し、その情報をそのビューに放射するだけの場合は、サービス プロパティに直接バインドするだけで十分だと思います。ビューで使用するモデル プロパティにサービス プロパティを単純にマップする定型コードを大量に記述したくありません。

さらに、angular でのパフォーマンスは 2 つのことに基づいています。1 つ目は、1 ページにあるバインディングの数です。2 つ目は、getter 関数のコストが高いことです。ミスコはここでこれについて話します

(サービス自体に適用されるデータ マッサージではなく) サービス データに対してインスタンス固有のロジックを実行する必要があり、その結果がビューに公開されるデータ モデルに影響する場合は、$watcher が適切であると言えます。関数がひどく高価でない限り。高価な関数の場合、ローカル (コントローラへの) 変数に結果をキャッシュし、$watcher 関数の外部で複雑な操作を実行してから、スコープをその結果にバインドすることをお勧めします。

注意点として、 $scope から直接プロパティをぶら下げはいけません。$scope変数はモデルではありません。モデルへの参照があります。

私の考えでは、サービスからビューに情報を単純に放射するための「ベストプラクティス」:

function TimerCtrl1($scope, Timer) {
  $scope.model = {timerData: Timer.data};
};

そして、あなたのビューには{{model.timerData.lastupdated}}.

于 2013-05-08T21:52:27.820 に答える
-1

どうですか

scope = _.extend(scope, ParentScope);

ParentScope はどこに挿入されたサービスですか?

于 2015-12-23T17:42:08.677 に答える
-2

最もエレガントなソリューション...

app.service('svc', function(){ this.attr = []; return this; });
app.controller('ctrl', function($scope, svc){
    $scope.attr = svc.attr || [];
    $scope.$watch('attr', function(neo, old){ /* if necessary */ });
});
app.run(function($rootScope, svc){
    $rootScope.svc = svc;
    $rootScope.$watch('svc', function(neo, old){ /* change the world */ });
});

また、私は EDA (イベント駆動型アーキテクチャ) を書いているので、次のようなことをする傾向があります [過度に単純化されたバージョン]:

var Service = function Service($rootScope) {
    var $scope = $rootScope.$new(this);
    $scope.that = [];
    $scope.$watch('that', thatObserver, true);
    function thatObserver(what) {
        $scope.$broadcast('that:changed', what);
    }
};

次に、目的のチャネルのコントローラーにリスナーを配置し、この方法でローカル スコープを最新の状態に保ちます。

結論として、「ベスト プラクティス」はあまりありません。むしろ、それが主に優先されます。私が後者のコードを推奨する理由は、EDA の結合が本質的に最も低いためです。この事実をあまり気にしないのであれば、同じプロジェクトに一緒に取り組むことは避けましょう。

お役に立てれば...

于 2015-02-10T03:09:53.970 に答える