Java 压缩/混淆 JavaScript 代码

限于喜欢 提交于 2020-08-11 13:29:16

基本上都是自己写的工具构建前端工程,压缩/混淆 JavaScript 代码的工具必不可少。我们是 Java 平台的,就是说用 Java 去压缩 JS,这样比较方便。虽然咱们可以外部调用 node 等专门的前端构建工具,但那样不省事,能在 Java 圈子里面搞定就行,我们不搞太复杂的。好~闲话不多说,先看看低配版的。

低配版

这个低配版就几个函数组成,没以前来其他第三方的包,故称为“低配版”。简单实用也可以,我也用了很久。

/**
 * This file is part of the Echo Web Application Framework (hereinafter "Echo").
 * Copyright (C) 2002-2009 NextApp, Inc.
 *
 * Compresses a String containing JavaScript by removing comments and
 * whitespace.
 */
public class JavaScriptSimpleCompressor {
	private static final char LINE_FEED = '\n';
	private static final char CARRIAGE_RETURN = '\r';
	private static final char SPACE = ' ';
	private static final char TAB = '\t';

	/**
	 * Compresses a String containing JavaScript by removing comments and
	 * whitespace.
	 * 
	 * @param script the String to compress
	 * @return a compressed version
	 */
	public static String compress(String script) {
		JavaScriptSimpleCompressor jsc = new JavaScriptSimpleCompressor(script);
		return jsc.outputBuffer.toString();
	}

	/** Original JavaScript text. */
	private String script;

	/**
	 * Compressed output buffer. This buffer may only be modified by invoking the
	 * <code>append()</code> method.
	 */
	private StringBuffer outputBuffer;

	/** Current parser cursor position in original text. */
	private int pos;

	/** Character at parser cursor position. */
	private char ch;

	/** Last character appended to buffer. */
	private char lastAppend;

	/** Flag indicating if end-of-buffer has been reached. */
	private boolean endReached;

	/** Flag indicating whether content has been appended after last identifier. */
	private boolean contentAppendedAfterLastIdentifier = true;

	/**
	 * Creates a new <code>JavaScriptCompressor</code> instance.
	 * 
	 * @param script
	 */
	private JavaScriptSimpleCompressor(String script) {
		this.script = script;
		outputBuffer = new StringBuffer(script.length());
		nextChar();

		while (!endReached) {
			if (Character.isJavaIdentifierStart(ch)) {
				renderIdentifier();
			} else if (ch == ' ') {
				skipWhiteSpace();
			} else if (isWhitespace()) {
				// Compress whitespace
				skipWhiteSpace();
			} else if ((ch == '"') || (ch == '\'')) {
				// Handle strings
				renderString();
			} else if (ch == '/') {
				// Handle comments
				nextChar();
				if (ch == '/') {
					nextChar();
					skipLineComment();
				} else if (ch == '*') {
					nextChar();
					skipBlockComment();
				} else {
					append('/');
				}
			} else {
				append(ch);
				nextChar();
			}
		}
	}

	/**
	 * Append character to output.
	 * 
	 * @param ch the character to append
	 */
	private void append(char ch) {
		lastAppend = ch;
		outputBuffer.append(ch);
		contentAppendedAfterLastIdentifier = true;
	}

	/**
	 * Determines if current character is whitespace.
	 * 
	 * @return true if the character is whitespace
	 */
	private boolean isWhitespace() {
		return ch == CARRIAGE_RETURN || ch == SPACE || ch == TAB || ch == LINE_FEED;
	}

	/**
	 * Load next character.
	 */
	private void nextChar() {
		if (!endReached) {
			if (pos < script.length()) {
				ch = script.charAt(pos++);
			} else {
				endReached = true;
				ch = 0;
			}
		}
	}

	/**
	 * Adds an identifier to output.
	 */
	private void renderIdentifier() {
		if (!contentAppendedAfterLastIdentifier)
			append(SPACE);
		append(ch);
		nextChar();
		while (Character.isJavaIdentifierPart(ch)) {
			append(ch);
			nextChar();
		}
		contentAppendedAfterLastIdentifier = false;
	}

	/**
	 * Adds quoted String starting at current character to output.
	 */
	private void renderString() {
		char startCh = ch; // Save quote char
		append(ch);
		nextChar();
		while (true) {
			if ((ch == LINE_FEED) || (ch == CARRIAGE_RETURN) || (endReached)) {
				// JavaScript error: string not terminated
				return;
			} else {
				if (ch == '\\') {
					append(ch);
					nextChar();
					if ((ch == LINE_FEED) || (ch == CARRIAGE_RETURN) || (endReached)) {
						// JavaScript error: string not terminated
						return;
					}
					append(ch);
					nextChar();
				} else {
					append(ch);
					if (ch == startCh) {
						nextChar();
						return;
					}
					nextChar();
				}
			}
		}
	}

