在Node.js中读和写XML

3,706 阅读9分钟

当你想到Node.js的时候,XML可能不是你首先想到的东西。但在有些情况下,你可能会发现你需要从Node.js应用程序中读取或写入XML。在npm上搜索 "XML "会得到3400多个结果,这并不是巧合;这里有相当多的与XML相关的包,专门用于处理XML的不同方式。

在这篇文章中,我们将使用一些最流行的npm包来探索一些真实世界的XML用例,包括。

请注意,本文中的代码示例是为了演示目的。详细的工作示例代码可在briandesousa/node-xml-demo 中找到。

如果你想按照本文的说明进行操作,你可能想先用express-generator生成一个Express应用程序。选择Pug视图引擎(视图代码示例被写成Pug模板)。

通过 HTTP 接收 XML

XML-RPC和SOAP网络服务曾经是应用程序之间交换数据的标准,但JSON APIs(即REST、GraphQL)已经完全取代了XML服务

尽管如此,在某些情况下,你仍然可能需要暴露一个基于XML的API,以允许其他应用程序将XML数据输入你的应用程序。幸运的是,npm上有很多包可以让你轻松地消费XML数据。

其中一个比较流行的XML包是xml2js,而Node.js的一个比较流行的应用框架是Express。自然,有一个Express中间件包,express-xml-bodyparser,将这两者联系在一起。让我们建立一个可以接收XML的Express路由。

首先,我们需要安装 express-xml-bodyparser 包。

npm install express-xml-bodyparser

然后将 express-xml-bodyparser 中间件添加到 Express 应用程序中。默认情况下,中间件将解析任何传入的请求,如果请求的Content-Type 头被设置为text/xml

// app.js
var express = require('express');
var xmlparser = require('express-xml-bodyparser');

var app = express();
app.use(xmlparser());
// other Express middleware and configurations

添加一个接收XML请求的/xml2js/customer 路由。

// routes/xml2js.js 
router.post('/xml2js/customer', function(req, res, next) {
  console.log('Raw XML: ' + req.rawBody);
  console.log('Parsed XML: ' + JSON.stringify(req.body));
  if (req.body.retrievecustomer) {
    var id = req.body.retrievecustomer.id;
    res.send(`<customer><id>${id}</id><fullName>Bob Smith</fullName></customer>`);
  } else {
    res.status(400).send('Unexpected XML received, missing <retrieveCustomer>');
  }
});

为了测试我们的路由,我们可以写一些客户端的JavaScript,发送一个XML请求。

