️This article has been over 2 years since the last update.
Closure Compiler是谷歌推出的一款Javascript压缩工具,在业界有广泛的使用。与传统压缩工具不同的地方在于,它将对代码的AST进行静态分析,而不是简单的正则表达式压缩。
在实际项目中,为了实现自动化提升代码质量,本文将通过在项目中引入此工具
- 定制js压缩打包流程
- 定制静态检测(比如jslint无法发现的业务特有BUG)
本文面向读者: 有一定AST基础,面向后端,希望控制代码质量的读者。由于AST的解析非常复杂,本文将只限制在使用工具上,而不会分析内部源码实现。
Getting Start
引入如下Maven依赖(如果不希望把它记入打包,可以把它配置为test的scope)
1 2 3 4 5 6
| <dependency> <groupId>com.google.javascript</groupId> <artifactId>closure-compiler</artifactId> <version>r2388</version> </dependency>
|
一个AST遍历的例子
1 2 3 4 5 6 7 8 9 10 11 12
| Compiler compiler = new Compiler(); Node parse = compiler.parse(SourceFile.fromCode("a.js", "var a = 1;")); NodeTraversal.traverse(compiler, parse, new NodeTraversal.Callback() { @Override public boolean shouldTraverse(NodeTraversal nodeTraversal, Node n, Node parent) { return true; } @Override public void visit(NodeTraversal t, Node n, Node parent) { System.out.println("n = " + n); } });
|
一个压缩的例子
1 2 3 4 5 6 7 8 9 10 11
| SourceFile sourceFile; sourceFile=SourceFile.fromCode("a.js", "function sum(left, right){return 2 + 3}");
CompilerOptions opts = new CompilerOptions(); CompilationLevel.SIMPLE_OPTIMIZATIONS.setOptionsForCompilationLevel(opts); Compiler compiler = new Compiler();
compiler.compile(Collections.emptyList(), Collections.singletonList(sourceFile), opts); String s = compiler.toSource(); System.out.println(s);
|
对JS代码进行静态转换
本部分将定制Closure的转换规则,以实现自动化代码变换。
预先准备
在完成new Compiler()
的构造后,通过下文的样板代码,我们加入了一个新的优化器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| DefaultPassConfig passes = new DefaultPassConfig(opts) { @Override protected List<PassFactory> getOptimizations() { List<PassFactory> optimizations = super.getOptimizations(); optimizations.add(0, new PassFactory("custom_pass", true) { @Override protected CompilerPass createInternal(AbstractCompiler compiler) { return new CustomCompilerPass(compiler); } }); return optimizations; } }; compiler.setPassConfig(passes);
|
接下来只用定制CustomCompilerPass
即可实现代码优化了
这里本质上就是IR到IR的三段编译器
函数名变成大写
代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| class CustomCompilerPass implements CompilerPass { AbstractCompiler compiler;
public CustomCompilerPass(AbstractCompiler compiler) { this.compiler = compiler; }
@Override public void process(Node externs, Node root) { NodeTraversal.traverse(compiler, root, new NodeTraversal.AbstractPostOrderCallback(){ @Override public void visit(NodeTraversal t, Node n, Node parent) { if (n.isFunction()){ Node firstChild = n.getFirstChild(); String qualifiedName = firstChild.getQualifiedName(); if (qualifiedName != null){ firstChild.setString(qualifiedName.toUpperCase()); compiler.reportCodeChange(); } } } }); } }
|
注意这里树的遍历是后续(PostOrder)遍历,代码树结构大致如下,注意FUNCTION有三个Child
1 2 3 4 5 6 7 8 9 10
| (BLOCK (SCRIPT (FUNCTION "sum" (PARAM_LIST ("left" "right") (BLOCK (RETURN (ADD (2 3))))))))
|
定制JS压缩
假设有如下JS,此JS来自UI-Router的SAP代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| $stateProvider.state('home', { url: "/", views: { "lazyLoadView": { controller: 'AppCtrl', templateUrl: 'partials/main.html' } }, resolve: { loadMyCtrl: ['$ocLazyLoad', function($ocLazyLoad) { return $ocLazyLoad.load(['js/a.js','js/b.js','js/c.js']); }] } });
|
我希望转换为支持在生产环境访问min.js的文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| $stateProvider.state('home', { url: "/", views: { "lazyLoadView": { controller: 'AppCtrl', templateUrl: 'partials/main.html' } }, resolve: { loadMyCtrl: ['$ocLazyLoad', function($ocLazyLoad) { return $ocLazyLoad.load(function(){ if (window.env == 'dev'){ return ['js/a.js','js/b.js','js/c.js']; } else { return ['js/state/home.min.js']; } }()); }] } });
|
我们需要首先匹配
1
| $ocLazyLoad.load(['js/a.js','js/b.js','js/c.js'])
|
它的AST如下
1 2 3 4 5 6 7 8
| (CALL (GETPROP "$ocLazyLoad" "load" (ARRAYLIT "js/a.js" "js/b.js" " js/c.js")
|
将要替换为
1 2 3 4 5 6 7
| $ocLazyLoad.load(function(){ if (window.env == 'dev'){ return ['js/a.js','js/b.js','js/c.js']; } else { return ['js/state/home.min.js']; } }()
|
需要转换的AST如下
1 2 3 4 5 6 7 8 9 10 11 12 13
| (CALL (GETPROP "$ocLazyLoad" "load" (CALL (FUNCTION "" (PARAM_LIST) (BLOCK (IF (EQ) (BLOCK) (BLOCK)))))
|
参考代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| @Override public void process(Node externs, Node root) { NodeTraversal.traverse(compiler, root, new NodeTraversal.AbstractPostOrderCallback() { @Override public void visit(NodeTraversal t, Node n, Node parent) { String state = ""; if (n.isGetProp() && n.getFirstChild().getString().equals("$stateProvider") && n.getLastChild().getString().equals("state")) { Node next = n.getNext(); state = next.getString(); } if (n.isGetProp() && n.getFirstChild().getString().equals("$ocLazyLoad") && n.getLastChild().getString().equals("load")) { Node next = n.getNext(); if (next.getChildCount() == 0){ return; } Node call = IR.call(IR.function(IR.name(""), IR.paramList(), IR.block( IR.ifNode( IR.sheq( IR.getprop(IR.name("window"), IR.string("env")), IR.string("dev")), IR.block( IR.returnNode(next.cloneTree()) ), IR.block( IR.returnNode(IR.arraylit(IR.string("js/state" + state + "/home.min.js"))) )) ))); next.getParent().replaceChild(next, call); compiler.reportCodeChange(); } } }); }
|
注意这里使用了一个外部变量window.env
,因此需要配置一个extern.js
1 2 3 4
|
window.prototype.env;
|
最终生成的压缩后的js如下
1
| $stateProvider.state("index",{url:"/",views:{lazyLoadView:{controller:"AppCtrl",templateUrl:"partials/main.html"}},resolve:{loadMyCtrl:["$ocLazyLoad",function(a){return a.load("dev"===window.env?["js/a.js","js/b.js","js/c.js"]:["js/state/home.min.js"])}]}});
|
自动加入依赖注入
很多人在使用依赖注入时总是忘记写成数组的形式,导致压缩后无法注入
使用前
1 2 3 4
| angular.module('test').controller(function ($http, $scope) { $http.post(); $scope.req = 1; });
|
使用后
1
| angular.module("test").controller(["$http","$scope",function(a,b){a.post();b.req=1}]);
|
实现方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| @Override public void process(Node externs, Node root) { NodeTraversal.traverse(compiler, root, new NodeTraversal.AbstractPostOrderCallback() { @Override public void visit(NodeTraversal t, Node n, Node parent) { if (n.isFunction() && n.getChildAtIndex(1).getChildCount()>0){ Node cloneTree = n.cloneTree(); Node paramNode = cloneTree.getChildAtIndex(1); Iterable<Node> children = paramNode.children(); List<Node> list = new ArrayList<>(); for (Node child : children) { list.add(IR.string(child.getString())); } list.add(cloneTree); n.getParent().replaceChild(n, IR.arraylit(list.toArray(new Node[]{}))); } } }); }
|
Eval问题
在项目中发现一处冲突,在某遗留代码的AjaxFileUpload.js(大概是2012年的框架吧)中
1 2
| data = eval("data="+data);
|
它被编译器优化后,变成了
1
| data=eval("data\x3d"+data);
|
这类优化可能是为了编码更加完善,但是的确产生了很多未知BUG。正确的解决方法是任何时候都不要使用eval,可以用自带的JSON进行解析。
总结
通过操作AST,可以实现
不足
- 工具开发成本相当高,投入产出比很低,适合有空闲的人来搞,后面的人不一定看的懂
- 此技术过于冷门,几乎没有Java的文档,但是对AST有兴趣的可以学习一个
参考