	/**
	 * Moves cursor past a line comment.
	 */
	private void skipLineComment() {
		while ((ch != CARRIAGE_RETURN) && (ch != LINE_FEED)) {
			if (endReached) {
				return;
			}
			nextChar();
		}
	}

	/**
	 * Moves cursor past a block comment.
	 */
	private void skipBlockComment() {
		while (true) {
			if (endReached) {
				return;
			}
			if (ch == '*') {
				nextChar();
				if (ch == '/') {
					nextChar();
					return;
				}
			} else
				nextChar();
		}
	}

	/**
	 * Renders a new line character, provided previously rendered character is not a
	 * newline.
	 */
	private void renderNewLine() {
		if (lastAppend != '\n' && lastAppend != '\r') {
			append('\n');
		}
	}

	/**
	 * Moves cursor past white space (including newlines).
	 */
	private void skipWhiteSpace() {
		if (ch == LINE_FEED || ch == CARRIAGE_RETURN) {
			renderNewLine();
		} else {
			append(ch);
		}
		nextChar();
		while (ch == LINE_FEED || ch == CARRIAGE_RETURN || ch == SPACE || ch == TAB) {
			if (ch == LINE_FEED || ch == CARRIAGE_RETURN) {
				renderNewLine();
			}
			nextChar();
		}
	}
}

压缩的 js 没啥逻辑错误,否则我也不会用那么久。只是有点蛋疼的是,这货居然把 Stirng 里面的空格都处理。因为写 vue 模板的时候,我用了多行字符串,换行符为 \,哈哈,有点高级的引用,低配版就搞不定了,也不怪你了,也不是什么大罪,才多少行代码唷。

调用方法如下:

JavaScriptSimpleCompressor.compress(jsCode);

具体压缩过程:

package com.ajaxjs.web;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Date;
import java.util.Objects;
import java.util.logging.Logger;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * 打包 js
 */
@WebServlet("/JsController")
public class JsController extends HttpServlet {
	private static final long serialVersionUID = 1L;

	protected void doGet(HttpServletRequest request, HttpServletResponse response)
			throws ServletException, IOException {
		String js = "// build date:" + new Date() + "\n";
		js += JavaScriptCompressor.compress(read(mappath(request, "js/ajaxjs-base.js"))) + "\n";
		js += JavaScriptCompressor.compress(read(mappath(request, "js/ajaxjs-list.js"))) + "\n";
		js += action(mappath(request, "js/widgets/"), true) + "\n";

		String output = request.getParameter("output"); // 保存位置
		Objects.requireNonNull(output, "必填参数");
		save(output + "\\WebContent\\asset\\js\\all.js", js);
		response.getWriter().append("Pack js Okay.");
	}

	static String frontEnd = "C:\\project\\wstsq\\WebContent\\asset\\css";

	/**
	 * 压缩 CSS 并将其保存到一个地方
	 */
	protected void doPost(HttpServletRequest request, HttpServletResponse response)
			throws ServletException, IOException {
		String css = request.getParameter("css"),
				file = request.getParameter("file") == null ? "main" : request.getParameter("file");
		String output = "";
		String saveFolder = request.getParameter("saveFolder") == null ? frontEnd : request.getParameter("saveFolder");

		Logger.getGlobal().info(request.getParameter("saveFolder"));

		try {
			save(saveFolder + "\\" + file, css);

			output = "{\"isOk\":true}";
		} catch (Throwable e) {
			e.printStackTrace();
			output = "{\"isOk\":false}";
		}

		response.getWriter().append(output);
	}

	/**
	 * 打包某个目录下所有的 js
	 * 
	 * @param _folder
	 * @param isCompress
	 * @return
	 */
	public static String action(String _folder, boolean isCompress) {
		StringBuilder sb = new StringBuilder();
		File folder = new File(_folder);
		File[] files = folder.listFiles();

		if (files != null)
			for (File file : files) {
				if (file.isFile()) {
					String jsCode = null;
					try {
						jsCode = read(file.toPath());
					} catch (IOException e) {
						e.printStackTrace();
					}
					sb.append("\n");
					sb.append(isCompress ? JavaScriptCompressor.compress(jsCode) : jsCode);
				}
			}

		return sb.toString();
	}

