diff --git a/fake/xml.fake.ts b/fake/xml.fake.ts index 75ef5e2..209a987 100644 --- a/fake/xml.fake.ts +++ b/fake/xml.fake.ts @@ -1,21 +1,49 @@ import { defineFakeRoute } from 'vite-plugin-fake-server/client'; +// 简单的实体转义,避免在 XML 中出现非法字符;先替换 & 再替换其他符号,防止二次转义 +function esc(s: string) { + return s + .replaceAll('&', '&') + .replaceAll('<', '<') + .replaceAll('>', '>') + .replaceAll('"', '"') + .replaceAll("'", '''); +} + +// 从原始 XML 文本中提取某个标签的文本内容(弱解析,仅用于演示) +function pickTag(xml: string, tag: string) { + // 说明:使用非贪婪匹配并允许标签上带属性;N.B. 未处理命名空间与嵌套同名标签的复杂情形 + const re = new RegExp(`<${tag}(?:\n|\r|\s|>|/)[^>]*>([\s\S]*?)<\/${tag}>`, 'i'); + const m = xml.match(re); + return m?.[1]?.trim() ?? ''; +} + // 通过 rawResponse 返回 XML 文本,便于在浏览器端演示 XML 解析 export default defineFakeRoute([ { method: 'GET', url: '/xml/sample', - rawResponse(_req, res) { - // 这里模拟一个简单的 XML 文档 + rawResponse(req, res) { + // 通过查询参数自定义返回的 XML 字段 + // 支持的参数:to, from, heading, body, id, createdAt + const url = new URL(req.url!, 'http://localhost'); + const q = url.searchParams; + const to = q.get('to') ?? 'George'; + const from = q.get('from') ?? 'John'; + const heading = q.get('heading') ?? 'Reminder'; + const body = q.get('body') ?? "Don't forget the meeting at 3 PM today."; + const id = q.get('id') ?? '42'; + const createdAt = q.get('createdAt') ?? '2025-09-02T10:00:00Z'; + const xml = ` - George - John - Reminder - Don't forget the meeting at 3 PM today. + ${esc(to)} + ${esc(from)} + ${esc(heading)} + ${esc(body)} - 42 - 2025-09-02T10:00:00Z + ${esc(id)} + ${esc(createdAt)} `; @@ -23,4 +51,70 @@ export default defineFakeRoute([ res.end(xml); }, }, + { + method: 'POST', + url: '/xml/submit', + rawResponse(req, res) { + const chunks: Buffer[] = []; + req.on('data', (chunk) => chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk))); + req.on('end', () => { + const contentType = String(req.headers['content-type'] || '').toLowerCase(); + const raw = Buffer.concat(chunks).toString('utf8'); + + // 默认值,与 GET 一致 + let to = 'George'; + let from = 'John'; + let heading = 'Reminder'; + let body = "Don't forget the meeting at 3 PM today."; + let id = '42'; + let createdAt = new Date().toISOString(); + + try { + if (contentType.includes('application/json')) { + const data = JSON.parse(raw || '{}'); + to = data.to ?? to; + from = data.from ?? from; + heading = data.heading ?? heading; + body = data.body ?? body; + id = data.id ?? id; + createdAt = data.createdAt ?? createdAt; + } else if (contentType.includes('application/xml') || contentType.includes('text/xml')) { + // 解析 XML 请求体中常用字段(演示用,未处理命名空间/CDATA 等复杂场景) + to = pickTag(raw, 'to') || to; + from = pickTag(raw, 'from') || from; + heading = pickTag(raw, 'heading') || heading; + body = pickTag(raw, 'body') || body; + id = pickTag(raw, 'id') || id; + createdAt = pickTag(raw, 'createdAt') || createdAt; + } else if (contentType.includes('application/x-www-form-urlencoded')) { + // 简单解析表单:key=value&…;此处演示用途,未处理复杂编码场景 + const params = new URLSearchParams(raw); + to = params.get('to') ?? to; + from = params.get('from') ?? from; + heading = params.get('heading') ?? heading; + body = params.get('body') ?? body; + id = params.get('id') ?? id; + createdAt = params.get('createdAt') ?? createdAt; + } + } catch { + // 忽略解析错误,保持默认值 + } + + const xml = ` + + ${esc(to)} + ${esc(from)} + ${esc(heading)} + ${esc(body)} + + ${esc(id)} + ${esc(createdAt)} + +`; + + res.writeHead(200, { 'Content-Type': 'application/xml; charset=UTF-8' }); + res.end(xml); + }); + }, + }, ]); diff --git a/src/pages/Page/API.page.vue b/src/pages/Page/API.page.vue index f0c7e15..b92c669 100644 --- a/src/pages/Page/API.page.vue +++ b/src/pages/Page/API.page.vue @@ -15,6 +15,10 @@ onMounted(() => { // 选择此方式的原因:可控性更强,便于处理编码、容错与字段抽取策略 let xmlText = $ref(null); let xmlParsed = $ref | null>(null); +let xmlPostText = $ref(null); +let xmlPostParsed = $ref | null>(null); +let xmlPostXmlText = $ref(null); +let xmlPostXmlParsed = $ref | null>(null); onMounted(async () => { try { @@ -22,6 +26,14 @@ onMounted(async () => { responseType: 'text', headers: { Accept: 'application/xml' }, transformResponse: [(data: string) => data], + params: { + to: 'Alice', + from: 'Bob', + heading: 'Greeting', + body: 'Hello from API.page.vue', + id: '1001', + createdAt: new Date().toISOString(), + }, }); xmlText = res.data; @@ -45,6 +57,78 @@ onMounted(async () => { } }); +// 演示 POST 提交 JSON,由服务端返回 XML,再解析 +onMounted(async () => { + try { + const res = await axios.post( + `${baseURL}/xml/submit`, + { + to: 'Tom', + from: 'Jerry', + heading: 'PostXML', + body: 'This is a POST body to XML service', + id: '9001', + createdAt: new Date().toISOString(), + }, + { + headers: { 'Content-Type': 'application/json', Accept: 'application/xml' }, + responseType: 'text', + transformResponse: [(data: string) => data], + }, + ); + xmlPostText = res.data; + const doc = new DOMParser().parseFromString(res.data, 'application/xml'); + const pick = (selector: string) => doc.querySelector(selector)?.textContent?.trim() ?? ''; + xmlPostParsed = { + to: pick('to'), + from: pick('from'), + heading: pick('heading'), + body: pick('body'), + id: pick('meta > id'), + createdAt: pick('meta > createdAt'), + }; + } catch (error) { + console.error('XML POST 请求失败: ', error); + } +}); + +// 演示 POST 以 XML 请求体提交,服务端解析 XML 并返回规范化的 XML +onMounted(async () => { + try { + const payload = ` + + XML-Client + Browser + XMLSubmit + Send by XML body + + 777 + ${new Date().toISOString()} + +`; + + const res = await axios.post(`${baseURL}/xml/submit`, payload, { + headers: { 'Content-Type': 'application/xml', Accept: 'application/xml' }, + responseType: 'text', + transformResponse: [(data: string) => data], + }); + xmlPostXmlText = res.data; + + const doc = new DOMParser().parseFromString(res.data, 'application/xml'); + const pick = (selector: string) => doc.querySelector(selector)?.textContent?.trim() ?? ''; + xmlPostXmlParsed = { + to: pick('to'), + from: pick('from'), + heading: pick('heading'), + body: pick('body'), + id: pick('meta > id'), + createdAt: pick('meta > createdAt'), + }; + } catch (error) { + console.error('XML POST(XML body) 请求失败: ', error); + } +}); + /* fetch('https://jsonplaceholder.typicode.com/posts/1') .then((response) => response.json()) .then((json) => console.log(json)); */ @@ -63,6 +147,18 @@ onMounted(async () => {

解析后的结果

{{ xmlParsed }}
+ +

POST 返回的 XML 字符串

+
{{ xmlPostText }}
+ +

POST 解析后的结果

+
{{ xmlPostParsed }}
+ +

POST(XML body) 返回的 XML 字符串

+
{{ xmlPostXmlText }}
+ +

POST(XML body) 解析后的结果

+
{{ xmlPostXmlParsed }}