Using JSON web services

In this section, we code a web server that communicates with our clients and runs the todo app; the todo data is sent to and from the web server in the JSON string format. Spiral s06 consists of a server and a client part. To run it, first start the server (lib/server/server.dart) in Dart Editor or from the console; it runs when you see in the server.dart tab in Dart Editor: Listening for GET and POST on http://127.0.0.1:8080 (If it does not run, use run/manage launches). Then, start one or more clients (web/app.html) in Dartium. Locally, the client still saves the data in IndexedDB. Our screen has two new buttons:

  • To server: The client converts the data into the JSON format and sends it to the server, where the data is stored in the main memory (post data to server)
  • From server: Another client (on a different machine) can request the server data to update its local database (get data from the server)

This is how the tasks application at this stage looks like:

Using JSON web services

Server communication in Spiral s06

The following is the client code (from lib/view/view.dart) for To server (posting data):

ButtonElement toServer = querySelector('#to-server'),
    toServer.onClick.listen((MouseEvent e) {
      var request = new HttpRequest();                         (1)
      request.onReadyStateChange.listen((_) {                  (2)
        if (request.readyState == HttpRequest.DONE &&
            request.status == 200) {
          // Data saved OK on server.
          serverResponse = 'Server: ' + request.responseText;
        } else if (request.readyState == HttpRequest.DONE &&
            request.status == 0) {
          // Status is 0...most likely the server isn't running.
          serverResponse = 'No server';
        }
      });

      var url = 'http://127.0.0.1:8080';
      request.open('POST', url);                               (3)
      request.send(_tasksStore.tasks.toJsonString());          (4)
    });

In line (1), a new client request is made. From line (3), we see that the method is POST. In line (4), the data from the tasks collection is sent to the server. Then, the client listens to a possible server response ( status and responseText) in the onReadyStateChange event. HttpStatus 200 indicates that everything went fine. The code for fromServer (getting data) is shown as follows:

ButtonElement fromServer = querySelector('#from-server'),
fromServer.onClick.listen((MouseEvent e) {
      HttpRequest.getString('http://127.0.0.1:8080')
        .then((result) {                                    (5)
          String jsonString = result;
          serverResponse = 'Server: ' + result;
          print('JSON text from the server: ${jsonString}'),
          if (jsonString != '') {
            List<Map> jsonList = JSON.decode(jsonString);   (6)
            print('JSON list from the server: ${jsonList}'),
            _tasksStore.loadFromJson(jsonList)              (7)
              .then((_) {
                var tasks = _tasksStore.tasks;
                _clearElements();
                loadElements(tasks);
              })
              .catchError((e) {
                                print('error in loading data into IndexedDB from JSON list'),
           });
       }
    });
});

We get the data in the JSON format from the server with the getString method. The response from the server containing the data is stored in result in line (5), decoded in List<Map> in line (6), and added to IndexedDB through the loadFromJson method (line (7)). Of course, the print statements are only needed as a way to log what takes place and what can be left out. We will improve this code in Spiral s07.

However, what happens on the server? The server is started through the following code:

import 'dart:io';
import 'dart:convert';

const String HOST = "127.0.0.1"; // or: "localhost"
const int PORT = 8080;
List<Map> jsonList;

void main() {
  start();
}

start() {
  HttpServer.bind(HOST, PORT).then((server) {
    server.listen((HttpRequest request) {
      switch (request.method) {                               (1)
        case 'GET':
          handleGet(request);
          break;
        case 'POST':
          handlePost(request);
          break;
        case 'OPTIONS':
          handleOptions(request);
          break;
        default: defaultHandler(request);
      }
    },
    onError: print);                                         (2)
  })
   	 .catchError(print)
    .whenComplete(() => print('Listening for GET and POST on http://$HOST:$PORT'));
}

In line (1) in the listen handler, we match the method of the request. Notice in line (2), the onError handler, which is, in fact, the second optional parameter of the listen method (onError: print could also be written as onError: (e) => print(e) to see an error while trying to start the server on port 80. Then, you get SocketException). Everything between lines (1) and (2) is the (anonymous) onData handler of listen.

In the To server situation, handlePost is executed:

void handlePost(HttpRequest request) {
  print('${request.method}: ${request.uri.path}'),
  request.listen((List<int> buffer) {                          (3)
    var jsonString = new String.fromCharCodes(buffer);
    jsonList = JSON.decode(jsonString);                        (4)
    print('JSON list in POST: ${jsonList}'),                   (5)
  },
  onError: print);
}

Here, in the listen handler the client data is loaded in the buffer in line (3). It is then decoded to the List<Map> jsonList variable on the server storing the data in memory (line (4)). The server prints to its console in line (5):

