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()
,则会发生错误,原因是 mod2
为 undefined
,mod2.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中,你可以见到大量的丑陋代码。
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,但是用户通过某个全局引用找到了我的实现类,那就麻烦大了。
所以我还是决定维持目前的实现不变。