Bulletproof 20150913 - VS Code 尝试,与代码重整背后的 TypeScript 编译

VS Code 尝试,与代码重构(算得上么)时发生各种事情,追根溯源看的 TypeScript 编译

脑抽去试了一下 Visual Studio Code,发现:效率真是不错呢!

但是很要命,在 JavaScript 运行调试上只支持 Node.js。对于 nw,我们可以写一个小把戏蒙混 VS Code(缺点是无法侦听调试端口):

/tsconfig.json:

{
    "compilerOptions": {
        "module": "commonjs",
        "target": "es5"
    }
}

/app.js:

// 换成 nw.js 的执行文件路径
var nwpath = "./nwjs/nw.exe";
var child_process = require("child_process");
child_process.spawnSync(nwpath, ["."]);
process.exit();

/.vscode/launch.json 的设置项 program 设置为 "./app.js"

然后启动调试即可。

但是还是很难受,特别 JavaScript 下对内置函数的提示比 WebStorm 差很多。不过我倒是很欣赏 Ctrl+Shift+P 唤出的命令条,对于我这种喜欢用键盘描述的人来说少用鼠标轻松多了,也不用像 vim 那样记指令。

然后我拜访了其模仿对象 Sublime Text。Sublime Text 的包管理源估计被墙了,只好悻悻地放弃。


想着在进一步拓展之前先把结构整理好,要不这样下去整体就崩了,到后面越来越乱。

于是花了一天的时间调整代码结构。原本以为是一个晚上的事,来来去去一天就过去了。现在的结构清晰多了。代码位于 https://github.com/Hozuki/Bulletproof/

在调整的时候遇到了坑爹的问题。我本来是想将对象创建在本地的,这样在头中包含就可以了,不用额外的初始化代码,就像这样:

<html>
    <head>
        <script type="text/javascript" src="bulletproof.js"></script>
    <head>
    <body>
        <script type="text/javascript">
            // 这里控制加载示例什么的
        </script>
</html>

