From 138a4df582558a01a2a44238e063b5a9f236bc7b Mon Sep 17 00:00:00 2001 From: chenli Date: Sat, 3 Sep 2022 11:47:22 +0800 Subject: [PATCH 1/4] =?UTF-8?q?feat:=20=E6=8F=90=E4=BA=A4=E4=BD=9C?= =?UTF-8?q?=E4=B8=9A,=E5=A2=9E=E5=8A=A0=E5=A4=8D=E6=9D=82=E7=94=A8?= =?UTF-8?q?=E4=BE=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- index.html | 12 +++ server.js | 182 +++++++++++++++++++++++++++++++++++++++++++++ test/index.spec.js | 14 ++++ 3 files changed, 208 insertions(+) create mode 100644 index.html diff --git a/index.html b/index.html new file mode 100644 index 0000000..27c39eb --- /dev/null +++ b/index.html @@ -0,0 +1,12 @@ + + + + + + + Document + + +
Test SpanVue SSR Example
+ + \ No newline at end of file diff --git a/server.js b/server.js index a5cefe0..7894163 100644 --- a/server.js +++ b/server.js @@ -1 +1,183 @@ // TODO 监听3000端口,便于执行test +import http from 'node:http' + +const Template = { + default: { + tagName: 'div', + type: 'element', + attributes: [ + { + name: 'id', + value: 'app' + } + ], + children: [ + { + tagName: 'div', + type: 'element', + children: [ + { + type: 'text', + content: 'Vue SSR Example' + } + ] + } + ] + }, + complex: { + tagName: 'div', + type: 'element', + attributes: [ + { + name: 'id', + value: 'app' + } + ], + children: [ + { + tagName: 'span', + type: 'element', + attributes: [ + { + name: 'id', + value: 'testSpan' + }, + { + name: 'style', + value: 'color: red;' + } + ], + children: [ + { + type: 'text', + content: 'Test Span' + } + ] + }, + { + type: 'text', + content: 'Vue SSR Example' + }, + { + tagName: 'ul', + type: 'element', + attributes: [ + { + name: 'id', + value: 'ulNode' + }, + { + name: 'data-spm', + value: 'spm1' + } + ], + children: [ + { + tagName: 'li', + type: 'element', + children: [ + { + tagName: 'a', + type: 'element', + attributes: [ + { + name: 'href', + value: 'https://www.baidu.com' + } + ], + children: [ + { + type: 'text', + content: 'go to baidu' + } + ] + } + ] + }, + { + tagName: 'li', + type: 'element', + children: [ + { + type: 'text', + content: 'do nothing' + } + ] + } + ] + } + ] + } +} + +function createHtml(element = Template.default) { + // 文本节点直接生成文本 + if (element.type === 'text') { + return element.content + } + + if (element.type === 'element') { + // 处理属性 + let attrs = `` + + if (Array.isArray(element.attributes)) { + element.attributes.forEach(attr => { + attrs += ` ${attr.name}="${attr.value}"` + }) + } + + // 处理子节点 + let children = `` + + if (Array.isArray(element.children)) { + element.children.forEach(child => { + children += createHtml(child) + }) + } + + return `<${element.tagName}${attrs}>${children}` + } +} + +const server = http.createServer((req, res) => { + res.writeHead(200, { + 'Content-Type': 'text/html;charset=utf-8' + }) + let templateKey = 'default' + + if (req.url && /\?/.test(req.url)) { + const search = req.url.replace(/(.*)\?/, '') + const queryArr = search.split('&') + + for (const item of queryArr) { + const query = item.split('=') + + if (query[0] === 'template') { + templateKey = query[1] + break + } + } + } + + const element = Template[templateKey] + const content = createHtml(element) + const html = ` + + + + + + Document + + + ${content} + + ` + res.end(html) +}) + +server.on('clientError', (err, socket) => { + socket.end('HTTP/1.1 400 Bad Request\r\n\r\n') +}) + +server.listen(3000) diff --git a/test/index.spec.js b/test/index.spec.js index 0567ead..c7fcb07 100644 --- a/test/index.spec.js +++ b/test/index.spec.js @@ -12,3 +12,17 @@ describe('NuxtLogo', () => { expect(bodyInnerHTML.trim()).toBe('
Vue SSR Example
') }) }) + +describe('NuxtLogo2', () => { + test('is a Vue instance', async () => { + const browser = await puppeteer.launch({ ignoreDefaultArgs: ['--disable-extensions'] }); + const page = await browser.newPage(); + // 指定渲染模板 + await page.goto('http://localhost:3000?template=complex') + const bodyHandle = await page.$('body'); + const bodyInnerHTML = await page.evaluate(dom => dom.innerHTML, bodyHandle); + await bodyHandle.dispose(); + browser.close(); + expect(bodyInnerHTML.trim()).toBe('
Test SpanVue SSR Example
') + }) +}) From 68f3fc8a9710ad99e2da53d24815b4ec6429bfd1 Mon Sep 17 00:00:00 2001 From: chenli Date: Sat, 3 Sep 2022 12:13:42 +0800 Subject: [PATCH 2/4] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0=E7=9B=B4?= =?UTF-8?q?=E6=8E=A5=E4=BC=A0JSON=E6=B8=B2=E6=9F=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server.js | 16 ++++++-- test/index.spec.js | 100 ++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 112 insertions(+), 4 deletions(-) diff --git a/server.js b/server.js index 7894163..28fb43f 100644 --- a/server.js +++ b/server.js @@ -143,7 +143,7 @@ const server = http.createServer((req, res) => { res.writeHead(200, { 'Content-Type': 'text/html;charset=utf-8' }) - let templateKey = 'default' + let element if (req.url && /\?/.test(req.url)) { const search = req.url.replace(/(.*)\?/, '') @@ -153,13 +153,23 @@ const server = http.createServer((req, res) => { const query = item.split('=') if (query[0] === 'template') { - templateKey = query[1] + if (Template[query[1]]) { + element = Template[query[1]] + } break } + + if (query[0] === 'json') { + try { + element = JSON.parse(decodeURIComponent(query[1])) + break + } catch (error) { + console.error(error) + } + } } } - const element = Template[templateKey] const content = createHtml(element) const html = ` diff --git a/test/index.spec.js b/test/index.spec.js index c7fcb07..a8270ce 100644 --- a/test/index.spec.js +++ b/test/index.spec.js @@ -13,7 +13,7 @@ describe('NuxtLogo', () => { }) }) -describe('NuxtLogo2', () => { +describe('NuxtLogo 指定模板类型', () => { test('is a Vue instance', async () => { const browser = await puppeteer.launch({ ignoreDefaultArgs: ['--disable-extensions'] }); const page = await browser.newPage(); @@ -26,3 +26,101 @@ describe('NuxtLogo2', () => { expect(bodyInnerHTML.trim()).toBe('
Test SpanVue SSR Example
') }) }) + +describe('NuxtLogo 使用JSON参数', () => { + test('is a Vue instance', async () => { + const browser = await puppeteer.launch({ ignoreDefaultArgs: ['--disable-extensions'] }); + const page = await browser.newPage(); + // 指定渲染JSON + const url = `http://localhost:3000?json=${encodeURIComponent(JSON.stringify({ + tagName: 'div', + type: 'element', + attributes: [ + { + name: 'id', + value: 'app' + } + ], + children: [ + { + tagName: 'span', + type: 'element', + attributes: [ + { + name: 'id', + value: 'testSpan' + }, + { + name: 'style', + value: 'color: red;' + } + ], + children: [ + { + type: 'text', + content: 'Test Span' + } + ] + }, + { + type: 'text', + content: 'Vue SSR Example' + }, + { + tagName: 'ul', + type: 'element', + attributes: [ + { + name: 'id', + value: 'ulNode' + }, + { + name: 'data-spm', + value: 'spm1' + } + ], + children: [ + { + tagName: 'li', + type: 'element', + children: [ + { + tagName: 'a', + type: 'element', + attributes: [ + { + name: 'href', + value: 'https://www.baidu.com' + } + ], + children: [ + { + type: 'text', + content: 'go to baidu' + } + ] + } + ] + }, + { + tagName: 'li', + type: 'element', + children: [ + { + type: 'text', + content: 'do nothing' + } + ] + } + ] + } + ] + }))}` + await page.goto(url) + const bodyHandle = await page.$('body'); + const bodyInnerHTML = await page.evaluate(dom => dom.innerHTML, bodyHandle); + await bodyHandle.dispose(); + browser.close(); + expect(bodyInnerHTML.trim()).toBe('
Test SpanVue SSR Example
') + }) +}) From 6a003bfeebfb370a63223b6a7a6ccd400a2725be Mon Sep 17 00:00:00 2001 From: chenli Date: Sat, 3 Sep 2022 12:31:52 +0800 Subject: [PATCH 3/4] =?UTF-8?q?feat:=20=E5=BF=BD=E7=95=A5script=E6=A0=87?= =?UTF-8?q?=E7=AD=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- index.html | 12 ------------ server.js | 6 ++++++ test/index.spec.js | 11 +++++++++++ 3 files changed, 17 insertions(+), 12 deletions(-) delete mode 100644 index.html diff --git a/index.html b/index.html deleted file mode 100644 index 27c39eb..0000000 --- a/index.html +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - Document - - -
Test SpanVue SSR Example
- - \ No newline at end of file diff --git a/server.js b/server.js index 28fb43f..f06443f 100644 --- a/server.js +++ b/server.js @@ -117,6 +117,10 @@ function createHtml(element = Template.default) { } if (element.type === 'element') { + // 忽略script标签,避免注入风险http://localhost:3000?json=%7B%22tagName%22%3A%22div%22%2C%22type%22%3A%22element%22%2C%22attributes%22%3A%5B%7B%22name%22%3A%22id%22%2C%22value%22%3A%22app%22%7D%5D%2C%22children%22%3A%5B%7B%22tagName%22%3A%22script%22%2C%22type%22%3A%22element%22%2C%22children%22%3A%5B%7B%22type%22%3A%22text%22%2C%22content%22%3A%22alert(1)%22%7D%5D%7D%2C%7B%22tagName%22%3A%22span%22%2C%22type%22%3A%22element%22%2C%22attributes%22%3A%5B%7B%22name%22%3A%22id%22%2C%22value%22%3A%22testSpan%22%7D%2C%7B%22name%22%3A%22style%22%2C%22value%22%3A%22color%3A%20red%3B%22%7D%5D%2C%22children%22%3A%5B%7B%22type%22%3A%22text%22%2C%22content%22%3A%22Test%20Span%22%7D%5D%7D%2C%7B%22type%22%3A%22text%22%2C%22content%22%3A%22Vue%20SSR%20Example%22%7D%2C%7B%22tagName%22%3A%22ul%22%2C%22type%22%3A%22element%22%2C%22attributes%22%3A%5B%7B%22name%22%3A%22id%22%2C%22value%22%3A%22ulNode%22%7D%2C%7B%22name%22%3A%22data-spm%22%2C%22value%22%3A%22spm1%22%7D%5D%2C%22children%22%3A%5B%7B%22tagName%22%3A%22li%22%2C%22type%22%3A%22element%22%2C%22children%22%3A%5B%7B%22tagName%22%3A%22a%22%2C%22type%22%3A%22element%22%2C%22attributes%22%3A%5B%7B%22name%22%3A%22href%22%2C%22value%22%3A%22https%3A%2F%2Fwww.baidu.com%22%7D%5D%2C%22children%22%3A%5B%7B%22type%22%3A%22text%22%2C%22content%22%3A%22go%20to%20baidu%22%7D%5D%7D%5D%7D%2C%7B%22tagName%22%3A%22li%22%2C%22type%22%3A%22element%22%2C%22children%22%3A%5B%7B%22type%22%3A%22text%22%2C%22content%22%3A%22do%20nothing%22%7D%5D%7D%5D%7D%5D%7D + if (element.tagName === 'script') { + return '' + } // 处理属性 let attrs = `` @@ -152,6 +156,7 @@ const server = http.createServer((req, res) => { for (const item of queryArr) { const query = item.split('=') + // 指定渲染模板可由后端配置 if (query[0] === 'template') { if (Template[query[1]]) { element = Template[query[1]] @@ -159,6 +164,7 @@ const server = http.createServer((req, res) => { break } + // 指定一个JSON渲染 if (query[0] === 'json') { try { element = JSON.parse(decodeURIComponent(query[1])) diff --git a/test/index.spec.js b/test/index.spec.js index a8270ce..28a7af7 100644 --- a/test/index.spec.js +++ b/test/index.spec.js @@ -42,6 +42,17 @@ describe('NuxtLogo 使用JSON参数', () => { } ], children: [ + // 尝试插入一个script标签,会被后端忽略 + { + tagName: 'script', + type: 'element', + children: [ + { + type: 'text', + content: 'alert(1)' + } + ] + }, { tagName: 'span', type: 'element', From bbfa55fc666cb3891e556ffaf21091e02ff92ff4 Mon Sep 17 00:00:00 2001 From: chenli Date: Sat, 3 Sep 2022 12:40:59 +0800 Subject: [PATCH 4/4] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0=E6=B3=A8?= =?UTF-8?q?=E9=87=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server.js b/server.js index f06443f..b90b47f 100644 --- a/server.js +++ b/server.js @@ -117,7 +117,7 @@ function createHtml(element = Template.default) { } if (element.type === 'element') { - // 忽略script标签,避免注入风险http://localhost:3000?json=%7B%22tagName%22%3A%22div%22%2C%22type%22%3A%22element%22%2C%22attributes%22%3A%5B%7B%22name%22%3A%22id%22%2C%22value%22%3A%22app%22%7D%5D%2C%22children%22%3A%5B%7B%22tagName%22%3A%22script%22%2C%22type%22%3A%22element%22%2C%22children%22%3A%5B%7B%22type%22%3A%22text%22%2C%22content%22%3A%22alert(1)%22%7D%5D%7D%2C%7B%22tagName%22%3A%22span%22%2C%22type%22%3A%22element%22%2C%22attributes%22%3A%5B%7B%22name%22%3A%22id%22%2C%22value%22%3A%22testSpan%22%7D%2C%7B%22name%22%3A%22style%22%2C%22value%22%3A%22color%3A%20red%3B%22%7D%5D%2C%22children%22%3A%5B%7B%22type%22%3A%22text%22%2C%22content%22%3A%22Test%20Span%22%7D%5D%7D%2C%7B%22type%22%3A%22text%22%2C%22content%22%3A%22Vue%20SSR%20Example%22%7D%2C%7B%22tagName%22%3A%22ul%22%2C%22type%22%3A%22element%22%2C%22attributes%22%3A%5B%7B%22name%22%3A%22id%22%2C%22value%22%3A%22ulNode%22%7D%2C%7B%22name%22%3A%22data-spm%22%2C%22value%22%3A%22spm1%22%7D%5D%2C%22children%22%3A%5B%7B%22tagName%22%3A%22li%22%2C%22type%22%3A%22element%22%2C%22children%22%3A%5B%7B%22tagName%22%3A%22a%22%2C%22type%22%3A%22element%22%2C%22attributes%22%3A%5B%7B%22name%22%3A%22href%22%2C%22value%22%3A%22https%3A%2F%2Fwww.baidu.com%22%7D%5D%2C%22children%22%3A%5B%7B%22type%22%3A%22text%22%2C%22content%22%3A%22go%20to%20baidu%22%7D%5D%7D%5D%7D%2C%7B%22tagName%22%3A%22li%22%2C%22type%22%3A%22element%22%2C%22children%22%3A%5B%7B%22type%22%3A%22text%22%2C%22content%22%3A%22do%20nothing%22%7D%5D%7D%5D%7D%5D%7D + // 忽略script标签,避免注入风险 if (element.tagName === 'script') { return '' } @@ -156,7 +156,7 @@ const server = http.createServer((req, res) => { for (const item of queryArr) { const query = item.split('=') - // 指定渲染模板可由后端配置 + // 指定渲染模板,实际应用中可在后台配置若干模板,用户可以配置要显示的模板进行展示 if (query[0] === 'template') { if (Template[query[1]]) { element = Template[query[1]]