// views/xml2js.pug
fetch('/xml2js/customer', {
  method: 'POST',
  headers: {
    'Content-Type': 'text/xml'
  },
  body: '<retrieveCustomer><id>39399444</id></retrieveCustomer>'
})
.then(response => {
  console.log('Response status: ' + response.status);
  return response.text();
})
.then(responseText => console.log('Response text: ' + responseText)
.catch(error => console.log('Error caught: ' + error));

路由返回一个XML响应。如果你看一下服务器的控制台输出,你可以看到服务器收到的请求体。

Raw XML: <retrieveCustomer><id>39399444</id></retrieveCustomer>
Parsed XML: {"retrievecustomer":{"id":["39399444"]}}

等一下--你是否注意到收到的原始XML和expression-xml-bodyparser中间件返回的对象之间有什么不同?

原始XML的retrieveCustomer XML标签是骆驼字母,但JSON对象上的retrievecustomer 键是小写的。这是因为express-xml-bodyparser中间件正在配置xml2js解析器,包括设置一个选项将所有的XML标签转换为小写。

我们希望JSON对象的属性与原始请求中的XML标签完全一致。幸运的是,我们可以指定我们自己的xml2js选项并覆盖中间件提供的默认值。

修改先前添加到Express应用程序中的xmlparser 中间件,包括一个配置对象,并将normalizeTags 选项设置为false

// app.js
app.use(xmlparser({
  normalizeTags: false
}));

重新运行客户端代码并查看服务器控制台日志。现在标签名称应该在原始XML和解析后的JSON对象之间匹配。

Raw XML: <retrieveCustomer><id>39399444</id></retrieveCustomer>
Parsed XML: {"retrieveCustomer":{"id":["39399444"]}}

xml2js包还提供了其他几个选项,允许你自定义XML的解析方式。参见npm上的xml2js,以获得完整的选项列表。

用模式验证XML

XML的另一个常见用途是作为不同应用程序之间交换数据的格式,有时在不同的组织之间。通常,一个XML模式(XSD)被用来定义每个应用程序应该期望发送和接收的XML消息结构。每个应用程序根据模式验证传入的XML数据。

XML数据可以通过各种方式在应用程序之间传输。例如,应用程序可以通过HTTP连接接收XML,或者通过SFTP连接保存在文件系统中的平面文件。

虽然有相当多的npm包可用于处理XML,但当你还需要XML模式验证时,选择就比较有限了。让我们来看看libxmljs2包,它支持XML模式验证。我们将编写一些代码,从服务器的文件系统中加载一个XML模式,并使用它来验证一些进入的XML数据。

首先,在你的应用程序根目录下的一个schemas 目录中创建XML模式。

<!-- schemas/session-info.xsd -->
<xs:schema attributeFormDefault="unqualified" elementFormDefault="qualified" xmlns:xs="http://www.w3.org/2001/XMLSchema">
  <xs:element name="sessionInfo">
    <xs:complexType>
      <xs:sequence>
        <xs:element type="xs:int" name="customerId"/>
        <xs:element type="xs:string" name="customerName"/>
        <xs:element type="xs:string" name="token"/>
      </xs:sequence>
      <xs:attribute type="xs:long" name="id"/>
    </xs:complexType>
  </xs:element>
</xs:schema>

安装libxmljs2软件包。

npm install lib4xml4js2

创建一个新的/libxmljs2/validateSessionXml 路线。

// routes/libxmljs2.js
var express = require('express');
var router = express.Router();
var libxmljs = require('libxmljs2');
var fs = require('fs');
var path = require('path');

var router = express.Router();

router.post('/libxmljs2/validateSessionXml', (req, res, next) => {
  var xmlData = req.body;

  // parse incoming XML data
  var xmlDoc = libxmljs.parseXml(xmlData);  

  // load XML schema from file system
  var xmlSchemaDoc = loadXmlSchema('session-info.xsd');

  // validate XML data against schema
  var validationResult = xmlDoc.validate(xmlSchemaDoc);

  // return success or failure with validation errors
  if (validationResult) {
    res.status(200).send('validation successful');
  } else {
    res.status(400).send(`${xmlDoc.validationErrors}`);
  }  
});

function loadXmlSchema(filename) {
  var schemaPath = path.join(__dirname, '..', 'schemas', filename);
  var schemaText = fs.readFileSync(schemaPath, 'utf8');
  return libxmljs.parseXml(xmlSchema); 
}

提示:如果你还在使用前面例子中的express-xml-bodyparser中间件,你可能需要修改第2行,使用req.rawBody ,而不是req.body ,以旁通xlm2js ,而访问原始请求字符串。

在第14行,libxmljs2的parseXml() 函数解析了请求中的XML。它返回一个libxmljs.Document 对象,该对象暴露了一个validate() 函数,该函数接受另一个包含XML模式的libxmljs.Document 。验证函数将返回true 或一个包含验证错误列表的字符串。在第23-27行,我们根据验证结果返回一个适当的响应。

第30行的loadXmlSchema() 函数使用标准的Node.jspathfs 模块从服务器的文件系统加载一个XML模式。再一次,我们使用parseXml() 函数将模式文件的内容解析为一个libxmljs.Document 对象。XML模式在最后只是XML文档。

现在我们已经实现了一个路由,我们可以编写一些简单的客户端JavaScript,用一个有效的XML请求测试我们的路由。

// views/libxmljs2.pug
fetch('/libxmljs2/validateSessionXml', {
  method: 'POST',
  headers: { 'Content-Type': 'text/xml' },
  body: `<?xml version="1.0"?>
<sessionInfo id="45664434343">
  <customerId>39399444</customerId>
  <customerName>Bob Smith</customerName>
  <token>343ldf0bk343bz43lddd</token>
</sessionInfo>`
})
.then(response => response.text())
.then(response => console.log(response))
.catch(error => console.log('Error caught: ' + error));
// console output: validation successful

我们还可以发送一个无效的XML请求,观察返回的验证错误。

// views/libxmljs2.pug
fetch('/libxmljs2/validateSessionXml', {
  method: 'POST',
  headers: { 'Content-Type': 'text/xml' },
  body: `<?xml version="1.0"?>
<sessionInfo id="45664434343a">
  <customerName>Bob Smith</customerName>
  <token>343ldf0bk343bz43lddd</token>
</sessionInfo>`
})
.then(response => response.text())
.then(response => console.log(response))
.catch(error => console.log('Error caught: ' + error));
// console output: Error: Element 'customerName': This element is not expected. Expected is ( customerId ).

从libxmljs2的validate() 函数返回的错误信息相当详细;但是,错误信息的格式使得我们很难解析单个错误信息并将其映射为终端用户友好的文本。除此以外,根据模式验证XML所需的代码很少。

操纵HTML内容

如果你的应用程序需要操作HTML怎么办?与我们到目前为止所涉及的例子不同,HTML在技术上不符合XML规范。

有几个npm包专门处理HTML与XML的细微差别。Cheerio就是其中之一。让我们来看看我们如何使用Cheerio来读取、处理和返回一个HTML片段。

首先,安装Cheerio软件包。

npm install cheerio

创建一个新的/cheerio/highlightTable 路线。

// routes/cheerio.js
var cheerio = require('cheerio');
var express = require('express');
var router = express.Router();

router.post('/cheerio/highlightTable', (req, res, next) => {
  // decode HTML framgent in request body
  var decodedHtml = decodeURI(req.body.encodedHtml);

  try {
    // parse HTML fragment
    var $ = cheerio.load(decodedHtml);

    // use the cheerio selector to locate all table cells in the HTML
    $('td').each(function() {
      tableCellText = $(this).text();
      tableCellNumber = parseFloat(tableCellText);
      if (tableCellNumber) {
        // highlight cells based on their numeric value
        if (tableCellNumber >= 0) {
          $(this).prop('style', 'background-color: #90ee90');
        } else {
          $(this).prop('style', 'background-color: #fa8072');
        }
      }
  } catch(err) {
    return res.status(500).send({ error: err});
  }

  // only return the HTML fragment that was received in the request
  updatedHtml = $('body').html();
  res.status(200).send({ encodedHtml: encodeURI(updatedHtml) });
});

在第8行,使用内置的decodeURI 函数对encodedHtml 请求属性进行解码。在JSON请求中发送和接收HTML字符串时,我们必须对其进行编码,以避免特殊字符,如双引号,与JSON的语法相冲突。

在第12行,Cheerio的load() 函数被用来解析HTML片段。这个函数返回一个选择器对象,其API几乎与jQuery核心API相同。

然后在第15-25行使用这个选择器来定位和提取HTML片段中所有表格单元的文本。由选择器提供的prop() 函数在第21行和第23行被用来修改HTML片段,添加新的style 属性。

在第31行,选择器被用来从HTML片段中提取body 元素,并将其作为一个HTML字符串返回。即使在请求中传递的HTML片段不包含外部的<html><body> 标签,Cheerio也会自动将HTML片段包装成一个正确结构的HTML文档。这发生在调用load() 函数的时候。

最后,HTML字符串被编码并在第32行发回给客户端。

让我们写一些客户端的JavaScript来测试这个路由。

// views/cheerio.pug
var sampleHtml = 
  '<table border="1" cellpadding="5px" cellspacing="0">\n' +
  '  <tr>\n' +
  '    <td>Sammy Steakhouse Inc.</td>\n' +
  '    <td>-130.33</td>\n' +
  '  </tr>\n' +
  '  <tr>\n' +
  '    <td>ATM deposit</td>\n' +
  '    <td>500.00</td>\n' +
  '  </tr>\n' +
  '  <tr>\n' +
  '    <td>Government cheque deposit</td>\n' +
  '    <td>150.00</td>\n' +
  '  </tr>\n' +
  '</table>';

fetch('/cheerio/highlightTable', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    encodedHtml: encodeURI(sampleHtml)
  })
})
.then(response => response.json())
.then(response => {
  if (response.error) {
    console.log('Error received: ' + response.error);
  } else {
    decodedHtml = decodeURI(response.encodedHtml);
    console.log('Received HTML: ' + decodedHtml);
    // console output:
    // <table border="1" cellpadding="5px" cellspacing="0">
    //  <tbody><tr>
    //    <td>Sammy Steakhouse Inc.</td>
    //    <td style="background-color: #fa8072">-130.33</td>
    //  </tr>
    //  <tr>
    //    <td>ATM deposit</td>
    //    <td style="background-color: #90ee90">500.00</td>
    //  </tr>
    //  <tr>
    //    <td>Government cheque deposit</td>
    //    <td style="background-color: #90ee90">150.00</td>
    //  </tr>
    // </tbody></table>
  }
})
.catch(error => console.log('Error caught: ' + ${error})); 

