课程地址
ts 开发环境搭建
npm i -g typescript
查看安装位置:
$ npm root -g
C:\Users\Daniel\AppData\Roaming\npm\node_modules
创建 hello.ts
:
console.log("hello, ts");
编译 ts 文件,得到 js 文件:
$ tsc foo.ts
类型声明
后置的类型声明
let a: number;
a = 10;
a = 33;
// a = "hello";
let b: string = "hello";
let c = true; // Automatic type inference
function sum(a: number, b: number): number {
return a + b;
}
console.log(sum(1, 2));
类型
// 字面量类型
let a: 10; // const a = 10;
// 联合类型
let b: "male" | "female";
let c: boolean | string;
c = true;
c = "hello"
// any 与 unknown
let d: any;
d = "hello";
let e: unknown;
e = "hello";
let s: string = d;
// s = e; Type 'unknown' is not assignable to type 'string'
s = e as string;
s = <string>e;
function f(): void {
return;
}
function f2(): never { // 永远不会返回结果,终止进程
throw new Error("error");
}
unknown
本质上是一个类型安全的 any
// 对象类型
let b: {
name: string,
age?: number, // ? -> optional
};
b = {
name: "sunwukong"
};
let c: {
name: string,
[propName: string]: any,
};
// 函数类型
let d: (a: number, b: number) => number;
d = function(a, b) {
return a + b;
}
// 数组类型
let e: string[];
e = ["a", "b", "c"];
let g: Array<number>;
g = [1, 2, 3];
// 元组
let h: [string, string];
// enum
enum Gender {
Male = 0,
Female = 1,
}
let p: {name: string, gender: Gender}
p = {
name: "sunwukong",
gender: Gender.Male,
}
// & 类型
let j: {name: string} & {age: number};
// 类型别名
type t1 = 1 | 2 | 3 | 4 | 5;
let m: t1;
tsc 编译选项
自动编译文件
tsc foo.ts -w # 文件改变时自动编译
tsc # 根据 tsconfig.json 编译
tsconfig.json
{
/*
** 表示任意目录
* 表示任意文件
*/
"include": ["./*"],
"exclude": ["foo.ts"],
// "files": ["./app.ts", "./index.ts"],
"compilerOptions": {
"target": "ES6", // 指定 es 版本
// "module": "ES6",
// "lib": ["DOM"]
"outDir": "./dist",
// "outFile": "./dist/app.js", // 将编译结果合并到 app.js
"allowJs": false,
"checkJs": false, // 对 js 也做类型检查
"removeComments": false,
"noEmit": true, // 不生成编译后的文件,只检查
"noEmitOnError": true, // 当有错误时不生成编译后的文件
"strict": true, // 打开下面的所有严格检查
/*
"alwaysStrict": true, // -> use strict
"noImplicitAny": true,
"noImplicitThis": true,
"strictNullChecks": true,
*/
},
}
使用 webpack 打包 ts 代码
npm init -y # 初始化项目
npm i -D webpack webpack-cli typescript ts-loader
配置 webpack.config.js
const path = require("path");
module.exports = {
entry: "./src/index.ts",
output: {
path: path.resolve(__dirname + "/dist"),
filename: "bundle.js",
},
module: {
rules: [
{
test: /\.ts$/,
use: "ts-loader",
exclude: /node_modules/
}
]
},
mode: 'development',
}
配置 tsconfig.json
{
"compilerOptions": {
"module": "es6",
"target": "es6",
"strict": true,
}
}
在 package.json
中增加 script
配置
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "webpack"
},
最后 npm run build
即可
安装 3 个插件:
npm i -D html-webpack-plugin # 生成 html 测试文件
npm i -D webpack-dev-server # 编辑后自动重启服务
npm i -D clean-webpack-plugin # 清除 dist 目录再构建
npm i -D @babel/core @babel/preset-env babel-loader core-js
在 package.json
中编写 webpack-server 的启动脚本:
"scripts": {
"start": "webpack serve --open"
},
在 webpack.config.ts
中引入插件(使用 template.html 作为生成模板):
const path = require("path");
const HTMLWebpackPlugin = require("html-webpack-plugin");
const {CleanWebpackPlugin} = require("clean-webpack-plugin");
module.exports = {
entry: "./src/index.ts",
output: {
path: path.resolve(__dirname + "/dist"),
filename: "bundle.js",
},
module: {
rules: [
{
test: /\.ts$/,
use: [
{
loader: "babel-loader",
options: {
presets: [
[
"@babel/preset-env",
{
targets: {
"chrome": "88"
},
"corejs": "3",
"useBuiltIns": "usage" // 按需加载
}
]
]
}
},
"ts-loader"
],
exclude: /node_modules/
}
]
},
mode: 'development',
plugins: [
new HTMLWebpackPlugin({
// title: "mytitle"
template: "./src/template.html"
}),
new CleanWebpackPlugin(),
],
resolve: { // 设置引用模块
extensions: [".ts", ".js"]
}
}
OOP
class
class Person {
name: string = "sunwukong";
static age: number = 18;
readonly gender: string = "M";
sayHello() {
console.log("hello");
}
}
let p = new Person();
console.log(p);
console.log(Person.age);
p.sayHello();
构造函数和 this
class Dog {
name: string;
age: number;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
bark() {
console.log(`${this.name}: wangwang`);
}
}
let d = new Dog("wangcai", 4);
console.log(d);
d.bark();
继承
class Animal {
name: string;
age: number;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
sayHello() {
console.log("sayHello()");
}
}
class Dog extends Animal{
run(){
console.log(`${this.name} is running`);
}
sayHello(): void { // 子类重写父类方法
console.log("wangwangwang");
}
}
class Cat extends Animal{
sayHello(): void { // 子类重写父类方法
console.log("miaomiaomiao");
}
}
const d1 = new Dog("wangcai", 5);
console.log(d1);
d1.sayHello();
const c1 = new Cat("mimi", 3);
c1.sayHello();
super
super 表示父类
class Animal {
name: string;
constructor(name: string) {
this.name = name;
}
sayHello() {
console.log("sayHello()");
}
}
class Dog extends Animal {
age: number;
constructor(name: string, age: number) {
super(name);
this.age = age;
}
sayHello(): void {
console.log("wangwangwang");
}
}
const d1 = new Dog("wangcai", 3);
console.log(d1); // Dog { name: 'wangcai', age: 3 }
d1.sayHello(); // wangwangwang
抽象类
使用 abstract
修饰的类,不能用于实例化对象,只能被继承
抽象类中可以添加抽象方法,抽象方法必须被子类重写
abstract class Animal {
name: string;
constructor(name: string) {
this.name = name;
}
abstract sayHello(): void; // 必须被子类重写
}
class Dog extends Animal {
sayHello(): void {
console.log("wangwangwang");
}
}
const d1 = new Dog("wangcai");
console.log(d1); // Dog { name: 'wangcai', age: 3 }
d1.sayHello(); // wangwangwang
接口
用来定义一个类的结构(一个类应该包含哪些属性和方法,做类型限制)
接口中的所有属性都不能带有实际的值,接口只定义对象的结构(全是抽象方法),而不考虑实际值
interface Person{
name: string;
age: number;
}
// 接口可以分离定义
interface Person {
gender: string;
sayHello(): void;
}
class Male implements Person {
name: string;
age: number;
gender: string;
constructor(name: string, age: number, gender: string) {
this.name = name;
this.age = age;
this.gender = gender;
}
sayHello(): void {
console.log("hello");
}
}
属性访问控制
class Person {
private _name: string;
private _age: number;
constructor(name: string, age: number) {
this._name = name;
this._age = age;
}
get name() { // per.name
return this._name;
}
set name(name: string) {
this._name = name;
}
get age() {
return this._age;
}
set age(age: number) {
if (age >= 0) {
this._age = age;
}
}
}
const p1 = new Person("sunwukong", 18);
console.log(p1);
p1.name = "zhubajie";
console.log(p1);
console.log(p1.age);
class C {
// 直接将属性和访问控制定义在构造函数中
constructor(public name: string, public age: number) {
}
}
泛型
function fn<T> (a: T): T {
return a;
}
fn(10);
fn<string>("hello");
function bar<T, K>(a: T, b: K): T {
console.log(b);
return a;
}
bar<number, string>(10, "hello");
interface Inter {
length: number;
}
// 使用接口约束泛型的类型参数
function foo<T extends Inter>(a: T): number {
return a.length;
}
foo("123"); // string 有 length
foo({length: 10});
// 类的泛型
class C<T> {
name: T;
constructor(name: T) {
this.name = name;
}
}
const c = new C<string>("sunwukong");
贪吃蛇练习
// package.json
{
"name": "snake",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "webpack",
"start": "webpack serve --open"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@babel/core": "^7.23.9",
"@babel/preset-env": "^7.23.9",
"babel-loader": "^9.1.3",
"clean-webpack-plugin": "^4.0.0",
"core-js": "^3.35.1",
"css-loader": "^6.10.0",
"html-webpack-plugin": "^5.6.0",
"less": "^4.2.0",
"less-loader": "^12.2.0",
"style-loader": "^3.3.4",
"ts-loader": "^9.5.1",
"typescript": "^5.3.3",
"webpack": "^5.89.0",
"webpack-cli": "^5.1.4",
"webpack-dev-server": "^4.15.1"
}
}
// tsconfig.json
{
"compilerOptions": {
"module": "es6",
"target": "es6",
"strict": true,
"noEmitOnError": true
}
}
// webpack.config.ts
const path = require("path");
const HTMLWebpackPlugin = require("html-webpack-plugin");
const {CleanWebpackPlugin} = require("clean-webpack-plugin");
module.exports = {
entry: "./src/index.ts",
output: {
path: path.resolve(__dirname + "/dist"),
filename: "bundle.js",
},
module: {
rules: [
{
test: /\.ts$/,
use: [
{
loader: "babel-loader",
options: {
presets: [
[
"@babel/preset-env",
{
targets: {
"chrome": "88"
},
"corejs": "3",
"useBuiltIns": "usage" // 按需加载
}
]
]
}
},
"ts-loader"
],
exclude: /node_modules/
},
{
test: /\.less$/,
use: [
"style-loader",
"css-loader",
"less-loader"
]
}
]
},
mode: 'development',
plugins: [
new HTMLWebpackPlugin({
// title: "mytitle"
template: "./src/template.html"
}),
new CleanWebpackPlugin(),
],
resolve: { // 设置引用模块
extensions: [".ts", ".js"]
}
}
// index.ts
import "./style/index.less"
import Food from "./modules/food"
import ScorePanel from "./modules/score_panel"
import GameControl from "./modules/game_control";
const f = new Food();
f.change();
const gc = new GameControl();
// snake.ts
class Snake {
head: HTMLElement;
bodies: HTMLCollection;
element: HTMLElement;
constructor() {
this.head = document.querySelector("#snake > div")!;
this.bodies = document.getElementById("snake")!.getElementsByTagName("div");
this.element = document.getElementById("snake")!;
}
get X() {
return this.head.offsetLeft;
}
get Y() {
return this.head.offsetTop;
}
set X(x: number) {
if (this.X === x) return;
if (x < 0 || x > 290) {
throw new Error("snake_dead");
}
if (this.bodies[1] && (this.bodies[1] as HTMLElement).offsetLeft === x) {
if (x > this.X) {
x = this.X - 10;
} else {
x = this.X + 10;
}
}
this.moveBody();
this.head.style.left = x + "px";
this.checkHeadBody();
}
set Y(y: number) {
if (this.Y === y) return;
if (y < 0 || y > 290) {
throw new Error("snake_dead");
}
if (this.bodies[1] && (this.bodies[1] as HTMLElement).offsetTop === y) {
if (y > this.Y) {
y = this.Y - 10;
} else {
y = this.Y + 10;
}
}
this.moveBody();
this.head.style.top = y + "px";
this.checkHeadBody();
}
addBody() {
this.element.insertAdjacentHTML("beforeend", "<div></div>");
}
moveBody() {
for (let i = this.bodies.length - 1; i > 0; i--) {
let X = (this.bodies[i - 1] as HTMLElement).offsetLeft;
let Y = (this.bodies[i - 1] as HTMLElement).offsetTop;
(this.bodies[i] as HTMLElement).style.left = X + "px";
(this.bodies[i] as HTMLElement).style.top = Y + "px";
}
}
checkHeadBody() {
for (let i = 1; i < this.bodies.length; i++) {
let bd = this.bodies[i] as HTMLElement;
if (this.X === bd.offsetLeft && this.Y === bd.offsetTop) {
throw new Error("snake_dead");
}
}
}
}
export default Snake;
// score_panel.ts
class ScorePanel {
score = 0;
level = 1;
scoreEle: HTMLElement;
levelEle: HTMLElement;
maxLevel: number;
upScore: number;
constructor(maxLevel: number = 10, upScore: number = 10) {
this.scoreEle = document.getElementById("score")!;
this.levelEle = document.getElementById("level")!;
this.maxLevel = maxLevel;
this.upScore = upScore;
}
addScore() {
this.scoreEle.innerHTML = ++this.score + "";
if (this.score % this.upScore === 0) {
this.levelUp();
}
}
levelUp() {
if (this.level < this.maxLevel) {
this.levelEle.innerHTML = ++this.level + "";
}
}
}
export default ScorePanel;
// game_control.ts
import Snake from "./snake";
import Food from "./food";
import ScorePanel from "./score_panel";
class GameControl {
snake: Snake;
food: Food;
scorePanel: ScorePanel;
direction: string = "";
isLive = true;
constructor() {
this.snake = new Snake();
this.food = new Food();
this.scorePanel = new ScorePanel();
this.init();
}
keydownHandler = (event: KeyboardEvent) => {
this.direction = event.key;
}
init() {
document.addEventListener("keydown", this.keydownHandler);
this.run();
}
run() {
let X = this.snake.X;
let Y = this.snake.Y;
switch(this.direction) {
case "ArrowUp":
Y -= 10;
break;
case "ArrowDown":
Y += 10;
break;
case "ArrowLeft":
X -= 10;
break;
case "ArrowRight":
X += 10;
break;
}
this.checkEat(X, Y);
try {
this.snake.X = X;
this.snake.Y = Y;
} catch (e) {
alert((e as Error).message);
this.isLive = false;
}
this.isLive && setTimeout(this.run.bind(this), 300 - (this.scorePanel.level - 1) * 30);
}
checkEat(x: number, y: number) {
if (x === this.food.X && y === this.food.Y) {
console.log("ate food");
this.food.change();
this.scorePanel.addScore();
this.snake.addBody();
}
}
}
export default GameControl;
// food.ts
class Food {
element: HTMLElement;
constructor() {
// `!` 表示判定该语句的结果不可能为空
this.element = document.getElementById("food")!;
}
get X() {
return this.element.offsetLeft;
}
get Y() {
return this.element.offsetTop;
}
change() {
let left = Math.round(Math.random() * 29)* 10;
let top = Math.round(Math.random() * 29)* 10;
this.element.style.left = top + "px";
this.element.style.top = left + "px";
}
}
export default Food;
// index.less
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font: bold 20px Courier;
}
#main {
width: 360px;
height: 420px;
background-color: #b7d4a8;
margin: 100px auto;
border: 10px solid black;
border-radius: 10px;
display: flex;
flex-flow: column;
align-items: center;
justify-content: space-around;
#stage {
width: 304px;
height: 304px;
border: 2px solid black;
position: relative;
#snake {
&>div {
width: 10px;
height: 10px;
background-color: black;
border: 1px solid #b7d4a8;
position: absolute;
}
}
#food {
width: 10px;
height: 10px;
position: absolute;
left: 40px;
top: 100px;
display: flex;
flex-flow: row wrap;
justify-content: space-between;
align-content: space-between;
&>div {
width: 4px;
height: 4px;
background-color: black;
}
}
}
#score-panel {
width: 300px;
display: flex;
justify-content: space-between;
}
}