Whimsical开发第一个月总结

进度

项目已经启动了一个月了,编辑器部分的整体进度在15%左右,进度不是很快,当前核心内容已经有了雏形,主要实现了一下功能:

  • 编辑器Layout
  • 事件调度
  • 历史快照
  • 画布分层
  • 属性配置联动
  • 设计了logo

演示

record-1m.2022-11-11 16_58_33.gif

一些技术选型

这里部分内容借鉴了formily的编辑器designable,属性配置联动部分的表单更是选用了formily进行驱动。

但是在事件调度和状态管理上方案有所不同

  • 状态管理:mobx
    • 使用mobx是为了降低项目维护的复杂度
    • mobx使用了严格模式,不允许直接通过赋值进行状态改变
  • 事件调度:rxjs
    • 当前使用rxjs是为了不去仔细考虑这里的设计,将重点放在整体框架上
    • 因为仅使用了rxjs中的subject,后续可能会对该部分进行重写
  • 拖拽系统使用了react-DND
    • 为了能更快速的开发,干脆对react-DND进行了汉化,当前是用到什么汉化什么,预计今年内会将文档全部汉化

这里没有详细写技术选型中的思考,如果有需要可以私信我,我可以多水一篇文章。

感想

项目启动至今,基本每天会抽1个小时左右写一点代码,从现在的心态上来看还OK,很充实。 中间有一段时间还有一些功利心作祟,想着无论如何要提交些什么,为了提交而提交, 现在就很释然了,尽力而为,心态放松后好像效率反而更高了。 总体来说,是个不错的尝试,当前除了时间比较少,还没有什么太大的瓶颈。

11月也会努力更新,期待感兴趣的同学和我交流。

项目地址:https://github.com/gaofeiyu/whimsical

typescript路径映射踩坑

问题由来

我们通常引用一个npm模块,直接引用模块的名称就可以了,如 import module from 'module' 。
但是如果引用自己开发的模块,往往要使用相对路径的方式找到目标模块,如 import myComponent from '../../../components/myComponent' 。
这种方式可以正常开发,可以完成业务需求,但是维护起来就很难弄了,因为通过这种相对路径的方式你很难一下定位这个组件在哪里,在项目复杂的时候,可能会产生预期外的结果。

别名设置

其实这个问题的解决方案已经很成熟了,从各种构建工具及命令中都有相应的设置。
我这里只针对 typescript 做说明,因为他提供的工具集有点坑。

假如我想实现 import myComponent from '@components/myComponent' 。
而我的目录结构为

./
└── src
    ├── components
    └── views
          └── app.ts

tsconfig.json