在第22行,使用浏览器内置的encodeURI 函数对HTML片段进行编码,这与我们在上一个例子中在服务器端对其进行解码的方式相似。

你会注意到正在发送的原始HTML片段(第3-16行)和返回的修改后的HTML片段(在第32-46行的注释中演示)之间有一些区别。

  • 一些额外的标签,如<tbody> ,被添加到返回的HTML中。这些标签是由Cheerio的load() 功能自动添加的。Cheerio在加载HTML时非常宽容:它将添加额外的标签以确保HTML是符合标准的HTML文档
  • style 属性被添加到每个<td> 标签中,正如预期的那样。

生成SVG图像

与其他例子相比,这个例子更直观、更有趣。让我们通过修改XML源代码来操作一些SVG图像。

首先,简单介绍一下可扩展矢量图(SVG)。SVG是一种基于XML的图像格式,所有主要的浏览器都支持。SVG的XML是由一系列定义不同类型的形状的元素组成的。每个形状都可以包含CSS样式以定义其外观。通常情况下,你会使用一个工具来生成SVG XML,而不是手工编码,但是考虑到SVG图像只是XML,通过JavaScript进行图像操作是可能的。

我们将创建一个路由,接受三种颜色,从服务器的文件系统中加载一个SVG图像,将颜色应用于图像中的某些形状,并将其返回给客户端进行渲染。我们将使用svgson包在SVG的XML和JSON之间进行转换,以简化我们需要编写的代码来处理图像。

