From a2a6ea850a7dc5ef83b54ee09b5da6e1af2fab34 Mon Sep 17 00:00:00 2001 From: yutent Date: Tue, 31 Oct 2023 12:28:27 +0800 Subject: [PATCH] =?UTF-8?q?2.0=E9=87=8D=E6=9E=84;=E8=A1=A5=E5=85=A8?= =?UTF-8?q?=E5=AE=8C=E6=95=B4=E7=9A=84http=E7=8A=B6=E6=80=81=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- index.js | 221 +++++++++++++++++++++++++--------------------- lib/http-code.js | 32 ++++++- lib/mime-tpyes.js | 48 ++++++++++ 3 files changed, 196 insertions(+), 105 deletions(-) create mode 100644 lib/mime-tpyes.js diff --git a/index.js b/index.js index 90ac99f..c8558ae 100644 --- a/index.js +++ b/index.js @@ -5,18 +5,51 @@ import fs from 'iofs' import STATUS_TEXT from './lib/http-code.js' +import { MIME_TYPES, CHARSET_TYPES } from './lib/mime-tpyes.js' import { serialize } from './lib/cookie.js' export default class Response { - - rendered = false + #req = null + #res = null + + #charset = 'utf-8' + status = 200 + + #ended = false constructor(req, res) { - this.#req = req this.#res = res - this.rendered = !!res.rendered + this.#ended = !!res.ended + } + + get ended() { + return this.#ended + } + + set charset(v) { + this.#charset = v || 'utf-8' + } + + get type() { + return this.#res.getHeader('content-type') + } + + set type(v) { + let mime = MIME_TYPES[v] || MIME_TYPES.stream + mime += CHARSET_TYPES[v] ? '; charset=' + this.#charset : '' + this.set('Content-Type', mime) + } + + set body(buf = null) { + if (this.#ended) { + return this + } + + this.#ended = true + this.#res.writeHead(this.status, STATUS_TEXT[this.status]) + this.#res.end(buf) } /** @@ -25,20 +58,14 @@ export default class Response { * @param {String} msg [错误提示信息] */ error(msg, code = 500) { - if (this.rendered) { + if (this.#ended) { return } msg = msg || STATUS_TEXT[code] - this.status(code) - this.set('Content-Type', 'text/html; charset=utf-8') - this.end( - `
Http Status: ${code}
${msg}
` - ) - } - - status(code = 404) { - this.statusCode = code + this.status = code + this.type = 'html' + this.body = `
Http Status: ${code}
${msg}
` } /** @@ -47,23 +74,23 @@ export default class Response { * @param {Boolean} f [是否永久重定向] */ redirect(url, f = false) { - if (this.rendered) { + if (this.#ended) { return } if (!/^(http[s]?|ftp):\/\//.test(url)) { url = '//' + url } this.set('Location', url) - this.status(f ? 301 : 302) - this.end('') + this.status = f ? 301 : 302 + this.body = null } /** * [location 页面跳转(前端的方式)] */ location(url) { - var html = `` - if (this.rendered) { + let html = `` + if (this.#ended) { return } this.render(html) @@ -71,46 +98,77 @@ export default class Response { // 以html格式向前端输出内容 render(data, code) { - if (this.rendered) { + if (this.#ended) { return } data += '' data = data || STATUS_TEXT[code] - this.set('Content-Type', 'text/html') + this.type = 'html' this.set('Content-Length', Buffer.byteLength(data)) if (code) { - this.status(code) + this.status = code } - this.end(data) + this.body = data } // 文件下载 - sendfile(target, filename) { - if (this.rendered) { + sendfile(target, filename = 'untitled') { + if (this.#ended) { return } - var data + let data, start, end - this.set('Content-Type', 'application/force-download') - this.set('Accept-Ranges', 'bytes') - this.set('Content-Disposition', `attachment;filename="${filename}"`) + if (!this.type) { + this.status = 206 + this.type = 'stream' + this.set('Content-Disposition', `attachment;filename="${filename}"`) + this.set('Accept-Ranges', 'bytes') + + let range = this.#req.headers['range'] || '' + if (range) { + range = range.replace('bytes=', '') + if (range.includes(',')) { + // 多重范围的range请求, 暂时不支持, 直接返回整个文件 + } else { + range = range.split('-').map(n => +n) + ;[start, end] = range + + if (end === 0) { + end = void 0 + } + } + } + } if (Buffer.isBuffer(target)) { data = target } else { if (typeof target === 'string') { - var stat = fs.stat(target) + let stat = fs.stat(target) if (stat.isFile()) { - this.set('Content-Length', stat.size) - fs.origin.createReadStream(target).pipe(this.origin.res) - return + let size = stat.size + if (start !== void 0) { + if (end === void 0) { + size -= start + } else { + size = end - start + } + } + this.set('Content-Length', size) + return fs.origin + .createReadStream(target, { start, end }) + .pipe(this.#res) } } data = Buffer.from(target + '') } + if (start !== void 0) { + data = data.slice(start, end) + } + this.set('Content-Length', data.length) - this.end(data) + this.body = data } /** @@ -120,91 +178,56 @@ export default class Response { * @param {Str/Obj} data [额外数据] * @param {Str} callback [回调函数名] */ - send(code = 200, msg = 'success', data = null, callback = null) { - var output + send(code = 200, msg = '', data, callback) { + let output - if (this.rendered) { + if (this.#ended) { return } - if (typeof code !== 'number') { - if (typeof code === 'object') { - data = code - code = 200 - msg = STATUS_TEXT[code] - } else { - msg = code + '' - code = 400 - } - } else if (typeof msg === 'object') { + + if (msg && typeof msg === 'object') { + callback = data data = msg - code = code || 200 - msg = STATUS_TEXT[code] || 'success' + msg = STATUS_TEXT[code] + } else { + msg = msg || STATUS_TEXT[code] } output = { code, msg, data } output = JSON.stringify(output) if (callback) { - callback = callback.replace(/[^\w\-\.]/g, '') + callback = callback.replace(/[^\w\.]/g, '') output = callback + '(' + output + ')' } - this.set('Content-Type', 'application/json') + this.type = 'json' this.set('Content-Length', Buffer.byteLength(output)) // 只设置200以上的值 - if (code && code > 200) { - this.status(code) + if (code && code >= 200 && code <= 599) { + this.status = code } - this.end(output) - } - - end(buf) { - var code = 200 - if (this.rendered) { - return this - } - if (this.statusCode) { - code = this.statusCode - delete this.statusCode - } - this.rendered = true - this.origin.res.writeHead(code, STATUS_TEXT[code]) - this.origin.res.end(buf || '') + this.body = output } /** * [get 读取已写入的头信息] */ get(key) { - return this.origin.res.getHeader(key) + return this.#res.getHeader(key) } /** * [set 设置头信息] */ set(key, val) { - if (this.rendered) { + if (this.#ended) { return this } - if (arguments.length === 2) { - var value = Array.isArray(val) ? val.map(String) : String(val) - - if ( - key.toLowerCase() === 'content-type' && - typeof value === 'string' && - value.indexOf('charset') < 0 - ) { - value += '; charset=utf-8' - } - - this.origin.res.setHeader(key, value) - } else { - for (let i in key) { - this.set(i, key[i]) - } - } + let value = Array.isArray(val) ? val.map(String) : String(val) + this.#res.setHeader(key, value) return this } @@ -214,26 +237,18 @@ export default class Response { * @param {String} val [description] */ append(key, val) { - if (this.rendered) { + if (this.#ended) { return } - var prev = this.get(key) - var value + let prev = this.get(key) || [] - if (Array.isArray(val)) { - value = val - } else { - value = [val] + if (!Array.isArray(prev)) { + prev = [prev] } - if (prev) { - if (Array.isArray(prev)) { - value = prev.concat(val) - } else { - value = [prev].concat(val) - } - } - return this.set(key, value) + prev = prev.concat(val) + + return this.set(key, prev) } /** @@ -244,7 +259,7 @@ export default class Response { */ cookie(key, val, opts = {}) { //读取之前已经写过的cookie缓存 - var cache = this.get('set-cookie') + let cache = this.get('set-cookie') if (cache) { if (!Array.isArray(cache)) { cache = [cache] diff --git a/lib/http-code.js b/lib/http-code.js index d293f16..59d2b2b 100644 --- a/lib/http-code.js +++ b/lib/http-code.js @@ -2,6 +2,7 @@ export default { '100': 'Continue', '101': 'Switching Protocols', '102': 'Processing', + '103': 'Early Hints', '200': 'OK', '201': 'Created', '202': 'Accepted', @@ -11,6 +12,7 @@ export default { '206': 'Partial Conten', '207': 'Multi-Status', '208': 'Already Reported', + '218': 'This is fine', '226': 'IM Used', '300': 'Multiple Choices', '301': 'Moved Permanently', @@ -40,20 +42,32 @@ export default { '416': 'Requested Range Not Satisfiable', '417': 'Expectation Failed', '418': "I'm a teapot", + '419': 'Page Expired', '420': 'Enhance Your Caim', '421': 'Misdirected Request', '422': 'Unprocessable Entity', '423': 'Locked', '424': 'Failed Dependency', - '425': 'Unordered Collection', + '425': 'Too Early', '426': 'Upgrade Required', '428': 'Precondition Required', '429': 'Too Many Requests', + '430': 'Would Block', '431': 'Request Header Fields Too Large', + '440': 'Login Time-Out', '444': 'No Response', + '449': 'Retry With', '450': 'Blocked by Windows Parental Controls', '451': 'Unavailable For Legal Reasons', + '460': 'Client Closed Connection Prematurely', + '463': 'Too Many Forwarded IP Addresses', + '464': 'Incompatible Protocol', '494': 'Request Header Too Large', + '495': 'SSL Certificate Error', + '496': 'SSL Certificate Required', + '497': 'HTTP Request Sent to HTTPS Port', + '498': 'Invalid Token', + '499': 'Token Required or Client Closed Request', '500': 'Internal Server Error', '501': 'Not Implemented', '502': 'Bad Gateway', @@ -63,6 +77,20 @@ export default { '506': 'Variant Also Negotiates', '507': 'Insufficient Storage', '508': 'Loop Detected', + '509': 'Bandwidth Limit Exceeded', '510': 'Not Extended', - '511': 'Network Authentication Required' + '511': 'Network Authentication Required', + '520': 'Web Server Is Returning an Unknown Error', + '521': 'Web Server Is Down', + '522': 'Connection Timed Out', + '523': 'Origin Is Unreachable', + '524': 'A Timeout Occurred', + '525': 'SSL Handshake Failed', + '526': 'Invalid SSL Certificate', + '527': 'Railgun Listener to Origin', + '529': 'The Service Is Overloaded', + '530': 'Site Frozen', + '561': 'Unauthorized', + '598': 'Network Read Timeout Error', + '599': 'Network Connect Timeout Error', } diff --git a/lib/mime-tpyes.js b/lib/mime-tpyes.js new file mode 100644 index 0000000..8603ae4 --- /dev/null +++ b/lib/mime-tpyes.js @@ -0,0 +1,48 @@ +// + +export const CHARSET_TYPES = { + html:1, + txt:1, + css:1, + scss:1, + xml:1, + js:1, + json: 1,webmanifest:1 +} + +export const MIME_TYPES = { + html: 'text/html', + txt: 'text/plain', + css: 'text/css', + xml: 'text/xml', + gif: 'image/gif', + jpg: 'image/jpeg', + webp: 'image/webp', + tiff: 'image/tiff', + png: 'image/png', + svg: 'image/svg+xml', + ico: 'image/x-icon', + bmp: 'image/x-ms-bmp', + js: 'application/javascript', + json: 'application/json', + webmanifest: 'application/json', + mp3: 'audio/mpeg', + ogg: 'audio/ogg', + m4a: 'audio/x-m4a', + mp4: 'video/mp4', + webm: 'video/webm', + ttf: 'font/font-ttf', + woff: 'font/font-woff', + woff2: 'font/font-woff2', + wast: 'application/wast', + wasm: 'application/wasm', + stream: 'application/octet-stream' +} + +MIME_TYPES.vue = MIME_TYPES.js +MIME_TYPES.scss = MIME_TYPES.css +MIME_TYPES.htm = MIME_TYPES.html +MIME_TYPES.jpeg = MIME_TYPES.jpg +MIME_TYPES.tif = MIME_TYPES.tiff + +