{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@components/*": ["src/components/*"]
    }
  }
}

这里的关键配置是 baseUrl 和 paths 
具体说明可以参见文档
https://www.tslang.cn/docs/handbook/module-resolution.html#virtual-directories-with-rootdirs

webpack

假设有一个 webpack.config.js 文件在 ./ 下面

resolve: {
  alias: {
      '@components': path.resolve(__dirname, './src/components'),
  }
}

这个比较基础,直接查看webpack文档就可以了
https://www.webpackjs.com/configuration/resolve/#resolve-alias

通过tsc编译会产生的问题

该文章并不是要教你怎么使用别名,而是 typescript 的开发中有个坑。

用tsc编译的后,映射的路径不会处理,将导致编译后的代码找不到模块

比如上面的 import myComponent from '@components/myComponent' ,如果你使用 commonjs 模式编译后应该是 const myComponent require('@components/myComponent') ,结果就是找不到这个模块。

解决方案一:使用webpack等构建工具进行编译

可以通过 ts-loader 等组件进行编译,来规避掉这个问题

解决方案二:使用module-alias

我现在的场景是,就想使用 tsc -w 来进行开发,那么经过一番寻找,比较优秀的方案就是 module-alias 了。
https://www.npmjs.com/package/module-alias
最简单的用法就是将 const moduleAlias = require('module-alias') 放到你的项目入口处。
然后设置你的 package.json ,以前面的结构为例。

"_moduleAliases": {
  "@components" : "./src/components"
}

这样就可以达到 tsc 编译后,仍然可以找到设置别名的路径了。
至于 module-alias 的原理是什么可以看一下他的npm模块的描述,我就不擅自翻译,免得误人子弟。 

Typescript中文教程-模块

1、第一步

让我们用我们通篇都用的这个例子来开始吧。我们将写一个最简单的验证器。就像当你检查一个用户给页面提交一个表单或检查外部提供的数据文件的格式时可能用到的那样。

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
26
27
28
29
30
31
interface StringValidator {
    isAcceptable(s: string): boolean;
}
var lettersRegexp = /^[A-Za-z]+$/;
var numberRegexp = /^[0-9]+$/;
class LettersOnlyValidator implements StringValidator {
    isAcceptable(s: string) {
        return lettersRegexp.test(s);
    }
}
class ZipCodeValidator implements StringValidator {
    isAcceptable(s: string) {
        return s.length === 5 && numberRegexp.test(s);
    }
}
// Some samples to try
var strings = [‘Hello’, ‘98052’, ‘101’];
// Validators to use
var validators: { [s: string]: StringValidator; } = {};
validators[‘ZIP code’] = new ZipCodeValidator();
validators[‘Letters only’] = new LettersOnlyValidator();
// Show whether each string passed each validator
strings.forEach(s => {
    for (var name in validators) {
        console.log(‘”‘ + s + ‘” ‘ + (validators[name].isAcceptable(s) ? ‘ matches ‘ : ‘ does not match ‘) + name);
    }
});

 

2、添加模块

当我们添加更多的验证器时,我们想要一种能保持跟踪我们的类型并且不用担心和其他对象名发生冲突的组织结构。咱们把对象放进模块里而不用把很多不同的名字放进全局的命名空间里。

在这个例子里,我们把所有验证器相关的类型放进一个叫Validation的模块里。因为我们希望在模块外可见其类和接口,我们用关键字export引用之。相反的,变量lettersRegexp和numberRegexp是实现的细节,所以不会被引用并且在外部是不可见的。

Modularized Validators

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
26
27
28
29
30
31
32
33
module Validation {
    export interface StringValidator {
        isAcceptable(s: string): boolean;
    }
    var lettersRegexp = /^[A-Za-z]+$/;
    var numberRegexp = /^[0-9]+$/;
    export class LettersOnlyValidator implements StringValidator {
        isAcceptable(s: string) {
            return lettersRegexp.test(s);
        }
    }
    export class ZipCodeValidator implements StringValidator {
        isAcceptable(s: string) {
            return s.length === 5 && numberRegexp.test(s);
        }
    }
}
// Some samples to try
var strings = [‘Hello’, ‘98052’, ‘101’];
// Validators to use
var validators: { [s: string]: Validation.StringValidator; } = {};
validators[‘ZIP code’] = new Validation.ZipCodeValidator();
validators[‘Letters only’] = new Validation.LettersOnlyValidator();
// Show whether each string passed each validator
strings.forEach(s => {
    for (var name in validators) {
        console.log(‘”‘ + s + ‘” ‘ + (validators[name].isAcceptable(s) ? ‘ matches ‘ : ‘ does not match ‘) + name);
    }
});

 

3、分成多个文件

随着我们应用的增长,我们想将代码分成多个文件以易于维护。

这里我们将 Validation模块分成多个文件。甚至即使几个文件分割开来,在同一个地方定义的对同一个模块仍然互相有作用。因为有文件之间的依赖关系,我们添加了参考标签告诉编译器文件之间的关系。我们的测试代码仍然不变不变。

Validation.ts

1
2
3
4
5
module Validation {
    export interface StringValidator {
        isAcceptable(s: string): boolean;
    }
}

LettersOnlyValidator.ts

1
2
3
4
5
6
7
8
9
/// <reference path=”Validation.ts” />
module Validation {
    var lettersRegexp = /^[A-Za-z]+$/;
    export class LettersOnlyValidator implements StringValidator {
        isAcceptable(s: string) {
            return lettersRegexp.test(s);
        }
    }
}

ZipCodeValidator.ts

1
2
3
4
5
6
7
8
9
/// <reference path=”Validation.ts” />
module Validation {
    var numberRegexp = /^[0-9]+$/;
    export class ZipCodeValidator implements StringValidator {
        isAcceptable(s: string) {
            return s.length === 5 && numberRegexp.test(s);
        }
    }
}

Test.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/// <reference path=”Validation.ts” />
/// <reference path=”LettersOnlyValidator.ts” />
/// <reference path=”ZipCodeValidator.ts” />
// Some samples to try
var strings = [‘Hello’, ‘98052’, ‘101’];
// Validators to use
var validators: { [s: string]: Validation.StringValidator; } = {};
validators[‘ZIP code’] = new Validation.ZipCodeValidator();
validators[‘Letters only’] = new Validation.LettersOnlyValidator();
// Show whether each string passed each validator
strings.forEach(s => {
    for (var name in validators) {
        console.log(‘”‘ + s + ‘” ‘ + (validators[name].isAcceptable(s) ? ‘ matches ‘ : ‘ does not match ‘) + name);
    }
});

 

4、编译

一旦涉及多个文件,我们需要确保所有的编译后的代码被加载。有两种方式实现。

首先,我们使用 –out 标签将所有输入文件串联起来成一个单独的 JavaScript 输出文件:

1
tsc –out sample.js Test.ts

编译器将按照文件内的参考标签自动排布文件顺序。你也可以单独制定各个文件:

1
tsc –out sample.js Validation.ts LettersOnlyValidator.ts ZipCodeValidator.ts Test.ts

我们可以使用文件编译(默认)为每个输入文件输出一个JavaScript文件。如果多个JS文件,我们需要在网页上使用<script>标签以合适的顺序加载输出文件。例如:

MyTestPage.html (excerpt)

1
2
3
4
  <script src=”Validation.js” type=”text/javascript” />
    <script src=”LettersOnlyValidator.js” type=”text/javascript” />
    <script src=”ZipCodeValidator.js” type=”text/javascript” />
    <script src=”Test.js” type=”text/javascript” />

 

5、使用外部模块

TypeScript 也有外部模块的概念。使用外部模块的两种情况:node.js 和require.js. 不使用 node.js和require.js 的应用不必使用外部模块也可以很好的用上面提到的内部模块来组织起来。

在外部模块,文件间的关系依照 imports和exports 在文件级别上指定。在TypeScript任何文件包含一个顶级import或export被认为是外部模块

下面,我们将之前的例子转变成使用外部模块的。注意我们不再使用module关键词,文件本身就是由其文件名构成的模块。

参考标签被换成 import声明来确认模块间的依存关系. Import声明有两部分:由文件名所确认的名字和require关键字所确认的模块路径。

1
import someMod = require(‘someModule’);

我们通过使用导出关键字在一个顶级声明指定哪些对象是可见的外部模块,类似于在内部模块里定义公共区域。

编译时,我们必须使用一个特殊的模块标记在命令行里。例如,node.js使用 –module commonjs;对于require.js使用 — uodele amd 。例如:

1
tsc –module commonjs Test.ts

当编译时,每个外部模块都将是一个独立的.js文件。跟参考标签一样,编译器将会根据输出声明来编译文件依存关系。

Validation.ts

1
2
3
export interface StringValidator {
    isAcceptable(s: string): boolean;
}

LettersOnlyValidator.ts

1
2
3
4
5
6
7
import validation = require(‘./Validation’);
var lettersRegexp = /^[A-Za-z]+$/;
export class LettersOnlyValidator implements validation.StringValidator {
    isAcceptable(s: string) {
        return lettersRegexp.test(s);
    }
}

ZipCodeValidator.ts

1
2
3
4
5
6
7
import validation = require(‘./Validation’);
var numberRegexp = /^[0-9]+$/;
export class ZipCodeValidator implements validation.StringValidator {
    isAcceptable(s: string) {
        return s.length === 5 && numberRegexp.test(s);
    }
}

Test.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import validation = require(‘./Validation’);
import zip = require(‘./ZipCodeValidator’);
import letters = require(‘./LettersOnlyValidator’);
// Some samples to try
var strings = [‘Hello’, ‘98052’, ‘101’];
// Validators to use
var validators: { [s: string]: validation.StringValidator; } = {};
validators[‘ZIP code’] = new zip.ZipCodeValidator();
validators[‘Letters only’] = new letters.LettersOnlyValidator();
// Show whether each string passed each validator
strings.forEach(s => {
    for (var name in validators) {
        console.log(‘”‘ + s + ‘” ‘ + (validators[name].isAcceptable(s) ? ‘ matches ‘ : ‘ does not match ‘) + name);
    }
});

 

6、代码生成外部模块

根据编译时指定的目标模块,编译器会生为 node.js [commonjs]或 require.js [amd] 模块载入系统。更多关于生成代码时调用哪个定义和需求,查阅相应的模块载入器。

这个例子展示了输入和输出模块代码时时名字是如何被翻译的。

SimpleModule.ts

1
2
import m = require(‘mod’);
export var t = m.something + 1;

AMD / RequireJS SimpleModule.js:

1
2
3
define([“require”, “exports”, ‘mod’], function(require, exports, m) {
    exports.t = m.something + 1;
});

CommonJS / Node SimpleModule.js:

1
2
var m = require(‘mod’);
exports.t = m.something + 1;

 

7、Export =

在先前的例子里,当我们使用每个验证器时每个模块只输出一个值。在这样的情况下,当只使用一个符号也能很好的工作时使用这些他们限定的符号很麻烦。

export = syntax 指定一个模块里导出的单一对象。可以是类,接口,函数或枚举类型。当导入时,这个输出符号将被直接使用并且不会被任何名字限制。

下面,我们使用一个从每个模块里用export= syntax导出的对象简化Validator。这个简化代码简单的使用‘zipValidator’而不用’zip.ZipCodeValidator’。

Validation.ts

1
2
3
export interface StringValidator {
    isAcceptable(s: string): boolean;
}

LettersOnlyValidator.ts

1
2
3
4
5
6
7
8
import validation = require(‘./Validation’);
var lettersRegexp = /^[A-Za-z]+$/;
class LettersOnlyValidator implements validation.StringValidator {
    isAcceptable(s: string) {
        return lettersRegexp.test(s);
    }
}
export = LettersOnlyValidator;

ZipCodeValidator.ts

1
2
3
4
5
6
7
8
import validation = require(‘./Validation’);
var numberRegexp = /^[0-9]+$/;
class ZipCodeValidator implements validation.StringValidator {
    isAcceptable(s: string) {
        return s.length === 5 && numberRegexp.test(s);
    }
}
export = ZipCodeValidator;

Test.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import validation = require(‘./Validation’);
import zipValidator = require(‘./ZipCodeValidator’);
import lettersValidator = require(‘./LettersOnlyValidator’);
// Some samples to try
var strings = [‘Hello’, ‘98052’, ‘101’];
// Validators to use
var validators: { [s: string]: validation.StringValidator; } = {};
validators[‘ZIP code’] = new zipValidator();
validators[‘Letters only’] = new lettersValidator();
// Show whether each string passed each validator
strings.forEach(s => {
    for (var name in validators) {
        console.log(‘”‘ + s + ‘” ‘ + (validators[name].isAcceptable(s) ? ‘ matches ‘ : ‘ does not match ‘) + name);
    }
});

 

8、别名

另一种可以简化这种模块是对常用对象使用 import q = x.y.z 这种短名字。不必为import x = require(‘name’) 这种载入模块的符号迷糊,这个语法创建了一个指定符号的别名。你可以使用任何一种符号给这种输入{普通参考至别名},包括输入的输出模块创建的对象。

Basic Aliasing

1
2
3
4
5
6
7
8
9
module Shapes {
    export module Polygons {
        export class Triangle { }
        export class Square { }
    }
}
import polygons = Shapes.Polygons;
var sq = new polygons.Square(); // Same as ‘new Shapes.Polygons.Square()’

 

9、使用其他JavaScript库

为了描述TypeScript没有的库的类型,我们需要声明这个库暴露的API。因为大多数JavaScript库只暴露一些顶层对象,模块是很好的表现他们的方法。我们称之为声明不能定义一个实现“环境”。通常这些定义在.d.ts文件中,如果你熟悉c或c++可以把这些想象成 .h或 ‘extern’。让我们看一下一些既有内部又有外部的例子。

内部模块环境

著名的库D3定义其功能在一个全局的对象叫做‘D3’里。因为这个库是通过一个 Script标签而不是一个模块载入的,其声明通过内部模块定义其形状。为了使TypeScript查看其形状,我们使用一个内部环境模块声明。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
declare module D3 {
    export interface Selectors {
        select: {
            (selector: string): Selection;
            (element: EventTarget): Selection;
        };
    }
    export interface Event {
        x: number;
        y: number;
    }
    export interface Base extends Selectors {
        event: Event;
    }
}
declare var d3: D3.Base;

D3.d.ts (simplified excerpt)

外部环境模块

在 node.js,大多数任务通过载入一个或多个模块来完成。我们可以定义其每个自己的 .d.ts模块使用高级输出声明。但更方便一点的是将其写在一个长一点的.d.ts 文件里。为了这么做,我们引用模块的名字。其有可能成为一个长的输入。例子如下:

node.d.ts (simplified excerpt)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
declare module “url” {
    export interface Url {
        protocol?: string;
        hostname?: string;
        pathname?: string;
    }
    export function parse(urlStr: string, parseQueryString?, slashesDenoteHost?): Url;
}
declare module “path” {
    export function normalize(p: string): string;
    export function join(…paths: any[]): string;
    export var sep: string;
}

现在我们可以使用 /// <reference> node.d.ts 标签然后载入这个模块使用 例如

1
2
3
///<reference path=”node.d.ts”/>
import url = require(“url”);
var myUrl = url.parse(“http://www.typescriptlang.org”);

 

10、模块陷阱

这部分我们将讨论几种常见的使用内部和外部的模块陷阱和如何避免他们。

/// <reference> to an external module

一个常见的错误是使用///<reference> 标签来指定外部模块文件而不是使用import 。为了弄清楚其中的差别,我们先要了解三种编译器可以定位外部模块文件信息的方法。

提议中是找到一个用 import x = require(..) 声明命名的 .ts文件 。这个文件必须是一个有着顶级 import或export声明的文件。

第二种是找到一个.d.ts文件。跟上面一样,除了一个实际的文件,也要有顶级 import或export声明。

最后一种是使用“外部文件环境声明”。我们用一个合适的名字‘declare’之。

myModules.d.ts

1
2
3
4
// In a .d.ts file or .ts file that is not an external module:
declare module “SomeModule” {
    export function fn(): string;
}

myOtherModule.ts

1
2
/// <reference path=”myModules.d.ts” />
import m = require(“SomeModule”);

这个 reference标签让我们找到包含外部环境模块声明的文件。这是几个TypeScript例子里使用node.d.ts 文件的方法。

不必要的命名空间

如果你正把一个内部模块转换成外部模块,像下面这样来结束这个文件很容易:
shapes.ts

1
2
3
4
export module Shapes {
    export class Triangle { /* … */ }
    export class Square { /* … */ }
}