	/**
	 * 获取磁盘真實地址
	 * 
	 * @param cxt          Web 上下文
	 * @param relativePath 相对地址
	 * @return 绝对地址
	 */
	public static String mappath(HttpServletRequest request, String relativePath) {
		String absolute = request.getServletContext().getRealPath(relativePath);

		if (absolute != null)
			absolute = absolute.replace('\\', '/');
		return absolute;
	}

	public static String read(Path path, Charset encode) throws IOException {
		if (Files.isDirectory(path))
			throw new IOException("参数 fullpath:" + path.toString() + " 不能是目录,请指定文件");

		if (!Files.exists(path))
			throw new IOException(path.toString() + " 不存在");

		return new String(Files.readAllBytes(path), encode);
	}

	public static String read(String fullpath, Charset encode) throws IOException {
		Path path = Paths.get(fullpath);
		return read(path, encode);
	}

	public static String read(Path path) throws IOException {
		return read(path, StandardCharsets.UTF_8);
	}

	public static String read(String fullpath) throws IOException {
		return read(fullpath, StandardCharsets.UTF_8);
	}

	public static void saveClassic(String fullpath, String content) throws IOException {
		File file = new File(fullpath);
		if (file.isDirectory())
			throw new IOException("参数 fullpath:" + fullpath + " 不能是目录,请指定文件");

		try (FileOutputStream fop = new FileOutputStream(file)) {
			if (!file.exists())
				file.createNewFile();

			fop.write(content.getBytes());
			fop.flush();
		}
	}

	public void test() throws IOException {
		String content = read("c://temp//newfile.txt");
		save("c://temp//newfile2.txt", content);
	}

	public static void save(String fullpath, String content) throws IOException {
		Path path = Paths.get(fullpath);

		if (Files.isDirectory(path))
			throw new IOException("参数 fullpath:" + fullpath + " 不能是目录,请指定文件");

		if (!Files.exists(path))
			Files.createFile(path);

		Logger.getGlobal().info(path.toString());
		Files.write(path, content.getBytes());

	}
}

YUI Compressor

于是得用第三方库了。第一时间想到 YUI Compressor,这是我当年学前端就有了(“史前”),不过很遗憾居然不支持 ES5 的箭头函数,直接报错,要是你可以忽略 Error 也行呀,——可是显然对新语法不兼容,无法压缩出来,于是也只能放弃鸟~唉 跟不上形势了, 14年最后更新停留在 2.4.8,不支持新 JS 不能爱呀。

简单用法如下。

private static String yuicompressor(String code) {
	String result = null;

	try (StringWriter writer = new StringWriter();
			InputStream in = new ByteArrayInputStream(code.getBytes());
			Reader reader = new InputStreamReader(in);) {
		JavaScriptCompressor compressor = new JavaScriptCompressor(reader, e);
		compressor.compress(writer, -1, true, false, false, false);
		result = writer.toString();
	} catch (EvaluatorException | IOException e) {
		e.printStackTrace();
	}
	return result;
}

private static ErrorReporter e = new ErrorReporter() {
	@Override
	public void warning(String message, String sourceName, int line, String lineSource, int lineOffset) {
		if (line < 0)
			System.err.println("/n[WARNING] " + message);
		else
			System.err.println("/n[WARNING] " + line + ':' + lineOffset + ':' + message);
	}

	@Override
	public void error(String message, String sourceName, int line, String lineSource, int lineOffset) {
		if (line < 0)
			System.err.println("/n[ERROR] " + message);
		else
			System.err.println("/n[ERROR] " + line + ':' + lineOffset + ':' + message);
	}

	@Override
	public EvaluatorException runtimeError(String message, String sourceName, int line, String lineSource,
			int lineOffset) {
		error(message, sourceName, line, lineSource, lineOffset);
		return new EvaluatorException(message);
	}
};

压缩 CSS 也可以。

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Writer;

import org.mozilla.javascript.ErrorReporter;
import org.mozilla.javascript.EvaluatorException;

import com.yahoo.platform.yui.compressor.CssCompressor;
import com.yahoo.platform.yui.compressor.JavaScriptCompressor;

/**
 * JS、CSS压缩工具 https://blog.csdn.net/jianggujin/article/details/80202559
 * 
 * @author jianggujin
 *
 */
public class CompressorUtils {

	public void compressJS(File js, Writer out) throws Exception {
		compressJS(js, out, -1, true, true, false, false);
	}

