5486°

分享一个JS/CSS 在线合并压缩的方案-Compressor-Java 实现

 

大家好,这几天正好做站点优化,也看了写工具,在线的非在线的。

如:grunt,php的minify,yui等

今天给大家介绍下我引用了YUI做了在线的效果,主要生成文件以及合并CSS的效果。下面介绍下过程;

1、由于我使用的是Freemarker 去做的View 所有我做了个标签;

package me.duzhi.blog.plugins.compress;

import com.jfinal.kit.StrKit; import freemarker.core.Environment; import freemarker.ext.beans.BeansWrapper; import freemarker.ext.beans.BeansWrapperBuilder; import freemarker.template.*;

import java.io.IOException; import java.util.List; import java.util.Map;

/**

  • @author [email protected]
  • @date 一月 07, 2017 */

public class CompressDirective implements TemplateDirectiveModel { @SuppressWarnings("rawtypes") public void execute(Environment env, Map params, TemplateModel[] loopVars, TemplateDirectiveBody body) throws TemplateException, IOException {

    Object var = params.get("var");
    if (var == null) {
        throw new TemplateModelException("assets tag attribute var can not be null!");
    }
    String varName = var.toString();
    if (StrKit.isBlank(varName)) {
        throw new TemplateModelException("assets tag attribute var can not be null!");
    }
    Object file = params.get("file");
    if (file == null) {
        throw new TemplateModelException("assets tag attribute file can not be null!");
    }

    String fileName = file.toString();
    if (StrKit.isBlank(fileName)) {
        throw new TemplateModelException("assets tag attribute file can not be null!");
    }
    Object ido = params.get("id");
    if (ido == null) {
        throw new TemplateModelException("assets tag attribute id can not be null!");
    }
    String id = ido.toString();
    if (StrKit.isBlank(id)) {
        throw new TemplateModelException("assets tag attribute id can not be null!");
    }

    List path = CompressKit.getPath(id, fileName);

    BeansWrapper beansWrapper = new BeansWrapperBuilder(Configuration.getVersion()).build();
    for (String s : path) {
        env.setVariable(varName, beansWrapper.wrap(s));
        body.render(env.getOut());
    }
}

}

上面是标签的代码;

  <@compressor var="href" file="/subs/me_dz_cm.jjs" id="layout002">
&lt;[email protected]&gt;</code></pre> 

 

在下面是核心代码:

package me.duzhi.blog.plugins.compress;

import com.jfinal.kit.HashKit; import com.jfinal.kit.PathKit; import com.jfinal.kit.StrKit; import com.jfinal.log.Log; import com.yahoo.platform.yui.compressor.CssCompressor; import com.yahoo.platform.yui.compressor.JavaScriptCompressor; import org.apache.commons.io.FileUtils; import org.apache.commons.io.FilenameUtils; import org.apache.commons.io.IOUtils; import org.mozilla.javascript.ErrorReporter; import org.mozilla.javascript.EvaluatorException;

import java.io.*; import java.net.URL; import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.regex.Matcher; import java.util.regex.Pattern;

/**

  • @author [email protected]
  • @date 一月 07, 2017 */