顶级模块形状shape包括三角Triangle 和四边形Square没什么用处。这将让使用你的模块的用户产生困惑和厌烦。

shapeConsumer.ts

1
2
import shapes = require(‘./shapes’);
var t = new shapes.Shapes.Triangle(); // shapes.Shapes?

TypeScript里外部模块的一个关键特性是两个外部模块从不贡献相同的命名范围。因为模块的用户决定如何设置他,没有必要在命名空间里主动的包含输出符号。

重申一下为什么不应该尝试给你的外部模块内容使用命名空间,命名空间的核心思想是保护本地组结构和防止命名冲突。因为外部模块本身就是一个本地组,他的顶级名称被其输入的代码所定义,所以没必要加一个额外的模块层给输出对象。

修订的例子:
shapes.ts

1
2
export class Triangle { /* … */ }
export class Square { /* … */ }

shapeConsumer.ts

1
2
import shapes = require(‘./shapes’);
var t = new shapes.Triangle();

 

权衡外部模块

就像JS文件和其模块有一对一的通信。TypeScript在额外模块源文件和他放出的JS文件也有一个一对一通信。其影响是不能使用 –out 编译器切换至链接多个外部模块源文件进一个单独的JavaScript文件。

Typescript中文教程-类

传统JavaScript关注功能和基于原型的继承为基本手段,建立可重用的组件,但这可能会觉得有点尴尬,对程序员来说当熟练使用面向对象的方法来开发时,类所继承的功能和对象是由其创建的。从ECMAScript 6,JavaScript程序员可以使用这种面向对象的基于类的方法构建应用程序。在typescript里,我们现在允许开发人员使用这些技术,无需等待下一个版本的JavaScript在所有主要的浏览器和平台JavaScript和编译它们。