POST: /
JSON list in POST: [{title: washing dishes, completed: true, updated: 2013-08-08 15:40:51.999}, {title: walking the dog, completed: true, updated: 2013-08-08 10:30:47.794}, {title: cleaning the kitchen, completed: false, updated: 2013-08-08 15:21:44.626}, {title: buying vegetables, completed: false, updated: 2013-08-08 10:32:20.707}]

In the From server situation, handleGet is executed:

void handleGet(HttpRequest request) {
  HttpResponse res = request.response;                         (6)
  print('${request.method}: ${request.uri.path}'),
  addCorsHeaders(res);                                         (7)
  res.headers.contentType =
      new ContentType("application", "json", charset: 'utf-8'),(8)
  if (jsonList != null) {
    String jsonString = JSON.encode(jsonList);                 (9)
    print('JSON list in GET: ${jsonList}'),
    res.write(jsonString);                                    (10)
  }
  res.close();                                                (11)
}

Here, the response is prepared from line (6) onwards; line (8) sets the content type of the server response to the JSON text. In line (9), the jsonList server variable is encoded to a JSON string and written into the response stream in line (10), which is then closed in line (11). The server prints out the following:

GET: /
JSON list in GET: [{title: washing dishes, completed: true, updated: 2013-08-08 15:40:51.999}, {title: walking the dog, completed: true, updated: 2013-08-08 10:30:47.794}, {title: cleaning the kitchen, completed: false, updated: 2013-08-08 15:21:44.626}, {title: buying vegetables, completed: false, updated: 2013-08-08 10:32:20.707}]

The client then prints out:

JSON list from the server: ... same list as above ...

In line (7), the addCorsHeaders method adds the following so-called CORS (Cross Origin Resource Sharing) headers to the response:

void addCorsHeaders(HttpResponse response) {
  response.headers.add('Access-Control-Allow-Origin', '*, '),
  response.headers.add('Access-Control-Allow-Methods', 'POST, OPTIONS'),
  response.headers.add('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept'),
}

In order to prevent cross-site scripting attacks, browser vendors have added a same origin policy to their browsers. If your web page comes from a server at URL domain1, you can only send requests to the same domain1 (remember that a domain consists of a name and port, and not just name—localhost:8080 and localhost:8081 are two different domains). If the server sends CORS headers back in the response, the client can also send requests to other servers. In general, it is not safe to use CORS headers. However, for development purposes, it is useful to allow them so that you can run apps from Dart Editor that uses 3030 by default for its internal server.

Spiral s07

But wait! Something is not yet right; the data of a new client overwrites on the server the data from a previous client, so we have to implement some form of data integration:

  • To server (POST) adds local tasks without conflicting titles to data on the server
  • From server (GET) removes tasks with conflicting titles from local data and adds tasks without conflicting titles to local data

In handlePost on the server (bin/server.dart), we will now call _integrateDataFromClient(jsonList), which contains the algorithm for the merging of tasks:

_integrateDataFromClient(List<Map> jsonList) {
  var clientTasks = new Tasks.fromJson(jsonList);
  var serverTasks = tasks;
  var serverTaskList = serverTasks.toList();
  for (var serverTask in serverTaskList) {
    if (!clientTasks.contains(serverTask.title)) {
      serverTasks.remove(serverTask);
    }
  }
  for (var clientTask in clientTasks) {
    if (serverTasks.contains(clientTask.title)) {
      var serverTask = serverTasks.find(clientTask.title);
      if (serverTask.updated.millisecondsSinceEpoch <
          clientTask.updated.millisecondsSinceEpoch) {
        serverTask.completed = clientTask.completed;
        serverTask.updated = clientTask.updated;
      }
    } else {
      serverTasks.add(clientTask);
    }
  }
}

This means that the server now has to know about the model (class Task/Tasks) to realize that we share the model between the client and server by making it into a library (lib/shared_model.dart):

library shared_model;
import 'dart:convert';
part 'model/model.dart';

By importing this in server.dart and idb_client.dart as well:

import 'package:client_server/shared_model.dart';

First, start the server and then two or more clients, for example, the first in Dartium and the second in Chrome or another browser (run as JavaScript) (see doc/use.txt). Then, juggle a few tasks between them!

As an improvement to the completeTasks method in Spiral s03, we now have the complete method, which guarantees that it will wait until all the update tasks are finished by using Future.wait on a futureList list of all the following tasks:

Future complete() {
    var futureList = new List<Future>();
    for (var task in tasks) {
      if (!task.completed) {
        task.completed = true;
        task.updated = new DateTime.now();
        futureList.add(update(task));
      }
    }
    return Future.wait(futureList);
}
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.223.172.132