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

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

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

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

/tsconfig.json:

1
2
3
4
5
6
{
"compilerOptions": {
"module": "commonjs",
"target": "es5"
}
}

/app.js:

1
2
3
4
5
// 换成 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/

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

1
2
3
4
5
6
7
8
9
<html>
<head>
<script type="text/javascript" src="bulletproof.js"></script>
<head>
<body>
<script type="text/javascript">
// 这里控制加载示例什么的
</script>
</html>

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

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

1
2
3
4
5
6
7
8
9
10
11
// 可以
declare interface Interface {
}

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

引用的方式分别为:

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

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

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
// 在 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):

1
2
3
4
5
6
7
8
// 在 y.ts 调用
/// <reference path="x.d.ts"/>
import mod2 = mod1.mod2;
declare module mod1 {
function a() {
var c = new mod2.Class();
}
}

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

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

1
2
3
4
5
6
7
8
9
10
11
var mod1;
(function (mod1) {
var mod2;
(function (mod2) {
var Class = (function () {
function Class() {
}
})();
})(mod2 || (mod2 = {}));
mod1.mod2 = mod2;
})(mod1 || (mod1 = {}));

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

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

1
2
3
4
5
6
7
8
export module mod1 {
export module mod2 {
export class Class {
constructor() {
}
}
}
}

结果:

1
2
3
4
5
6
7
8
9
10
11
12
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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 就好。测试了一下,正解。

1
2
3
4
5
6
7
8
module mod1 {
export module mod2 {
export class Class {
constructor() {
}
}
}
}

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

1
2
3
4
5
6
7
8
9
10
11
12
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 的方式调用。

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

1
2
3
4
5
6
7
8
9
10
11
<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>

但是也有缺陷。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 下面这一行别名是必须的,否则会编译失败
// 即使加了,运行也会失败
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,但是用户通过某个全局引用找到了我的实现类,那就麻烦大了。

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

分享到 评论