1、类

让我们看看一个简单的基于类的例子:

1
2
3
4
5
6
7
8
9
10
11
class Greeter {
    greeting: string;
    constructor(message: string) {
        this.greeting = message;
    }
    greet() {
        return “Hello, ” + this.greeting;
    }
}
var greeter = new Greeter(“world”);

如果你用过C#或java的话对于以上语法会很熟悉。我们声明了一个新类‘Greeter’,这个类有三个成员,一个属性‘greeting’,一个构造器,一个方法‘greet’。
你需要注意的是当指定类的一个成员时在前面加上‘this’,这表示,这是一个成员的访问。
在最后一行我们构造一个Greeter的实例使用“new”。这要求我们前面定义的构造函数,创建一个新对象的Greeter类型,并运行构造函数来初始化它。

2、继承

在 TypeScript,我们可以使用常见的面向对象模式。当然,基于类的编程是最基本的。模式之一是能够使用继承扩展现有的类来创建新的

让我们看一个例子:

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
26
27
28
29
class Animal {
    name:string;
    constructor(theName: string) { this.name = theName; }
    move(meters: number = 0) {
        alert(this.name + ” moved ” + meters + “m.”);
    }
}
class Snake extends Animal {
    constructor(name: string) { super(name); }
    move(meters = 5) {
        alert(“Slithering…”);
        super.move(meters);
    }
}
class Horse extends Animal {
    constructor(name: string) { super(name); }
    move(meters = 45) {
        alert(“Galloping…”);
        super.move(meters);
    }
}
var sam = new Snake(“Sammy the Python”);
var tom: Animal = new Horse(“Tommy the Palomino”);
sam.move();
tom.move(34);

这个例子中有很多其他语言所包含的继承特性。在这里,我们看到使用‘extends’的关键字来创建一个子类。你可以看到这个“Horse”和“Snake”子类基类‘Animal’,获得其功能。

示例还展示了能够覆盖基类中的方法与专门子类的方法。这里‘Snake’和‘Horse’都创建了一个‘move’方法覆盖了基类‘Animal’的‘move’,赋给每个类特定的功能。

3、私有/公有修改器

默认的公有

你可能注意到上面的例子我们没有使用‘public’关键字使类的每一个成员都可见。类似C#的语言需要明确的关键字‘public’来时每个成员可见。在TypeScript,默认每个成员都是公有的。

你还可以马克私有成员,所以你控制什么是公开可见的类。我们可以写前一节的“Animal”类如下:

