UNPKG

cooperation

Version:
640 lines (549 loc) 21.5 kB
const http = require('http'); const url = require('url'); const assign = require('assign-deep'); //const querystring = require('querystring'); const fs = require("fs"); const path = require("path"); const minify = require('html-minifier').minify; const terser = require("terser"); const kill = require('cross-port-killer').kill; const log = require('../util/log'); //多网卡情况后期支持测试 /** * http服务, * 前端友好无干扰,前端不需要做任何配置和多余的工作 * 需要依赖前端的EasyScript库的base.class.js * 特色: * 1、高性能,极轻量hppt服务 * 2、动静分离 * 3、修改后端代码免重启 * 4、资源合并,降低请求量 * 5、资源缓存,降低磁盘io * 6、首屏数据、首屏渲染,缩短相应时间,提升用户体验(有空再搞) * 7、自动前端模块化处理,降低前端非业务逻辑复杂度 * 8、前端定向缓存减少请求 * @param port 监听端口 * @param dev 是否开启开发者开关,开启后代码修改免重启,生产环境一定为false或者不填写!! */ const NAME = path.basename(__filename, '.js'); const projectPath = path.join(__dirname, '../../../'); const cacheMap = new Map(); //SOAP风格,协议是面向资源的,已废弃,灵活性太大,难以规范。 //RESTFUL风格,是面向服务的,采用,限制死客户端操作。 const httpMethod = new Set([ "GET", //请求资源 "POST", //创建资源 "PUT", //幂等修改(整体) "PATCH", //非幂等修改(局部) "DELETE", //删除 "OPTIONS" ]); /* get /users/{userId} 获取userId对应的user信息 delete /users/{userId} 删除userId对应的user post /users 创建一个新的user put /users/{userId} 更改userId对应的user信息 patch /users/{userId} 更改userId对应的user信息 */ let isCache = false; let isDev = false; let isCors = false; //约定的目录结构 const dir = { public: path.join(projectPath, 'main/public'), page: path.join(projectPath, 'main/public/page'), resource: path.join(projectPath, 'main/public/resource'), api: path.join(projectPath, 'main/server/api/') }; module.exports = ({ port = 80,//端口 //cb = null,//回调 cors = true,//跨域 dev = false,//开发模式 cache = false,//静态资源 //requestInterceptor = null //api的header拦截器 } = {}) => { cache && console.log(NAME + ":开启缓存模式提高性能的同时会产生内存消耗"); isCache = cache; dev && console.log(NAME + ":使用了发者模式"); isDev = dev; cors && console.log(NAME + ":启用了允许跨域"); isCors = cors; console.log(NAME + ":监听了" + port + "端口",); kill(port).then(pids => { http.createServer(server).listen(port); /*if (typeof cb === 'function') { cb(); }*/ }).catch(err => console.error(err)) }; function server(req, res) { if (isCors) { res.setHeader('Access-Control-Allow-Origin', '*'); res.setHeader('Access-Control-Allow-Credentials', true); res.setHeader("Access-Control-Allow-Methods", "PUT,POST,GET,DELETE,OPTIONS"); res.setHeader('Access-Control-Allow-Headers', 'Origin,X-Requested-With,Content-Type,Accept,Authorization'); } let pathname = url.parse(req.url).pathname; if (pathname === '/') pathname += 'index'; let arr = pathname.split('/'); if (arr[1] === 'api') { //api的路由链路 if (arr.length < 4) { output(res, null, null, "路由错误"); return; } //api方法 let method = req.method.toUpperCase(); if (!httpMethod.has(method)) { output(res, null, null, "请求方法错误"); return; } //地址栏参数 let reqParam = url.parse(req.url, true).query; //路径参数 reqParam["_"] = arr[3]; //处理消息的程序 let file = dir.api + arr[2] + '.js'; fs.exists(file, exists => { if (!exists) { output(res, null, null, "不存在:" + file); } else { if (req.method.toUpperCase() === 'OPTIONS') {//OPTIONS if (isCors) { res.writeHead(200); res.end(); } else { res.writeHead(403, {'Content-Type': 'text/html;charset=utf-8'}); res.end('不允许跨域'); } } else { //路径参数 let postData = ""; //持续读写内容,因为post可能很大 req.on("data", data => postData += data); req.on("end", () => { try { reqParam = assign(reqParam, JSON.parse(postData || "{}")); require(file)(method, reqParam, data => output(res, data, 'api'), req); } catch (err) { output(res, err.message, 'err'); } }); } } }); } else if (arr[1] === 'resource') {//公共静态资源 output(res, dir.public + pathname); } else { if (path.extname(pathname)) {//资源 if (pathname.includes("favicon.ico")) { output(res, dir.public + "/resource/img/favicon.ico"); return; } let file = null; if (arr.length === 4) { file = dir.page + pathname; } else if (arr.length === 5) { file = dir.page + '/' + arr[1] + '/module/' + arr[2] + '/' + arr[3] + '/' + arr[4]; } output(res, file); } else { //页面资源 let pathDir = dir.page + '/' + arr[1]; fs.exists(pathDir + '/', exists => { if (!exists) { output(res, null, null, "文件不存在:" + pathDir); } else { //读取缓存 if (!isDev && isCache && cacheMap.has(pathDir + '/')) { res.writeHead(200, {'Content-Type': 'text/html;charset=utf-8'}); res.end(cacheMap.get(pathDir + '/')); return; } //并行拼装文件 let step = 0; let files = {'style.css': '', 'app.html': '', 'script.js': ''}; for (let f in files) { let file = pathDir + '/' + f; fs.readFile(file, 'utf8', (err, data) => { if (err) { //if (f === 'app.html') { output(res, null, null, err); return; //} //data = ''; } files[f] = data; ++step; if (step === 3) {//文件读取完毕, //编译html中的图片路径 files['app.html'] = htmlImgCompiler(arr[1], '', files['app.html']); //编译css中的图片 files['style.css'] = moduleCssCompiler(arr[1], '', files['style.css']); //执行拼装 make(files, pathDir, html => { //压缩 if (!isDev) { html = minify(html, { collapseWhitespace: true, conservativeCollapse: true, keepClosingSlash: true, minifyCSS: true, minifyJS: (text, inline) => terser.minify(text).code, minifyURLs: true, removeScriptTypeAttributes: true, removeStyleLinkTypeAttributes: true, removeComments: true }); } //输出 output(res, html, 'page'); //缓存 !isDev && isCache && cacheMap.set(pathDir + '/', html); }); } }) } } }) } } } function output(res, file, type = null, msg = null) { let ContentType = 'charset=utf-8'; //404 if (!file) { log(msg, "STACK"); res.writeHead(404, {'Content-Type': ContentType}); res.end(); return; } //API if (type === 'api') { res.writeHead(200, {'Content-Type': 'application/json;' + ContentType}); res.end(file); return; } //500 if (type === 'err') { log(file, "STACK"); res.writeHead(500, {'Content-Type': ContentType}); res.end(file); return; } if (type === 'page') { res.writeHead(200, {'Content-Type': 'text/html;' + ContentType}); res.end(file); return; } //判断扩展名 switch (path.extname(file)) { case '.html': ContentType = 'text/html;' + ContentType; break; case '.js': ContentType = 'application/javascript;' + ContentType; break; case '.css': ContentType = 'text/css;' + ContentType; break; case '.json': ContentType = 'application/json;' + ContentType; break; case '.jpg': case '.jpeg': ContentType = 'image/jpeg;'; break; case '.png': ContentType = 'application/x-png;'; break; case '.gif': ContentType = 'image/gif;'; break; default: ContentType = 'application/octet-stream;'; } //判断缓存 if (isCache && cacheMap.has(file)) { res.writeHead(200, {'Content-Type': ContentType}); res.end(cacheMap.get(file)); return; } fs.exists(file, exists => { if (!exists) { res.writeHead(404, {'Content-Type': ContentType}); res.end(); return; } fs.readFile(file, (err, data) => { if (err) { res.writeHead(500, {'Content-Type': ContentType}); res.end(err.message); return; } res.writeHead(200, {'Content-Type': ContentType}); res.end(data); isCache && cacheMap.set(file, data); }); }) } function make(files, mainPath, cb) { let html = files['app.html'], style = files['style.css'], script = files['script.js']; //删除注释 html = html.replace(/<!--.*-->/gim, ''); //修改对象 script = script.replace(/class (\S*) {/, ` const app = new class{ constructor() { this.moduleMap = new Map(); this.DOM(); this.INIT(); this.EVENT(); } getModule(moduleName) { return this.moduleMap.get(moduleName); } setModule(moduleName, module) { this.moduleMap.set(moduleName, module); } destroyModule(moduleName) { let module = this.getModule(moduleName); module.destroy(); this.moduleMap.delete(moduleName); } reloadModule(name = null) { name ? this.getModule(name).init() : this.moduleMap.forEach(v => v.init &&v.init()); } `); //追加模块操作函数 //执行new script += '()'; //解析 扩展<module entry=''/> let moduleReg = /<module.*?(?:>|\/>)/gi, //entry属性 entryReg = /entry=['"]?([^'"]*)['"]?/i, //id属性 idReg = /id=['"]?([^'"]*)['"]?/i, //class属性 classReg = /class=['"]?([^'"]*)['"]?/i, //scope属性 scopeReg = /scope=['"]?([^'"]*)['"]?/i; let arr = html.match(moduleReg); if (arr) { let moduleLen = arr.length; for (var i = 0; i < arr.length; i++) { //获取属性 let moduleStr = arr[i]; //entry let entryRes = arr[i].match(entryReg), entryAttr = null; //检查entry if (entryRes) { entryAttr = entryRes[1]; } else { html = html.replace(moduleStr, '<div>模块未定义entry属性</div>'); moduleLen--; if (moduleLen === 0) { //组装 html = htmler(style, script, html); cb(html); } continue; } //属性 let attrObj = {}; //class let classRes = arr[i].match(classReg); classRes && (attrObj['class'] = classRes[1]); //id let idRes = arr[i].match(idReg); idRes && (attrObj['id'] = idRes[1]); //scope let scopeRes = arr[i].match(scopeReg); //console.log(scopeRes); //scopeRes && (attrObj['scope'] = scopeRes[1]); //页面资源 let modulePath = null; let pageName = null; if (scopeRes && scopeRes[1] === 'global') { modulePath = dir.resource + '/module/' + entryAttr; pageName = 'resource'; } else { modulePath = mainPath + '/module/' + entryAttr; pageName = mainPath.split('page/')[1]; } //======> fs.exists(modulePath + '/', exists => { //并行拼装文件 if (!exists) { html = html.replace(moduleStr, '<div>模块目录未找到:' + modulePath + '</div>'); moduleLen--; if (moduleLen === 0) { //组装 html = htmler(style, script, html); cb(html); } } else { let step = 0; let moduleFiles = {'style.css': '', 'app.html': '', 'script.js': ''}; for (let f in moduleFiles) { let file = modulePath + '/' + f; fs.readFile(file, 'utf8', (err, data) => { if (err) { if(f === 'app.html'){ data = '<div>模块目录未找到:' + modulePath + '</div>' } if(f === 'style.css'){ //TODO 处理没有css的时候 data = ''; } if(f === 'script.js'){ //TODO 处理没有js的时候 data = ''; } } moduleFiles[f] = data; ++step; if (step === 3) {//文件读取完毕, //追加到主页面 //替换<module/> html = moduleHtmlCompiler(pageName, attrObj['id'] || entryAttr, moduleFiles['app.html'], moduleStr, html, attrObj); //style追加 style += moduleCssCompiler(pageName, attrObj['id'] || entryAttr, moduleFiles['style.css']); //js追加 script += moduleJsCompiler(pageName, attrObj['id'] || entryAttr, moduleFiles['script.js']); moduleLen--; if (moduleLen == 0) { //组装 html = htmler(style, script, html); cb(html); } } }) } } }) //============ } } else { //组装 html = htmler(style, script, html); cb(html); } } //组装页面 function htmler(style, script, html) { let styleDom = style ? '<style>' + style + '</style>' : '', scriptDom = script ? '<script>document.addEventListener("DOMContentLoaded", () => {' + script + '\n})</script>' : ''; html = html.replace('</head>', styleDom + '</head>'); html = html.replace('</head>', scriptDom + '</head>'); return html; } //编译html中的图片 function htmlImgCompiler(pageName, moduleName, htmlStr) { //追加里面的模块src let imgReg = /<img.*?(?:>|\/>)/gi, srcReg = /src=['"]?([^'"]*)['"]?/i; //所有图片 let arr = htmlStr.match(imgReg); if (arr) { let imgLen = arr.length; for (var i = 0; i < imgLen; ++i) { let srcAttr = arr[i].match(srcReg); if (srcAttr && srcAttr.length >= 2) { let src = srcAttr[1]; if (!src.indexOf('img/')) { //替换掉img标签标签内的src let newImg = arr[i].replace(src, '/' + pageName + '/' + moduleName + src); //替换掉页面里的img标签 htmlStr = htmlStr.replace(arr[i], newImg); } else if (!src.indexOf('/img/')) { //页面的图片 htmlStr = htmlStr.replace(src, '/' + pageName + src); } } } } return htmlStr; } //编译html function moduleHtmlCompiler(pageName, moduleName, htmlStr, moduleStr, html, domAttr) { //处理图片 htmlStr = htmlImgCompiler(pageName, moduleName + '/', htmlStr); //处理模块 let clazz = domAttr['class'] ? ' class="' + domAttr['class'] + '"' : ''; let id = ' id="' + (domAttr['id'] ? domAttr['id'] : moduleName) + '"'; html = html.replace(moduleStr, '<div' + clazz + id + '>' + htmlStr + '</div>'); return html; } //编译css function moduleCssCompiler(pageName, moduleName, cssStr) { let backgroundReg = /.*background[^;"]+url\(([^\)]+)\).*/gi, urlReg = /url\(['".]?([^'".]*)['".]\)?/i; let wrapCss = ''; let arr = cssStr.match(backgroundReg); if (arr) { let backgroundLen = arr.length; for (var i = 0; i < backgroundLen; i++) { let urlArr = arr[i].match(urlReg); if (urlArr && urlArr.length >= 2) { let url = urlArr[1]; if (!url.indexOf('img/')) { //替换内部的 let newImg = arr[i].replace(url, '/' + pageName + '/' + moduleName + '/' + url); //替换文件里的 cssStr = cssStr.replace(arr[i], newImg); } else if (!url.indexOf('/img/')) { let newImg = arr[i].replace(url, '/' + pageName + url); cssStr = cssStr.replace(arr[i], newImg); } } } } //兼容多写 let cssArr = cssStr.split('}'); cssArr.pop(); cssArr.forEach(v => { let start = v.indexOf('{'); let name = v.substr(0, start); let value = v.substr(start || 0); if (moduleName) { if (name.includes(',')) name = name.replace(',', ',#' + moduleName + '>'); wrapCss += '\n#' + moduleName + '>' + name + value + '}' } else { wrapCss += '\n' + name + value + '}' } }); return wrapCss; } //编译js function moduleJsCompiler(pageName, moduleName, jsStr) { //jsStr = jsStr.replace(/class (\S*) {/, 'class ' + ejs.capitalize(moduleName) + ' {'); //简化书写调用模块 jsStr = jsStr.replace(/MODULE/g, 'this.APP.getModule'); jsStr = jsStr.replace(/DOMAIN/g, 'this.DOMAIN'); jsStr = jsStr.replace(/NAME/g, 'this.NAME'); //js里面也有html jsStr = htmlImgCompiler(pageName, moduleName + '/', jsStr); //包装 jsStr = jsStr.replace( /class (\S*) {/, `;\napp.setModule("${moduleName}",new class{ constructor(APP, DOMAIN) { this.APP = APP; this.DOMAIN = DOMAIN; this.NAME = "${moduleName}"; this.DOM(); this.INIT(); this.EVENT(); } ` ) + '(app,document.querySelector("#' + moduleName + '")))'; return jsStr; }