首先安装svgson包。

npm install svgson

创建一个新的/svgson/updateSVGImageColors 路线。

// routes/svgson.js
var express = require('express');
var fs = require('fs');
var path = require('path');
var { 
  parse: svgsonParse,
  stringify: svgsonStringify
} = require('svgson')

var router = express.Router();

router.post('/svgson/updateSVGImageColors', function(req, res, next) {
    // 3 colors provided to stylize the SVG image
    var { color1, color2, color3 } = req.body;

    // load the original SVG image from the server's file system
    var svgImageXML = loadSVGImageXML('paint.svg');

    // use svgson to convert the SVG XML to a JSON object
    svgsonParse(svgImageXML).then(json => {

      // get the shape container that contains the paths to be manipulated
      gElement = json.children.find(elem => elem.name == 'g'
        && elem.attributes.id == 'g1727');      

      // update styles on specific path shapes
      updatePathStyleById(gElement, 'path995', 'fill:#000000', 'fill:' + color1);
      updatePathStyleById(gElement, 'path996', 'fill:#ffffff', 'fill:' + color2);
      updatePathStyleById(gElement, 'path997', 'fill:#ffffff', 'fill:' + color3);

      // convert JSON object back to SVG XML
      svgImageXML = svgsonStringify(json);

      // return SVG XML
      res.status(200).send(svgImageXML);
    });
});