取消了所有的 export,添加引用(/// <reference path=""/>)。费了好大劲编译通过了,但是运行失败了。

这才了解 .d.ts 主要是用作内置模块/已引用脚本的声明的。

// 可以
declare interface Interface {
}

declare module bulletproof {
    interface Interface {
    }
    class Class {
        constructor();
    }
}

引用的方式分别为:

var i1: Interface;
var i2: bulletproof.Interface;
var c1: bulletproof.Class = new bulletproof.Class();

但是如果是在模块内,像普通的编程语言一样调用就会产生问题。如果不集中在一个文件里的话,就会报“找不到名称”(找不到标识符),例如这样的代码:

// 在 x.d.ts 声明
declare module mod1 {
    module mod2 {
        class Class {
            constructor();
        }
    }
}
// 在 x.ts 实现
module mod1 {
    module mod2 {
        class Class {
            constructor() {
            }
        }
    }
}
// 在 y.ts 调用
/// <reference path="x.d.ts"/>
declare module mod1 {
    function a() {
        var c = new mod1.mod2.Class();
        //var c = new mod2.Class();
    }
}

于是为了能编译通过,要创建别名(alias):

// 在 y.ts 调用
/// <reference path="x.d.ts"/>
import mod2 = mod1.mod2;
declare module mod1 {
    function a() {
        var c = new mod2.Class();
    }
}

这样能编译通过。但是运行时,如果你执行了函数 a(),则会发生错误,原因是 mod2undefinedmod2.Class() 调用非法。

为什么会这样?看一下生成的代码:

var mod1;
(function (mod1) {
    var mod2;
    (function (mod2) {
        var Class = (function () {
            function Class() {
            }
        })();
    })(mod2 || (mod2 = {}));
    mod1.mod2 = mod2;
})(mod1 || (mod1 = {}));

注意到什么了?根本就没有对作为参数的 mod2 产生影响!在控制台查看一下就能看到,mod2 执行后是一个空对象({})。

此次我们进行一点修改,添加 export

export module mod1 {
    export module mod2 {
        export class Class {
            constructor() {
            }
        }
    }
}

结果:

var mod1;
(function (mod1) {
    var mod2;
    (function (mod2) {
        var Class = (function () {
            function Class() {
            }
        })();
        mod2.Class = Class;
    })(mod1.mod2 || (mod1.mod2 = {}));
    mod1.mod2 = mod2;
})(exports.mod1 || (exports.mod1 = {}));

熟悉 Node.js 的同学就看到了:这就可以被 require() 了!

于是在最近的一次commit中,你可以见到大量的丑陋代码。

例如 bulletproof-mx.ts

import bulletproof_flash = require("./bulletproof-flash");

export module bulletproof.mx {

    import flash = bulletproof_flash.bulletproof.flash;

    export module containers {

        export class Canvas extends flash.display.DisplayObjectContainer {

            public constructor(root:flash.display.DisplayObject, parent:flash.display.DisplayObjectContainer) {
                super(root, parent, false);
            }

        }

    }

}

这个 bulletproof_flash 真是不得已而为之啊。调用也很糟糕,需要手工注入(kevlar.js):

var bulletproof = {};
var injector = require("../build/bulletproof-injector");

function appendBulletproofModule(bp, injector, moduleName) {
    "use strict";
    var module = require(moduleName);
    injector.inject(module.bulletproof, bp);
}

appendBulletproofModule(bulletproof, injector, "../build/bulletproof-org");
appendBulletproofModule(bulletproof, injector, "../build/bulletproof-mic");
appendBulletproofModule(bulletproof, injector, "../build/bulletproof-thirdparty");
// ...
this.bulletproof = new bulletproof.Bulletproof();
this.bulletproof.initialize();
bulletproof.AdvancedDanamaku.initialize(container, video);
bulletproof.AdvancedDanamaku.start();

这个 injector 也是一个丑陋的设计。

另外,JavaScript 的顺序执行也是一个头疼的东西。原来的代码里有一个 bulletproof.Bulletproof 类和 bulletproof.bilidanmaku.AdvAdapter 的交叉引用。就是这个交叉引用导致编译和运行失败。

JavaScript 的解释是顺序、单向的。内部采用的引用计数也使得相互指向变成了很危险的事情(会出现无法回收的情况)。我惯用的 VB .NET 和 C# 是运行在 CLR 上的,垃圾回收是标记-清除式的,访问对象信息是通过元数据(metadata)的,因此可以随便指。

所以重新规划了两个类的职责。完成后发现原先的设计确实有问题。现在是在 bulletproof-bilidanmaku.ts 中的 bulletproof.AdvancedDanmaku 类负责管理跟高级弹幕有关的东西,包括 BiliBili 高级弹幕接口,而不是那个总协调者 bulletproof.Bulletproof 来干了。


又脑抽了,写着写着就意识到,只要顶层的模块不 export module 就好。测试了一下,正解。

module mod1 {
    export module mod2 {
        export class Class {
            constructor() {
            }
        }
    }
}

这个样子,编译出来的就会成这样:

var mod1;
(function (mod1) {
    var mod2;
    (function (mod2) {
        var Class = (function () {
            function Class() {
            }
        })();
        mod2.Class = Class;
    })(mod1.mod2 || (mod1.mod2 = {}));
    mod1.mod2 = mod2;
})(mod1 || (mod1 = {}));

看到没有,mod1 被直接写入了全局变量,其他的照样可以类似其他类 C#/Java 的方式调用。

这样引用就简单了,不需要额外的启动脚本:

<html>
    <head>
        <script type="text/javascript" src="bulletproof.js"></script>
        <!-- <script src="..."></script> -->
    <head>
    <body>
        <script type="text/javascript">
            var bp = new bulletproof.Bulletproof();
            // 这里控制加载示例什么的
        </script>
</html>

但是也有缺陷。

第一,一定不能把没声明的放在前面。一定要调用的话,要使用完全限定名称类似物。例如这样的实现代码,运行会失败:

// 下面这一行别名是必须的,否则会编译失败
// 即使加了,运行也会失败
import mod2 = mod1.mod2;
module mod1.mod2.mod3 {
    export function a() {
        mod2.Class.doSomething();
    }
}
module mod1.mod2 {
    export class Class {
        static doSomething(): void {
        }
    }
}

原因是上面的 import 被编译成了 var mod2 = mod1.mod2;。调用 a() 的时候,实际引用的是那个还没出现的 mod1.mod2,值为 undefined,完整调用成了 undefined.Class.doSomething()

如果先实现顶层的成员,再声明别名,最后实现内层成员的话,就没问题,因为此时 mod1.mod2 有了值。

第二,顶层模块是全局的。如果模块里有潜在能把浏览器搞崩溃的代码,就给攻击者提供了工具。特别是对于 Bulletproof 这种需要运行代码的工具来说,即使我给用户提供一个安全的 API,但是用户通过某个全局引用找到了我的实现类,那就麻烦大了。

所以我还是决定维持目前的实现不变。

分享到 评论