Compare commits

..

4 Commits

Author SHA1 Message Date
yutent 212945cf85 完成2.0版重构 2023-10-31 14:23:48 +08:00
yutent 13a837a285 补全mime-types 2023-10-31 12:44:51 +08:00
yutent a2a6ea850a 2.0重构;补全完整的http状态码 2023-10-31 12:28:27 +08:00
yutent e32ca8c726 重构2.0 2023-10-30 18:45:06 +08:00
5 changed files with 245 additions and 122 deletions

240
index.js
View File

@ -5,12 +5,55 @@
import fs from 'iofs' import fs from 'iofs'
import STATUS_TEXT from './lib/http-code.js' import STATUS_TEXT from './lib/http-code.js'
import { MIME_TYPES, CHARSET_TYPES } from './lib/mime-tpyes.js'
import { serialize } from './lib/cookie.js' import { serialize } from './lib/cookie.js'
export default class Response { export default class Response {
#req = null
#res = null
#charset = 'utf-8'
status = 200
#ended = false
constructor(req, res) { constructor(req, res) {
this.origin = { req, res } this.#req = req
this.rendered = !!res.rendered this.#res = res
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 length(val) {
this.set('Content-Length', val)
}
set body(buf = null) {
if (this.#ended) {
return
}
this.#ended = true
this.#res.writeHead(this.status, STATUS_TEXT[this.status])
this.#res.end(buf)
} }
/** /**
@ -19,20 +62,14 @@ export default class Response {
* @param {String} msg [错误提示信息] * @param {String} msg [错误提示信息]
*/ */
error(msg, code = 500) { error(msg, code = 500) {
if (this.rendered) { if (this.#ended) {
return return
} }
msg = msg || STATUS_TEXT[code] msg = msg || STATUS_TEXT[code]
this.status(code) this.status = code
this.set('Content-Type', 'text/html; charset=utf-8') this.type = 'html'
this.end( this.body = `<fieldset><legend>Http Status: ${code}</legend><pre>${msg}</pre></fieldset>`
`<fieldset><legend>Http Status: ${code}</legend><pre>${msg}</pre></fieldset>`
)
}
status(code = 404) {
this.statusCode = code
} }
/** /**
@ -41,70 +78,99 @@ export default class Response {
* @param {Boolean} f [是否永久重定向] * @param {Boolean} f [是否永久重定向]
*/ */
redirect(url, f = false) { redirect(url, f = false) {
if (this.rendered) { if (this.#ended) {
return return
} }
if (!/^(http[s]?|ftp):\/\//.test(url)) {
url = '//' + url
}
this.set('Location', url) this.set('Location', url)
this.status(f ? 301 : 302) this.status = f ? 301 : 302
this.end('') this.body = null
} }
/** /**
* [location 页面跳转(前端的方式)] * [location 页面跳转(前端的方式)]
*/ */
location(url) { location(url) {
var html = `<html><head><meta http-equiv="refresh" content="0;url=${url}"></head></html>` if (this.#ended) {
if (this.rendered) {
return return
} }
let html = `<html><head><meta http-equiv="refresh" content="0;url=${url}"></head></html>`
this.render(html) this.render(html)
} }
// 以html格式向前端输出内容 // 以html格式向前端输出内容
render(data, code) { render(data, code) {
if (this.rendered) { if (this.#ended) {
return return
} }
data += ''
data = data || STATUS_TEXT[code]
this.set('Content-Type', 'text/html')
this.set('Content-Length', Buffer.byteLength(data))
if (code) { if (code) {
this.status(code) this.status = code
} }
this.end(data) data += ''
data = data || STATUS_TEXT[this.status]
this.type = 'html'
this.set('Content-Length', Buffer.byteLength(data))
this.body = data
} }
// 文件下载 // 文件下载
sendfile(target, filename) { sendfile(target, filename = 'untitled') {
if (this.rendered) { if (this.#ended) {
return return
} }
var data let data, start, end
this.set('Content-Type', 'application/force-download') if (!this.type) {
this.set('Accept-Ranges', 'bytes') this.status = 206
this.type = 'stream'
this.set('Content-Disposition', `attachment;filename="${filename}"`) 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)) { if (Buffer.isBuffer(target)) {
data = target data = target
} else { } else {
if (typeof target === 'string') { if (typeof target === 'string') {
var stat = fs.stat(target) let stat = fs.stat(target)
if (stat.isFile()) { if (stat.isFile()) {
this.set('Content-Length', stat.size) let size = stat.size
fs.origin.createReadStream(target).pipe(this.origin.res) if (start !== void 0) {
return 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 + '') data = Buffer.from(target + '')
} }
if (start !== void 0) {
data = data.slice(start, end)
}
this.set('Content-Length', data.length) this.set('Content-Length', data.length)
this.end(data) this.body = data
} }
/** /**
@ -112,93 +178,55 @@ export default class Response {
* @param {Num} code [返回码] * @param {Num} code [返回码]
* @param {Str} msg [提示信息] * @param {Str} msg [提示信息]
* @param {Str/Obj} data [额外数据] * @param {Str/Obj} data [额外数据]
* @param {Str} callback [回调函数名]
*/ */
send(code = 200, msg = 'success', data = null, callback = null) { send(code = 200, msg = '', data) {
var output let output
if (this.rendered) { if (this.#ended) {
return return
} }
if (typeof code !== 'number') {
if (typeof code === 'object') { if (msg && typeof msg === 'object') {
data = code data = msg
code = 200
msg = STATUS_TEXT[code] msg = STATUS_TEXT[code]
} else { } else {
msg = code + '' msg = msg || STATUS_TEXT[code]
code = 400
}
} else if (typeof msg === 'object') {
data = msg
code = code || 200
msg = STATUS_TEXT[code] || 'success'
} }
output = { code, msg, data } output = JSON.stringify({ code, msg, data })
output = JSON.stringify(output)
if (callback) { this.type = 'json'
callback = callback.replace(/[^\w\-\.]/g, '')
output = callback + '(' + output + ')'
}
this.set('Content-Type', 'application/json')
this.set('Content-Length', Buffer.byteLength(output)) this.set('Content-Length', Buffer.byteLength(output))
// 只设置200以上的值 // 只设置200以上的值
if (code && code > 200) { if (code && code >= 200 && code <= 599) {
this.status(code) this.status = code
} }
this.end(output) this.body = 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 || '')
} }
/** /**
* [get 读取已写入的头信息] * [get 读取已写入的头信息]
*/ */
get(key) { get(key) {
return this.origin.res.getHeader(key) return this.#res.getHeader(key)
} }
/** /**
* [set 设置头信息] * [set 设置头信息]
*/ */
set(key, val) { set(key, val) {
if (this.rendered) { if (this.#ended) {
return this return this
} }
if (arguments.length === 2) { let value = Array.isArray(val) ? val.map(String) : String(val)
var value = Array.isArray(val) ? val.map(String) : String(val) this.#res.setHeader(key, value)
return this
if (
key.toLowerCase() === 'content-type' &&
typeof value === 'string' &&
value.indexOf('charset') < 0
) {
value += '; charset=utf-8'
} }
this.origin.res.setHeader(key, value) delete(key) {
} else { this.#res.removeHeader(key)
for (let i in key) {
this.set(i, key[i])
}
}
return this return this
} }
@ -208,26 +236,18 @@ export default class Response {
* @param {String} val [description] * @param {String} val [description]
*/ */
append(key, val) { append(key, val) {
if (this.rendered) { if (this.#ended) {
return return
} }
var prev = this.get(key) let prev = this.get(key) || []
var value
if (Array.isArray(val)) { if (!Array.isArray(prev)) {
value = val prev = [prev]
} else {
value = [val]
} }
if (prev) { prev = prev.concat(val)
if (Array.isArray(prev)) {
value = prev.concat(val) return this.set(key, prev)
} else {
value = [prev].concat(val)
}
}
return this.set(key, value)
} }
/** /**
@ -238,7 +258,7 @@ export default class Response {
*/ */
cookie(key, val, opts = {}) { cookie(key, val, opts = {}) {
//读取之前已经写过的cookie缓存 //读取之前已经写过的cookie缓存
var cache = this.get('set-cookie') let cache = this.get('set-cookie')
if (cache) { if (cache) {
if (!Array.isArray(cache)) { if (!Array.isArray(cache)) {
cache = [cache] cache = [cache]

View File

@ -3,16 +3,16 @@
* @date 2020/09/20 15:08:50 * @date 2020/09/20 15:08:50
*/ */
var KEY_REGEXP = /^[\u0009\u0020-\u007e\u0080-\u00ff]+$/ const KEY_REGEXP = /^[\u0009\u0020-\u007e\u0080-\u00ff]+$/
// var SPLIT_REGEXP = /; */ // const SPLIT_REGEXP = /; */
var encode = encodeURIComponent const encode = encodeURIComponent
// var decode = decodeURIComponent // const decode = decodeURIComponent
/** /**
* [serialize 序列化对象] * [serialize 序列化对象]
*/ */
export function serialize(key, val, opts) { export function serialize(key, val, opts) {
var pairs = [] let pairs = []
if (!KEY_REGEXP.test(key)) { if (!KEY_REGEXP.test(key)) {
return '' return ''
} }

View File

@ -2,6 +2,7 @@ export default {
'100': 'Continue', '100': 'Continue',
'101': 'Switching Protocols', '101': 'Switching Protocols',
'102': 'Processing', '102': 'Processing',
'103': 'Early Hints',
'200': 'OK', '200': 'OK',
'201': 'Created', '201': 'Created',
'202': 'Accepted', '202': 'Accepted',
@ -11,6 +12,7 @@ export default {
'206': 'Partial Conten', '206': 'Partial Conten',
'207': 'Multi-Status', '207': 'Multi-Status',
'208': 'Already Reported', '208': 'Already Reported',
'218': 'This is fine',
'226': 'IM Used', '226': 'IM Used',
'300': 'Multiple Choices', '300': 'Multiple Choices',
'301': 'Moved Permanently', '301': 'Moved Permanently',
@ -40,20 +42,32 @@ export default {
'416': 'Requested Range Not Satisfiable', '416': 'Requested Range Not Satisfiable',
'417': 'Expectation Failed', '417': 'Expectation Failed',
'418': "I'm a teapot", '418': "I'm a teapot",
'419': 'Page Expired',
'420': 'Enhance Your Caim', '420': 'Enhance Your Caim',
'421': 'Misdirected Request', '421': 'Misdirected Request',
'422': 'Unprocessable Entity', '422': 'Unprocessable Entity',
'423': 'Locked', '423': 'Locked',
'424': 'Failed Dependency', '424': 'Failed Dependency',
'425': 'Unordered Collection', '425': 'Too Early',
'426': 'Upgrade Required', '426': 'Upgrade Required',
'428': 'Precondition Required', '428': 'Precondition Required',
'429': 'Too Many Requests', '429': 'Too Many Requests',
'430': 'Would Block',
'431': 'Request Header Fields Too Large', '431': 'Request Header Fields Too Large',
'440': 'Login Time-Out',
'444': 'No Response', '444': 'No Response',
'449': 'Retry With',
'450': 'Blocked by Windows Parental Controls', '450': 'Blocked by Windows Parental Controls',
'451': 'Unavailable For Legal Reasons', '451': 'Unavailable For Legal Reasons',
'460': 'Client Closed Connection Prematurely',
'463': 'Too Many Forwarded IP Addresses',
'464': 'Incompatible Protocol',
'494': 'Request Header Too Large', '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', '500': 'Internal Server Error',
'501': 'Not Implemented', '501': 'Not Implemented',
'502': 'Bad Gateway', '502': 'Bad Gateway',
@ -63,6 +77,20 @@ export default {
'506': 'Variant Also Negotiates', '506': 'Variant Also Negotiates',
'507': 'Insufficient Storage', '507': 'Insufficient Storage',
'508': 'Loop Detected', '508': 'Loop Detected',
'509': 'Bandwidth Limit Exceeded',
'510': 'Not Extended', '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',
} }

71
lib/mime-tpyes.js Normal file
View File

@ -0,0 +1,71 @@
//
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',
xml: 'text/xml',
txt: 'text/plain',
css: 'text/css',
js: 'application/javascript',
json: 'application/json',
webmanifest: 'application/json',
wast: 'application/wast',
wasm: 'application/wasm',
gif: 'image/gif',
jpg: 'image/jpeg',
webp: 'image/webp',
tiff: 'image/tiff',
png: 'image/png',
apng: 'image/apng',
svg: 'image/svg+xml',
ico: 'image/x-icon',
bmp: 'image/x-ms-bmp',
mid: 'audio/midi',
midi: 'audio/x-midi',
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',
zip: 'application/zip',
'7zip': 'application/zip',
rar: 'application/rar',
gz: 'application/x-gzip',
tar: 'application/x-tar',
tgz: 'application/x-tar',
pdf: 'application/pdf',
psd: 'application/x-photoshop',
doc: 'application/msword',
xls: 'application/vnd.ms-excel',
ppt: 'application/vnd.ms-powerpoint',
docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
pptx: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
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

View File

@ -1,11 +1,15 @@
{ {
"name": "@gm5/response", "name": "@gm5/response",
"version": "1.3.1", "version": "2.0.0",
"type": "module", "type": "module",
"description": "对Http的response进一步封装, 提供常用的API", "description": "对Http的response进一步封装, 提供常用的API",
"main": "index.js", "main": "index.js",
"author": "yutent", "author": "yutent",
"keywords": ["fivejs", "response", "http"], "keywords": [
"fivejs",
"response",
"http"
],
"dependencies": { "dependencies": {
"iofs": "^1.5.0" "iofs": "^1.5.0"
}, },