1
2
3
4
5
6
7
class Animal {
    private name:string;
    constructor(theName: string) { this.name = theName; }
    move(meters: number) {
        alert(this.name + ” moved ” + meters + “m.”);
    }
}

 

4、智能化私有

TypeScript是一种结构化类型系统。当我们比较两种不同类型时,无需关注他们从哪里来,如果每个成员的类型相兼容,那么就说他们兼容。

当我们比较私有类型的成员时却有所不同。对于两种兼容的类型来说,如果其中一个是私有成员,那另一个也一定得是起源于相同的声明的私有成员。

让我们来看一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Animal {
    private name:string;
    constructor(theName: string) { this.name = theName; }
}
class Rhino extends Animal {
constructor() { super(“Rhino”); }
}
class Employee {
    private name:string;
    constructor(theName: string) { this.name = theName; }
}
var animal = new Animal(“Goat”);
var rhino = new Rhino();
var employee = new Employee(“Bob”);
animal = rhino;
animal = employee; //error: Animal and Employee are not compatible

在这个例子里,有两个类‘Animal’和‘Rhino’,‘Rhino’是‘Animal’的子类。我们还有一个新类‘Employee’,它的结构和‘Animal’完全相同。我们创建这些类的一些实例然后把他们互相赋值来看看会发生什么。由于‘Animal’和‘Rhino’都有来自‘Animal’的同一声明的私有属性名字(name):字符串(string),他们是兼容的。然而,对于‘Employee’是不同的。当我们试着将一个‘Employee’赋值给‘Animal’发生了一个类型不兼容的错误。甚至‘Employee’有同样的叫‘name’的私有成员,但这个跟‘Animal’里创建的并不是同一个。

5、参数属性

关键字“public”和“private”也给你一个通过创建参数属性为创建和初始化类的成员的简写。属性让您可以创建并初始化一个成员在同一步骤里。这里对前面的例子进一步修订。注意我们完全放弃“theName”,构造器里创建和初始化’name‘成员时只使用缩短的‘private name:string‘参数。

1
2
3
4
5
6
class Animal {
    constructor(private name: string) { }
    move(meters: number) {
        alert(this.name + ” moved ” + meters + “m.”);
    }
}

以这种方式使用“private”创建并初始化一个私有成员,公有成员也同理。

 6、存取标记

TypeScript支持getter / setter的拦截访问一个对象的成员。这将给你一种更精细的成员访问对象的方式。

让我们转换一个简单的类使用’get‘和’set‘的类。首先,我们从一个不用获取和设置的例子开始。

1
2
3
4
5
6
7
8
9
class Employee {
    fullName: string;
}
var employee = new Employee();
employee.fullName = “Bob Smith”;
if (employee.fullName) {
    alert(employee.fullName);
}

虽然允许人们直接改变fullName十分方便,但是如果人们可以随意改变名字这可能给我们带来麻烦。

在这种情况下,我们在修改Employee之前确保有个可用的密码。将直接进入fullName替换成检查密码的’set‘。我们添加一个相应的“get”让前面的示例无缝地继续工作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
var passcode = “secret passcode”;
class Employee {
    private _fullName: string;
    get fullName(): string {
        return this._fullName;
    }
    set fullName(newName: string) {
        if (passcode && passcode == “secret passcode”) {
            this._fullName = newName;
        }
        else {
            alert(“Error: Unauthorized update of employee!”);
        }
    }
}
var employee = new Employee();
employee.fullName = “Bob Smith”;
if (employee.fullName) {
    alert(employee.fullName);
}

现在我们的访问器检查密码来验证我们自己,我们可以修改密码来看看当密码不匹配时警告框将告诉我们没有更新Employee的权限。

7、静态属性

到目前为止,我们只讨论了类的实例成员。当对象实例化的时候将会出现。我们也能建立类的静态static成员。将在类本身出现而不是类的实例里。在这个例子里,我们在’origin‘上使用’static‘,作为girds的通用值。每个实例通过类里预先设置好的名字来访问。跟’this‘类似,在静态实例访问处我们使用’Grid‘。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Grid {
    static origin = {x: 0, y: 0};
    calculateDistanceFromOrigin(point: {x: number; y: number;}) {
        var xDist = (point.x – Grid.origin.x);
        var yDist = (point.y – Grid.origin.y);
        return Math.sqrt(xDist * xDist + yDist * yDist) / this.scale;
    }
    constructor (public scale: number) { }
}
var grid1 = new Grid(1.0);  // 1x scale
var grid2 = new Grid(5.0);  // 5x scale
alert(grid1.calculateDistanceFromOrigin({x: 10, y: 10}));
alert(grid2.calculateDistanceFromOrigin({x: 10, y: 10}));

 

8、进阶技术

8.1构造函数

当你在TypeScript声明一个类,你实际上是同时创建多个声明。第一个是类的实例的类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
class Greeter {
    greeting: string;
    constructor(message: string) {
        this.greeting = message;
    }
    greet() {
        return “Hello, ” + this.greeting;
    }
}
var greeter: Greeter;
greeter = new Greeter(“world”);
alert(greeter.greet());

在这里,当我们说“var greeter”,我们使用Greeter的类型实例的类。这几乎是其他面向对象语言的程序员的第二天性。

我们还创建另一个值,我们称之为constructor function当我们’new‘类的实例时的函数调用。在实践中看看这是什么样子,让我们看一下上面的例子创建的JavaScript:

1
2
3
4
5
6
7
8
9
10
11
12
13
var Greeter = (function () {
    function Greeter(message) {
        this.greeting = message;
    }
    Greeter.prototype.greet = function () {
        return “Hello, ” + this.greeting;
    };
    return Greeter;
})();
var greeter;
greeter = new Greeter(“world”);
alert(greeter.greet());

这里‘var Greeter’将设置构造函数。当我们调用‘new’操作符并运行该函数时,我们得到一个类的实例。构造函数也包含类的所有静态成员。另一种认识每个类的方法是其由实例instance和静态static两方面组成。