public class CompressKit {

private static final Log log = Log.getLog(CompressKit.class);
private static final Charset UTF_8 = Charset.forName("UTF-8");
private static final String JS_EXT = ".js", CSS_EXT = ".css";
private static final String PROTOCOL = "^https?://.+$";

// 考虑到线上环境基本不会频繁更改css,js文件为了性能故缓存
public static ConcurrentMap&lt;String, String&gt; COMBO_MAP = new ConcurrentHashMap&lt;String, String&gt;();

/**
 * 压缩工具
 *
 *
 * @param name
 * @param fileName 待压缩的文件列表文件 /assets/assets.jjs
 * @return String 返回压缩完成之后的路径
 * @throws IOException 文件不存在时异常
 */
public static List getPath(String name, String fileName) throws IOException {
    List pathList = new ArrayList();

    String rootPath = PathKit.getWebRootPath();
    // 路径判读
    if (!fileName.startsWith("/")) {
        fileName = "/" + fileName;
    }
    String path = COMBO_MAP.get(fileName);
    if (StrKit.isBlank(path)) {
        return combo(rootPath, fileName);
    }
    File assetsFile = new File(rootPath + path);
    // 文件存在则直接返回路径
    if (assetsFile.exists()) {
        pathList.add(path);
        return pathList;
    }
    return combo(rootPath, fileName);
}

/**
 * 压缩css,js帮助
 *
 * @param rootPath 项目路径
 * @param fileList 合并压缩的文件列表
 * @param isCss    是否是css
 * @param out      输出流
 * @throws IOException Io异常
 */
private static void compressorHelper(String rootPath, List fileList, boolean isCss, Writer out) throws IOException {
    Reader in = null;
    InputStream input = null;
    try {
        if (isCss) {
            for (String path : fileList) {
                boolean isRomte = isRomte(path);
                input = isRomte ? new URL(path).openStream() : new FileInputStream(rootPath + path);
                // css 文件内容,并处理路径问题
                String context = repairCss(IOUtils.toString(input, UTF_8), path);
                if (path.indexOf(".min.") &gt; 0 || isRomte) {// 对.min.css的css放弃压缩
                    out.append(context);
                } else {
                    CssCompressor css = new CssCompressor(new StringReader(context));
                    css.compress(out, -1);
                }
                input.close();
                input = null;
            }
        } else {
            // nomunge: 混淆,verbose:显示信息消息和警告,preserveAllSemiColons:保留所有的分号 ,disableOptimizations 禁止优化
            boolean munge = true, verbose = false, preserveAllSemiColons = false, disableOptimizations = false;
            for (String path : fileList) {
                boolean isRomte = isRomte(path);
                input = isRomte ? new URL(path).openStream() : new FileInputStream(rootPath + path);
                in = new InputStreamReader(input, UTF_8);
                if (path.indexOf(".min.") &gt; 0 || isRomte) { // 对.min.js,和远程js放弃压缩
                    out.append(IOUtils.toString(in));
                } else {
                    JavaScriptCompressor compressor = new JavaScriptCompressor(in, new ErrorReporter() {
                        public void warning(String message, String sourceName,
                                            int line, String lineSource, int lineOffset) {
                            if (line &lt; 0) {
                                log.error("\n[WARNING] " + message);
                            } else {
                                log.error("\n[WARNING] " + line + ':' + lineOffset + ':' + message);
                            }
                        }

                        public void error(String message, String sourceName,
                                          int line, String lineSource, int lineOffset) {
                            if (line &lt; 0) {
                                log.error("\n[ERROR] " + message);
                            } else {
                                log.error("\n[ERROR] " + line + ':' + lineOffset + ':' + message);
                            }
                        }

                        public EvaluatorException runtimeError(String message, String sourceName,
                                                               int line, String lineSource, int lineOffset) {
                            error(message, sourceName, line, lineSource, lineOffset);
                            return new EvaluatorException(message);
                        }
                    });
                    compressor.compress(out, -1, munge, verbose, preserveAllSemiColons, disableOptimizations);
                }
                input.close();
                input = null;
                in.close();
                in = null;
            }
        }
        out.flush();
    } catch (IOException e) {
        throw e;
    } finally {
        IOUtils.closeQuietly(in);
        IOUtils.closeQuietly(input);
    }
}

/**
 * 将css文件里的图片相对路径修改为绝对路径
 *
 * @param content 内容
 * @param path    路径
 * @return String css
 */
private static String repairCss(String content, String path) {
    Pattern p = Pattern.compile("url\\([\\s]*['\"]?((?!['\"]?https?://|['\"]?data:|['\"]?/).*?)['\"]?[\\s]*\\)"); // 感谢Code Life(程式人生)的正则
    Matcher m = p.matcher(content);
    StringBuffer sb = new StringBuffer();
    while (m.find()) {
        String url = m.group(1).trim();
        StringBuffer cssPath = new StringBuffer("url(").append(FilenameUtils.getFullPath(path)).append(url).append(")");
        m.appendReplacement(sb, cssPath.toString());
    }
    m.appendTail(sb);
    content = sb.toString();
    return content;
}

/**
 * 压缩工具
 *
 * @param rootPath 项目路径
 * @param fileName 待压缩的文件列表文件 /assets/assets.jjs
 * @return String 返回压缩完成之后的路径
 * @throws IOException 文件不存在时异常
 */
private static List combo(String rootPath, String fileName) throws IOException {
    File assetsConfig = new File(rootPath + fileName);
    // 待压缩的文件列表不存在时抛出异常
    if (!assetsConfig.exists()) {
        throw new IOException(fileName + " not found...");
    }
    // 读取文件中的js或者css路径
    List list = FileUtils.readLines(assetsConfig, UTF_8);
    // 文件内容md5
    StringBuilder fileMd5s = new StringBuilder();
    StringBuilder config = new StringBuilder();
    Iterator it = list.iterator();
    String lines;
    while (it.hasNext()) {
        lines = it.next();
        config.append(lines);
        if (StrKit.isBlank(lines)) {
            it.remove();
            continue;
        }
        // 去除首尾空格
        lines = lines.trim();
        // #开头的行注释
        if (lines.startsWith("#")) {
            it.remove();
            continue;
        }
        // 远程服务器上的资源文件,不参与MD5
        if (isRomte(lines)) {
            continue;
        }
        // 对错误地址修复
        if (!lines.startsWith("/")) {
            lines = "/" + lines;
        }
        String filePath = rootPath + lines;
        File file = new File(filePath);
        if (!file.exists()) {
            throw new IOException(file.getName() + " not found...");
        }
        String content = FileUtils.readFileToString(file, UTF_8);
        fileMd5s.append(HashKit.md5(content));
    }
    fileMd5s.append(HashKit.md5(config.toString()));
   /* if (Jpress.isDevMode()) {
        return list;
    }*/
    // 文件更改时间集合hex,MD5取中间8位
    String hex = HashKit.md5(fileMd5s.toString()).substring(8, 16);
    boolean isCss = true;
    if (fileName.endsWith(".jjs")) {
        isCss = false;
    }
    // /assets/assets.jjs
    int index = fileName.lastIndexOf("/");
    fileName = fileName.substring(0,index+1)+"min/"+fileName.substring(index+1);
    String comboName = fileName.substring(0, fileName.indexOf('.'));
    String newFileName = comboName + '-' + hex + (isCss ? CSS_EXT : JS_EXT);

    String newPath = rootPath + newFileName;
    File file = new File(newPath);
    // 判断文件是否已存在,已存在直接返回
    if (file.exists()) {
        list.clear();
        list.add(newFileName);
        return list;
    }
    // 将合并的结果写入文件,异常时将文件删除
    OutputStream output = null;
    Writer out = null;
    try {
        output = new FileOutputStream(newPath);
        out = new OutputStreamWriter(output, UTF_8);
        compressorHelper(rootPath, list, isCss, out);
        // 装载文件路径
        COMBO_MAP.put(fileName, newFileName);
    } catch (Exception e) {
        FileUtils.deleteQuietly(file);
        log.error(e.getMessage(), e);
        throw new RuntimeException(fileName + " 压缩异常,请检查是否有依赖问题!");
    } finally {
        IOUtils.closeQuietly(output);
        IOUtils.closeQuietly(out);
    }
    list.clear();
    list.add(newFileName);
    return list;
}

/**
 * 判断文件是否为远程资源文件,远程资源文件不进行压缩
 *
 * @param path
 * @return
 */
private static boolean isRomte(String path) {
    if (StrKit.isBlank(path)) {
        return false;
    }
    return path.trim().matches(PROTOCOL);
}

}

 

大家自行参考哦,又需要的可以根据需要更新。

更新个:

   
        com.yahoo.platform.yui
        yuicompressor
        2.4.8
    
    
        commons-io
        commons-io
        2.5
    
    </code></pre> 

 

最终效果:


全部评论: 0

    我有话说: