- TypeScript是JavaScript的超集。
- 它对JS进行了扩展,向JS中引入了类型的概念,并添加了许多新的特性。
- TS代码需要通过编译器编译为JS,然后再交由JS解析器执行。
- TS完全兼容JS,换言之,任何的JS代码都可以直接当成JS使用。
- 相较于JS而言,TS拥有了静态类型,更加严格的语法,更强大的功能;TS可以在代码执行前就完成代码的检查,减小了运行时异常的出现的几率;TS代码可以编译为任意版本的JS代码,可有效解决不同JS运行环境的兼容问题;同样的功能,TS的代码量要大于JS,但由于TS的代码结构更加清晰,变量类型更加明确,在后期代码的维护中TS却远远胜于JS。
1 基本使用
1.1 安装
sudo npm i -g typescript
出现 Version x.x.x 便是成功tsc -v
编译成js
tsc 01_helloTS.ts
tsc 文件名.ts
2 基本类型
2.1 类型声明
- 类型声明是TS非常重要的一个特点
- 通过类型声明可以指定TS中变量(参数、形参)的类型
- 指定类型后,当为变量赋值时,TS编译器会自动检查值是否符合类型声明,符合则赋值,否则报错
- 简而言之,类型声明给变量设置了类型,使得变量只能存储某种类型的值 ```javascript let 变量: 类型;
let 变量: 类型 = 值;
function fn(参数: 类型, 参数: 类型): 类型{ … }
例:
```javascript
let a: number = 1
function sum(a: number, b: number): number {
return a + b
}
console.log(sum(a, 3)); // 4
2.2 自动类型判断
- TS拥有自动的类型判断机制
- 当对变量的声明和赋值是同时进行的,TS编译器会自动判断变量的类型
- 所以如果你的变量的声明和赋值时同时进行的,可以省略掉类型声明
2.3 字面量类型
let color: 'red' | 'blue' | 'black';
let num: 1 | 2 | 3 | 4 | 5;
| 或
上面的num除了1-5外赋任何值都会报错
也可以用来联合多个类型
let num : string | number
num = 99
num = 'hello world'
console.log(num); // hello world
2.4 任意类型any
相当于对该变量关闭了类型检测
let d: any = 4;
d = 'hello';
d = true;
若是声明变量不指定类型,TS解析器会自动判断变量的类型为any(隐式any)
2.5 未知类型unknown
相当于有类型安全的any
unknown类型变量不能直接赋给其他变量,但是判断后的可以
let notSure: unknown = 4;
let str: string
str = notSure
let notSure: unknown = 4;
let str: string
if (typeof notSure === 'string'){
str = notSure
}
2.6 类型断言
或者使用类型断言来告诉编译器
let notSure: unknown = 4;
let str: string
str = notSure as string
// 或者
str = <string>notSure
2.7 类型void和never
void 表示为空,以函数为例,就表示没有返回值
never 表示永远不回返回结果
2.8 对象类型object
{}用来指定可以包含哪些属性
let obj: {
name: string
age: number
}
obj = {
name: 'hehe',
age: 19
sex: ''
}
在属性名后加上?表示可选
let obj: {
name: string,
age: number,
sex?: string
}
[propName: string]: any []里面表示任意字符的属性名, 即任意类型属性
let obj: {
name: string,
[propName: string]: any
}
2.9 设置函数结构类型声明
限制传入参数是number,返回值也是number
let func: (a:number, b:number) => number;
func = function(n1, n2) {
return n1 + n2
}
2.10 数组类型
let arr: string[]
arr = ['1','2','3']
let arr: Array<string>
arr = ['1','2','3']
2.11 元组类型tuple
固定长度的数组
let tup: [string, number] = ['yoxi', 123]
2.22 枚举enum
enum Gender {
Male = 0,
Female = 1
}
let obj: {name: string, gender: Gender}
obj = {
name:'猴子',
gender: Gender.Female
}
console.log(obj.gender === Gender.Female); // true
2.23 & 且
let obj: { name: string } & { age: number }
obj = {name: '猴子', age: 18}
对象必须满足两个条件
2.24 类型别名
type myType = 1 | 2 | 3
let key: myType
key = 1
key = 'hello'
最后一个报错
3 编译选项
3.1 自动编译文件
tsc xxx.ts -w
3.2 自动编译整个项目
- 如果直接使用tsc指令,则可以自动将当前项目下的所有ts文件编译为js文件。
- 但是能直接使用tsc命令的前提时,要先在项目根目录下创建一个ts的配置文件 tsconfig.json
- tsconfig.json是一个JSON文件,添加配置文件后,只需只需 tsc 命令即可完成对整个项目的编译
- ts编译器可以根据它信息来对代码进行编译
3.3 tsconfig.json中的属性
配置选项:
- include
- 定义希望被编译文件所在的目录
- 默认值:[“*/“]
- 所有src目录和tests目录下的文件都会被编译
* 表示任意目录{
"include":["src/**/*", "tests/**/*"]
}
表示任意文件
- exclude
- 定义需要排除在外的目录
- 默认值:[“node_modules”, “bower_components”, “jspm_packages”]
src下hello目录下的文件都不会被编译{
"exclude": ["./src/hello/**/*"]
}
- extends
- 定义被继承的配置文件
当前配置文件中会自动包含configs目录下base.json中的所有配置信息{
"extends": "./configs/base"
}
- 定义被继承的配置文件
- files
- 指定被编译文件的列表,只有需要编译的文件少时才会用到
列表中的文件都会被TS编译器所编译"files": [
"core.ts",
"sys.ts",
"types.ts",
"scanner.ts",
"parser.ts",
"utilities.ts",
"binder.ts",
"checker.ts",
"tsc.ts"
]
- 指定被编译文件的列表,只有需要编译的文件少时才会用到
compilerOptions
- 编译选项是配置文件中非常重要也比较复杂的配置选项
- 在compilerOptions中包含多个子选项,用来完成对编译的配置
target
- 设置ts代码编译的目标版本
- 可选值:ES3(默认)、ES5、ES6/ES2015、ES7/ES2016、ES2017、ES2018、ES2019、ES2020、ESNext
{
"compilerOptions": {
"target": "ES6"
}
}
lib
- 指定代码运行时所包含的库(宿主环境)
- 可选值:ES5、ES6/ES2015、ES7/ES2016、ES2017、ES2018、ES2019、ES2020、ESNext、DOM、WebWorker、ScriptHost ……
{
"compilerOptions": {
"target": "ES6",
"lib": [
"ES6",
"DOM"
],
"outDir": "dist",
"outFile": "dist/aa.js"
}
}
module
- 设置编译后代码使用的模块化系统(规范)
- 可选值:CommonJS、UMD、AMD、System、ES2020、ESNext、None、ES2015
{
"compilerOptions": {
"module": "CommonJS"
}
}
outDir
- 编译后文件的所在目录
- 默认情况下,编译后的js文件会和ts文件位于相同的目录,设置outDir后可以改变编译后文件的位置
- 设置后编译后的js文件将会生成到dist目录
{
"compilerOptions": {
"outDir": "dist"
}
}
outFile
- 将所有的文件编译为一个js文件
- 默认会将所有的编写在全局作用域中的代码合并为一个js文件,如果module制定了None、System或AMD则会将模块一起合并到文件之中
{
"compilerOptions": {
"outFile": "dist/app.js"
}
}
rootDir
- 指定代码的根目录,默认情况下编译后文件的目录结构会以最长的公共目录为根目录,通过rootDir可以手动指定根目录
{
"compilerOptions": {
"rootDir": "./src"
}
}
- 指定代码的根目录,默认情况下编译后文件的目录结构会以最长的公共目录为根目录,通过rootDir可以手动指定根目录
allowJs
- 是否对js文件编译
checkJs
- 是否对js文件进行检查
{
"compilerOptions": {
"allowJs": true,
"checkJs": true
}
}
- 是否对js文件进行检查
removeComments
- 是否删除注释
- 默认值:false
- noEmit
- 不对代码进行编译
- 默认值:false
- sourceMap
- 是否生成sourceMap
- 默认值:false
- 严格检查
- strict
- 启用所有的严格检查,默认值为true,设置后相当于开启了所有的严格检查
- alwaysStrict
- 总是以严格模式对代码进行编译
- noImplicitAny
- 禁止隐式的any类型
- noImplicitThis
- 禁止类型不明确的this
- strictBindCallApply
- 严格检查bind、call和apply的参数列表
- strictFunctionTypes
- 严格检查函数的类型
- strictNullChecks
- 严格的空值检查
- strictPropertyInitialization
- 严格检查属性是否初始化
- strict
- 额外检查
- noFallthroughCasesInSwitch
- 检查switch语句包含正确的break
- noImplicitReturns
- 检查函数没有隐式的返回值
- noUnusedLocals
- 检查未使用的局部变量
- noUnusedParameters
- 检查未使用的参数
- noFallthroughCasesInSwitch
- 高级
- allowUnreachableCode
- 检查不可达代码
- 可选值:
- true,忽略不可达代码
- false,不可达代码将引起错误
- noEmitOnError
- 有错误的情况下不进行编译
- 默认值:false
- allowUnreachableCode
3.4 常用小测试tsconfig.json配置
{
"compilerOptions": {
"target": "ES2015",
"module": "ES2015",
"strict": true,
"outDir": "./dist"
},
"include": [
"./src/**/*"
]
}
直接在根目录下tsc -w
开启监视模式
4 webpack整合TypeScript
1⃣️ 进入项目根目录,初始化项目
npm init -y
主要作用:创建package.json文件
2⃣️ 安装依赖
npm i -D webpack webpack-cli webpack-dev-server typescript ts-loader clean-webpack-plugin html-webpack-plugin clean-webpack-plugin
- webpack:构建工具webpack
- webpack-cli:webpack的命令行工具
- webpack-dev-server:webpack的开发服务器
- typescript:ts编译器
- ts-loader:ts加载器,用于在webpack中编译ts文件
- html-webpack-plugin:webpack中html插件,用来自动创建html文件
- clean-webpack-plugin:webpack中的清除插件,每次构建都会先清除目录
3⃣️ 根目录下创建webpack的配置文件webpack.config.js
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
// webpack中所有配置信息都应该写在module.exports中
module.exports = {
optimization:{
minimize: false // 关闭代码压缩,可选
},
// 指定文件入口
entry: "./src/index.ts",
devtool: "inline-source-map",
devServer: {
contentBase: './dist'
},
// 指定打包文件所在目录
output: {
path: path.resolve(__dirname, "dist"),
// 打包后的文件
filename: "bundle.js",
// 配置打包的环境
environment: {
arrowFunction: false // 关闭webpack的箭头函数,可选
}
},
// 设置引用要导入的模块
resolve: {
extensions: [".ts", ".js"] // 以.ts和.js文件都可以作为模块使用
},
// 指定webpack打包时要使用模块
module: {
// 指定要加载的规则
rules: [
{
test: /\.ts$/, // 匹配以ts结尾的文件
use: { // 要使用的loader
loader: "ts-loader" // 使用ts-loader去处理以ts结尾的文件
},
// 要排除文件
exclude: /node_modules/
}
]
},
plugins: [
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
title:'TS测试' // 设置网页标题
}),
]
}
4⃣️ 根目录下创建tsconfig.json,配置可以根据自己需要
{
"compilerOptions": {
"target": "ES2015",
"module": "ES2015",
"strict": true,
"noEmitOnError": true
}
}
ES2015 就是 ES6"noEmitOnError": true
有错不进行编译
5⃣️ 修改package.json添加如下配置
{
...略...
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "webpack",
"start": "webpack serve --open chrome.exe"
},
...略...
}
"build": "webpack"
, 建立是使用webpack"start": "webpack serve --open chrome.exe"
保存通过谷歌浏览器打开
第一个启动方式是npm run build
第二个启动方式是npm start
5 安装babel
经过一系列的配置,使得TS和webpack已经结合到了一起,除了webpack,开发中还经常需要结合babel来对代码进行转换以使其可以兼容到更多的浏览器,在上述步骤的基础上,通过以下步骤再将babel引入到项目中。
5.1 安装依赖
npm i -D @babel/core @babel/preset-env babel-loader core-js
- @babel/core:babel的核心工具
- @babel/preset-env:babel的预定义环境
- @babel-loader:babel在webpack中的加载器
core-js:core-js用来使老版本的浏览器支持新版ES语法
5.2 修改webpack.config.js配置文件
...略...
module: {
rules: [
{
test: /\.ts$/,
use: [
{
loader: "babel-loader",
options:{
// 预定义
presets: [
[
"@babel/preset-env",
{
"targets":{
"chrome": "58",
"ie": "11"
},
"corejs":"3", // 指定版本
"useBuiltIns": "usage" // 使用corejs方式,usage按需加载
}
]
]
}
},
{
loader: "ts-loader", // 使用ts-loader去处理以ts结尾的文件
}
],
exclude: /node_modules/
}
]
}
...略...
5.3 webpack.config.js总配置
```typescript // 引入一个包 const path = require(‘path’); // 引入html插件 const HTMLWebpackPlugin = require(‘html-webpack-plugin’); // 引入clean插件 const { CleanWebpackPlugin } = require(‘clean-webpack-plugin’);
// webpack中的所有的配置信息都应该写在module.exports中 module.exports = {
// 指定入口文件
entry: "./src/index.ts",
// 指定打包文件所在目录
output: {
// 指定打包文件的目录
path: path.resolve(__dirname, 'dist'),
// 打包后文件的文件
filename: "bundle.js",
// 告诉webpack不使用箭头
environment:{
arrowFunction: false
}
},
// 指定webpack打包时要使用模块
module: {
// 指定要加载的规则
rules: [
{
// test指定的是规则生效的文件
test: /\.ts$/,
// 要使用的loader
use: [
// 配置babel
{
// 指定加载器
loader:"babel-loader",
// 设置babel
options: {
// 设置预定义的环境
presets:[
[
// 指定环境的插件
"@babel/preset-env",
// 配置信息
{
// 要兼容的目标浏览器
targets:{
"chrome":"58",
"ie":"11"
},
// 指定corejs的版本
"corejs":"3",
// 使用corejs的方式 "usage" 表示按需加载
"useBuiltIns":"usage"
}
]
]
}
},
'ts-loader'
],
// 要排除的文件
exclude: /node-modules/
}
]
},
// 配置Webpack插件
plugins: [
new CleanWebpackPlugin(),
new HTMLWebpackPlugin({
title: "这是一个自定义的title"
// template: "./src/index.html"
}),
],
// 用来设置引用模块
resolve: {
extensions: ['.ts', '.js']
}
};
<a name="K64uZ"></a>
# 6 面向对象
面向对象很简单,简而言之就是程序之中所有的操作都需要通过对象来完成。<br />计算机程序的本质就是对现实事物的抽象,抽象的反义词是具体。<br />一个事物到了程序中就变成了一个对象。
<a name="gKfWL"></a>
## 6.1 类(class)
所谓的类可以理解为对象的模型,程序中可以根据类创建指定类型的对象
```typescript
class 类名 {
属性名: 类型;
constructor(参数: 类型){
this.属性名 = 参数;
}
方法名(){
....
}
}
示例:
class Person{
// 实例属性
name: string;
age: number;
// 静态属性
static sex: string;
// 只读属性
readonly phone:number = 110
// 构造函数
constructor(name: string, age: number){
this.name = name;
this.age = age;
}
sayHello(){
console.log(`大家好,我是${this.name}`);
}
}
Person.sex // 类属性(静态属性)
使用
const p = new Person('孙悟空', 18);
p.sayHello();
- 实例属性: 通过实例去访问
- 静态属性(类属性):直接通过类访问(实例属性访问化提示不存在)
- 只读属性: 只能看,不能改,需要初始值
- 只读属性和静态属性混合:
static readonly sex: string = 'boy';
readonly只能放在static后 - 构造函数:构造不同属性的实例对象(在调用new 类()时会自动调用构造函数,即在创建对象时调用)
- this指向: this表示当前实例
6.2 继承
- 使用继承后,子类将会拥有父类所有的方法和属性
- 通过继承可以将多个类中共有的代码写在一个父类中,
- 这样只需要写一次即可让所有的子类都同时拥有父类中的属性和方法
- 如果希望在子类中添加一些父类中没有的属性或方法直接加就行
- 如果在子类中添加了和父类相同的方法,则子类方法会覆盖掉父类的方法(方法重写)
```typescript
class Animal {
name: string
age: number
constructor(name: string, age: number) {
} sayHello() {this.name = name
this.age = age
} }console.log('动物叫');
class Dog extends Animal {
run() {
console.log(${this.name} 在跑
);
}
sayHello() {
console.log(${this.name} 在叫
);
}
}
const dog = new Dog(‘大黄’, 4) dog.sayHello() // 大黄 在叫
<a name="X2MQN"></a>
## 6.3 super
在类方法中super表示当前类的父类<br />常用于子类新增属性时,调用父类构造函数
```typescript
class Animal {
name: string
age: number
constructor(name: string, age: number) {
this.name = name
this.age = age
}
sayHello() {
console.log('动物叫');
}
}
class Dog extends Animal {
sex:string
constructor(name: string, age: number,sex:string) {
super(name,age)
this.sex = sex
}
sayHello() {
console.log(`${this.name} 在叫`);
}
}
const dog = new Dog('大黄', 4, 'boy')
dog.sayHello() // 大黄 在叫
6.4 抽象类
- 以abstract开头的类是抽象类,
- 抽象类和其他类区别不大,只是不能用来创建对象
- 抽象类就是专门用来被继承的类
- 抽象类中可以添加抽象方法
- 抽象方法使用 abstract开头,没有方法体(即不能有具体实现)
- 抽象方法只能定义在抽象类中,子类必须对抽象方法进行重写
抽象类可以有实际值 ```typescript abstract class Animal { name: string;
constructor(name: string) {
this.name = name;
}
abstract sayHello():void }
class Dog extends Animal{ sayHello() { console.log(‘汪汪汪汪!’); } }
const dog = new Dog(‘旺财’) dog.sayHello() // 汪汪汪汪
<a name="Lr5tx"></a>
## 6.5 接口
- 接口用来定义一个类结构,用来定义一个类中应该包含哪些属性和方法
- 同时接口也可以当成类型声明去使用
- 接口可以在定义类的时候去限制类的结构,接口中的所有的属性都不能有实际的值
- 接口只定义对象的结构,而不考虑实际值
- 在接口中所有的方法都是抽象方法
```typescript
interface myInter{
name: string;
sayHello():void;
}
class MyClass implements myInter{
name: string;
constructor(name: string) {
this.name = name;
}
sayHello(){
console.log('大家好~~');
}
}
6.6 属性封装
TS中属性具有三种修饰符:
- public(共有,默认值),可以在类、子类和对象中修改
- protected ,可以在类、子类中修改
- private(私有) ,可以在类中修改(若是想在类外修改,只能通过提供方法暴露出去)
- 即通过设置getter/setter(存取器)来访问修改私有属性
protected:
class Person{
protected name: string;
protected age: number;
constructor(name: string, age: number){
this.name = name; // 可以修改
this.age = age;
}
sayHello(){
console.log(`大家好,我是${this.name}`);
}
}
class Employee extends Person{
constructor(name: string, age: number){
super(name, age);
this.name = name; //子类中可以修改
}
}
const p = new Person('孙悟空', 18);
p.name = '猪八戒';// 不能修改
private:
class Person{
private name: string;
private age: number;
constructor(name: string, age: number){
this.name = name; // 可以修改
this.age = age;
}
sayHello(){
console.log(`大家好,我是${this.name}`);
}
}
class Employee extends Person{
constructor(name: string, age: number){
super(name, age);
this.name = name; //子类中不能修改
}
}
const p = new Person('孙悟空', 18);
p.name = '猪八戒';// 不能修改
6.7 存取器(getter/setter)
在类中定义一组读取、设置私有属性的方法,这种对属性读取或设置的属性被称为属性的存取器
class Person{
private _name: string;
constructor(name: string){
this._name = name;
}
get name(){
return this._name;
}
set name(name: string){
this._name = name;
}
}
const p1 = new Person('孙悟空');
console.log(p1.name); // 通过getter读取name属性
p1.name = '猪八戒'; // 通过setter修改name属性
6.8 创建类中属性简洁方式
class C{
constructor(public name: string, public age: number) {
}
}
等同于
class C{
name: string;
age: number
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
}
6.9 静态属性
- 静态属性(方法),也称为类属性。使用静态属性无需创建实例,通过类即可直接使用
- 静态属性(方法)使用static开头
```typescript
class Tools{
static PI = 3.1415926;
static sum(num1: number, num2: number){
} }return num1 + num2
console.log(Tools.PI); console.log(Tools.sum(123, 456));
<a name="3lMF6"></a>
## 6.10 泛型(Generic)
定义一个函数或类时,有些情况下无法确定其中要使用的具体类型(返回值、参数、属性的类型不能确定),此时泛型便能够发挥作用。
```typescript
function test(arg: any): any{
return arg;
}
- 使用any会关闭TS的类型检查,
- 其次这样设置也不能体现出参数和返回值是相同的类型
// <T>属于定义,后两个T 分别是参数类型T,函数返回类型T
function test<T>(arg: T): T{
return arg;
}
<T>
就是泛型,T是我们给这个类型起的名字(不一定非叫T),设置泛型后即可在函数中使用T来表示该类型。所以泛型其实很好理解,就表示某个类型。
直接使用:test(10)
指定类型:test<number>(10)
多个范型:
function test<T, K>(a: T, b: K): K{
return b;
}
test<number, string>(10, "hello");
类中使用:
class MyClass<T>{
prop: T;
constructor(prop: T){
this.prop = prop;
}
}
对泛型的范围进行约束:
interface MyInter{
length: number;
}
function test<T extends MyInter>(arg: T): number{
return arg.length;
}
T extends MyInter表示泛型T必须是MyInter的子类,不一定非要使用接口类,抽象类同样适用。
7 综合案例贪吃蛇
在上面安装babel基础上开始
7.1 安装css相关插件
npm i -D less less-loader css-loader style-loader
less-loader 是将less 和wabpack整合的工具
配置webpack.config.js
// 引入一个包
const path = require('path');
// 引入html插件
const HTMLWebpackPlugin = require('html-webpack-plugin');
// 引入clean插件
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
// webpack中的所有的配置信息都应该写在module.exports中
module.exports = {
// 指定入口文件
entry: "./src/index.ts",
// 指定打包文件所在目录
output: {
// 指定打包文件的目录
path: path.resolve(__dirname, 'dist'),
// 打包后文件的文件
filename: "bundle.js",
// 告诉webpack不使用箭头
environment: {
arrowFunction: false
}
},
// 指定webpack打包时要使用模块
module: {
// 指定要加载的规则
rules: [
{
// test指定的是规则生效的文件
test: /\.ts$/,
// 要使用的loader
use: [
// 配置babel
{
// 指定加载器
loader: "babel-loader",
// 设置babel
options: {
// 设置预定义的环境
presets: [
[
// 指定环境的插件
"@babel/preset-env",
// 配置信息
{
// 要兼容的目标浏览器
targets: {
"chrome": "58",
"ie": "11"
},
// 指定corejs的版本
"corejs": "3",
// 使用corejs的方式 "usage" 表示按需加载
"useBuiltIns": "usage"
}
]
]
}
},
'ts-loader'
],
// 要排除的文件
exclude: /node-modules/
},
// 设置less文件处理
{
test: /\.less$/,
use: [
"style-loader",
"css-loader",
"less-loader"
]
}
]
},
// 配置Webpack插件
plugins: [
new CleanWebpackPlugin(),
new HTMLWebpackPlugin({
// title: "这是一个自定义的title"
template: "./src/index.html"
}),
],
// 用来设置引用模块
resolve: {
extensions: ['.ts', '.js']
}
};
less中use部分越先执行越在最后一个。
7.2 css兼容性处理
自动给一些样式在打包时添加前缀
npm i -D postcss postcss-loader postcss-preset-env
webpack.config.js中less部分修改
// 设置less文件处理
{
test: /\.less$/,
use: [
"style-loader",
"css-loader",
// 引入postcss
{
loader: "postcss-loader",
options: {
postcssOptions:{
plugins:[
[
"postcss-preset-env",
{
browsers: 'last 2 versions' // 使用最新的2个版本的浏览器
}
]
]
}
}
},
"less-loader"
]
}
7.3 HTML部分
<div id="main">
<!-- 游戏舞台 -->
<div id="stage">
<!-- 蛇 -->
<div id="snake">
<!-- 表示蛇的各部分 -->
<div></div>
</div>
<!-- 食物 -->
<div id="food">
<div></div>
<div></div>
<div></div>
<div></div>
</div>
</div>
<!-- 记分面板 -->
<div id="score-panel">
<div>
score: <span id="score">0</span>
</div>
<div>
level: <span id="level">1</span>
</div>
</div>
</div>
7.4 Less
@bg-color: #b7d4a8; // 变量
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font: bold 20px "Courier"
}
// 主窗口
#main {
width: 360px;
height: 420px;
background-color: @bg-color;
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: #000;
border-radius: 3px;
border: 1px solid @bg-color;
position: absolute;
}
}
#food {
width: 10px;
height: 10px;
position: absolute;
left: 10px;
top: 100px;
display: flex;
flex-flow: row wrap; // wrap 自动换行
justify-content: space-between;
align-content: space-between;
&>div{
width: 4px;
height: 4px;
background-color: #000;
transform: rotate(45deg);
}
}
}
#score-panel {
width: 304px;
display: flex;
justify-content: space-between;
}
}
7.5 蛇类Snake.ts
class Snake {
head: HTMLElement // 蛇头
bodies: HTMLCollection // 蛇身(含蛇头)
element: HTMLElement // 蛇的容器
constructor() {
this.head = document.querySelector('#snake>div')!
this.element = document.getElementById('snake')!
this.bodies = this.element.getElementsByTagName('div')
}
// 蛇头xy轴
get X() {
return this.head.offsetLeft
}
get Y() {
return this.head.offsetTop
}
set X(value: number) {
if (this.X === value) return
if (value < 0 || value > 290) {
// 撞墙了
throw new Error('蛇撞墙了')
}
// 不能反方向走
if (this.bodies[1] && (this.bodies[1] as HTMLElement).offsetLeft === value) {
// 如果发生掉头,让蛇向反方向继续移动
// 新value大于旧X,说明蛇往右,让其继续往左
value = value > this.X ? this.X - 10 :this.X + 10
}
this.moveBody()
this.head.style.left = value + 'px'
this.checkHeadBody()
}
set Y(value: number) {
if (this.Y === value) return
if (value < 0 || value > 290) {
// 撞墙了
throw new Error('蛇撞墙了')
}
// 不能反方向走
if (this.bodies[1] && (this.bodies[1] as HTMLElement).offsetTop === value) {
// 如果发生掉头,让蛇向反方向继续移动
value = value > this.Y ? this.Y - 10 : this.Y + 10
}
this.moveBody()
this.head.style.top = value + '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('撞到自己了~~')
}
}
}
}
export default Snake
7.6 食物类 Food
class Food{
element: HTMLElement;
constructor() {
// ! 指示不会为空
this.element = document.querySelector('#food')!
}
// 获取x轴坐标
get X(){
return this.element.offsetLeft
}
// 获取y轴坐标
get Y() {
return this.element.offsetTop
}
// 修改食物的位置
change() {
// 蛇移动一格是10,所以食物坐标必须是整10
// 食物位置最小0,最大290
this.element.style.top = Math.round(Math.random()*29)*10 + 'px'
this.element.style.left = Math.round(Math.random()*29)*10 + 'px'
}
}
export default Food
7.7 记分类 ScorePanel
// 记分牌类
class ScorePanel {
score = 0
level = 0
scoreEle: HTMLElement
levelEle: HTMLElement
maxLevel: number // 限制等级
upScore: number // 升级
constructor(maxLevel:number=10, upScore:number = 10) {
this.scoreEle = document.querySelector('#score')!
this.levelEle = document.querySelector('#level')!
this.maxLevel = maxLevel
this.upScore = upScore
}
// 加分方法
addScore() {
this.scoreEle.innerHTML = ++this.score + ''
// 分数满upScore升级
if(this.score % this.upScore === 0) {
this.levelUp()
}
}
// 提升等级方法
levelUp() {
if(this.level < this.maxLevel) {
this.levelEle.innerHTML = ++this.level + ''
}
}
}
export default ScorePanel
7.8 控制类 GameControl
import Snake from "./Snake";
import Food from "./Food";
import ScorePanel from "./ScorePanel";
// 控制其他所有类
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()
}
// 初始话游戏
init() {
// 绑定按键事件 修改this指向,不是修改this指向的是document
document.addEventListener('keydown', this.keydownHandler.bind(this))
this.run()
}
// 键盘按下响应函数
keydownHandler(event: KeyboardEvent) {
this.direction = event.key // 按键方向
}
// 移动方法
run() {
let x = this.snake.X
let y = this.snake.Y
switch (this.direction) {
case 'ArrowUp':
case 'Up':
y -= 10
break
case 'ArrowDown':
case 'Down':
y += 10
break
case 'ArrowLeft':
case 'Left':
x -= 10
break
case 'ArrowRight':
case 'Right':
x += 10
break
}
// 是否吃到食物
this.checkEat(x, y)
try {
this.snake.X = x
this.snake.Y = y
} catch (e) {
alert(e.message + 'GAME OVER')
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) {
this.food.change()
this.scorePanel.addScore()
this.snake.addBody()
}
}
}
export default GameControl
7.9 入口index.ts
import './style/index.less'
import GameControl from './moduls/GameControl'
new GameControl()