UNPKG

@dazejs/framework

Version:

Daze.js - A powerful web framework for Node.js

645 lines (566 loc) 13.3 kB
/** * Copyright (c) 2018 Chan Zewail * * This software is released under the MIT License. * https://opensource.org/licenses/MIT */ import getType from 'cache-content-type'; import compressible from 'compressible'; import contentDisposition from 'content-disposition'; import { OutgoingHttpHeaders, ServerResponse } from 'http'; import { extname } from 'path'; import { Stream } from 'stream'; import * as zlib from 'zlib'; import { Container } from '../../container'; import { Cookie } from '../cookie'; import { Application } from '../../foundation/application'; import { Request } from '../request'; import { Resource } from '../../resource/resource'; import { View } from '../../view'; import { ViewFactory } from '../../view/factory'; import { Statusable } from './statusable'; import { Paginator } from '../../pagination'; import { Str } from '../../utils/str' const encodingMethods = { gzip: zlib.createGzip, deflate: zlib.createDeflate }; const defaultContentTypes = { JSON: 'application/json; charset=utf-8', PLAIN: 'text/plain; charset=utf-8', OCTET: 'application/octet-stream' }; export class Response extends Statusable { /** * application */ protected app: Application = Container.get('app'); /** * response statusCode */ protected _code: number; /** * response Header */ protected _header: OutgoingHttpHeaders; /** * response data */ protected _data: any; /** * response cookies */ protected cookies: Cookie[] = []; /** * need to response force */ protected _isForce = false /** * 需要加密 */ protected _needEncrypt = false; /** * patched methods */ // [key: string]: any; constructor(data?: any, code = 200, header: OutgoingHttpHeaders = {}) { super(); /** * status code * @type */ this._code = code; /** * http headers * @type */ this._header = this.parseHeaders(header); /** * init data */ this.setData(data); } /** * code setter */ set code(code) { this.setCode(code); } /** * code getter */ get code() { return this._code; } /** * data setter */ set data(data) { this.setData(data); } /** * data getter */ get data() { return this._data; } /** * set to force response */ force() { this._isForce = true; return this; } /** * is force response */ isForce() { return this._isForce; } /** * parse init headers * @param headers */ private parseHeaders(headers: OutgoingHttpHeaders) { const keys = Object.keys(headers); const _headers: OutgoingHttpHeaders = {}; for (const key of keys) { _headers[key.toLocaleLowerCase()] = headers[key]; } return _headers; } /** * throw http exception with code and message * @param message exception message * @param code exception code */ error(message: any, code = 404) { this.setCode(code); this.setData(message); return this; } /** * set success data in ctx.body * @param data data * @param code http code */ success(data: any, code = 200) { this.setCode(code); this.setData(data); return this; } /** * get http header */ getHeader(name: string) { return this._header[name.toLowerCase()]; } /** * Set response header * The original response headers are merged when the name is passed in as object * * @param name Response header parameter name * @param value Response header parameter value */ setHeader(name: any, value: any) { this._header[name.toLowerCase()] = value; return this; } /** * remove header from response * @param name */ removeHeader(name: any) { delete this._header[name.toLowerCase()]; return this; } /** * getHeader alias * @public */ getHeaders() { return this._header; } /** * setHeader alias * @public */ setHeaders(headers: any) { const keys = Object.keys(headers); for (const key of keys) { this.setHeader(key.toLowerCase(), headers[key]); } return this; } /** * get http code * @public */ getCode() { return this.code; } /** * getCode alias * @public */ getStatus() { return this.getCode(); } /** * set code * @public * @param code status */ setCode(code = 200) { if (code) this._code = code; return this; } /** * setCode alias * @public * @param code status */ setStatus(code: number) { return this.setCode(code); } /** * get return data * @public */ getData() { return this._data; } /** * set content-type * @param type */ setType(type: string) { const _type = getType(type); if (_type) { this.setHeader('Content-Type', _type); } return this; } /** * get content type */ getType() { const type = this.getHeader('Content-Type') as string; if (!type) return ''; return type.split(';', 1)[0]; } /** * set length header * @param length */ setLength(length: number) { this.setHeader('Content-Length', length); return this; } /** * get response data length */ getLength() { const length = this.getHeader('Content-Length') as string; return length ? parseInt(length, 10) : 0; } /** * set vary header * @param field */ setVary(field: string) { const varyHeader = this.getHeader('Vary') || ''; const varys = String(varyHeader).split(','); varys.push(field); this.setHeader('Vary', varys.filter((v: string) => !!v).join(',')); } /** * LastModified * @public * @param time time * @returns this */ lastModified(time: string | Date | number) { if (time instanceof Date) { this.setHeader('Last-Modified', time.toUTCString()); return this; } if (typeof time === 'string' || typeof time === 'number') { this.setHeader('Last-Modified', (new Date(time)).toUTCString()); return this; } return this; } /** * Expires * @param time time */ expires(time: string) { this.setHeader('Expires', time); return this; } /** * ETag * @param eTag eTag */ eTag(eTag: string) { if (!(/^(W\/)?"/.test(eTag))) { this.setHeader('ETag', `"${eTag}"`); return this; } this.setHeader('ETag', eTag); return this; } /** * CacheControl * @param cache cache setting */ cacheControl(cache: string) { this.setHeader('Cache-Control', cache); return this; } /** * Set the page to do no caching * @public */ noCache() { this.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, post-check=0, pre-check=0'); this.setHeader('Pragma', 'no-cache'); return this; } /** * ContentType * @public * @param contentType The output type * @param charset The output of language */ contentType(contentType: string, charset = 'utf-8') { this.setHeader('Content-Type', `${contentType}; charset=${charset}`); return this; } /** * Contents-dispositions are set as "attachments" to indicate that the client prompts to download. * Optionally, specify the filename to be downloaded. * @public * @param filename * @returns this */ attachment(filename = '', options: any) { if (filename) this.setType(extname(filename)); this.setHeader('Content-Disposition', contentDisposition(filename, options)); return this; } /** * attachment alias * @public * @param filename */ download(data: any, filename = '', options: any) { return this.setData(data).attachment(filename, options); } /** * handle Resource data */ transformData(request: any) { const data = this.getData(); if (!data) return data; if (data instanceof Resource) { this.setType('json'); return data.output(); } if (data instanceof View) { this.setType('html'); return (new ViewFactory(data)).output(request); } if (data instanceof Paginator) { this.setType('json'); return data.toJSON(); } return data; } /** * response with cookie instance * @param _cookie */ withCookie(_cookie: any) { if (_cookie instanceof Cookie) { this.cookies.push(_cookie); } return this; } /** * response with cookie * @param key * @param value * @param options */ cookie(key: string, value: any, options: any = {}) { this.withCookie(new Cookie(key, value, options)); return this; } /** * set json response type */ json(data?: any) { this.setType('json'); if (data) this.setData(data); return this; } /** * set html response type */ html(data?: any) { this.setType('html'); if (data) this.setData(data); return this; } /** * set html response type */ text(data?: any) { this.setType('text'); if (data) this.setData(data); return this; } async commitCookies(request: any) { for (const _cookie of this.cookies) { request.cookies.set(_cookie.getName(), _cookie.getValue(), _cookie.getOptions()); } if (this.app.needsSession) { await request.session().autoCommit(); } } /** * send headers * @param res */ sendHeaders(res: ServerResponse) { if (res.headersSent) return this; const code = this.getCode(); const headers = this.getHeaders(); res.writeHead(code, headers); return this; } /** * Set the returned data * @public * @param data Returned data */ setData(data: any) { this._data = data; return this; } /** * 加密 * @returns */ encrypt(encrypt = true) { this._needEncrypt = encrypt; return this; } /** * prepare response * @param request */ prepare(request: Request) { let data = this.transformData(request); if (this._needEncrypt || request.needEncrypt) { const config = this.app.get('config') const key = config.get('app.key', 'dazejs') const iv = config.get('app.iv', null) data = Str.aesEncrypt(JSON.stringify(data), key, iv) this.setHeader('encrypted', true) this.setHeader('content-type', defaultContentTypes.PLAIN); } const shouldSetType = !this.getHeader('content-type'); // if no content if (data === null || typeof data === 'undefined') { this.setCode(204); this.removeHeader('content-type'); this.removeHeader('content-length'); this.removeHeader('transfer-encoding'); return data || ''; } // string if (typeof data === 'string' || typeof data === 'number' || typeof data === 'boolean') { if (shouldSetType) this.setHeader('content-type', defaultContentTypes.PLAIN); this.setHeader('content-length', Buffer.byteLength(data.toString())); return data.toString(); } // buffer if (Buffer.isBuffer(data)) { if (shouldSetType) this.setHeader('content-type', defaultContentTypes.OCTET); this.setHeader('content-length', data.length); return data; } // stream if (typeof data.pipe === 'function') { this.removeHeader('content-length'); if (shouldSetType) this.setHeader('content-type', defaultContentTypes.OCTET); return data; } // json this.removeHeader('content-length'); if (shouldSetType) this.setHeader('content-type', defaultContentTypes.JSON); return data; } /** * send data * @param request * @public */ async send(request: Request) { const data = this.prepare(request); if (this.app.get('config').get('app.compress')) { return this.endWithCompress(request, data); } return this.end(request, data); } /** * end response * @param request * @param data */ async end(request: Request, data: any) { const { res } = request; // commit cookies await this.commitCookies(request); // responses if (typeof data === 'string' || Buffer.isBuffer(data)) { this.sendHeaders(res); return res.end(data); } if (data instanceof Stream) { this.sendHeaders(res); return data.pipe(res); }; // json const jsonData = JSON.stringify(data); if (!res.headersSent) { this.setHeader('content-length', Buffer.byteLength(jsonData)); } this.sendHeaders(res); return res.end(jsonData); } /** * end with compress * @param request */ endWithCompress(request: Request, data: any) { // if compress is disable const encoding: string | false = request.acceptsEncodings('gzip', 'deflate', 'identity'); if (!encoding || encoding === 'identity') return this.end(request, data); if (!compressible(this.getType())) return this.end(request, data); const threshold = this.app.get('config').get('app.threshold', 1024); if (threshold > this.getLength()) return this.end(request, data); this.setHeader('Content-Encoding', encoding); this.removeHeader('Content-Length'); const stream = encodingMethods[encoding as 'gzip' | 'deflate']({}); if (data instanceof Stream) { data.pipe(stream); } else { stream.end(data); } return this.end(request, stream); } }