XMLHttpRequest y XML

Aún tengo muchas dudas con AJAX especialmente en la parte que gestiona los XML. Para intentar aclararme me he propuesto hacer un ejemplo comparativo para recuperar y cargar en el DOM documentos XML y/o HTML. Tengo estos documentos de prueba que puede también leer directamente con el navegador:

  • prueba.xml, un XML sin espacios de nombres (namespaces).
  • parque.xml, otro XML con espacios de nombres que ya había usado como ejemplo en el Glosario XHTML+CSS.
  • html.html, un documento HTML-4.01 que es servido con el tipo de contenido text/html.
  • xhtml.html, un documento XHTML-1.0 que es servido con el tipo de contenido text/html.
  • xhtml.xhtml, un documento XHTML-1.0 que es servido con el tipo de contenido application/xhtml+xml (este no se abre con IE8 o anterior).

Los documentos de ejemplo HTML son los que había usado en el artículo que habla de las diferencias entre HTML y XHTML, en el apartado de Diferencias en la presentación XHTML.

Con responseXML podemos cargar en el DOM del navegador los nodos de un XML. Luego podemos acceder a ellos por ejemplo con el método getElementsByTagName(tag), childNodes, firstChild, etc. Otros métodos usuales como getElementById() me dan algunos problemas. Pero también existe la posibilidad de usar XPath que es otra forma de seleccionar nodos. Sin embargo no he podido aclararme puesto que los navegadores actuales no se comportan igual. Navegadores actualizados que uso hoy 3 septiembre 2011: Internet Explorer 8.0 (IE), Firefox 6.0 (FF), Google Chrome 13.0 (CH), Safari 5.0 (SA) y Opera 11.51 (OP). Aún no tengo instalado Windows 7 por lo que no puedo probar esto en IE9.

Un objeto para gestionar XML: objHttp

He creado el módulo JavaScript gestor-xml.js que maneja un objeto que denomino objHttp con los siguientes objetivos:

  1. Crear objetos basados en XMLHttpRequest para cargar documentos XML.
  2. Seleccionar nodos con los métodos del DOM y también con XPath. Unificar distintos comportamientos de los navegadores para el método getElementById() así como los selectNodes() y selectSingleNode() de XPath.
  3. Unificar el comportamiento ante los espacios de nombres, especialmente con los espacios de nombres por defecto (llamados a veces espacios de nombres nulos).

Luego hacemos una prueba seleccionando nodos y comprobando su correcto funcionamiento. Con documentos XML servidos como text/xml es suficiente cargar el responseXML del objeto XMLHttpRequest. Pero para documentos text/html y application/xhtml+xml la cosa se complica. Para IE uso una carga alternativa en el objeto ActiveXObject("msxml2.DOMDocument.6.0") a partir del responseText del XMLHttpRequest. Para otros navegadores cargo ese texto en el DOMParser. Ambos permiten cargar en el DOM los nodos de un literal de texto XML. Los resultados se muestran en la siguiente tabla, que se actualizarán con el botón, pudiendo recargar los objetos con alternativas o forzando a que sólo use XMLHttpRequest. En la cabecera de este documento podemos encontrar este script:

var docs = new Array();
var urls = new Array("ejemplos/prueba.xml", "ejemplos/parque.xml", "ejemplos/html.html",
        "ejemplos/xhtml.html","ejemplos/xhtml.xhtml");
function recargarObjetos(){
    var alternativas = !document.getElementById("alterna").checked;
    for (var i=0; i< urls.length; i++){
        docs[i] = new objHttp(urls[i], alternativas);
        docs[i].ejecutaEventoListo = function (){
            if (docs[0].cargado && docs[1].cargado && 
            docs[2].cargado && docs[3].cargado &&
            docs[4].cargado) llenarPruebas();  
        }
        docs[i].crear();
    }
}

function llenarPruebas() {
    var noFunciona = '<span class="rojo">No funciona</span>';

    //objeto cargado
    for (var i=0; i<docs.length; i++){
        document.getElementById("t0-" + i).innerHTML = docs[i].objetoXml;
    }
    ...
}

Se trata de construir los cinco objetos con new objHttp(urls[i], alternativas), sobreescribir el método docs[i].ejecutaEventoListo para que empiece a llenar la tabla cuando los cinco estén cargados y finalmente crear los objetos. La función llenarPruebas() es la que rellena la tabla.

Tabla de resultados XML cargado con XMLHttpRequest, msxml2.DOMDocument.6.0 o DOMParser

Aplicación

