Last active
January 13, 2021 07:14
-
-
Save herudi/126fd895aef994b3a42fc66d42bc45ab to your computer and use it in GitHub Desktop.
Native nodejs http server like express
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const http = require('http'); | |
const pathnode = require('path'); | |
const { parse: parseqs } = require('querystring'); | |
const { parse: parsenodeurl } = require('url'); | |
const METHODS = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS', 'ALL']; | |
const PRE_METHOD = 'GET,POST'; | |
const PUSH = Array.prototype.push; | |
const TYPE = 'Content-Type'; | |
const JSON_TYPE = 'application/json'; | |
const JSON_CHARSET = JSON_TYPE + ';charset=utf-8'; | |
function addRes(res, eng) { | |
res.set = function (name, value) { | |
this.setHeader(name, value); | |
return this; | |
}; | |
res.get = function (name) { | |
return this.getHeader(name); | |
}; | |
res.code = function (code) { | |
this.statusCode = code; | |
return this; | |
}; | |
res.status = function (code) { | |
this.statusCode = code; | |
return this; | |
}; | |
res.type = function (type) { | |
this.setHeader(TYPE, type); | |
return this; | |
}; | |
res.json = function (data) { | |
data = JSON.stringify(data); | |
this.setHeader(TYPE, JSON_CHARSET); | |
this.end(data); | |
}; | |
res.send = function (data) { | |
if (typeof data === 'string') this.end(data); | |
else if (typeof data === 'object') this.json(data); | |
else this.end(data || http.STATUS_CODES[this.statusCode]); | |
}; | |
res.render = function (src, ...args) { | |
let idx = src.indexOf('.'), | |
obj = eng[Object.keys(eng)[0]], | |
pathfile = pathnode.join(obj.basedir, src + obj.ext); | |
if (idx !== -1) { | |
obj = eng[src.substring(idx)]; | |
pathfile = pathnode.join(obj.basedir, src); | |
} | |
return obj.render(res, pathfile, ...args); | |
}; | |
} | |
function defError(err, req, res, next) { | |
let code = err.status || err.code || err.statusCode || 500; | |
if (typeof code !== 'number') code = 500; | |
res.statusCode = code; | |
res.end(err.message || 'Something went wrong'); | |
} | |
function defNotFound(message) { | |
return function (req, res, next) { | |
res.statusCode = 404; | |
res.end(`Route ${message} not found`) | |
} | |
} | |
function findBase(pathname) { | |
let iof = pathname.indexOf('/', 1); | |
if (iof !== -1) return pathname.substring(0, iof); | |
return pathname; | |
} | |
function parseurl(req) { | |
let str = req.url, url = req._parsedUrl; | |
if (url && url._raw === str) return url; | |
return (req._parsedUrl = parsenodeurl(str)); | |
} | |
function findFn(arr) { | |
let ret = [], i = 0, len = arr.length; | |
for (; i < len; i++) { | |
if (typeof arr[i] === 'function') ret.push(arr[i]); | |
} | |
return ret; | |
} | |
function pathRegex(path) { | |
if (path instanceof RegExp) return { params: null, regex: path }; | |
let pattern = path.replace(/\/:[a-z]+/gi, '/([^/]+?)'); | |
let regex = new RegExp(`^${pattern}/?$`, 'i'); | |
let matches = path.match(/\:([a-z]+)/gi); | |
let params = matches && matches.map(e => e.substring(1)); | |
return { params, regex }; | |
} | |
function mutateRoute(route, c_route) { | |
METHODS.forEach(el => { | |
if (c_route[el] !== void 0) { | |
if (route[el] === void 0) route[el] = []; | |
route[el] = route[el].concat(c_route[el]); | |
}; | |
}); | |
return route; | |
} | |
function patchRoutes(arg, args, routes) { | |
let prefix = '', midds = [], i = 0, len = routes.length, ret = {}; | |
midds = midds.concat(findFn(args)); | |
if (typeof arg === 'string' && arg.length > 1 && arg.charAt(0) === '/') prefix = arg; | |
for (; i < len; i++) { | |
let el = routes[i]; | |
let { params, regex } = pathRegex(prefix + el.path); | |
el.handlers = midds.concat(el.handlers); | |
if (ret[el.method] === void 0) ret[el.method] = []; | |
ret[el.method].push({ params, regex, handlers: el.handlers }); | |
} | |
return ret; | |
}; | |
function renderEngine(obj) { | |
return function (res, source, ...args) { | |
if (obj.options) args.push(obj.options); | |
if (!args.length) args.push({ settings: obj.settings }); | |
if (typeof obj.engine === 'function') { | |
obj.engine(source, ...args, (err, out) => { | |
if (err) throw err; | |
res.setHeader(TYPE, 'text/html; charset=utf-8'); | |
res.end(out); | |
}); | |
} | |
} | |
} | |
class Router { | |
constructor() { | |
this.route = { 'MIDDS': [] }; | |
this.c_routes = []; | |
this.get = this.on.bind(this, 'GET'); | |
this.post = this.on.bind(this, 'POST'); | |
this.put = this.on.bind(this, 'PUT'); | |
this.patch = this.on.bind(this, 'PATCH'); | |
this.delete = this.on.bind(this, 'DELETE'); | |
this.options = this.on.bind(this, 'OPTIONS'); | |
this.head = this.on.bind(this, 'HEAD'); | |
this.all = this.on.bind(this, 'ALL'); | |
} | |
on(method, path, ...handlers) { | |
this.c_routes.push({ method, path, handlers }); | |
return this; | |
} | |
getRoute(method, path, notFound) { | |
if (this.route['ALL'] !== void 0) { | |
if (this.route[method] === void 0) this.route[method] = []; | |
this.route[method] = this.route[method].concat(this.route['ALL']); | |
} | |
let i = 0, j = 0, el, routes = this.route[method] || [], matches = [], params = {}, handlers = [], len = routes.length, nf; | |
while (i < len) { | |
el = routes[i]; | |
if (el.regex.test(path)) { | |
nf = false; | |
if (el.params) { | |
matches = el.regex.exec(path); | |
while (j < el.params.length) params[el.params[j]] = matches[++j] || null; | |
} | |
PUSH.apply(handlers, el.handlers); | |
break; | |
} | |
i++; | |
} | |
if (notFound) handlers.push(notFound); | |
else handlers.push(defNotFound(method + path)); | |
return { params, handlers, nf }; | |
} | |
}; | |
class Tinex extends Router { | |
constructor({ useServer, useParseUrl } = {}) { | |
super(); | |
this.server = useServer; | |
this.parseurl = useParseUrl || parseurl; | |
this.mroute = {}; | |
this.error = defError; | |
this.notFound = undefined; | |
this.engine = {}; | |
} | |
onError(fn) { | |
this.error = fn; | |
} | |
onNotFound(fn) { | |
this.notFound = fn; | |
} | |
on(method, path, ...handlers) { | |
let { params, regex } = pathRegex(path); | |
if (this.route[method] === void 0) this.route[method] = []; | |
this.route[method].push({ params, regex, handlers }); | |
return this; | |
} | |
use(...args) { | |
let arg = args[0], larg = args[args.length - 1], len = args.length; | |
if (len === 1 && typeof arg === 'function') this.route['MIDDS'].push(arg); | |
else if (typeof arg === 'string' && typeof larg === 'function') { | |
let prefix = arg === '/' ? '' : arg, fns = []; | |
fns.push((req, res, next) => { | |
req.url = req.url.substring(prefix.length) || '/'; | |
req.path = req.path ? req.path.substring(prefix.length) || '/' : '/'; | |
next(); | |
}); | |
fns = fns.concat(findFn(args)); | |
this.route[prefix] = fns; | |
} | |
else if (typeof larg === 'object' && larg.c_routes) mutateRoute(this.route, patchRoutes(arg, args, larg.c_routes)); | |
else if (typeof larg === 'object' && larg.engine) { | |
let defaultDir = pathnode.join(pathnode.dirname(require.main.filename || process.mainModule.filename), 'views') | |
let ext = arg.ext, | |
basedir = pathnode.resolve(arg.basedir || defaultDir), | |
render = arg.render; | |
if (render === void 0) { | |
let engine = (typeof arg.engine === 'string' ? require(arg.engine) : arg.engine); | |
if (typeof engine === 'object' && engine.renderFile !== void 0) engine = engine.renderFile; | |
ext = ext || ('.' + (typeof arg.engine === 'string' ? arg.engine : 'html')); | |
render = renderEngine({ engine, options: arg.options, settings: { views: basedir, ...(arg.set ? arg.set : {}) } }) | |
} | |
this.engine[ext] = { ext, basedir, render }; | |
} | |
else if (Array.isArray(larg)) { | |
let el, i = 0, len = larg.length; | |
for (; i < len; i++) { | |
el = larg[i]; | |
if (typeof el === 'object' && el.c_routes) mutateRoute(this.route, patchRoutes(arg, args, el.c_routes)); | |
else if (typeof el === 'function') this.route['MIDDS'].push(el); | |
}; | |
} | |
else this.route['MIDDS'] = this.route['MIDDS'].concat(findFn(args)); | |
return this; | |
} | |
lookup(req, res) { | |
let cnt = 0; for (let k in this.mroute) cnt++; | |
if (cnt > 32) this.mroute = {}; | |
let url = this.parseurl(req), | |
key = req.method + url.pathname, | |
obj = this.mroute[key], i = 0, | |
next = (err = null) => { | |
if (err) return this.error(err, req, res, next); | |
obj.handlers[i++](req, res, next); | |
}; | |
if (obj === void 0) { | |
obj = this.getRoute(req.method, url.pathname, this.notFound); | |
if (PRE_METHOD.indexOf(req.method) !== -1 && obj.nf !== void 0) this.mroute[key] = obj; | |
} | |
addRes(res, this.engine); | |
req.originalUrl = req.url; | |
req.params = obj.params; | |
req.path = url.pathname; | |
req.query = parseqs(url.query); | |
req.search = url.search; | |
if (obj.m === void 0) { | |
let prefix = findBase(url.pathname), midds = this.route['MIDDS']; | |
if (this.route[prefix] !== void 0) obj.handlers = this.route[prefix].concat(obj.handlers); | |
obj.handlers = midds.concat(obj.handlers); | |
obj.m = true; | |
if (PRE_METHOD.indexOf(req.method) !== -1 && obj.nf !== void 0) this.mroute[key] = obj; | |
}; | |
next(); | |
} | |
listen(port = 3000, ...args) { | |
const server = this.server || http.createServer(); | |
server.on('request', this.lookup.bind(this)); | |
server.listen(port, ...args); | |
} | |
}; | |
let tinex = ({ useServer, useParseUrl } = {}) => new Tinex({ useServer, useParseUrl }); | |
tinex.router = () => new Router(); | |
tinex.Router = Router; | |
module.exports = tinex; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Usage
Simple
Middleware
Router
Body Parser
Template Engine
Serve Static
Error Handling