function updatePathStyleById(containerElem, pathId, oldStyle, newStyle) {
  pathElem = containerElem.children.find(elem => elem.attributes.id == pathId);
  pathElem.attributes.style = pathElem.attributes.style.replace(oldStyle, 
    newStyle);
}

function loadSVGImageXML(filename) {
  var svgImagePath = path.join(__dirname, '..', 'public', 'images', filename);
  return fs.readFileSync(svgImagePath, 'utf8');
}

在这段代码中,有相当多的事情要做。让我们把它分解一下,突出几个关键的概念。

在第5-8行,从svgson包中导入了parsestringify 模块。这些模块的名字很普通,但我们可以使用对象重构来给它们起一个更简洁的名字,如svgsonParsesvgsonStingify

在第17行,loadSVGImageXML() 函数被用来从服务器的文件系统中使用本地Node.js模块加载预定义的SVG图片的内容。正在使用的图片是 [paint.svg](https://github.com/briandesousa/node-xml-demo/blob/main/public/images/paint.svg).这就是它开始时的样子。

Black And White Image Of Paint And A Paintbrush

原始的paint.svg图像。

第20-36行是发生图像处理魔法的地方。SVG的XML被转换为一个JSON对象。标准的JavaScript被用来浏览对象树,找到我们想要操作的三个路径形状。下面是JSON对象树(左侧)与SVG XML(右侧)的比较,以帮助可视化。注意,为了简洁起见,有些元素已经被删除了。

Code Highlighting SVG JSON Object Tree Being Compared To SVG XML

SVG JSON对象树与SVG XML的比较。

在第27-29行调用的updatePathStyleById() 辅助函数通过其ID定位一个路径形状,并将其填充样式替换为使用请求中提供的颜色构建的新填充样式。

SVG JSON对象在第32行被转换回SVG XML字符串,并在第35行返回给客户端。

让我们写一些客户端的JavaScript来测试这个路由。

// views/svgson.pug
fetch('/svgson/updateSVGImageColors', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    color1: '#FF0000', 
    color2: '#00FF00',
    color3: '#0000FF'
  })
})
.then(response => response.text())
.then(svgImageXml => console.log(svgImageXml))
.catch(error => console.log('Error caught: ' + error));

如果我们要渲染这个返回的SVG XML,就会是这样的结果。

Green, Blue, And Red Paint With A Paintbrush

更新的paint.svg图片。

总结一下。你已经受够了XML了吗?

我们涵盖了XML的一些常见用途,从枯燥的基于XML的数据交换到SVG图像处理。下面是我们所看的npm包的摘要,以及区别于它们的关键特征。

npm包关键特征
xml2js
  • 可以在XML和JavaScript之间进行双向转换
  • 提供了几个选项,可以用来改变XML的解析方式
  • 与使用 express-xml-bodyparser 中间件的 Express 搭配良好

| | libxmljs2 |

  • 可以根据XML模式解析和验证XML

| | Cheerio |

  • 专注于解析和操作HTML
  • 可用于加载HTML或XML文档
  • 宽容的设计;会添加缺失的HTML标签以确保HTML的有效性

| | svgson |

  • 可以在SVG图像(XML)和JSON之间进行双向转换
  • 使得用JavaScript操作SVG图像变得更加容易

|

The postRead and writing XML in Node.jsappeared first onLogRocket Blog.