UNPKG

res-minify

Version:

服务端实时压缩,合并'js','css'的express中间件.

479 lines (448 loc) 13.7 kB
var fs = require("fs"); var path = require('path'); var CleanCSS = require('@cnwhy/clean-css'); var UglifyJS = require("uglify-js"); var less = require("less"); var Mime = require('mime-types'); var _ = require('underscore'); var Queue = require("promise-queue-plus"); // var q = qf.Q; var q = Queue.Promise; var readFile = q.denodeify(fs.readFile), fs_stat = q.denodeify(fs.stat), readdir = q.denodeify(fs.readdir), exists = q.denodeify(fs.exists), access = q.denodeify(fs.access); //内存缓存,对像 var memoryFiles = {}; var defConfigs = { "compressCSS": true //css压缩 ,"reAbsolute": false //是否转换CSS中原有的绝对路径 ,"compressJS": true //js压缩 ,"compileLess": true //编译Less文件 ,"serverCache": true //服务器缓存 ,"fileMaxAge": 60 * 60 * 24 * 7 //浏览器缓存时间秒 默认一周 ,"cacheFilePaths": "" //缓存目录 不设置则缓存到内存 ,"directoryList":false //目录浏览 ,"defaultFile":["index.html","index.htm","default.html","default.htm"] //默认文件,开启目录浏览后失效; ,"baseUrl": undefined } var UglifyJSConfig = { mangle:{ except : ['require'] } } /** * 存入缓存 * * @param {any} data * @param {any} cacheOption * @param {any} fun */ function saveCache(data, cacheOption) { var option = cacheOption; //option.cachePath = resServerConfig.cacheFilePaths; if (option.Type === "memory") { memoryFiles[option.Key] = data; } else if (option.Type === "file") { access(option.fPath).then(null,function(){ return q.denodeify(fs.mkdir)(option.fPath); }).then(function(){ return q.denodeify(fs.writeFile)(path.join(option.fPath,option.Key),data); }).catch(function(err){ console.error(err); }) } } /** * 获取缓存 * * @param {any} cacheOption * @returns string / ReadStream */ function getCache(cacheOption){ //console.log("getCache"); if (cacheOption.Type === "memory") { return q.resolve(memoryFiles[cacheOption.Key]); }else if(cacheOption.Type === "file"){ var cfile = path.join(cacheOption.fPath,cacheOption.Key); return access(cfile,fs.R_OK).then(function(){ return fs.createReadStream(cfile); },function(){return null;}) } } /** * 获取默认文件 * * @param {any} fpath * @param {any} files * @returns */ function getDefFile(fpath,files){ for(var i = 0; i < files.length; i++){ var rp = path.join(fpath,files[i]) try{ fs.accessSync(rp,fs.R_OK); return rp; }catch(e){} } } /** * 返回文件流 * * @param {any} realPath * @returns */ function sendFile(realPath) { return q.resolve(0).then(function(){ return fs.createReadStream(realPath) }); } //目录浏览 function showFileList(realPath,req,res,next){ return q.denodeify(fs.readdir)(realPath).then(function(files){ var html = []; html.push('<!DOCTYPE html>'); html.push('<html>') html.push('<head><title><%= decodeURIComponent(title) %></title></head>') html.push('<body>') html.push('<h1>“<%= decodeURIComponent(title) %>” 目录浏览:</h1>') html.push('<ul>') html.push('<%if(parsed){%><li><a href="<%=title%>../">..</a></li><%}%>') html.push('<% for(var i = 0;i<files.length;i++){%>') html.push('<li><a href="<%= title + encodeURIComponent(files[i]) %>"><%= files[i] %></a></li>') html.push('<%}%>') html.push('</ul>') html.push('</body>') html.push('</html>') var template = _.template(html.join('')); res.send(template({ "title": path.normalize(path.join(req.baseUrl,req.path+"/")) ,parsed: req.path !== "" && req.path !== "/" ,"files": files })) throw 200; }) } /** * 压缩CSS * * @param {any} cssStr * @param {any} relativeTo //文件目路径 * @param {any} rootPath //物理基础路径 用于绝对路径的`@import` 和 reabase url * @param {any} baseUrl //url 基础路径 * @param {any} reAbsolute //是否 根据baseUrl 修改 CSS 中的绝对路径 * @returns */ function compileCss(cssStr,relativeTo,rootPath,baseUrl,reAbsolute){ var cssOptions = { relativeTo: relativeTo, //文件目路径 root:rootPath, //物理基础路径 用于绝对路径的`@import` 和 reabase url baseUrl:baseUrl, //url 基础路径 reAbsolute:reAbsolute, //是否 根据baseUrl 修改 CSS 中的绝对路径 } var deferred = q.defer(); new CleanCSS(cssOptions).minify(cssStr,function(error, minified){ if(error) return deferred.reject(error); deferred.resolve(minified.styles); }); return deferred.promise; } /** * 处理CSS文件 * * @param {any} realPath * @param {any} rootPath * @param {any} baseUrl * @param {any} reAbsolute * @returns */ function compileCssFile(realPath,rootPath,baseUrl,reAbsolute){ var arg = Array.prototype.slice.call(arguments,0); arg[0] = path.dirname(realPath); return readFile(realPath).then(function(data){ arg.unshift(data.toString()); return compileCss.apply(null,arg) }) } /** * 处理less文件 * * @param {any} realPath * @param {any} rootPath * @param {any} baseUrl * @param {any} reAbsolute * @param {any} compressCSS * @returns */ function compileLessFile(realPath,rootPath,baseUrl,reAbsolute,compressCSS){ var arg = Array.prototype.slice.call(arguments,0,4); arg[0] = path.dirname(realPath); return readFile(realPath).then(function(data){ var lessOptions = { paths:[path.dirname(realPath)] //relativeUrls:true, //rootpath:baseUrl, //syncImport:true, //processImports:true, //strictImports:true, //compress:Con.compressCSS } return q.denodeify.call(less,less.render)(data.toString(),lessOptions).then(function(output){ if(!compressCSS) return output.css; arg.unshift(output.css); return compileCss.apply(null,arg); }) }) } var emptfn = function(){}; var throwCode = function(code){ return function(){ throw code || 404; } } var t404 = throwCode(404); var t200 = throwCode(200); //单个文件处理中间件 module.exports = function resServer(rootPath,configs){ var Con = _.extend({},defConfigs,configs); if(!fs.existsSync(rootPath)){ console.warn(rootPath+" 不存在!") return function(req,res,next){next();} } /** * 根据文件设置header * * @param {any} realPath * @param {any} arg * @returns */ function getheader(realPath,arg){ var mime = Mime.contentType(path.extname(realPath)) || 'text/plain', //文件类型对应的mime maxAge = this.fileMaxAge || 60 * 60 * 24 * 30, //fileMaxAge单位为秒 expires = new Date(Date.now() + maxAge * 1000); //过期的具体时间 //mime += mime.indexOf("text/") == 0 ? ";charset=utf-8" : ""; //对文本文件类型默认加上charset var header = {'Content-Type': mime} if(Con.serverCache) { //服务器缓存开关 header['Expires'] = expires.toUTCString(); //浏览器缓存的到期时间,低于"Cache-Control"的"max-age"; header['Cache-Control'] = 'max-age=' + maxAge; //浏览器缓存时长(秒); header['Last-Modified'] = arg.mtime.toUTCString(); //加上文件修改时间; } return header; } return function(req,res,next){ var realPath,baseUrl; baseUrl = Con.baseUrl == undefined ? req.baseUrl : Con.baseUrl; realPath = path.normalize(path.join(rootPath, decodeURIComponent(req.path))); //文件实际路径 function sendFile(fp,arg){ //return fs.createReadStream(fp).then(function(){ res.set(arg.header); fs.createReadStream(fp).pipe(res); throw 200; //}); } function argInit(stat){ //计算文件修改时间 var u_serch = req._parsedUrl.search; var isDirectory = stat.isDirectory(); var arg = { realPath: realPath, isDirectory: isDirectory, isFile : stat.isFile(), isCancatFile : isDirectory && u_serch && u_serch.indexOf("??") == 0 } if(arg.isCancatFile){//合并文件基本变量 var filesStr = u_serch.substr(2).replace(/\?.*$/,""); var files = filesStr ? filesStr.split(/\s*\,+\s*/):[]; if(files.length <= 0){ arg.isCancatFile = false; return arg; } arg.files = files.slice(0); //多文件则找最后被修改的文件做为 修改时间 arg.files_realPaths = files.map(function(v){ return path.normalize(path.join(realPath,v)); }) var p_list = arg.files_realPaths.map(function(v){ if(v.indexOf(rootPath) !== 0) return q.reject(new Error("超出权限范围!")) return fs_stat(v); }) return q.all(p_list).then(function(list){ var k = 0; list.forEach(function(v){ if(v.mtime > k){ k = v.mtime } }) arg.mtime = k; return arg; }) }else if(arg.isFile){ arg.mtime = stat.mtime; return arg; }else if(arg.isDirectory){ if(Con.directoryList){ return showFileList(realPath,req,res,next); }else{ var df = getDefFile(realPath,Con.defaultFile); if(df){ return sendFile(df,arg); }else{ throw 404; } } }else{ console.warn("[resMinify] "+realPath+" 类型不支持!") throw 404; } } //浏览器304处理 function comm304(arg){ if (Con.fileMaxAge && req.headers['if-modified-since'] && req.headers['if-modified-since'] == arg.mtime.toUTCString()) { //浏览器缓存,并判断缓存后文件是否更改 res.status(304).end(); //res.writeHead(304, "Not Modified");res.end(); // return q.reject(304); throw 304; } // return q.resolve(arg); return arg; } //文件类型 及 header function commHeader(arg){ if(arg.isFile){//单个文件基本变量 arg.type = path.extname(arg.realPath).toLowerCase().substr(1) || "*"; //获取文件类型 arg.header = getheader(arg.realPath,arg); } if(arg.isCancatFile){ var firstFile = arg.files_realPaths[0]; arg.type = path.extname(firstFile).toLowerCase().substr(1); arg.type && (arg.header = getheader(firstFile,arg)); } return arg; } //处理缓存 function commCatch(arg){ var cacheName,cacheOption; if(Con.serverCache){ if(arg.isFile){ cacheName = (baseUrl+req.path).replace(/[\/\\]/g, "&"); }else if(arg.isCancatFile){ cacheName = (baseUrl+req.path).replace(/[\/\\]/g, "&") + "#" + arg.files.join("+").replace(/[\/\\]/g, "&") } if (cacheName) { cacheOption = { Key : cacheName, Type : Con.cacheFilePaths ? "file" : "memory", fPath : Con.cacheFilePaths } //内存缓存 return getCache(cacheOption).then(function(cache){ if(typeof cache == "string"){ //resEnd(cache,1,arg); res.writeHead(200,arg.header); res.end(cache); }else if(cache && typeof cache.pipe == 'function'){ res.writeHead(200,arg.header); cache.pipe(res); }else{ arg.cacheOption = cacheOption; return arg; } throw 200; }).then(null,function(err){ if(err === 200) throw err; return arg; }) } } return arg; } function resEnd(str,arg,isCache){ return q.resolve(0).then(function(){ res.set(arg.header); res.end(str); Con.serverCache && !isCache && saveCache(str, arg.cacheOption); throw 200; }) } function commFile(type,realPath,files){ //var type = path.extname(realPath).toLowerCase().replace(".", "") || "*"; //获取文件类型 type = type === "less" ? "css" : type; var _type = path.extname(realPath).toLowerCase().replace(".", ""); if (_type === "less" && type === "css" && Con.compileLess) { return compileLessFile(realPath,rootPath,baseUrl,Con.reAbsolute,Con.compressCSS) }else if(_type === "css" && type === "css" && Con.compressCSS){ return compileCssFile(realPath,rootPath,baseUrl,files ? false : Con.reAbsolute); }else if(_type === "js" && type === "js" && Con.compressJS){ return q.delay(0).then(function(){ return UglifyJS.minify(realPath,UglifyJSConfig) }).then(function(minifyjs){ return minifyjs.code; }) } return q.reject('此类型文件不支持处理!'); } function commCancatFile(arg){ var defs = [],realPaths = arg.files_realPaths; if(arg.type === 'js'){ // if((arg.isFile && Con.compressJS) || arg.isCancatFile){ return q.delay(0).then(function(){ return UglifyJS.minify(arg.isFile ? realPath : realPaths,UglifyJSConfig) }).then(function(minifyjs){ return resEnd(minifyjs.code,arg); }) // } }else if(arg.type === 'less' || arg.type === 'css'){ for (var i = 0, j = realPaths.length; i < j; i++) { (function(i){ var fp = realPaths[i]; // var def = compileLessFile(fp,rootPath,baseUrl,Con.reAbsolute,true); var def = commFile(arg.type,fp) defs.push(def); })(i) } return q.all(defs).then(function(dataArr){ var strs = dataArr.join(''); if(arg.type === 'less'){ if(Con.compressCSS) return compileCss(strs,realPath,rootPath,baseUrl,false); else return strs; }else if(arg.type === 'css'){ return compileCss(strs,realPath,rootPath,baseUrl,false); } }).then(function(data){ return resEnd(data,arg) }) }else{ throw new Error("不支持此类文件的合并"); } } fs_stat(realPath).then(argInit,t404) .then(comm304) .then(commHeader) .then(commCatch) .then(function(arg){ if(arg.isFile){ return q.resolve(0).then(function(){ return commFile(arg.type,arg.realPath).then(function(data){ return resEnd(data,arg) },function(){ return arg; }) }).then(function(){ return sendFile(realPath,arg); }) } if(arg.isCancatFile){ return commCancatFile(arg); } throw 404; }).catch(function(err){ if(err === 404) return next(); if(err === 200 || err === 304) return; next(err); }) } }