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
| 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
| declare module mod1 { module mod2 { class Class { constructor(); } } }
module mod1 { module mod2 { class Class { constructor() { } } } }
declare module mod1 { function a() { var c = new mod1.mod2.Class(); } }
|
于是为了能编译通过,要创建别名(alias):
1 2 3 4 5 6 7 8
|
import mod2 = mod1.mod2; declare module mod1 { function a() { var c = new mod2.Class(); } }
|
这样能编译通过。但是运行时,如果你执行了函数 a()
,则会发生错误,原因是 mod2
为 undefined
,mod2.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> <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,但是用户通过某个全局引用找到了我的实现类,那就麻烦大了。
所以我还是决定维持目前的实现不变。