Archivoprueba.xmlparque.xmlhtml.htmlxhtml.htmlxhtml.xhtml
Objeto cargado
ContentType
NS original
NS final
(1) DOM - getElementsByTagName() - Obteniendo el elemento raíz. En estos ejemplos obj es nuestro objeto declarado que contiene el árbol de nodos en obj.doc, bien un responseXML del XMLHttpRequest o de algún otro alternativo (DOMParser o msxml2.DOMDocument.6.0). La letra R representa el resultado. Al actualizar tendremos en color azul estos resultados y deberán ser iguales que los esperados en color verde. El método obj.getElementsByTagName() se construye en el objeto aplicándose sin modificaciones al árbol de nodos del objeto. Es lo mismo que hacer obj.doc.getElementsByTagName() pero así evitamos una referencia intermedia. En este ejemplo obtenemos el nombre del nodo (nodeName) del primer nodo del árbol que viene a ser el elemento raíz.
AcciónR = obj.getElementsByTagName("*")[0].nodeName
Esperadopruebaparquehtml
Resultado
(2) DOM - getElementById() - Buscar documento con atributo ID. Se usa getInnerText() para obtener el texto interior. Nuevamente construyo obj.getElementById() sobre el objeto, pues sólo CH es capaz de obtener el elemento con este método del DOM, mientras que para IE-FF-SA-OP es necesario usar otro mecanismo.
AcciónR = getInnerText(obj.getElementById("idprueba"))
EsperadoTexto idprueba123456789Texto1
Resultado
(3) DOM - documentElement - Elemento raíz. Al objeto se le construye la propiedad documentElement que referencia al elemento raíz del documento. Esta propiedad se obtiene de la misma de igual nombre del responseXML o de sus alternativos cuando son cargados.
AcciónR = obj.documentElement.nodeName
Esperadopruebaparquehtml
Resultado
(4) DOM - firstChild - Primer hijo. En este caso recuperamos el nombre del nodo (nodeName) del primer hijo del elemento raíz. Hay que tener en cuenta que IE ignora los espacios entre elementos mientras que otros navegadores no. En este caso he escrito los XML-HTML con ambos elementos sin espacios para que FF-SA-CH-OP recuperen el primer hijo y no un nodo de tipo texto (#text).
AcciónR = obj.documentElement.firstChild.nodeName
Esperadonodo1cochehead
Resultado
(5) DOM - childNodes - Los hijos de un nodo. Recuperamos todos los hijos del elemento raíz, devolviendo el nodo cero que viene a ser el primer hijo del elemento raíz, como el ejemplo anterior. Para un HTML como <html><head>...</head><body>...</body></html> se recupera el elemento head. Veáse que no hay espacios entre <html> y <head> por lo que FF-SA-CH-OP no devuelven un nodo texto.
AcciónR = obj.documentElement.childNodes[0].nodeName
Esperadonodo1cochehead
Resultado
(6) DOM - Seleccionando con DOM y espacio de nombres. En este ejemplo usamos getElementsByTagName() para obtener algunos elementos sólo con métodos del DOM. Especialmente interesa recuperar un elemento con espacio de nombres como persona:nombre.
AcciónR = doc.getElementsByTagName(TAG)[0].nodeName
TAG = "nodo3"TAG = "persona:nombre"TAG = "style"
Esperadonodo3persona:nombrestyle
Resultado
(7) DOM/XPath - selectNodes(). Con XPath podemos seleccionar nodos de otra forma quizás más potente si sabemos usar las expresiones de XPath. Este método obj.selectNodes() lo construyo en el objeto con el método obj.doc.selectNodes() para IE mientras que para los otros hay que usar obj.doc.evaluate(). En estos ejemplos representamos con XPATH una expresión que viene a recorrer el árbol de nodos separando cada nivel por una barra. Aquí recuperamos el nombre del nodo de algunos elementos. Puede suceder que a un documento se le hayan declarado más de un espacio de nombres, siendo uno de ellos el espacio de nombres por defecto (también conocido como espacio de nombres nulo, aunque no es del todo correcto). En este caso debemos prefijar los nodos del espacio por defecto con "_:" en la expresión XPath (para que funcione en FF-SA-CH-OP, pues IE lo ignorará).
AcciónR = obj.selectNodes(XPATH)[0].nodeName
XPATH = "prueba/nodo1/nodo2"XPATH = "_:parque/
_:coche/_:nombre"
XPATH = "html/body/span"
Esperadonodo2nombrespan
Resultado
(8) DOM/XPath - selectNodes(). Este ejemplo es similar al anterior pero muestra como podemos recuperar un nodo con prefijo como persona:conductor en el documento parque.xml con dos espacios de nombres. Observe como también podemos usar tagName en lugar de nodeName para obtener el nombre del nodo.
AcciónR = obj.selectNodes(XPATH)[0].tagName
XPATH = "prueba/nodo1/nodo3"XPATH = "_:parque/
_:coche/persona:conductor"
XPATH = "html/body/a/img"
Esperadonodo3persona:conductorimg
Resultado
(9) DOM/XPath - selectNodes(). La función construida es obj.selectNodes(xpath, enNodo) de tal forma que si no se pasa el argumento enNodo se aplicará al elemento raíz. En este ejemplo usamos DOM para obtener un nodo y aplicar un XPath en él. Luego recuperamos el primero hijo con firstChild que será un nodo texto y obtenemos su valor con value (nodeValue) (lo mismo que hacer un getInnerText() sobre el nodo).
AcciónNODO = obj.getElementsByTagName(TAG)[0];
R = obj.selectNodes(XPATH, NODO)[0].firstChildnodeValue
TAG = "nodo1"
XPATH = "nodo3"
TAG = "coche"
XPATH = "_:nombre"
TAG = "body"
XPATH = "p/b[@id='bbb']"
EsperadoTexto 1/3Seat 0001AZestructura
Resultado
(10) DOM/XPath - selectSingleNode(). Este método nos permite seleccionar un único nodo que cumpla la expresión. En este ejemplo además se hace uso de la función global tipoNodo() que se encuentra en el módulo gestor-xml.js y que nos devuelve el nombre de la constante del tipo de nodo. Dado que se obtiene nodos, textos y atributos aplicamos la diyuntiva NODO.nodeValue||NODO.nodeName para recuperar bien el nombre del nodo o su valor según el tipo de nodo.
AcciónNODO = obj.selectSingleNode(XPATH);
R1 = "(" + NODO.nodeType + ") " + tipoNodo(NODO.nodeType);
R2 = (NODO.nodeValue||NODO.nodeName)
XPATH = "prueba/nodo1/nodo2[@id='otroid']"XPATH = "_:parque/_:transporte/
persona:nombre/text()"
XPATH = "html/head/link[@rel='icon']/@href"
Esperados(1) ELEMENT_NODE
nodo2
(3) TEXT_NODE
Juan Equis
(2) ATTRIBUTE_NODE
/icon.ico
Resultados

Comparativo de soportes XML

A raíz de los resultados anteriores en los navegadores que he usado, llego a la siguiente conclusión de nivel de soporte con estas versiones de navegadores Internet Explorer 8.0, Firefox 6.0, Google Chrome 13.0, Safari 5.0 y Opera 11.51:

NavegadorXML
(text/xml)
XML+namespaces
(text/xml)
HTML
(text/html)
XHTML
(text/html)
XHTML
(application/xhtml+xml)
Sólo usando el XMLResponse de XMLHttpRequest los navegadores ejecutarán:
ExplorerDOM+XPATHDOM+XPATHningunoningunoninguno
FirefoxDOM+XPATHDOM+XPATHningunoningunoDOM
OperaDOM+XPATHDOM+XPATHDOM+XPATHDOMDOM
SafariDOM+XPATHDOM+XPATHDOM+XPATHDOMDOM
ChromeDOM+XPATHDOM+XPATHDOM+XPATHDOMDOM
Si se permiten alternativas para que ejecuten DOM y XPATH, los navegadores usarán estos objetos:
ExplorerXMLHttpRequestXMLHttpRequestmsxml2.DOMDocument.6.0msxml2.DOMDocument.6.0msxml2.DOMDocument.6.0
FirefoxXMLHttpRequestXMLHttpRequestDOMParserDOMParserDOMParser
OperaXMLHttpRequestXMLHttpRequestXMLHttpRequestDOMParserDOMParser
SafariXMLHttpRequestXMLHttpRequestXMLHttpRequestDOMParserDOMParser
ChromeXMLHttpRequestXMLHttpRequestXMLHttpRequestDOMParserDOMParser

Por lo tanto si estamos trabajado con XML servidos como text/xml es suficiente con usar el responseXML del XMLHttpRequest. Pero si esos XML tienen espacios de nombres o bien queremos manejar un documento HTML o XHTML, ya no es suficiente con esto. Hay preguntas que yo sinceramente no sabría responder cuando manejamos estos HTML como árboles de nodos, o incluso para los XML cuando hay uno o más espacios de nombres:

  • ¿Porqué IE no acepta un DOCTYPE en los documentos xhtml.html y xhtml.xhtml?
  • ¿Porqué IE no trabaja con el espacio de nombres por defecto xmlns="http://www.w3.org/1999/xhtml" de esos documentos?
  • ¿Porqué IE en cambio si funciona si modificamos ese espacio de nombres a xmlns:="http://www.w3.org/1999/xhtml"?
  • ¿Porqué el resto de navegadores tienen problemas con los espacios de nombres por defecto, especialmente usando XPath?. Para estos navegadores (FF,SA,CH,OP) XPath se consigue con el método evaluate(). Pero sólo he podido hacerlo funcionar con espacios de nombres por defecto modificándolo. Por ejemplo, con xmlns="http://www.w3.org/1999/xhtml" lo he convertido en xmlns:_="http://www.w3.org/1999/xhtml", de tal forma que el espacio de nombres por defecto es un guion bajo. Pero entonces las expresiones XPath deben contenerlo, por ejemplo: "_:parque/_:coche/persona:conductor".

Propiedades y métodos del objHttp

El constructor es new objHttp(archivo, alternativas=true, navegador="", mostrarErrores=true, metodo="GET", asincrono=true).

En Diciembre 2013 he eliminado definitivamente la diferenciación por navegador de todos los JavaScript de este sitio. En el test observará que puede seleccionar manualmente el tipo de navegador pues no se realiza ninguna detección automática. He modificado el script agregando el argumento navegador para opcionalmente pasarle alguno entre "msie", "opera", "firefox", "safari", "chrome", "otros".

Variables globales

  • constNodeType, function tipoNodo(numero): constantes y función que nos devuelve el nombre de un tipo de nodo.
  • objetoResolver = null, function resolver(prefix): este objeto así como la función nos permitirán resolver los nombres de espacios para FF-CH-SA-OP.

Propiedades

  • this.archivo: string para guardar el argumento del mismo nombre para usar en el método open() del XMLHttpRequest.
  • this.alternativas: Booleano, valor por defecto true que permite usar otras alternativas como msxml2.DOMDocument.6.0 o DOMParser si fuera necesario. En otro caso sólo se usa el responseXML del XMLHttpRequest.
  • this.mostrarErrores: booleano por defecto true. En producción quizás no interesa que se muestren esos errores.
  • this.metodo: string por defecto "GET" para usar el método open() del XMLHttpRequest.
  • this.asincrono: booleano por defecto true también para el open().
  • this.nsOriginal, this.nsFinal: object inicialmente nulos que en caso de que existan espacios de nombres se les incluirán los mismos como propiedades de ese objeto.
  • this.nsActivos, entero que indica el número de espacios de nombres activos pues con algún navegador si sólo existe un único espacio de nombres será eliminado.
  • this.obj, una referencia al objeto XMLHttpRequest.
  • this.text, string que contendrá el responseText de la respuesta.
  • this.doc, un objeto que contendrá el responseXML o cualquiera de los resultantes de las alternativas msxml2.DOMDocument.6.0 o DOMParser.
  • this.contentType, un string con el tipo servido: text/xml, text/html o application/xhtml+xml.
    En Diciembre 2013 he agregado la directiva AddDefaultCharset utf-8 al servidor Apache para que incorpore la codificación con el contentType. Ahora aparecerá text/html; charset=utf-8 para los tipos text/html. He modificado el script para adaptarlo a esto.
  • this.navegador = queNavegador(), un string que nos dirá que navegador se está usando.
    En Diciembre 2013 he eliminado definitivamente la diferenciación por navegador de todos los JavaScript de este sitio. En el test observará que puede seleccionar manualmente el tipo de navegador pues no se realiza ninguna detección automática. Aunque el enlace anterior le lleva a un código del archivo donde se encuentra (general.js), sin embargo esa función ya no existe en el archivo original.
  • this.objetoXml un string informativo de la alternativa usada.
  • this.documentElement, una referencia al nodo raíz del XML.
  • this.cargado: un booleano que nos dirá si la petición y la respuesta finalizaron y el XML fue cargado en el DOM.

Métodos

  • this.crear(): después de aplicar el constructor con new ObjHttp(archivo) este método creará el objeto XMLHttpRequest, se encargará de hacer la petición y gestionará el evento onreadystatechange para cargar el XML.
  • this.ejecutaEventoListo = function(){}: una declaración de función que puede ser sobreescrita externamente y que se ejecuta cuando se completa la petición y se recibe la respuesta.
  • this.stringNs(cual): El argumento cual será "original" o "final". Los nombres de espacios están almacenados como propiedades de los objetos this.nsOriginal y this.nsFinal. Es a partir de este último donde se resuelven para la selección de elementos. Pero la única forma que conseguí para que funcione con FF-SA-CH-OP es forzando la creación de un objeto mediante eval("objetoResolver = {" + this.stringNs("final") + "};"). El objetoResolver es una variable global del módulo y por lo tanto hay que actualizarla cada vez que necesitemos resolver un nombre de espacio. Por lo tanto este método nos sirve para construir la cadena "ns1":"abc", "ns2":"def" y luego aplicar el eval().
  • this.selectNodes(xpath[, enNodo]): selecciona nodos con XPath, pasándose la expresión y el nodo donde se aplicará. Si no se pasa éste se aplica al raíz.
  • this.selectSingleNode(xpath[, enNodo]): selecciona el primer nodo de un conjunto de nodos que satisfaga la expresión.
  • this.getElementsByTagName(tag): selecciona elementos por el nombre del nodo. Permite también pasar nodos con espacio de nombres como abc:def.
  • this.getElementById(id): selecciona elementos por el atributo ID.