javascript - AngularJS : เริ่มต้นเซอร์วิสด้วยข้อมูลแบบอะซิงโครนัส
ฉันมีบริการ AngularJS ที่ฉันต้องการเริ่มต้นด้วยข้อมูลแบบอะซิงโครนัส อะไรทำนองนี้:
myModule.service('MyService', function($http) {
var myData = null;
$http.get('data.json').success(function (data) {
myData = data;
});
return {
setData: function (data) {
myData = data;
},
doStuff: function () {
return myData.getSomeData();
}
};
});
อย่างชัดเจนว่าสิ่งนี้จะใช้ไม่ได้เพราะถ้ามีบางอย่างพยายามเรียก doStuff() ก่อนที่ myData จะถูกส่งกลับ ฉันจะได้ข้อยกเว้น null pointer จากที่ฉันสามารถดูได้จากการอ่านคำถามอื่น ๆ บางส่วนที่ถูกถามที่นี่และที่นี่ ฉันมีตัวเลือกอยู่ไม่กี่วิธี แต่ไม่มีวิธีใดที่ดูสะอาด (บางทีฉันอาจพลาดอะไรไปบางอย่าง)
ตั้งค่าบริการด้วย "run"
เมื่อกำหนดค่าแอปของฉัน ให้ทำสิ่งนี้:
myApp.run(function ($http, MyService) {
$http.get('data.json').success(function (data) {
MyService.setData(data);
});
});
แล้วบริการของฉันจะดูเหมือนแบบนี้:
myModule.service('MyService', function() {
var myData = null;
return {
setData: function (data) {
myData = data;
},
doStuff: function () {
return myData.getSomeData();
}
};
});
สิ่งนี้ใช้งานได้บางครั้ง แต่ถ้าข้อมูลแบบอะซิงโครนัสใช้เวลานานกว่าการที่ทุกอย่างจะถูกเริ่มต้น ฉันจะพบข้อยกเว้นตัวชี้ว่างเมื่อฉันเรียก doStuff()
ใช้วัตถุสัญญา
นี่อาจจะได้ผล ข้อเสียอย่างเดียวก็คือทุกที่ที่ฉันเรียกใช้ MyService ฉันจะต้องรู้ว่า doStuff() คืนค่าเป็น promise และโค้ดทั้งหมดจะต้องใช้ then เพื่อโต้ตอบกับ promise ฉันคงอยากรอจนกว่า myData จะกลับมาก่อนที่จะโหลดแอปพลิเคชันของฉัน
การติดตั้ง Bootstrap ด้วยตนเอง
angular.element(document).ready(function() {
$.getJSON("data.json", function (data) {
// can't initialize the data here because the service doesn't exist yet
angular.bootstrap(document);
// too late to initialize here because something may have already
// tried to call doStuff() and would have got a null pointer exception
});
});
ตัวแปร Javascript ระดับโลก ฉันสามารถส่ง JSON ของฉันโดยตรงไปยังตัวแปร Javascript ระดับโลก:
HTML :
<script type="text/javascript" src="data.js"></script>
data.js:
var dataForMyService = {
// myData here
};
แล้วมันจะสามารถใช้ได้เมื่อตอนเริ่มต้น MyService:
myModule.service('MyService', function() {
var myData = dataForMyService;
return {
doStuff: function () {
return myData.getSomeData();
}
};
});
สิ่งนี้ก็ใช้ได้เหมือนกัน แต่ว่าฉันจะมีตัวแปรจาวาสคริปต์ระดับโลกซึ่งดูไม่ดี
นี่เป็นตัวเลือกเดียวของฉันใช่ไหม? มีตัวเลือกใดในนี้ที่ดีกว่าตัวอื่น ๆ หรือไม่? ฉันรู้ว่านี่เป็นคำถามที่ค่อนข้างยาว แต่ฉันอยากจะแสดงให้เห็นว่าฉันได้พยายามสำรวจตัวเลือกทั้งหมดของฉันแล้ว การแนะนำใด ๆ จะเป็นที่ชื่นชมอย่างมาก
คำตอบ #1
คุณได้ดูที่ $routeProvider.when('/path',{ resolve:{...} แล้วหรือยัง? มันสามารถทำให้แนวทางของ promise ดูสะอาดขึ้นเล็กน้อย:
เปิดเผยพรอมิสในบริการของคุณ:
app.service('MyService', function($http) {
var myData = null;
var promise = $http.get('data.json').success(function (data) {
myData = data;
});
return {
promise:promise,
setData: function (data) {
myData = data;
},
doStuff: function () {
return myData;//.getSomeData();
}
};
});
เพิ่ม resolve ลงในการตั้งค่าเส้นทางของคุณ:
app.config(function($routeProvider){
$routeProvider
.when('/',{controller:'MainCtrl',
template:'<div>From MyService:<pre>{{data | json}}</pre></div>',
resolve:{
'MyServiceData':function(MyService){
// MyServiceData will also be injectable in your controller, if you don't want this you could create a new promise with the $q service
return MyService.promise;
}
}})
}):
ตัวควบคุมของคุณจะไม่ถูกสร้างขึ้นก่อนที่การพึ่งพาทั้งหมดจะได้รับการแก้ไข:
app.controller('MainCtrl', function($scope,MyService) {
console.log('Promise is now resolved: '+MyService.doStuff().data)
$scope.data = MyService.doStuff();
});
ฉันได้สร้างตัวอย่างที่ plnkr: http://plnkr.co/edit/GKg21XH0RwCMEQGUdZKH?p=preview
คำตอบ #2
อิงตามวิธีแก้ปัญหาของ Martin Atkins นี่คือวิธีแก้ปัญหา Angular ล้วนที่สมบูรณ์และกระชับ:
(function() {
var initInjector = angular.injector(['ng']);
var $http = initInjector.get('$http');
$http.get('/config.json').then(
function (response) {
angular.module('config', []).constant('CONFIG', response.data);
angular.element(document).ready(function() {
angular.bootstrap(document, ['myApp']);
});
}
);
})();
โซลูชันนี้ใช้ฟังก์ชันนิรนามที่ทำงานเองเพื่อรับบริการ $http, ขอการกำหนดค่า, และฉีดมันเข้าไปในค่าคงที่ที่เรียกว่า CONFIG เมื่อมันพร้อมใช้งาน
เมื่อเสร็จสมบูรณ์แล้ว เรารอจนกว่าสเอกสารจะพร้อม จากนั้นจึงบูตแอป Angular
นี่เป็นการปรับปรุงเล็กน้อยจากวิธีแก้ของมาร์ติน ซึ่งเลื่อนการดึงค่ากำหนดไปหลังจากที่เอกสารพร้อม เท่าที่ฉันทราบ ไม่มีเหตุผลที่จะต้องเลื่อนการเรียก $http สำหรับเรื่องนั้น
การทดสอบหน่วย
หมายเหตุ: ฉันได้ค้นพบว่าวิธีแก้ปัญหานี้ใช้ไม่ได้ดีเมื่อทำการทดสอบหน่วยเมื่อโค้ดถูกรวมอยู่ในไฟล์ app.js ของคุณ เหตุผลสำหรับเรื่องนี้คือโค้ดด้านบนจะทำงานทันทีเมื่อไฟล์ JS ถูกโหลด ซึ่งหมายความว่าเฟรมเวิร์กการทดสอบ (Jasmine ในกรณีของฉัน) ไม่มีโอกาสในการจัดเตรียมการจำลองการทำงานของ $http
วิธีแก้ของฉัน ซึ่งฉันยังไม่พอใจเต็มที่ คือการย้ายโค้ดนี้ไปที่ไฟล์ index.html ของเรา เพื่อให้โครงสร้างพื้นฐานการทดสอบหน่วย Grunt/Karma/Jasmine ไม่เห็นมัน
คำตอบ #3
ฉันใช้วิธีที่คล้ายกับที่ @XMLilley อธิบายไว้ แต่ต้องการความสามารถในการใช้บริการของ AngularJS เช่น $http เพื่อโหลดการตั้งค่าและทำการเริ่มต้นเพิ่มเติมโดยไม่ต้องใช้ API ระดับต่ำหรือ jQuery
การใช้ resolve บนเส้นทางก็ไม่ใช่ตัวเลือกเช่นกัน เพราะฉันต้องการให้ค่าพร้อมใช้งานเป็นค่าคงที่เมื่อแอปของฉันเริ่มทำงาน แม้กระทั่งในบล็อก module.config()
ฉันสร้างแอป AngularJS ขนาดเล็กที่โหลดการตั้งค่า กำหนดเป็นค่าคงที่ในแอปจริง และบูตสแตรปมัน
// define the module of your app
angular.module('MyApp', []);
// define the module of the bootstrap app
var bootstrapModule = angular.module('bootstrapModule', []);
// the bootstrapper service loads the config and bootstraps the specified app
bootstrapModule.factory('bootstrapper', function ($http, $log, $q) {
return {
bootstrap: function (appName) {
var deferred = $q.defer();
$http.get('/some/url')
.success(function (config) {
// set all returned values as constants on the app...
var myApp = angular.module(appName);
angular.forEach(config, function(value, key){
myApp.constant(key, value);
});
// ...and bootstrap the actual app.
angular.bootstrap(document, [appName]);
deferred.resolve();
})
.error(function () {
$log.warn('Could not initialize application, configuration could not be loaded.');
deferred.reject();
});
return deferred.promise;
}
};
});
// create a div which is used as the root of the bootstrap app
var appContainer = document.createElement('div');
// in run() function you can now use the bootstrapper service and shutdown the bootstrapping app after initialization of your actual app
bootstrapModule.run(function (bootstrapper) {
bootstrapper.bootstrap('MyApp').then(function () {
// removing the container will destroy the bootstrap app
appContainer.remove();
});
});
// make sure the DOM is fully loaded before bootstrapping.
angular.element(document).ready(function() {
angular.bootstrap(appContainer, ['bootstrapModule']);
});
ดูการใช้งานจริง (โดยใช้ $timeout แทน $http) ที่นี่: http://plnkr.co/edit/FYznxP3xe8dxzwxs37hi?p=preview
อัปเดต
ผมขอแนะนำให้ใช้แนวทางที่อธิบายไว้ด้านล่างโดย Martin Atkins และ JBCP
อัปเดต 2
เพราะว่าฉันต้องการมันในหลายโปรเจกต์ ฉันจึงเพิ่งปล่อยโมดูล bower ที่ดูแลเรื่องนี้: https://github.com/philippd/angular-deferred-bootstrap
ตัวอย่างที่โหลดข้อมูลจากแบ็คเอนด์และตั้งค่าคงที่ที่เรียกว่า APP_CONFIG บนโมดูล AngularJS:
deferredBootstrapper.bootstrap({
element: document.body,
module: 'MyApp',
resolve: {
APP_CONFIG: function ($http) {
return $http.get('/api/demo-config');
}
}
});
คำตอบ #4
กรณี "manual bootstrap" สามารถเข้าถึงบริการของ Angular ได้โดยการสร้าง injector ด้วยตนเองก่อนการ bootstrap. injector เริ่มต้นนี้จะทำงานแบบอิสระ (ไม่เชื่อมกับองค์ประกอบใด ๆ) และรวมเพียงชุดย่อยของโมดูลที่ถูกโหลด. หากสิ่งที่คุณต้องการคือบริการหลักของ Angular เพียงอย่างเดียว การโหลด ng ก็เพียงพอ, ดังนี้:
angular.element(document).ready(
function() {
var initInjector = angular.injector(['ng']);
var $http = initInjector.get('$http');
$http.get('/config.json').then(
function (response) {
var config = response.data;
// Add additional services/constants/variables to your app,
// and then finally bootstrap it:
angular.bootstrap(document, ['myApp']);
}
);
}
);
คุณสามารถใช้กลไก module.constant เพื่อทำให้ข้อมูลพร้อมใช้งานสำหรับแอปของคุณได้ ตัวอย่างเช่น:
myApp.constant('myAppConfig', data);
ตอนนี้ myAppConfig สามารถถูกฉีดเข้าไปใช้งานได้เหมือนกับบริการอื่น ๆ และโดยเฉพาะอย่างยิ่งมันสามารถใช้งานได้ในช่วงขั้นตอนการกำหนดค่า:
myApp.config(
function (myAppConfig, someService) {
someService.config(myAppConfig.someServiceConfig);
}
);
หรือสำหรับแอปขนาดเล็ก คุณสามารถฉีดการตั้งค่าระดับโลกโดยตรงไปยังบริการของคุณ แต่อย่างไรก็ตามจะต้องแลกกับการกระจายความรู้เกี่ยวกับรูปแบบการตั้งค่าทั่วทั้งแอปพลิเคชัน
แน่นอน เนื่องจากการทำงานแบบอะซิงก์ที่นี่จะบล็อกการบูตสแตรปของแอปพลิเคชัน และดังนั้นจะบล็อกการคอมไพล์/ลิงก์ของเทมเพลต จึงเป็นเรื่องฉลาดที่จะใช้ไดเรกทีฟ ng-cloak เพื่อป้องกันไม่ให้เทมเพลตที่ยังไม่ถูกประมวลผลแสดงผลในระหว่างทำงาน คุณยังสามารถให้สัญลักษณ์การโหลดบางอย่างใน DOM โดยการให้ HTML บางส่วนที่จะแสดงขึ้นเฉพาะจนกว่า AngularJS จะเริ่มทำงาน:
<div ng-if="initialLoad">
<!-- initialLoad never gets set, so this div vanishes as soon as Angular is done compiling -->
<p>Loading the app.....</p>
</div>
<div ng-cloak>
<!-- ng-cloak attribute is removed once the app is done bootstrapping -->
<p>Done loading the app!</p>
</div>
ฉันสร้างตัวอย่างที่สมบูรณ์และใช้งานได้ของวิธีการนี้บน Plunker โดยโหลดการตั้งค่าจากไฟล์ JSON แบบคงที่เป็นตัวอย่าง
คำตอบ #5
ฉันมีปัญหาเดียวกัน: ฉันชอบวัตถุ resolve แต่สิ่งนั้นใช้ได้แค่กับเนื้อหาของ ng-view แล้วถ้าคุณมีตัวควบคุม (เช่น สำหรับการนำทางระดับบน) ที่อยู่ภายนอก ng-view และต้องถูกเริ่มต้นด้วยข้อมูลก่อนที่การกำหนดเส้นทางจะเริ่มขึ้นล่ะ? เราจะหลีกเลี่ยงการปรับเปลี่ยนฝั่งเซิร์ฟเวอร์เพียงเพื่อทำให้สิ่งนั้นใช้งานได้อย่างไร?
ใช้การบูทสแตรปแบบแมนนวลและค่าคงที่ของ Angular XHR แบบง่าย ๆ จะช่วยคุณดึงข้อมูล และคุณบูทสแตรป Angular ใน callback ของมัน ซึ่งช่วยจัดการกับปัญหาแบบอะซิงโครนัสของคุณ ในตัวอย่างด้านล่าง คุณไม่จำเป็นต้องสร้างตัวแปรแบบ global เลย ข้อมูลที่ได้จะมีอยู่เฉพาะใน scope ของ Angular ในรูปแบบที่สามารถ inject ได้ และจะไม่ปรากฏใน controllers, services เป็นต้น เว้นแต่ว่าคุณจะ inject มันเข้าไป (คล้ายกับการ inject ผลลัพธ์ของ resolve ลงใน controller สำหรับ routed view) หากคุณต้องการใช้งานข้อมูลนั้นเป็น service หลังจากนั้น คุณสามารถสร้าง service, inject ข้อมูล และไม่มีใครรู้ตัว
ตัวอย่าง :
//First, we have to create the angular module, because all the other JS files are going to load while we're getting data and bootstrapping, and they need to be able to attach to it.
var MyApp = angular.module('MyApp', ['dependency1', 'dependency2']);
// Use angular's version of document.ready() just to make extra-sure DOM is fully
// loaded before you bootstrap. This is probably optional, given that the async
// data call will probably take significantly longer than DOM load. YMMV.
// Has the added virtue of keeping your XHR junk out of global scope.
angular.element(document).ready(function() {
//first, we create the callback that will fire after the data is down
function xhrCallback() {
var myData = this.responseText; // the XHR output
// here's where we attach a constant containing the API data to our app
// module. Don't forget to parse JSON, which `$http` normally does for you.
MyApp.constant('NavData', JSON.parse(myData));
// now, perform any other final configuration of your angular module.
MyApp.config(['$routeProvider', function ($routeProvider) {
$routeProvider
.when('/someroute', {configs})
.otherwise({redirectTo: '/someroute'});
}]);
// And last, bootstrap the app. Be sure to remove `ng-app` from your index.html.
angular.bootstrap(document, ['NYSP']);
};
//here, the basic mechanics of the XHR, which you can customize.
var oReq = new XMLHttpRequest();
oReq.onload = xhrCallback;
oReq.open("get", "/api/overview", true); // your specific API URL
oReq.send();
})
ตอนนี้ ค่าคงที่ NavData ของคุณมีอยู่แล้ว ดำเนินการต่อและฝังมันเข้าไปในคอนโทรลเลอร์หรือเซอร์วิส:
angular.module('MyApp')
.controller('NavCtrl', ['NavData', function (NavData) {
$scope.localObject = NavData; //now it's addressable in your templates
}]);
แน่นอน การใช้วัตถุ XHR เปล่า ๆ จะตัดคุณสมบัติบางอย่างที่ $http หรือ JQuery จัดการให้คุณออกไป แต่ตัวอย่างนี้ทำงานได้โดยไม่ต้องพึ่งพาไลบรารีพิเศษอย่างน้อยสำหรับ get แบบง่าย ๆ ถ้าคุณต้องการความสามารถมากขึ้นสำหรับคำขอของคุณ ให้โหลดไลบรารีภายนอกมาช่วย แต่ผมไม่คิดว่าจะสามารถเข้าถึง $http ของ angular หรือเครื่องมืออื่น ๆ ในบริบทนี้ได้
(โพสต์ที่เกี่ยวข้องกับ SO)
