JavaScript AST初体验
2018年4月12日

最近在做一个工具自动把外部的Web Worker文件引用内联化,即碰到代码里有new Worker('worker.js'),就去读取worker.js的代码,然后把原Web Woker引用内联化为:new Worker(window.URL.createObjectURL(new Blob(["/* worker.js的内容 */"])))。这个功能看似简单,我一开始想用正则表达式来匹配出Web Worker的实例化参数,但发现有很多极端情况需要去做特殊考虑,最终在了解并尝试了JavaScript AST(Abstract Syntax Tree抽象语法树)Parser后,所有的问题迎刃而解。

首先我们看最简单的情况:

var myWorker = new Worker('worker.js');

很简单,用正则:new Worker\('(.*\.js)'\)就能获取我们想拿的worker.js

那么如果引用是放在双引号中呢,这个正则就不奏效了。

var myWorker = new Worker("worker.js");

好吧,其实也难不倒我们,我们可以用正则的后向引用来解决。new Worker\((['"])(.*\.js)\1\)可以匹配双引号和单引号两种情况。

那么如果文件后伴有查询字符串呢?

var myWorker = new Worker('worker.js?v=1.0');

可能就要用这个正则来匹配了:new Worker\((['"])(.*\.js)(\?.*)?\1\)。不过我突然发现事情没这么简单了,看看下面这些变态的写法。

var myWorker = new Worker('worker.js?t=' + new Date().getTime());
var myWorker2 = new Worker('worker.js?isIE=' + (isIE() ? 'true' : 'false'));
var myWorker3 = new Worker(
    'worker.js' +
    '?v=1.0')

虽然这很少见,不过这些依然是合法的JavaScript语句,并且的确引用了worker.js这个外部文件,理应被内联化。

好吧,即使我决定不考虑这些变态的写法,但我又发现下面的字符串也会被我的正则匹配到,即使发生概率很低,但绝对是一个bug。这颠覆了我用正则表达式来实现的想法。

var str = "new Worker('worker.js')"

看起来我需要的并不是一个正则表达式,而是要一个JavaScript解析器来解析JS代码找出真正的new Worker表达式并拿到其中的纯字面量参数。

之前听说过JavaScript抽象语法树(AST)可以解析JS代码,在AST explorer实测后发现这正式我需要的,AST Parser可以准确找出所有的new Worker表达式,并找到所有的参数和位置。

[
    ...
    {
        ...
        "init": {
            "type": "NewExpression",
            "start": 15,
            "end": 64,
            "callee": {
                "type": "Identifier",
                "start": 19,
                "end": 25,
                "name": "Worker"
            },
            "arguments": [
                {
                    "type": "BinaryExpression",
                    "start": 26,
                    "end": 63,
                    "left": {
                        "type": "Literal",
                        "start": 26,
                        "end": 40,
                        "value": "worker.js?t=",
                        "raw": "'worker.js?t='"
                    },
                    "operator": "+",
                    "right": {
                        ...
                    }
                }
            ]
        }
    },
    {
        ...
        "init": {
            "type": "NewExpression",
            "start": 82,
            "end": 141,
            "callee": {
                "type": "Identifier",
                "start": 86,
                "end": 92,
                "name": "Worker"
            },
            "arguments": [
                {
                    "type": "BinaryExpression",
                    "start": 93,
                    "end": 140,
                    "left": {
                        "type": "Literal",
                        "start": 93,
                        "end": 110,
                        "value": "worker.js?isIE=",
                        "raw": "'worker.js?isIE='"
                    },
                    "operator": "+",
                    "right": {
                        ...
                    }
                }
            ]
        }
    },
    {
        ...
        "init": {
            "type": "NewExpression",
            "start": 159,
            "end": 201,
            "callee": {
                "type": "Identifier",
                "start": 163,
                "end": 169,
                "name": "Worker"
            },
            "arguments": [
                {
                    "type": "BinaryExpression",
                    "start": 175,
                    "end": 200,
                    "left": {
                        "type": "Literal",
                        "start": 175,
                        "end": 186,
                        "value": "worker.js",
                        "raw": "'worker.js'"
                    },
                    "operator": "+",
                    "right": {
                        "type": "Literal",
                        "start": 193,
                        "end": 200,
                        "value": "?v=1.0",
                        "raw": "'?v=1.0'"
                    }
                }
            ]
        }
    }
]

最终我选择了Acorn作为AST解析器,并完成了这个Web Worker内联化工具:https://github.com/js1016/worker-inlinify

分类

JavaScript