下面我们修改一下这个例子来看看这有什么不同:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Greeter {
    static standardGreeting = “Hello, there”;
    greeting: string;
    greet() {
        if (this.greeting) {
            return “Hello, ” + this.greeting;
        }
        else {
            return Greeter.standardGreeting;
        }
    }
}
var greeter1: Greeter;
greeter1 = new Greeter();
alert(greeter1.greet());
var greeterMaker: typeof Greeter = Greeter;
greeterMaker.standardGreeting = “Hey there!”;
var greeter2:Greeter = new greeterMaker();
alert(greeter2.greet());

这个例子里,‘greeter’的工作方式和前面相同。我们实例化‘Greeter’类,然后使用这个对象。这在前面我们见过。

下一步,我们直接使用类。这里我们建一个变量叫做‘greeterMaker’。这个变量将会保存这个类自己,或者说这是另一种构造函数。这里我们使用‘typeof Greeter’,意思是‘把Greeter类他自己的类型给我’而不是实例化这个类型。或者更精确的说是“把符号Greeter的类型给我”,即构造函数的类型。这将包含创建Greeter类实例的构造函数的所有静态成员.。跟前面调用一样,我们通过使用‘new’操作符在‘greeterMaker’前面来创建新的‘Greeter’实例。

8.2使用一个类作为一个接口

正如我们在前一节中说,类声明创建两件事:一种代表类的实例和构造函数。因为类创建类型,你可以在使用接口的地方使用它:

1
2
3
4
5
6
7
8
9
10
class Point {
    x: number;
    y: number;
}
interface Point3d extends Point {
    z: number;
}
var point3d: Point3d = {x: 1, y: 2, z: 3};

Typescript中文教程-接口

Typescript 的一个核心原则是类型检查关注于值的‘类型’。有时被称作‘鸭子类型’(duck typing)或‘结构类型’(structural subtyping)。在typescript里,接口扮演了命名这些类型的角色,同时也是在你的代码和链接你的外部工程里定义链接的有力方法。

1、我们的第一个接口interface

查看接口如何工作的最简单的方法是开始一个简单的实例:

1
2
3
4
5
6
function printLabel(labelledObj: {label: string}) {
  console.log(labelledObj.label);
}
var myObj = {size: 10, label: “Size 10 Object”};
printLabel(myObj);

类型检查将检查调用‘printLable’。‘printLable’函数有一个带有原型‘lable’类型为字符串string的对象参数。需要注意的是我们传入的对象实属性际要比此要多,但是最终编辑器只是检查符合参数要求的那个属性。

我们再写一次同样的例子,这一次使用接口来描述此函数需要一个字符串类型的属性‘lable’:

1
2
3
4
5
6
7
8
9
10
interface LabelledValue {
  label: string;
}
function printLabel(labelledObj: LabelledValue) {
  console.log(labelledObj.label);
}
var myObj = {size: 10, label: “Size 10 Object”};
printLabel(myObj);

接口‘LabledValue’是一个可以用来描述我们前面例子函数需求的名称。它同样要求有一个属性叫做‘lable’并且类型是字符串string。需要注意的是我们不需要像其他语言那样明确的声明我们传入‘printLable’实例的对象。这里主要是‘类型’起到关键作用。如果传入函数的对象符合要求就行。

值得指出的是类型检查器不会要求这些属性按照一定的顺序,只要接口的要求的属性出现并且是正确的类型即可。

2、可选属性

不是接口的所有属性都是必须的。一些存在特定的条件或者可能根本不存在。这些可选的属性在选择袋(option bags),即传入一个函数的只有一个对象,在填补了几个属性时,这种方式很受欢迎。

这里以下面的模式为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
interface SquareConfig {
  color?: string;
  width?: number;
}
function createSquare(config: SquareConfig): {color: string; area: number} {
  var newSquare = {color: “white”, area: 100};
  if (config.color) {
    newSquare.color = config.color;
  }
  if (config.width) {
    newSquare.area = config.width * config.width;
  }
  return newSquare;
}
var mySquare = createSquare({color: “black”});

可选的接口和其他接口类似,只是在每个可选属性里加入‘?’作为属性的声明的一部分。

可选属性的好处是,可以描述可能使用的属性同时捕捉到不应该被使用的属性。例如,我们传入‘createSquare’函数里的属性名字打错啦,编译器将会通知我们一个错误消息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
interface SquareConfig {
  color?: string;
  width?: number;
}
function createSquare(config: SquareConfig): {color: string; area: number} {
  var newSquare = {color: “white”, area: 100};
  if (config.color) {
    newSquare.color = config.collor;  // Type-checker can catch the mistyped name here
  }
  if (config.width) {
    newSquare.area = config.width * config.width;
  }
  return newSquare;
}
var mySquare = createSquare({color: “black”});

 

3、函数类型

接口也能够描述javascript对象包含的更宽范围的‘类型’。在添加一个对象的属性时,接口同样能描述函数的类型。

要用接口描述一个函数的类型,我们给这个接口一个调用签名。这是一个只有参数列表和返回值的函数声明。

1
2
3
interface SearchFunc {
  (source: string, subString: string): boolean;
}

一但我们定义了之后就可以像其他接口一样使用函数类型的接口。这里,我们展示如何通过创建一个函数类型的变量然后设置他的函数值是同样的类型。

1
2
3
4
5
6
7
8
9
10
var mySearch: SearchFunc;
mySearch = function(source: string, subString: string) {
  var result = source.search(subString);
  if (result == -1) {
    return false;
  }
  else {
    return true;
  }
}

对于函数类型的正确类型检查来说,参数的名字并不一定要一致。例如我们可以将上面的例子改写成下面这样:

1
2
3
4
5
6
7
8
9
10
var mySearch: SearchFunc;
mySearch = function(src: string, sub: string) {
  var result = source.search(subString);
  if (result == -1) {
    return false;
  }
  else {
    return true;
  }
}

