Communication Go-Javascript en cross-domain et champs privés

Echanger en json des objets entre un client javascript et un serveur écrit en go est facile grace aux packages http et json. Je décris ici une mise en place simple et cross-domain de cet échange tout en tirant parti de la casse pour séparer les champs privés et publics.

J’utilise jquery dans ce document mais il est aisé de s’en passer.

Côté serveur en go, supposons que vous ayez deux structures de données, une pour les messages entrants et une pour les messages sortants :

type MessageIn struct {
	TrucA	   uint
	TrucB      string
	TrucLourd  *DonnéesComplexes
}

type DonnéesComplexes struct {
	PleinDeChoses  []Chose
	DesBidules     []Bidules
	donnéesPrivées StructureUtiliséeCôtéServeur
}

type MessageOut struct {
	Erreur    string
	Machins   []string
	Text      string
}

Dans cette exemple TrucLourd est un pointeur, ainsi le décodeur json n’alloue-t-il pas une structure (lourde) inutile si le message du client ne la contient pas. La même logique pourrait être appliquée si necessaire dans la structure MessageOut (celle devant contenir la réponse du serveur au client).

Le code du serveur est très simple :

package main

import (
	"encoding/json"
	"fmt"
	"net/http"
)

const (
	port = 8001
)

type Serveur struct {
}

func getFormValue(hr *http.Request, name string) string {
	values := hr.Form[name]
	if len(values) > 0 {
		return values[0]
	}
	return ""
}

func envoieRéponse(w http.ResponseWriter, out *MessageOut) {
	bout, err := json.Marshal(out)
	if err != nil {
		fmt.Println("Erreur encodage réponse : ", err)
		return
	}
	fmt.Fprint(w, "recoitDuServeur(")
	w.Write(bout)
	fmt.Fprint(w, ")")
}

func (ms *Serveur) ServeHTTP(w http.ResponseWriter, hr *http.Request) {
	w.Header().Set("Access-Control-Allow-Origin", "*")
	w.Header().Set("Access-Control-Request-Method", "GET")
	hr.ParseForm()
	in := new(MessageIn)
	out := new(MessageOut)
	defer envoieRéponse(w, out)
	bin := ([]byte)(getFormValue(hr, "in"))
	err := json.Unmarshal(bin, in)
	if err != nil {
		out.Erreur = "Erreur décodage : " + err.Error()
		return
	}
	traiteLesDonnées(in, out)
}

func (server *Serveur) Start() {
	http.Handle("/", server)
	fmt.Printf("le serveur démarre sur le port %d\n", port)
	err := http.ListenAndServe(":"+strconv.Itoa(port), nil)
	if err != nil {
		fmt.Println("Erreur au lancement : ", err)
	}
}

func main() {
	ms := new(Serveur)
	ms.Start()
}

Tout est là, hormis la logique « métier » déportée dans traiteLesDonnées.

L’encodage en json, fait via json.Mashal n’encode que les données publiques (dont le nom commence par une majuscule) et transmet correctement nombres, tableaux, structures pointées, etc.

Les deux premières lignes de la fonction ServeHTTP assurent un traitement correct des requêtes en cross-domain.

L’utilisation du defer envoieRéponse permet de ne pas répéter cet appel, en particulier si l’on multiplie les conditions de sortie (et les return)

Le protocole de communication, indispensable pour le cross-domain, est JSONP, d’où, dans la réponse, l’encapsulation du json généré dans un appel à une fonction javascript (recoitDuServeur).

Côté client javascript, le code n’est guère plus complexe :

// message est un objet contenant les champs attendus par le serveur
// dans MessageIn (ou une partie)
function envoieAuServeur(message) {
	$.ajax(
		{
			url: URL_SERVEUR + '?in='+JSON.stringify(message),
			crossDomain: true,
			dataType: "jsonp"
		}
	);
	return true;
}

function recoitDuServeur(message) {
	// traitement des données recues en réponse
}

En fait, une partie de la « science » se trouve dans une fonction destinée à produire l’objet message et visant à assurer :

  • que l’objet message ne contient pas des prototypes
  • que les champs dont le nom commence par des minuscules soient exclus
  • que les 0 et nulls (inutiles) ne soient pas envoyés

Non seulement les champs dont le nom commence par une minuscule seraient inutiles car non reçus dans la structure (il existe des moyens mais ils ne m’intéressent pas) mais surtout il s’avère extrêmement commode de prolonger côté javascript cette logique d’identifier les champs publics (c’est-à-dire vus par l’autre partie) par leur majuscule. On peut ainsi facilement, des deux côté, les reconnaître lors de la programmation ou du débug et il n’y a pas de raison de se retenir, en particulier lorsque l’on manipule des structures un peu lourdes, d’intégrer aux données reçues de l’autre partie des données utiles localement (en particulier des données calculées cachées pour des raisons de performances).

J’utilise donc pour construire mon message sortant (ou des parties de mon message) la fonction suivante en javascript :

// bâtit une copie indépendante et transmissible en json à un serveur go :
// effectue une deep-copy de l'objet source mais en ignorant tous les
//  champs dont le nom ne commence pas par une majuscule. Ne copie pas
//  les prototypes ni les champs nulls (ou 0).
function goclone(source) {
	if ($.isArray(source)) {
		var clone = [];
		for (var i=0; i<source.length; i++) {
			if (source[i]) clone[i] = goclone(source[i]);
		}
		return clone;
	} else if (typeof(source)=="object") {
		var clone = {};
		for (var prop in source) {
			if (source[prop]) {
				var firstChar = prop.charAt(0);
				if (firstChar!=firstChar.toUpperCase()) continue;
				clone[prop] = goclone(source[prop]);
			}
		}
		return clone;
	} else {
		return source;
	}
}

Si vous voulez voir les sources de l’application réelle dont j’ai extrait cet exemple, voici

Une remarque pour vos tests, n’oubliez pas que peu de choses fonctionnent si vous ouvrez vos fichiers html en file://, il faut passer par un serveur http.

Les commentaires sont fermés.