RhoMobile applications typically use a RhoConnect server to synchronize data with backend systems but they can also retrieve and upload data from other services. In a warehouse application, for example, while you may use RhoConnect to manage products, you can integrate a different service to retrieve pre-processed data in order to build a dashboard interface, or see orders related to a particular customer.
A network call being synchronous or asynchronous refers to whether your code continues executing immediately after the call is made or it waits for the result before continuing.
Synchronous calls block execution until a result is received (or the operation times out). Asynchronous calls, on the other hand, return control to your code immediately. When the response is received (or an error occurs), a callback is invoked with the result.
Synchronous calls are conceptually very easy to work with but they have a very important drawback: your application will appear to hang until the request completes.
RhoMobile includes its own Network API with facilities for making GET and POST requests.
Ruby: :::ruby # synchronous request response = Rho::Network.get({ :url => “http://www.example.com” })
# the following line will only be reached after a response is received # or the request ends with an error if response["status"] == "ok" puts "Request complete, the server returned status code #{response["http_error"]} and body #{response["body"]}"" else puts "An error occurred" end
JavaScript: :::javascript // synchronous request var response = Rho.Network.get({ url : “http://www.example.com” });
# the following line will only be reached after a response is received # or the request ends with an error if (response.status) === "ok" alert("Request complete, the server returned status code "+params.http_error+" and body "+params.body); else alert("An error occurred"); end
Ruby: :::ruby # asynchronous request def get_request response = Rho::Network.get({ :url => “http://www.example.com” }, url_for(:action => :network_request_callback))
puts "The above code will not wait for the request to complete. This line will be executed immediately" end def network_request_callback if @params["status"] == "ok" puts "Request complete, the server returned status code #{response["http_error"]} and body #{response["body"]}"" else puts "An error occurred" end end
JavaScript: :::javascript // asynchronous request function get_request() { var response = Rho.Network.get({ url : “http://www.example.com”, }, network_request_callback); }
function network_request_callback(params) { if (params.status==="ok") { alert("Request complete, the server returned status code "+params.http_error+" and body "+params.body); } else { alert("An error occurred"); } }
Your request will be synchronous or asynchronous depending on whether you pass the callback
parameter
In general, asynchronous requests are preferable. Many times, however, you need the data before the user can continue working; in this case, the following pattern can be used:
jQuery also includes convenient APIs for making AJAX requests from JavaScript: jQuery.ajax
and jQuery.getJSON
function perform_ajax_request() { $.ajax({ url: "/app/MyController/get_data" }).done(function(data) { parsed = ...; // parse the received string alert("Customer of the day: "+parsed.customer_of_the_day+" with "+parsed.total_orders+" orders"); }).fail(function(request, textStatus, errorThrown) { alert("An error occurred: "+errorThrown); }).always(function() { alert("Request completed"); }); }
This snippet will:
* invoke the get_data
action in MyController
* if the request is successful, parse the received data and show it
* if an error occurred, show a message
* notify the user that the request completed (wheter it was successful or not)
You can return data from your controller in any format, but the easiest way to interchange information between JavaScript and Ruby is to use JSON; see the following example:
def get_data result = ::JSON.generate({ :customer_of_the_day => "John Doe", :total_orders => 42 }) # this is necessary because by default, controllers render their view together with a layout # in an AJAX request, we only want to return our data formatted as JSON but not the layout render :string => result, :use_layout_on_ajax => true end :::javascript function perform_json_request() { $.getJSON("/app/MyController/get_data") .done(function(data) { // the response is parsed as JSON and provided as the data parameter alert("Customer of the day: "+data.customer_of_the_day+" with "+data.total_orders+" orders"); }); }
One drawback of using jQuery.ajax
instead of the RhoMobile Network API is that, for security reasons, JavaScript code runs inside a sandbox. One of the rules of this sandbox, called the “same-origin policy”, mandates that code loaded from one origin (i.e., www.example.com) cannot communicate with code loaded from a different origin (www.example.net). This has the practical consequence that you can only make AJAX calls to the same server that the current page was loaded from, which in a native RhoMobile application case is the mobile device itself (localhost
). If for some reason you cannot or prefer not to use the RhoMobile Network API, there are several ways to work around this limitation: JSONP, CORS and using Ruby code as a local proxy.
If you need to make cross-domain requests but do not want to use the RhoMobile Network API in JavaScript, using Ruby code as a local proxy is usually the easiest approach in a native RhoMobile application
Although by default you can only make AJAX requests to the embedded server in a RhoMobile application, because your controllers can execute any code you need, you can easily create a custom controller that receives AJAX requests from the JavaScript in your page, connects to a remote server using the Network API as seen above and returns the results back to JavaScript. This is the only scenario where you should prefer to use synchronous requests.
Example:
Ruby controller: :::ruby def get_data request = Rho::Network.get({ :url => “http://www.example.com/get_data” })
# supposing our server returns valid JSON, we can just relay it to our JavaScript # otherwise we would parse the response, build our own result value and if request["status"]=="ok" result = request["body"] else result = {:status => "error"} end # this is necessary because by default, controllers render their view together with a layout # in an AJAX request, we only want to return our data formatted as JSON but not the layout render :string => result.to_json, :use_layout_on_ajax => true end
JavaScript: :::javascript function perform_json_request() { $.getJSON(“/app/MyController/get_data”) .done(function(data) { // the response is parsed as JSON and provided as the data parameter alert(“Customer of the day: ”+data.customer_of_the_day+“ with ”+data.total_orders+“ orders”); }); }
Although the sandbox does not allow AJAX requests to a different origin, it is possible to include <script>
tags in your HTML that do not necessarily have to come from the same server as your page. In regular web applications, it is common for a site to include common scripts like jQuery from a Content Delivery Network in order to take advantage of the browser cache:
<script src="http://code.jquery.com/jquery-1.10.1.min.js"></script>
While the page itself can reside on any domain, the script will be loaded from code.jquery.com
and executed.
Taking advantage of this fact, we can dynamically create a <script>
tag linking to an action in one of our controllers:
function call_json_action() { $("body").append("<script src='/app/MyController/get_data'></script>"); }
When the <script>
tag is added to the body
, the script it points to will be downloaded and executed, and our controller will return the data we want to use. However, the controller cannot return plain JSON because it would not do anything by itself: the browser would get the values but they would not be passed to our code anywhere.
Here is how JSONP solves this problem: together with the data, it includes a call to a function that will receive the payload:
JSON: :::javascript { “key” : “value”}
JSONP: :::javascript callback({“key” : “value”});
In the above example, the padding (the P
in JSONP
) is callback
but it can be anything. In fact, it is common to implement JSONP on the server side such that it takes a parameter called callback
that will determine the name of the function:
JavaScript: :::javascript function call_json_action() { $(“body”).append(“”); }
function process_values(data) { // the name of this function matches the callback name specified above }
Ruby controller: :::ruby def get_data json = ::JSON.generate({ “key” : “value”}) jsonp = “#{@params["callback”]}(#{json});“ render :string => jsonp, :use_layout_on_ajax => true end
The ideal use cases for deciding to implement JSONP are:
Using JSONP comes with some security implications: nothing guarantees that a server actually returns the data you expect instead of malicious code. You should only include scripts (and therefore, JSONP links) from servers that are under your control or that you completely trust. If the backend is compromised, it could return arbitrary JavaScript code to your application, potentially compromising it, too.
Due to limitations with JSONP, a new standard is emerging: Cross Origin Resource Sharing (CORS). CORS has the advantage of having been designed explicitly for the task of making requests across different origins. When the browser notes that the destination is not the same as the origin of the current page and sends an OPTIONS request with an additional Origin
header:
Origin: http://www.example.com
This Origin
header tells the destination server which domain the request comes from. If the server wants to allow the request, its response will include an Access-Control-Allow-Origin
header:
Access-Control-Allow-Origin: *
or
Access-Control-Allow-Origin: http://www.example.org
CORS has the advantage that it supports POST as well as other HTTP methods, unlike JSONP which can only make GET requests.
When implementing CORS on your server, receiving an
Origin
header is not, by itself, sufficient guarantee that the request actually came from a user on that site: remember that an attacker can create custom requests outside of a web browser. Do not send any sensitive information without further authentication.