分享一个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">
    
    <[email protected]>

 

在下面是核心代码:

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<String, String> COMBO_MAP = new ConcurrentHashMap<String, String>();

    /**
     * 压缩工具
     *
     *
     * @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.") > 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.") > 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 < 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 < 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
        
        

 

最终效果:

除特别注明外,本站所有文章均为duzhi原创,转载请注明出处来自https://www.duzhi.me/article/61.html

联系我们

******

在线咨询:点击这里给我发消息

邮件:ashang.peng#aliyun.com

QR code