Fork me on GitHub

Programming Design Notes

使用 Google Closure Compiler 在執行期間壓縮 Javascript

| Comments

趁有空又寫寫文章,今次介紹的是 Google Closure Compiler,其實之前已經介紹過一次: 線上 Javascript 工具,這個工具可以將 Javascript 的大小大幅降低。

使用線上工具去壓縮有一個缺點就是要將 Javascript 檔案儲存成 2 份,1 份是原始檔案,另 1 份是經壓縮內容的 Javascript,因為不可能更改經壓縮過的內容,每一次更改檔案就需要更改原始檔案,然後利用 Google Closure Compiler 線上工具再壓縮一次,再更新壓縮內容,而且經壓縮的 Javascript 在瀏覽器 debug 亦比較困難。雖然你可以在 HTML 將引入的 Javascript 檔案改為未經壓縮然後 debug,但萬一忘記改回就麻煩了。

幸好 Google Closure Compiler 有提供到 Java 使用的 API 來達到執行期間 (Run Time)Javascript 壓縮。

首先到 Google Code 下載: Google Closure Compiler

可用以下其中一個方法去壓縮:
protected String compress(InputStream inputStream) throws IOException{
Compiler compiler = new Compiler();

CompilerOptions options = new CompilerOptions();
CompilationLevel.SIMPLE_OPTIMIZATIONS
.setOptionsForCompilationLevel(options);

JSSourceFile extern = JSSourceFile.fromCode("externs.js", " ");
JSSourceFile input = JSSourceFile.fromInputStream("origin.js", inputStream);
compiler.compile(extern, input, options);

return compiler.toSource();
}

protected String compress(String str) throws IOException{
Compiler compiler = new Compiler();

CompilerOptions options = new CompilerOptions();
CompilationLevel.SIMPLE_OPTIMIZATIONS
.setOptionsForCompilationLevel(options);

JSSourceFile extern = JSSourceFile.fromCode("externs.js", " ");
JSSourceFile input = JSSourceFile.fromCode("origin.js", str);
compiler.compile(extern, input, options);

return compiler.toSource();
}

其中 CompilationLevel 分別有 3 個選項:
  • CompilationLevel.WHITESPACE_ONLY
  • CompilationLevel.SIMPLE_OPTIMIZATIONS
  • CompilationLevel.ADVANCED_OPTIMIZATIONS

WHITESPACE_ONLY 只會移除 Javascript 的空白。

SIMPLE_OPTIMIZATIONS 是最常用的一種,移除 Javascript 的空白,而且將一些 Local VariableLocal Function 名稱改變,並將一些沒有用到的 Variable 移除,大幅提高 Javascript 的壓縮率。

ADVANCED_OPTIMIZATIONS 是最高壓縮率的模式,將所有 VariableFunction 的名稱改變,有使用 Javascript Framework 不建議使用這個選項。

在真實環境中我們可以加入一個 Filter 去將 Javascript 壓縮:
package com.ctlok.pro.filter;

import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Map;
import java.util.logging.Level;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import com.google.javascript.jscomp.CompilationLevel;
import com.google.javascript.jscomp.Compiler;
import com.google.javascript.jscomp.CompilerOptions;
import com.google.javascript.jscomp.JSSourceFile;
import com.google.javascript.jscomp.WarningLevel;

public class ClosureCompilerFilter implements Filter {

//for cache
private final Map<String, String> compressedJs = new HashMap<String, String>();
private FilterConfig filterConfig;

public void init(FilterConfig filterConfig) throws ServletException {
this.filterConfig = filterConfig;

//Turn off the compiler log
Compiler.setLoggingLevel(Level.OFF);
}

public void destroy() {

}

public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {

HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse resp = (HttpServletResponse) response;

String uri = req.getRequestURI();

//Prevent compress the compressed Javascript, such as jquery.min.js, mootools.min.js, etc.
if (!uri.matches(".*\\.min\\.js$")){
String js = null;

if (compressedJs.containsKey(uri)){
//get from cache
js = compressedJs.get(uri);
}else{
String contextPath = req.getContextPath();
String jsPath = uri.substring(contextPath.length());

//get javascript file as stream
//getResourceAsStream cannot include context path
InputStream inputStream = filterConfig.getServletContext().getResourceAsStream(jsPath);
js = compress(inputStream);

//put to cache
compressedJs.put(uri, js);
}

resp.getWriter().write(js);
return;
}

chain.doFilter(request, response);
}

protected String compress(InputStream inputStream) throws IOException{
Compiler compiler = new Compiler();

CompilerOptions options = new CompilerOptions();
CompilationLevel.SIMPLE_OPTIMIZATIONS
.setOptionsForCompilationLevel(options);
WarningLevel.QUIET.setOptionsForWarningLevel(options);

JSSourceFile extern = JSSourceFile.fromCode("externs.js", "");
JSSourceFile input = JSSourceFile.fromInputStream("origin.js", inputStream);
compiler.compile(extern, input, options);

return compiler.toSource();
}

}

在 web.xml 加上:
<filter>
<display-name>ClosureCompilerFilter</display-name>
<filter-name>ClosureCompilerFilter</filter-name>
<filter-class>com.ctlok.pro.filter.ClosureCompilerFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>ClosureCompilerFilter</filter-name>
<url-pattern>*.js</url-pattern>
</filter-mapping>

這樣就可以令到 .js 結尾的 Javascript 檔案經壓縮再傳出去,又不會將 .min.js 已經壓縮過的 Javascript 又再壓縮一次。
經過壓縮後會儲存在 Map 內,畢竟壓縮的時間也不短。

例如我有一個 /js/myjs.js 的檔案:
function ctlok() {
var self = this;
var $ = jQuery;

this.publicFunction = function() {
localFunction();
};

var localFunction = function() {
var aaaaa = 'aaaaa';
$(':input').val(aaaaa);
};
}

經壓縮後變成:
function ctlok(){var a=jQuery;this.publicFunction=function(){a(":input").val("aaaaa")}};

範例下載:: Google-Closure-Compiler.zip