การใส่ขอบเขต $ ลงในฟังก์ชันบริการเชิงมุม ()


108

ฉันมีบริการ:

angular.module('cfd')
  .service('StudentService', [ '$http',
    function ($http) {
    // get some data via the $http
    var path = 'data/people/students.json';
    var students = $http.get(path).then(function (resp) {
      return resp.data;
    });     
    //save method create a new student if not already exists
    //else update the existing object
    this.save = function (student) {
      if (student.id == null) {
        //if this is new student, add it in students array
        $scope.students.push(student);
      } else {
        //for existing student, find this student using id
        //and update it.
        for (i in students) {
          if (students[i].id == student.id) {
            students[i] = student;
          }
        }
      }
    };

แต่เมื่อผมเรียกsave()ผมไม่ได้มีการเข้าถึงและได้รับ$scope ReferenceError: $scope is not definedดังนั้นขั้นตอนเชิงตรรกะ (สำหรับฉัน) คือการให้ save () ด้วย$scopeดังนั้นฉันจึงต้องจัดเตรียม / ฉีดไปที่ไฟล์service. ดังนั้นถ้าฉันทำเช่นนั้น:

  .service('StudentService', [ '$http', '$scope',
                      function ($http, $scope) {

ฉันได้รับข้อผิดพลาดต่อไปนี้:

ข้อผิดพลาด: [$ injector: unpr] ผู้ให้บริการที่ไม่รู้จัก: $ scopeProvider <- $ scope <- StudentService

ลิงก์ในข้อผิดพลาด (ว้าวที่เรียบร้อย!) ทำให้ฉันรู้ว่ามันเกี่ยวข้องกับหัวฉีดและอาจเกี่ยวข้องกับลำดับการประกาศไฟล์ js ฉันได้ลองเรียงลำดับใหม่index.htmlแล้ว แต่ฉันคิดว่ามันเป็นสิ่งที่ง่ายกว่านั้นเช่นวิธีที่ฉันฉีดเข้าไป

ใช้ Angular-UI และ Angular-UI-Router

คำตอบ:


183

สิ่ง$scopeที่คุณเห็นว่าถูกฉีดเข้าไปในคอนโทรลเลอร์ไม่ใช่บริการบางอย่าง (เช่นส่วนที่เหลือของสิ่งที่ฉีดได้) แต่เป็นวัตถุขอบเขต สามารถสร้างวัตถุขอบเขตจำนวนมากได้ (โดยปกติจะสืบทอดจากขอบเขตหลัก) รากของขอบเขตทั้งหมดคือ$rootScopeและคุณสามารถสร้างขอบเขตย่อยใหม่โดยใช้$new()วิธีการของขอบเขตใดก็ได้ (รวมถึง$rootScope)

จุดประสงค์ของขอบเขตคือ "รวม" งานนำเสนอและตรรกะทางธุรกิจของแอปของคุณ มันไม่สมเหตุสมผลมากที่จะผ่าน$scopeเข้าสู่บริการ

บริการเป็นอ็อบเจ็กต์เดี่ยวที่ใช้ (นอกเหนือจากสิ่งอื่น ๆ ) เพื่อแบ่งปันข้อมูล (เช่นระหว่างตัวควบคุมหลายตัว) และโดยทั่วไปจะห่อหุ้มโค้ดที่ใช้ซ้ำได้ (เนื่องจากสามารถแทรกและเสนอ "บริการ" ของพวกเขาในส่วนใดก็ได้ของแอปของคุณที่ต้องการ: คอนโทรลเลอร์, คำสั่งตัวกรองบริการอื่น ๆ ฯลฯ )

ฉันแน่ใจว่าแนวทางต่างๆจะได้ผลสำหรับคุณ หนึ่งคือ:
เนื่องจากStudentServiceเป็นผู้รับผิดชอบในการจัดการกับข้อมูลของนักเรียนคุณสามารถจัดStudentServiceเก็บข้อมูลของนักเรียนไว้หลายกลุ่มและปล่อยให้ "แบ่งปัน" กับใครก็ตามที่อาจสนใจ (เช่นของคุณ$scope) สิ่งนี้สมเหตุสมผลยิ่งขึ้นหากมีมุมมอง / ตัวควบคุม / ตัวกรอง / บริการอื่น ๆ ที่จำเป็นต้องเข้าถึงข้อมูลนั้น (หากยังไม่มีในตอนนี้อย่าแปลกใจถ้าพวกเขาเริ่มปรากฏขึ้นในไม่ช้า)
ทุกครั้งที่มีการเพิ่มนักเรียนใหม่ (โดยใช้save()วิธีการของบริการ) อาร์เรย์ของนักเรียนของบริการเองจะได้รับการอัปเดตและออบเจ็กต์อื่น ๆ ที่แชร์อาร์เรย์นั้นจะได้รับการอัปเดตโดยอัตโนมัติเช่นกัน

ตามแนวทางที่อธิบายไว้ข้างต้นโค้ดของคุณอาจมีลักษณะดังนี้:

angular.
  module('cfd', []).

  factory('StudentService', ['$http', '$q', function ($http, $q) {
    var path = 'data/people/students.json';
    var students = [];

    // In the real app, instead of just updating the students array
    // (which will be probably already done from the controller)
    // this method should send the student data to the server and
    // wait for a response.
    // This method returns a promise to emulate what would happen 
    // when actually communicating with the server.
    var save = function (student) {
      if (student.id === null) {
        students.push(student);
      } else {
        for (var i = 0; i < students.length; i++) {
          if (students[i].id === student.id) {
            students[i] = student;
            break;
          }
        }
      }

      return $q.resolve(student);
    };

    // Populate the students array with students from the server.
    $http.get(path).then(function (response) {
      response.data.forEach(function (student) {
        students.push(student);
      });
    });

    return {
      students: students,
      save: save
    };     
  }]).

  controller('someCtrl', ['$scope', 'StudentService', 
    function ($scope, StudentService) {
      $scope.students = StudentService.students;
      $scope.saveStudent = function (student) {
        // Do some $scope-specific stuff...

        // Do the actual saving using the StudentService.
        // Once the operation is completed, the $scope's `students`
        // array will be automatically updated, since it references
        // the StudentService's `students` array.
        StudentService.save(student).then(function () {
          // Do some more $scope-specific stuff, 
          // e.g. show a notification.
        }, function (err) {
          // Handle the error.
        });
      };
    }
]);

สิ่งหนึ่งที่คุณควรระวังเมื่อใช้แนวทางนี้คืออย่ากำหนดอาร์เรย์ของบริการซ้ำเพราะส่วนประกอบอื่น ๆ (เช่นขอบเขต) จะยังคงอ้างอิงอาร์เรย์เดิมและแอปของคุณจะพัง
เช่นการล้างอาร์เรย์ในStudentService:

/* DON'T DO THAT   */  
var clear = function () { students = []; }

/* DO THIS INSTEAD */  
var clear = function () { students.splice(0, students.length); }

ดูการสาธิตสั้น ๆนี้ด้วย


อัปเดตเล็กน้อย:

คำสองสามคำเพื่อหลีกเลี่ยงความสับสนที่อาจเกิดขึ้นขณะพูดคุยเกี่ยวกับการใช้บริการ แต่ไม่ได้สร้างขึ้นด้วยservice()ฟังก์ชัน

อ้างถึงเอกสารเมื่อ$provide :

เชิงมุมบริการเป็นวัตถุเดี่ยวที่สร้างขึ้นโดยโรงงานบริการ เหล่านี้โรงงานบริการฟังก์ชั่นซึ่งในที่สุดก็จะถูกสร้างขึ้นโดยผู้ให้บริการ ผู้ให้บริการที่มีฟังก์ชั่นคอนสตรัค เมื่อสร้างอินสแตนซ์พวกเขาจะต้องมีคุณสมบัติที่เรียกว่า$getซึ่งมีฟังก์ชันโรงงานบริการ
[... ]
... $provideบริการมีวิธีการช่วยเหลือเพิ่มเติมในการลงทะเบียนบริการโดยไม่ต้องระบุผู้ให้บริการ:

  • ผู้ให้บริการ (Provider) - ลงทะเบียนผู้ให้บริการกับ $ injector
  • ค่าคงที่ (obj) - ลงทะเบียนค่า / วัตถุที่ผู้ให้บริการและบริการสามารถเข้าถึงได้
  • ค่า (obj) - ลงทะเบียนค่า / วัตถุที่สามารถเข้าถึงได้โดยบริการเท่านั้นไม่ใช่ผู้ให้บริการ
  • factory (fn) - ลงทะเบียนฟังก์ชันโรงงานบริการ fn ซึ่งจะรวมอยู่ในอ็อบเจ็กต์ผู้ให้บริการซึ่งคุณสมบัติ $ get จะมีฟังก์ชันโรงงานที่กำหนด
  • บริการ (คลาส) - ลงทะเบียนฟังก์ชันตัวสร้างคลาสที่จะถูกรวมไว้ในอ็อบเจ็กต์ของผู้ให้บริการซึ่งคุณสมบัติ $ get จะสร้างอินสแตนซ์อ็อบเจ็กต์ใหม่โดยใช้ฟังก์ชันตัวสร้างที่กำหนด

โดยทั่วไปสิ่งที่กล่าวคือทุกบริการ Angular ได้รับการลงทะเบียนโดยใช้$provide.provider()แต่มีวิธี "ทางลัด" สำหรับบริการที่ง่ายกว่า (สองวิธีคือservice()และfactory())
ทุกอย่าง "เดือด" สำหรับบริการดังนั้นจึงไม่ได้สร้างความแตกต่างมากนักว่าคุณใช้วิธีใด (ตราบเท่าที่วิธีการนั้นครอบคลุมข้อกำหนดสำหรับบริการของคุณ)

BTW, providervs servicevs factoryเป็นหนึ่งในแนวคิดที่สับสนที่สุดสำหรับผู้มาใหม่ Angular แต่โชคดีที่มีทรัพยากรมากมาย (ที่นี่ใน SO) เพื่อให้ง่ายขึ้น (เพียงแค่ค้นหารอบ ๆ )

(ฉันหวังว่าจะกระจ่างขึ้น - แจ้งให้เราทราบหากไม่เป็นเช่นนั้น)


1
หนึ่งคำถาม. คุณบอกว่าบริการ แต่ตัวอย่างโค้ดของคุณใช้โรงงาน ฉันเพิ่งเริ่มเข้าใจความแตกต่างระหว่างโรงงานบริการและผู้ให้บริการเพียงแค่ต้องการให้แน่ใจว่าการไปกับโรงงานเป็นตัวเลือกที่ดีที่สุดเนื่องจากฉันใช้บริการ เรียนรู้มากมายจากตัวอย่างของคุณ ขอบคุณสำหรับเสียงซอและคำอธิบายที่ชัดเจนมาก
chris Frisina

3
@chrisFrisina: อัปเดตคำตอบพร้อมคำอธิบายเล็กน้อย โดยทั่วไปก็ไม่ได้ทำให้ความแตกต่างกันมากถ้าคุณใช้serviceหรือfactory- คุณจะจบยูและบริการเชิงมุม ตรวจสอบให้แน่ใจว่าคุณเข้าใจว่าแต่ละอย่างทำงานอย่างไรและเหมาะสมกับความต้องการของคุณหรือไม่
gkalpak

โพสต์ได้ดี! มันช่วยฉันได้มาก!
Oni1

ขอบคุณครับ! นี่คือบทความดีๆเกี่ยวกับเรื่องที่คล้ายกันstsc3000.github.io/blog/2013/10/26/…
Terafor

@ExpertSystem $scope.studentsจะว่างเปล่าถ้าโทร ajax ไม่เสร็จ? หรือ$scope.studentsกำลังจะถูกเติมบางส่วนหากบล็อกโค้ดนี้กำลังดำเนินการอยู่? students.push(student);
Yc Zhang

18

แทนที่จะพยายามแก้ไข$scopeภายในบริการคุณสามารถใช้$watchตัวควบคุมภายในเพื่อดูคุณสมบัติในบริการของคุณสำหรับการเปลี่ยนแปลงจากนั้นอัปเดตคุณสมบัติในไฟล์$scope. นี่คือตัวอย่างที่คุณอาจลองใช้ในคอนโทรลเลอร์:

angular.module('cfd')
    .controller('MyController', ['$scope', 'StudentService', function ($scope, StudentService) {

        $scope.students = null;

        (function () {
            $scope.$watch(function () {
                return StudentService.students;
            }, function (newVal, oldVal) {
                if ( newValue !== oldValue ) {
                    $scope.students = newVal;
                }
            });
        }());
    }]);

สิ่งหนึ่งที่ควรทราบก็คือภายในบริการของคุณเพื่อstudentsให้สามารถมองเห็นพร็อพเพอร์ตี้ได้จำเป็นต้องอยู่ในวัตถุบริการหรือthisเช่นนั้น:

this.students = $http.get(path).then(function (resp) {
  return resp.data;
});

12

(อันยาว) ... ถ้าคุณยืนยันที่จะ$scopeเข้าถึงบริการคุณสามารถ:

สร้างบริการ getter / setter

ngapp.factory('Scopes', function (){
  var mem = {};
  return {
    store: function (key, value) { mem[key] = value; },
    get: function (key) { return mem[key]; }
  };
});

ฉีดและเก็บขอบเขตคอนโทรลเลอร์ไว้ในนั้น

ngapp.controller('myCtrl', ['$scope', 'Scopes', function($scope, Scopes) {
  Scopes.store('myCtrl', $scope);
}]);

ตอนนี้รับขอบเขตภายในบริการอื่น

ngapp.factory('getRoute', ['Scopes', '$http', function(Scopes, $http){
  // there you are
  var $scope = Scopes.get('myCtrl');
}]);

ขอบเขตถูกทำลายอย่างไร?
.

9

บริการเป็นแบบซิงเกิลและไม่ใช่เหตุผลสำหรับขอบเขตที่จะฉีดเข้าไปในบริการ (ซึ่งในกรณีนี้คุณไม่สามารถแทรกขอบเขตในบริการได้) คุณสามารถส่งผ่านขอบเขตเป็นพารามิเตอร์ได้ แต่นั่นก็เป็นตัวเลือกการออกแบบที่ไม่ดีเช่นกันเนื่องจากคุณจะมีการแก้ไขขอบเขตในหลายที่ทำให้ยากต่อการดีบัก รหัสสำหรับจัดการกับตัวแปรขอบเขตควรอยู่ในตัวควบคุมและสายบริการไปที่บริการ


ฉันเข้าใจสิ่งที่คุณกำลังพูด อย่างไรก็ตามในกรณีของฉันฉันมีคอนโทรลเลอร์หลายตัวและฉันต้องการกำหนดค่าขอบเขตของพวกเขาด้วยนาฬิกา $ ชุดที่คล้ายกันมาก คุณจะทำอย่างไร / ที่ไหน ขณะนี้ฉันส่งขอบเขตเป็นพารามิเตอร์ไปยังบริการที่กำหนด $ watch
moritz

@moritz อาจใช้คำสั่งรอง (คำสั่งที่มีขอบเขต: เท็จดังนั้นจึงใช้ขอบเขตที่กำหนดโดยคำสั่งอื่น ๆ ) และสิ่งนั้นทำให้การเชื่อมโยงของ watchchess รวมถึงสิ่งอื่น ๆ ที่คุณต้องการ ด้วยวิธีนี้คุณสามารถใช้คำสั่งอื่น ๆ ในที่ใดก็ได้ที่คุณต้องการกำหนดนาฬิกาดังกล่าว เพราะการผ่านขอบเขตไปสู่การบริการนั้นค่อนข้างแย่มาก :) (เชื่อฉันฉันเคยไปที่นั่นทำแบบนั้นแล้ว
เอาหัวชนกำแพง

@TIMINeutron ที่ฟังดูดีกว่าการผ่านขอบเขตมากฉันจะลองในครั้งต่อไปที่สถานการณ์จะเกิดขึ้น! ขอบคุณ!
moritz

แน่นอน ฉันยังคงเรียนรู้ด้วยตัวเองและปัญหาเฉพาะนี้เป็นปัญหาที่ฉันเพิ่งแก้ไขด้วยวิธีนี้โดยเฉพาะและมันก็เป็นเสน่ห์สำหรับฉัน
tfrascaroli

3

คุณสามารถทำให้บริการของคุณไม่ทราบขอบเขตโดยสิ้นเชิง แต่ในตัวควบคุมของคุณอนุญาตให้อัปเดตขอบเขตแบบอะซิงโครนัสได้

ปัญหาที่คุณพบคือเนื่องจากคุณไม่ทราบว่ามีการโทร http แบบอะซิงโครนัสซึ่งหมายความว่าคุณจะไม่ได้รับค่าทันทีเท่าที่ควร ตัวอย่างเช่น

var students = $http.get(path).then(function (resp) {
  return resp.data;
}); // then() returns a promise object, not resp.data

มีวิธีง่ายๆในการแก้ไขปัญหานี้และการจัดหาฟังก์ชันการโทรกลับ

.service('StudentService', [ '$http',
    function ($http) {
    // get some data via the $http
    var path = '/students';

    //save method create a new student if not already exists
    //else update the existing object
    this.save = function (student, doneCallback) {
      $http.post(
        path, 
        {
          params: {
            student: student
          }
        }
      )
      .then(function (resp) {
        doneCallback(resp.data); // when the async http call is done, execute the callback
      });  
    }
.controller('StudentSaveController', ['$scope', 'StudentService', function ($scope, StudentService) {
  $scope.saveUser = function (user) {
    StudentService.save(user, function (data) {
      $scope.message = data; // I'm assuming data is a string error returned from your REST API
    })
  }
}]);

แบบฟอร์ม:

<div class="form-message">{{message}}</div>

<div ng-controller="StudentSaveController">
  <form novalidate class="simple-form">
    Name: <input type="text" ng-model="user.name" /><br />
    E-mail: <input type="email" ng-model="user.email" /><br />
    Gender: <input type="radio" ng-model="user.gender" value="male" />male
    <input type="radio" ng-model="user.gender" value="female" />female<br />
    <input type="button" ng-click="reset()" value="Reset" />
    <input type="submit" ng-click="saveUser(user)" value="Save" />
  </form>
</div>

สิ่งนี้ลบตรรกะทางธุรกิจบางส่วนของคุณเพื่อความกะทัดรัดและฉันยังไม่ได้ทดสอบโค้ดจริง ๆ แต่สิ่งนี้จะได้ผล แนวคิดหลักคือการส่งการเรียกกลับจากคอนโทรลเลอร์ไปยังบริการซึ่งจะเรียกในภายหลังในอนาคต หากคุณคุ้นเคยกับ NodeJS นี่เป็นแนวคิดเดียวกัน


ไม่แนะนำให้ใช้แนวทางนี้ ดูทำไม Callbacks จากสัญญา.thenวิธีการป้องกันแบบ
georgeawg

0

ตกอยู่ในสถานการณ์เดียวกัน ฉันลงเอยด้วยสิ่งต่อไปนี้ ดังนั้นที่นี่ฉันไม่ได้ฉีดวัตถุขอบเขตเข้าไปในโรงงาน แต่ตั้งค่าขอบเขต $ในตัวควบคุมโดยใช้แนวคิดของสัญญาที่ส่งคืนโดยบริการ$ http

(function () {
    getDataFactory = function ($http)
    {
        return {
            callWebApi: function (reqData)
            {
                var dataTemp = {
                    Page: 1, Take: 10,
                    PropName: 'Id', SortOrder: 'Asc'
                };

                return $http({
                    method: 'GET',
                    url: '/api/PatientCategoryApi/PatCat',
                    params: dataTemp, // Parameters to pass to external service
                    headers: { 'Content-Type': 'application/Json' }
                })                
            }
        }
    }
    patientCategoryController = function ($scope, getDataFactory) {
        alert('Hare');
        var promise = getDataFactory.callWebApi('someDataToPass');
        promise.then(
            function successCallback(response) {
                alert(JSON.stringify(response.data));
                // Set this response data to scope to use it in UI
                $scope.gridOptions.data = response.data.Collection;
            }, function errorCallback(response) {
                alert('Some problem while fetching data!!');
            });
    }
    patientCategoryController.$inject = ['$scope', 'getDataFactory'];
    getDataFactory.$inject = ['$http'];
    angular.module('demoApp', []);
    angular.module('demoApp').controller('patientCategoryController', patientCategoryController);
    angular.module('demoApp').factory('getDataFactory', getDataFactory);    
}());

0

รหัสสำหรับจัดการกับตัวแปรขอบเขตควรอยู่ในตัวควบคุมและสายบริการไปที่บริการ

คุณสามารถฉีด$rootScopeเพื่อวัตถุประสงค์ในการใช้และ$rootScope.$broadcast$rootScope.$on

$rootScopeมิฉะนั้นหลีกเลี่ยงการฉีด ดู

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.