函数的参数检查时每个类型对应参数的位置互相检查。同样我们的函数表达式的返回类型暗示了它返回的值(这里是真或假)。若函数返回的是数字或字符串,类型检查其将通知我们SearchFunc返回值与接口不匹配。

4、数组类型Array Types

跟我们用接口来描述函数类型相似,我们也能描述数组类型。数组类型有一个‘index’类型用来描述允许索引的对象的类型和一个相应的返回类型来访问索引。

1
2
3
4
5
6
interface StringArray {
  [index: number]: string;
}
var myArray: StringArray;
myArray = [“Bob”, “Fred”];

这里有两种索引支持类型,字符串和数字。可以同时支持两种索引形式,但约束数值类型的索引必须是字符类型索引返回类型的子类型。

索引签名是一种非常强有力的描述数组和‘dictionary’模式的方法,同时它也强制所有属性必须符合它的返回类型。在下面这个例子里,属性并没有符合通用索引,类型检查器将返回一个错误:

1
2
3
4
interface Dictionary {
  [index: string]: string;
  length: number;    // error, the type of ‘length’ is not a subtype of the indexer
}

 

5、类类型class types

实现一个接口

在C#或Java等语言里一种最常用的接口是明确地限制一个类符合特定的要求,在typescript里也是可以实现的。

1
2
3
4
5
6
7
8
interface ClockInterface {
    currentTime: Date;
}
class Clock implements ClockInterface  {
    currentTime: Date;
    constructor(h: number, m: number) { }
}

你也可以在一个接口中描述实现类中的方法,我们也用‘setTime’作为例子:

1
2
3
4
5
6
7
8
9
10
11
12
interface ClockInterface {
    currentTime: Date;
    setTime(d: Date);
}
class Clock implements ClockInterface  {
    currentTime: Date;
    setTime(d: Date) {
        this.currentTime = d;
    }
    constructor(h: number, m: number) { }
}

接口描述类的公共方面而不是其私有方面。这将阻止你用它来检查一个类实例也有特殊的私有方面的类型。

6、静态/实例的类之间的区别

在处理类和接口时,它有助于记住一个类两种类型:静态的类型和实例的类型。您可能会注意到,如果你通过构造签名创建一个接口并尝试创建一个类实现该接口你会得到一个错误:

1
2
3
4
5
6
7
8
interface ClockInterface {
    new (hour: number, minute: number);
}
class Clock implements ClockInterface  {
    currentTime: Date;
    constructor(h: number, m: number) { }
}

这是因为当一个类使用接口时,只检查实例的类。由于构造函数是静态的将不会被检查。

然而,您将需要直接使用类“静态”的一面。在这个例子我们直接使用类:

1
2
3
4
5
6
7
8
9
10
11
interface ClockStatic {
    new (hour: number, minute: number);
}
class Clock  {
    currentTime: Date;
    constructor(h: number, m: number) { }
}
var cs: ClockStatic = Clock;
var newClock = new cs(7, 30);

 

7、扩展接口

就像类,接口可以互相扩展。这个处理的任务复制一个接口的成员到另一个,在你单独的接口为可重用的组件时有更多的自由。

1
2
3
4
5
6
7
8
9
10
11
interface Shape {
    color: string;
}
interface Square extends Shape {
    sideLength: number;
}
var square = <Square>{};
square.color = “blue”;
square.sideLength = 10;

一个接口可以扩展多个接口,创建一个所有组合的接口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
interface Shape {
    color: string;
}
interface PenStroke {
    penWidth: number;
}
interface Square extends Shape, PenStroke {
    sideLength: number;
}
var square = <Square>{};
square.color = “blue”;
square.sideLength = 10;
square.penWidth = 5.0;

 

8、混合型

正如我们前面所提到的,接口可以描述出现在现实世界JavaScript的丰富的类型。由于JavaScript的动态和灵活的性质,你偶尔会遇到一个对象,是上面描述的一些类型的组合。

例子是一个对象,作为一个函数和一个对象的附加属性:

1
2
3
4
5
6
7
8
9
10
interface Counter {
    (start: number): string;
    interval: number;
    reset(): void;
}
var c: Counter;
c(10);
c.reset();
c.interval = 5.0;

与第三方JavaScript交互,您可能需要使用像上面完全描述的形状类型的模式。

Typescript中文教程-基础类型

基础类型(Basic Types)

为了使程序正常工作,我们需要以下基本数据类型:numbers数字,structures结构,strings字符串,boolean布尔型值等等。在typescript中,通过注入一种方便的枚举类型来帮助typescript支持javascript同样的数据类型。

1、Boolean布尔型

Javascript和typeScript(其他语言同理)同样叫做布尔型‘boolean’的值只包含最简单的两个值真和假(true/false)。

1
var isDone: boolean = false;

 

2、Number数字

跟javaScript一样,typescript所有的数都是浮点数类型。这些浮点数获得类型‘number’。

1
var height: number = 6;

 

3、string字符串

另一种在网页和服务器端的基本数据类型是字符型数据。跟其他语言一样,我们用‘string’来表示字符类型的数据。跟javascript一样,typescript也是用单引号‘’或双引号“”来包裹字符串型数据。

1
2
var name: string = “bob”;
name = ‘smith’;

 

4、Array数组

同样typescript也允许使用数组。数组类型可以用两种方法来表示。第一种使用元素的类型和后面紧跟的大括号‘[]’来表示素组每一个元素的类型:

1
var list:number[] = [1, 2, 3];

第二种方法是使用一个通用数组类型,Array<elemType>:

1
var list:Array<number> = [1, 2, 3];

 

5、Enum枚举

一个对javascript很有用的附加标准数据类型是枚举类型’enum’,和C#一样,枚举类型是一种十分友好的给出名称和名称的值的方法。