	public void compressJS(File js, Writer out, int linebreakpos, boolean munge, boolean verbose, boolean preserveAllSemiColons, boolean disableOptimizations) throws IOException {
		try (InputStreamReader in = new InputStreamReader(new FileInputStream(js), "UTF-8");) {
			JavaScriptCompressor compressor = new JavaScriptCompressor(in, new ErrorReporter() {
				@Override
				public void warning(String message, String sourceName, int line, String lineSource, int lineOffset) {
					System.err.println("[ERROR] in " + js.getAbsolutePath() + line + ':' + lineOffset + ':' + message);
				}

				@Override
				public void error(String message, String sourceName, int line, String lineSource, int lineOffset) {
					System.err.println("[ERROR] in " + js.getAbsolutePath() + line + ':' + lineOffset + ':' + message);
				}

				@Override
				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, linebreakpos, munge, verbose, preserveAllSemiColons, disableOptimizations);
		}
	}

	public void compressCSS(File css, Writer out) throws Exception {
		compressCSS(css, out, -1);
	}

	public void compressCSS(File css, Writer out, int linebreakpos) throws IOException {
		try (InputStreamReader in = new InputStreamReader(new FileInputStream(css), "UTF-8");) {
			CssCompressor compressor = new CssCompressor(in);

			compressor.compress(out, linebreakpos);
		}
	}
}

高配版——Google Closure Compiler

你大爷还是你大爷,谷歌这项目一直更新。实际上 Java 生态 js 压缩工具没啥好选择,只剩大爷这货了。二话不多说,先给出 Maven 坐标。

<!-- https://mvnrepository.com/artifact/com.google.javascript/closure-compiler -->
<dependency>
	<groupId>com.google.javascript</groupId>
	<artifactId>closure-compiler</artifactId>
	<version>v20200504</version>
</dependency>

Google Closure Compiler 的问题是文档不足,很少介绍 Java 里面的用法,有的早就过时了。好在找到这篇博文,可以顺利压缩。另外还有 Closure 专门的电子书

用法:

/**
 * 校验js语法、压缩js
 * 
 * @param code
 * @return
 */
public static String compileJs(String code) {
	CompilerOptions options = new CompilerOptions();
	// Simple mode is used here, but additional options could be set, too.
	CompilationLevel.WHITESPACE_ONLY.setOptionsForCompilationLevel(options);

	// To get the complete set of externs, the logic in
	// CompilerRunner.getDefaultExterns() should be used here.
	SourceFile extern = SourceFile.fromCode("externs.js", "function alert(x) {}");

	// The dummy input name "input.js" is used here so that any warnings or
	// errors will cite line numbers in terms of input.js.
//		SourceFile input = SourceFile.fromCode("input.js", code);

	SourceFile jsFile = SourceFile.fromFile(code);

	Compiler compiler = new Compiler();
	compiler.compile(extern, jsFile, options);

	// The compiler is responsible for generating the compiled code; it is not
	// accessible via the Result.
	if (compiler.getErrorCount() > 0) {
		StringBuilder sb = new StringBuilder();
		for (JSError jsError : compiler.getErrors()) {
			sb.append(jsError.toString());
		}

		// System.out.println(sb.toString());
	}
	
	return compiler.toSource();
}

相中方法: Result compile(JSSourceFile extern, JSSourceFile input, CompilerOptions options) 。input 和 options 容易理解,extern是什么?其实类的描述里也稍微提了下:

External variables are declared in ‘externs’ files. For instance, the file may include definitions for global javascript/browser objects such as window, document.

很显然可以没有 extern,但不能为 null。

文中提到:

三种压缩模式介绍

  • Whitespace only:只是简单的去除空格换行注释。
  • Simple:比Whitespace only更高端一点,在其基础上,还对局部变量的变量名进行缩短。这也是其他压缩工具所使用的压缩方式,如UglifyJS等,也是最为主流的压缩方式。比较安全。
  • Advanced:Advanced级别的压缩改变(破坏)了原有代码结构,直接输出代码最终运行结果,而且这种级别的压缩还会删除未调用的函数代码

注意:Advanced级别的压缩虽然对代码压缩做到了极致,但也改变(破坏)了原有代码结构,直接输出了代码最终运行结果,所以使用起来得异常小心,稍微有不规范可能就会引起压缩报错或者压缩成功后却不能正常运行。

不知为啥选择最简单的 Whitespace only 依然还是有 'use strict';,官方在线的例子又不会。我只好强行 replaceAll 替换掉。

参考

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!