1
2
enum Color {Red = 1, Green, Blue};
var c: Color = Color.Green;

或者甚至可以手动的设置枚举类型中所有的值。

1
2
enum Color {Red = 1, Green = 2, Blue = 4};
var c: Color = Color.Green;

枚举类型的一种非常方便的特性是根据枚举类型元素的数字的值可以访问到这个元素的名称。例如,我们只知道数值2,但我们并不确定上面枚举类型具体的情况,我们可以查询其相应的名字。

1
2
3
4
enum Color {Red = 1, Green, Blue};
var colorName: string = Color[2];
alert(colorName);

typescript1

6、Any类型

我们可能需要描述我们在写应用时还不确定的变量。这些值可能来自一些动态内容,例如来自第三方库或用户。在这种情况下,我们想要在编译时跳过其类型检查,这时我们需要给它添加‘any’标签。

1
2
3
var notSure: any = 4;
notSure = “maybe a string instead”;
notSure = false; // okay, definitely a boolean

Any类型是一种非常好的方法,帮助我们使用现有的JavaScript,允许你设置在编译时跳入跳出类型检查。

如果你只知道部分类型,‘any’类型也是很好帮助。例如,你有个混合类型元素的数组:

1
2
3
var list:any[] = [1, true, “free”];
list[1] = 100;

 

7、Viod空类型

跟‘any’相反的是‘void’类型,表示完全不含有任何类型的数据。你可能通常在没有返回值的函数中看到它。

1
2
3
function warnUser(): void {
    alert(“This is my warning message”);
}

TypeScript中文教程-新手上路

写在前面

本系列来自 @东北大客 http://14ms.net/ 的翻译,虽然没有全部翻译,但是仍然帮我我这种初学者且英语苦手的大忙。

我在东北大客翻译的基础上改了改错别字和一些描述上的问题,算是一点优化。

原文链接为:http://www.typescriptlang.org/

 

1、安装typescript支持 。

有两种主要方法获得typescript工具:通过node.js的包管理(npm),或者安装VS2012的TypeScript插件。

对于VS 2013用户,点击一下链接:

Install TypeScript for Visual Studio 2013

对于NPM用户:

1
> npm install -g typescript

 

2、在编辑器(如webstrom)中新建greeter.ts文件,敲入一下代码:

 

1
2
3
4
5
6
7
function greeter(person) {
    return “Hello, ” + person;
}
var user = “Jane User”;
document.body.innerHTML = greeter(user);

 

3、编译TypeScript文件

以上文件使用 .ts为扩展名,但实际代码仍为 javascript。可以直接拷贝粘贴至现有的Javascript 程序中。

在命令行里键入 如下命令,运行 TypeScript 编译器

注:当前目录为greeter.ts的目录。

请完成以上步骤1,即已经安装了typeScript支持。

1
> tsc greeter.ts

 

4、类型声明

结果是生成了一个带有相同代码的js文件。TypeScritpt 已经运行起来了。
接下来我们利用TypeScript提供的新工具进行一些改良。给greeter函数的person参数添加“:string”类型声明。

1
2
3
4
5
6
7
function greeter(person: string) {
    return “Hello, ” + person;
}
var user = “Jane User”;
document.body.innerHTML = greeter(user);

 

5、变量控制

类型声明是一种控制函数变量期望的简单方法。在这里,我们期望给greeter函数传递一个字符串类型的产生。我们将传递进来的参数改成数组试一下:

1
2
3
4
5
6
7
function greeter(person: string) {
    return “Hello, ” + person;
}
var user = [0, 1, 2];
document.body.innerHTML = greeter(user);

重新编译将会看到如下的错误:

1
greeter.ts(7,26): Supplied parameters do not match any signature of call target

 

6、接口

同样的,如果将调用greeter函数时不传入任何参数,TypeScript将会告诉你传入的参数个数不符。TypeScript提供根据私有声明和代码结构两种静态分析方法。
需要注意的是,以上仍然会建立greeter.js文件。尽管有错误仍然可以使用TypeScript,但TypeScript将会告知以上代码可能不会按照期望的方式运行。
接下来更进一步的改造我们的例子。我们使用接口interface来描述person对象,其包含名字和姓氏,即firstname和lastname。在TypeScript如果两个类型的内部结构兼容的话就会被兼容。这使得我们可以通过声明接口的形态即可实现接口,而不用详述一个接口的实现条款。

1
2
3
4
5
6
7
8
9
10
11
12
interface Person {
    firstname: string;
    lastname: string;
}
function greeter(person : Person) {
    return “Hello, ” + person.firstname + ” ” + person.lastname;
}
var user = {firstname: “Jane”, lastname: “User”};
document.body.innerHTML = greeter(user);

 

7、类的声明和构造

最后我们扩展类class。TypeScript支持现在的ES6基于类的面向对象程序声明。
下面通过构造函数和几个公有域创建一个Student类,需要注意的是Person和Student可以同时声明,并交给程序正确的抽象出来。

需要注意的是,构造函数的public参数是一种帮助我们快速建立属性的速记方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Student {
    fullname : string;
    constructor(public firstname, public middleinitial, public lastname) {
        this.fullname = firstname + ” ” + middleinitial + ” ” + lastname;
    }
}
interface Person {
    firstname: string;
    lastname: string;
}
function greeter(person : Person) {
    return “Hello, ” + person.firstname + ” ” + person.lastname;
}
var user = new Student(“Jane”, “M.”, “User”);
document.body.innerHTML = greeter(user);

 

8、添加HTML并运行

再运行一次 tsc greeter.js 我们发现生成的js 和以前我们写的代码相同。类class只是帮助我们建立基于OO的原型的方法。
下面添加 greeter.html 并在浏览器里打开就可以看见最终效果了。

1
2
3
4
5
6
7
<!DOCTYPE html>
<html>
    <head><title>TypeScript Greeter</title></head>
    <body>
        <script src=”greeter.js”></script>
    </body>
</html>