第一章 入门知识
0、TypeScript简介
- TypeScript是JavaScript的超集。
- 它对JS进行了扩展,向JS中引入了类型的概念,并添加了许多新的特性。
- TS代码需要通过编译器编译为JS,然后再交由JS解析器执行。
- TS完全兼容JS,换言之,任何的JS代码都可以直接当成JS使用。
- 相较于JS而言,TS拥有了静态类型,更加严格的语法,更强大的功能;TS可以在代码执行前就完成代码的检查,减小了运行时异常的出现的几率;TS代码可以编译为任意版本的JS代码,可有效解决不同JS运行环境的兼容问题;同样的功能,TS的代码量要大于JS,但由于TS的代码结构更加清晰,变量类型更加明确,在后期代码的维护中TS却远远胜于JS。
1、TypeScript 开发环境搭建
-
下载Node.js
- 64位:https://nodejs.org/dist/v14.15.1/node-v14.15.1-x64.msi
- 32位:https://nodejs.org/dist/v14.15.1/node-v14.15.1-x86.msi
-
安装Node.js
-
使用npm全局安装typescript
- 进入命令行
- 输入:npm i -g typescript
-
创建一个ts文件
-
使用tsc对ts文件进行编译
-
进入命令行
-
进入ts文件所在目录
-
执行命令:tsc xxx.ts
-
2、基本类型
-
类型声明
-
类型声明是TS非常重要的一个特点
-
通过类型声明可以指定TS中变量(参数、形参)的类型
-
指定类型后,当为变量赋值时,TS编译器会自动检查值是否符合类型声明,符合则赋值,否则报错
-
简而言之,类型声明给变量设置了类型,使得变量只能存储某种类型的值
-
语法:
-
let 变量: 类型; let 变量: 类型 = 值; function fn(参数: 类型, 参数: 类型): 类型{ ... }
-
-
-
自动类型判断
- TS拥有自动的类型判断机制
- 当对变量的声明和赋值是同时进行的,TS编译器会自动判断变量的类型
- 所以如果你的变量的声明和赋值时同时进行的,可以省略掉类型声明
-
代码例子:
//声明一个变量a,同时指定它的类型为number let a:number; //a的类型设置为数值型 number,在以后的使用过程中a的值只能是数字 a=10; a=33; //注意类型的声明一定要是小写的类型 //a="hello"; 出现错误!a只能为数值类型number let b:string; b="hello"; //b=123 //此处会出现错误,b只能为string类型,不能为其指定其他的数据类型 //声明完变量直接复制 //let c:boolean=true; //如果变量的声明赋值是同时进行的,ts可以自动的对变量进行类型检测 let d=false; d=true; //d=123; 错误!! // function sum(a,b){ // return a+b; // } // console.log(sum(123,456)); // console.log(sum(123,"456")); function sum(a:number,b:number){ return a+b; } console.log(sum(123,456)); //console.log(sum(123,"456")); //调用错误,a,b的值限定为number,所以不能为字符串 //console.log(sum(123,456,567)); //错误:!!对参数也有限制 不能输入超过定义的参数的类型 //sum(123) //错误!!输入的参数也不能少于定义函数的参数个数
-
类型:
类型 例子 描述 number 1, -33, 2.5 任意数字 string ‘hi’, “hi”, hi
任意字符串 boolean true、false 布尔值true或false 字面量 其本身 限制变量的值就是该字面量的值 any * 任意类型 unknown * 类型安全的any void 空值(undefined) 没有值(或undefined) never 没有值 不能是任何值 object {name:‘孙悟空’} 任意的JS对象 array [1,2,3] 任意JS数组 tuple [4,5] 元素,TS新增类型,固定长度数组 enum enum{A, B} 枚举,TS中新增类型 -
// 也可以直接使用字面量进行类型声明 let a: 10; a = 10; // 可以使用 | 来连接多个类型(联合类型) let b: "male" | "female"; //此处的b只接受male或者female的值 b = "male"; b = "female"; let c: boolean | string; c = true; c = 'hello'; // any 表示的是任意类型,一个变量设置类型为any后相当于对该变量关闭了TS的类型检测 // 使用TS时,不建议使用any类型 // let d: any; // 声明变量如果不指定类型,则TS解析器会自动判断变量的类型为any (隐式的any) let d; d = 10; d = 'hello'; d = true; // unknown 表示未知类型的值 let e: unknown; e = 10; e = "hello"; e = true; let s:string; // d的类型是any,它可以赋值给任意变量 s = d; e = 'hello'; // unknown 实际上就是一个类型安全的any // unknown类型的变量,不能直接赋值给其他变量 if(typeof e === "string"){ s = e; } // 类型断言,可以用来告诉解析器变量的实际类型 /* \* 语法: \* 变量 as 类型 \* <类型>变量 \* */ //第一种写法 s = e as string; //第二种写法 s = <string>e; // void 用来表示空,以函数为例,就表示没有返回值的函数 function fn(): void{ } // never 表示永远不会返回结果 function fn2(): never{ throw new Error('报错了!'); }
-
number
-
let decimal: number = 6; let hex: number = 0xf00d; let binary: number = 0b1010; let octal: number = 0o744; let big: bigint = 100n;
-
-
boolean
-
let isDone: boolean = false;
-
-
string
-
let color: string = "blue"; color = 'red'; let fullName: string = `Bob Bobbington`; let age: number = 37; let sentence: string = `Hello, my name is ${fullName}. I'll be ${age + 1} years old next month.`;
-
-
字面量
-
也可以使用字面量去指定变量的类型,通过字面量可以确定变量的取值范围
-
let color: 'red' | 'blue' | 'black'; let num: 1 | 2 | 3 | 4 | 5;
-
-
any
-
let d: any = 4; d = 'hello'; d = true;
-
-
unknown
-
let notSure: unknown = 4; notSure = 'hello';
-
-
void
-
let unusable: void = undefined;
-
-
never
-
function error(message: string): never { throw new Error(message); }
-
-
object(没啥用)
-
let obj: object = {};
-
-
array
-
let list: number[] = [1, 2, 3]; let list: Array<number> = [1, 2, 3];
-
-
tuple
-
let x: [string, number]; x = ["hello", 10]; //元组类型允许表示一个已知元素数量和类型的数组,各元素的类型不必相同。 比如,你可以定义一对值分别为 string和number类型的元组。
-
-
enum
-
enum Color { Red, Green, Blue, } let c: Color = Color.Green; enum Color { Red = 1, Green, Blue, } let c: Color = Color.Green; enum Color { Red = 1, Green = 2, Blue = 4, } let c: Color = Color.Green;
-
-
类型断言
-
有些情况下,变量的类型对于我们来说是很明确,但是TS编译器却并不清楚,此时,可以通过类型断言来告诉编译器变量的类型,断言有两种形式:
-
第一种
-
let someValue: unknown = "this is a string"; let strLength: number = (someValue as string).length;
-
-
第二种
-
let someValue: unknown = "this is a string"; let strLength: number = (<string>someValue).length
-
-
-
代码例子:
// object表示一个js对象
let a:object;
a={};
a=function(){};
//{} 用来指定对象中可以包含哪些属性
//语法:{属性名:属性值,属性名:属性值} 声明和赋值必须长度一致,多或者少都不行
//在属性值的后面加上?,表示属性是可选的
let b:{name:string,age?:number};
b={name:"孙悟空"};
//[propName:string]:any 表示任意的类型的属性 个数也不限制;
let c:{name:string,[propName:string]:any};
c={name:"猪八戒",age:18,gender:'男'};
/*
\* 设置函数结构的类型声明:
\* 语法:(形参:类型, 形参:类型 ...) => 返回值
\* */
let d:(a:number,b:number)=>number;
d=function(n1,n2):number{
return n1+n2;
}
//类型数组
/*
\* 数组的类型声明:
\* 类型[]
\* Array<类型>
\* */
//string[] 表示字符串数组
let e:string[];
e=["1","2","3"];
//number[] 表示数值数组
let f:number[];
f=[1,2,3];
let g:Array<number>;
g=[1,2,3];
//元组,元组就是固定长度的数组
// 语法:[类型,类型,类型]
let h:[string,string];
h=['hello','wode'];
enum Gender{
male=0,
female=1
}
let i:{name:string,gender:Gender};
i={
name:'孙悟空',
gender:Gender.male
}
console.log(i.gender===Gender.male);
//&表示同时 表示两者都必须需具备
let j:{name:string}&{age:number};
j={name:'孙悟空',age:18};
// 类型的别名 可以简化类型的使用
type myType=1|2|3|4|5;
let k:myType;
let l:myType;
k=2;
k=3;
3、编译选项
-
自动编译文件
-
编译文件时,使用 -w 指令后,TS编译器会自动监视文件的变化,并在文件发生变化时对文件进行重新编译。
-
示例:
-
tsc xxx.ts -w
-
-
-
自动编译整个项目
-
如果直接使用tsc指令,则可以自动将当前项目下的所有ts文件编译为js文件。
-
但是能直接使用tsc命令的前提时,要先在项目根目录下创建一个ts的配置文件 tsconfig.json
-
tsconfig.json是一个JSON文件,添加配置文件后,只需只需 tsc 命令即可完成对整个项目的编译
-
使用tsc -w命令对所以的ts文件进行自动监视变化,并重新编译。
-
tsconfig.json是ts编译器的配置文件,ts编译器可以根据它的信息来对代码进行编译
-
配置选项:
- include
- 定义希望被编译文件所在的目录/"include" 用来指定哪些ts文件需要被编译
- 默认值:["\*\*/\*"]
- 路径:** 表示任意目录
\* 表示任意文件
- 示例:
- ```json
"include":["src/**/*", "tests/**/*"]
```
- 上述示例中,所有src目录和tests目录下的文件都会被编译
- exclude
- 定义需要排除在外的目录 |不需要被编译的文件目录
- 默认值:["node_modules", "bower_components", "jspm_packages"]
- 示例:
- ```json
"exclude": ["./src/hello/**/*"]
```
- 上述示例中,src下hello目录下的文件都不会被编译
- extends
- 定义被继承的配置文件
- 示例:
- ```json
"extends": "./configs/base"
```
- 上述示例中,当前配置文件中会自动包含config目录下base.json中的所有配置信息
- files
- 指定被编译文件的列表,只有需要编译的文件少时才会用到
- 示例:
- ```json
"files": [
"core.ts",
"sys.ts",
"types.ts",
"scanner.ts",
"parser.ts",
"utilities.ts",
"binder.ts",
"checker.ts",
"tsc.ts"
]
```
- 列表中的文件都会被TS编译器所编译
- compilerOptions
- 编译选项是配置文件中非常重要也比较复杂的配置选项|compilerOptions 编译器的选项
- 在compilerOptions中包含多个子选项,用来完成对编译的配置
- 项目选项
- target
- 设置ts代码编译的目标版本 |target 用来指定ts被编译为的ES的版本
- 可选值:
- ES3(默认)、ES5、ES6/ES2015、ES7/ES2016、ES2017、ES2018、ES2019、ES2020、ESNext|'es3', 'es5', 'es6', 'es2015', 'es2016', 'es2017', 'es2018', 'es2019', 'es2020', 'esnext'
- 示例:
- ```json
"compilerOptions": {
"target": "ES6"
}
```
- 如上设置,我们所编写的ts代码将会被编译为ES6版本的js代码
- lib
- 指定代码运行时所包含的库(宿主环境)
- 可选值:
- ES5、ES6/ES2015、ES7/ES2016、ES2017、ES2018、ES2019、ES2020、ESNext、DOM、WebWorker、ScriptHost ......
- 'es5', 'es6', 'es2015', 'es7', 'es2016', 'es2017', 'es2018'
// 'es2019', 'es2020', 'esnext', 'dom', 'dom.iterable', 'webworker', 'webworker.importscripts', 'webworker.iterable', 'scri
//pthost', 'es2015.core', 'es2015.collection', 'es2015.generator', 'es2015.iterable', 'es2015.promise', 'es2015.proxy', 'es2015.r
//eflect', 'es2015.symbol', 'es2015.symbol.wellknown', 'es2016.array.include', 'es2017.object', 'es2017.sharedmemory', 'es2017.st
//ring', 'es2017.intl', 'es2017.typedarrays', 'es2018.asyncgenerator', 'es2018.asynciterable', 'es2018.intl', 'es2018.promise', '
//es2018.regexp', 'es2019.array', 'es2019.object', 'es2019.string', 'es2019.symbol', 'es2020.bigint', 'es2020.promise', 'es2020.s
//haredmemory', 'es2020.string', 'es2020.symbol.wellknown', 'es2020.intl', 'esnext.array', 'esnext.symbol', 'esnext.asynciterable
//', 'esnext.intl', 'esnext.bigint', 'esnext.string', 'esnext.promise', 'esnext.weakref'
// "lib": ["es6", "dom"]
- 示例:
- ```json
"compilerOptions": {
"target": "ES6",
"lib": ["ES6", "DOM"],
"outDir": "dist",
"outFile": "dist/aa.js"
}
```
- module
- 设置编译后代码使用的模块化系统|指定要使用的模块化的规范
- 可选值:
```
- CommonJS、UMD、AMD、System、ES2020、ESNext、None||'none', 'commonjs', 'amd', 'system', 'umd', 'es6', 'es2015', 'es2020', 'esnext'
```
- 示例:
- ```typescript
"compilerOptions": {
"module": "CommonJS"
}
```
```
```
- outDir
- 编译后文件的所在目录||用来指定编译后文件所在的目录
- 默认情况下,编译后的js文件会和ts文件位于相同的目录,设置outDir后可以改变编译后文件的位置
```
- 示例:
- ```json
"compilerOptions": {
"outDir": "dist"
}
```
```
```
- 设置后编译后的js文件将会生成到dist目录
```
- outFile
```
- 将所有的文件编译为一个js文件
- 默认会将所有的编写在全局作用域中的代码合并为一个js文件,如果module制定了None、System或AMD则会将模块一起合并到文件之中
```
- 示例:
```
- ```json
```
"compilerOptions": {
"outFile": "dist/app.js"
}
```
- rootDir
- 指定代码的根目录,默认情况下编译后文件的目录结构会以最长的公共目录为根目录,通过rootDir可以手动指定根目录
- 示例:
- ```json
"compilerOptions": {
"rootDir": "./src"
}
```
```
```
- allowJs // 是否对js文件进行编译,默认是false
```
- 是否对js文件编译
```
- checkJs //是否检查js代码是否符合语法规范,默认是false
- 是否对js文件进行检查
- 示例:
- ```json
"compilerOptions": {
"allowJs": true,
"checkJs": true
}
```
- removeComments 是否移除注释
```
- 是否删除注释
- 默认值:false
```
- noEmit //不生成编译后的文件
- 不对代码进行编译
- 默认值:false
- sourceMap
- 是否生成sourceMap
- 默认值:false
- 严格检查
- strict //所有严格检查的总开关
- 启用所有的严格检查,默认值为true,设置后相当于开启了所有的严格检查
- alwaysStrict // 用来设置编译后的文件是否使用严格模式,默认false
- 总是以严格模式对代码进行编译
- noImplicitAny //不允许隐式的any类型
- 禁止隐式的any类型
- noImplicitThis //不允许不明确类型的this
- 禁止类型不明确的this
- strictBindCallApply
- 严格检查bind、call和apply的参数列表
- strictFunctionTypes
- 严格检查函数的类型
- strictNullChecks //严格的检查空值
- 严格的空值检查
- strictPropertyInitialization
- 严格检查属性是否初始化
- 额外检查
- noFallthroughCasesInSwitch
- 检查switch语句包含正确的break
- noImplicitReturns
- 检查函数没有隐式的返回值
- noUnusedLocals
- 检查未使用的局部变量
- noUnusedParameters
- 检查未使用的参数
- 高级
- allowUnreachableCode
- 检查不可达代码
- 可选值:
- true,忽略不可达代码
- false,不可达代码将引起错误
- noEmitOnError
- 有错误的情况下不进行编译
- 默认值:false
代码示例:
{
/*
tsconfig.json是ts编译器的配置文件,ts编译器可以根据它的信息来对代码进行编译
"include" 用来指定哪些ts文件需要被编译
路径:** 表示任意目录
\* 表示任意文件
"exclude" 不需要被编译的文件目录
默认值:["node_modules", "bower_components", "jspm_packages"]
*/
"include": [
"./src/**/*"
],
// "exclude": [
// "./src/hello/**/*"
// ]
/*
compilerOptions 编译器的选项
*/
"compilerOptions": {
// target 用来指定ts被编译为的ES的版本
// 'es3', 'es5', 'es6', 'es2015', 'es2016', 'es2017', 'es2018', 'es2019', 'es2020', 'esnext'
"target": "es2015", //一般都是es2015
// module 指定要使用的模块化的规范
// 'none', 'commonjs', 'amd', 'system', 'umd', 'es6', 'es2015', 'es2020', 'esnext'
"module": "es2015",
// lib用来指定项目中要使用的库
//'es5', 'es6', 'es2015', 'es7', 'es2016', 'es2017', 'es2018'
// 'es2019', 'es2020', 'esnext', 'dom', 'dom.iterable', 'webworker', 'webworker.importscripts', 'webworker.iterable', 'scri
//pthost', 'es2015.core', 'es2015.collection', 'es2015.generator', 'es2015.iterable', 'es2015.promise', 'es2015.proxy', 'es2015.r
//eflect', 'es2015.symbol', 'es2015.symbol.wellknown', 'es2016.array.include', 'es2017.object', 'es2017.sharedmemory', 'es2017.st
//ring', 'es2017.intl', 'es2017.typedarrays', 'es2018.asyncgenerator', 'es2018.asynciterable', 'es2018.intl', 'es2018.promise', '
//es2018.regexp', 'es2019.array', 'es2019.object', 'es2019.string', 'es2019.symbol', 'es2020.bigint', 'es2020.promise', 'es2020.s
//haredmemory', 'es2020.string', 'es2020.symbol.wellknown', 'es2020.intl', 'esnext.array', 'esnext.symbol', 'esnext.asynciterable
//', 'esnext.intl', 'esnext.bigint', 'esnext.string', 'esnext.promise', 'esnext.weakref'
// "lib": ["es6", "dom"]
"outDir": "./dist",
// 将代码合并为一个文件
// 设置outFile后,所有的全局作用域中的代码会合并到同一个文件中
//"outFile": "./dist/app.js"
// 是否对js文件进行编译,默认是false
// "allowJs": true,
// 是否检查js代码是否符合语法规范,默认是false
// "checkJs": true,
// 是否移除注释
"removeComments": true,
// 不生成编译后的文件
"noEmit": false,
"noEmitOnError": true,
// 所有严格检查的总开关
"strict": true,
// 用来设置编译后的文件是否使用严格模式,默认false
"alwaysStrict": true,
// 不允许隐式的any类型
"noImplicitAny": true,
// 不允许不明确类型的this
"noImplicitThis": true,
// 严格的检查空值
"strictNullChecks": true
}
4、webpack
-
通常情况下,实际开发中我们都需要使用构建工具对代码进行打包,TS同样也可以结合构建工具一起使用,下边以webpack为例介绍一下如何结合构建工具使用TS。
-
步骤:
-
初始化项目
- 进入项目根目录,执行命令
npm init -y
- 主要作用:创建package.json文件
- 进入项目根目录,执行命令
-
下载构建工具
npm i -D webpack webpack-cli webpack-dev-server typescript ts-loader clean-webpack-plugin
-
共安装了7个包
- webpack
- 构建工具webpack
- webpack-cli
- webpack的命令行工具
- webpack-dev-server
- webpack的开发服务器 //需要在package里面配置 "start": "webpack serve --open --mode development"
- typescript
- ts编译器
- ts-loader
- ts加载器,用于在webpack中编译ts文件
- html-webpack-plugin
- webpack中html插件,用来自动创建html文件
- clean-webpack-plugin
- webpack中的清除插件,每次构建都会先清除目录
-
根目录下创建webpack的配置文件webpack.config.js
const path = require("path"); const HtmlWebpackPlugin = require("html-webpack-plugin"); const { CleanWebpackPlugin } = require("clean-webpack-plugin"); 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"] }, module: { rules: [ { test: /\.ts$/, use: { loader: "ts-loader" }, exclude: /node_modules/ } ] }, plugins: [ //清除插件配置 new CleanWebpackPlugin(), new HtmlWebpackPlugin({ // title:'TS测试' //指定自动打包的网页的标题 template:"./src/index.html" //指定自动打包生成html的模板文件 }), ]
}
-
根目录下创建tsconfig.json,配置可以根据自己需要
-
{ "compilerOptions": { "target": "ES2015", "module": "ES2015", "strict": true } }
-
-
修改package.json添加如下配置
-
{ ...略... "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "build": "webpack", "start": "webpack serve --open chrome.exe" }, ...略... }
-
-
在src下创建ts文件,并在并命令行执行
npm run build
对代码进行编译,或者执行npm start
来启动开发服务器
5、Babel
-
经过一系列的配置,使得TS和webpack已经结合到了一起,除了webpack,开发中还经常需要结合babel来对代码进行转换以使其可以兼容到更多的浏览器,在上述步骤的基础上,通过以下步骤再将babel引入到项目中。
-
安装依赖包:
npm i -D @babel/core @babel/preset-env babel-loader core-js
- 共安装了4个包,分别是:
- @babel/core
- babel的核心工具
- @babel/preset-env
- babel的预定义环境
- @babel-loader
- babel在webpack中的加载器
- core-js
- core-js用来使老版本的浏览器支持新版ES语法
- @babel/core
-
修改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" } ] ] } }, { loader: "ts-loader", } ], exclude: /node_modules/ } ] } ...略...
-
如此一来,使用ts编译后的文件将会再次被babel处理,使得代码可以在大部分浏览器中直接使用,可以在配置选项的targets中指定要兼容的浏览器版本。
-
第二章:面向对象
面向对象是程序中一个非常重要的思想,它被很多同学理解成了一个比较难,比较深奥的问题,其实不然。面向对象很简单,简而言之就是程序之中所有的操作都需要通过对象来完成。
- 举例来说:
- 操作浏览器要使用window对象
- 操作网页要使用document对象
- 操作控制台要使用console对象
一切操作都要通过对象,也就是所谓的面向对象,那么对象到底是什么呢?这就要先说到程序是什么,计算机程序的本质就是对现实事物的抽象,抽象的反义词是具体,比如:照片是对一个具体的人的抽象,汽车模型是对具体汽车的抽象等等。程序也是对事物的抽象,在程序中我们可以表示一个人、一条狗、一把枪、一颗子弹等等所有的事物。一个事物到了程序中就变成了一个对象。
在程序中所有的对象都被分成了两个部分数据和功能,以人为例,人的姓名、性别、年龄、身高、体重等属于数据,人可以说话、走路、吃饭、睡觉这些属于人的功能。数据在对象中被成为属性,而功能就被称为方法。所以简而言之,在程序中一切皆是对象。
1、类(class)
要想面向对象,操作对象,首先便要拥有对象,那么下一个问题就是如何创建对象。要创建对象,必须要先定义类,所谓的类可以理解为对象的模型,程序中可以根据类创建指定类型的对象,举例来说:可以通过Person类来创建人的对象,通过Dog类创建狗的对象,通过Car类来创建汽车的对象,不同的类可以用来创建不同的对象。
-
定义类:
-
class 类名 { 属性名: 类型; constructor(参数: 类型){ this.属性名 = 参数; } 方法名(){ .... } }
-
-
示例:
-
class Person{ name: string; age: number; constructor(name: string, age: number){ this.name = name; this.age = age; } sayHello(){ console.log(`大家好,我是${this.name}`); } }
-
-
使用类:
-
const p = new Person('孙悟空', 18); p.sayHello();
-
代码实例:
class Dog{
// name="旺财";
// age=3;
name:string
age:number
//被成为构造函数
//构造函数会在对象创建时调用
constructor(name:string,age:number){
//在实例的方法中,this就表示当前的实例
// console.log(this);
//可以通过this向新建的对象中添加属性
this.name=name,
this.age=age
}
bark(){
//在方法中可以通过this来表示当前调用方法的对象
//console.log(this);
console.log(this.name)
}
}
const dog=new Dog("小黑",4)
const dog2=new Dog("小白",4)
//console.log(dog);
//console.log(dog2);
dog2.bark();
2、面向对象的特点
-
封装
-
对象实质上就是属性和方法的容器,它的主要作用就是存储属性和方法,这就是所谓的封装
-
默认情况下,对象的属性是可以任意的修改的,为了确保数据的安全性,在TS中可以对属性的权限进行设置
-
只读属性(readonly):
- 如果在声明属性时添加一个readonly,则属性便成了只读属性无法修改
-
TS中属性具有三种修饰符:
- public(默认值),可以在类、子类和对象中修改
- protected ,可以在类、子类中修改
- private ,可以在类中修改
-
示例:
public
class Person{ public name: string; // 写或什么都不写都是public public 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 = '猪八戒';// 可以通过对象修改 ``` protected ```typescript 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 - ```typescript 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 = '猪八戒';// 不能修改 ```
-
属性存取器
- 对于一些不希望被任意修改的属性,可以将其设置为private
- 直接将其设置为private将导致无法再通过对象修改其中的属性
- 我们可以在类中定义一组读取、设置属性的方法,这种对属性读取或设置的属性被称为属性的存取器
- 读取属性的方法叫做setter方法,设置属性的方法叫做getter方法
示例:
```
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属性
属性的封装
//定义一个表示人的类
class Person{
//ts可以在属性前面添加属性的修饰符
/**
* public 修饰的属性可以在任意位置访问(修改) 默认值(包括子类)
* private 私有属性,私有属性只能在类的内部进行访问(修改)
* -通过在类中添加方法使得私有属性可以被外部访问
* protected 受包含的属性,只能在当前类和当前类的子类中使用(修改)
*/
private _name:string;
private _age:number;
constructor(name:string,age:number){
this._name=name;
this._age=age;
}
/**
* getter方法用来获取属性
* setter方法用来设置属性
* --它们被称为属性的存取器
*/
//定义方法,用来获取name属性
// getName(){
// return this._name;
// }
// //定义方法,用来设置name属性
// setName(value:string){
// this._name=value;
// }
// getAge(){
// return this._age;
// }
// setAge(value:number){
// //判断年龄是否合法
// if(value>0){
// this._age=value
// }
// }
//在TS中设置getter方法的方式
get name(){
console.log("get name 执行了!!!")
return this._name;
}
set name(value){
this._name=value;
}
get age(){
return this._age;
}
set age(value){
if(value>0){
this._age=value;
}
}
}
const per=new Person("孙悟空",22);
//console.log(per);
/**
* 现在的属性是在对象中设置的,属性可以任意的被修改
* 属性可以任意被修改将会导致对象中的数据变得分非常不安全
*
*/
// per._name="猪八戒";
// per._age=38;
// per.setName("猪八戒");
// per.setAge(-33)
// console.log(per.getName(),per.getAge());
per.name="猪八戒";
// per.age=33;
per.age=-33; //改不了
console.log(per);
// class C{
// name:string;
// age:number
// constructor(name:string, age:number){
// this.name=name;
// this.age=age;
// }
// } //等价于下面的写法
class C{
//可以将属性直接定义在构造函数里面
constructor(public name:string,public age:number)
}
}
const c=new C("xxx",111);
console.log(c)
-
静态属性
-
静态属性(方法),也称为类属性。使用静态属性无需创建实例,通过类即可直接使用
-
静态属性(方法)使用static开头
-
示例:
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)); ```
-
-
this
- 在类中,使用this表示当前对象
-
继承
-
继承时面向对象中的又一个特性
-
通过继承可以将其他类中的属性和方法引入到当前类中
- 示例:
class Animal{ name: string; age: number; constructor(name: string, age: number){ this.name = name; this.age = age; } } class Dog extends Animal{ bark(){ console.log(`${this.name}在汪汪叫!`); } } const dog = new Dog('旺财', 4); dog.bark(); ```
-
通过继承可以在不修改类的情况下完成对类的扩展
-
重写
-
发生继承时,如果子类中的方法会替换掉父类中的同名方法,这就称为方法的重写
-
示例:
//定义一个Animail类 class Animail{ name:string; age:number; constructor(name:string,age:number){ this.name=name; this.age=age; } sayHello(){ console.log('动物在叫~~'); } } //定义一个表示狗的类 //使Dog继承Animal类 /** * Dog extends Animail * 此时,Animal被称为父类,Dog被称为子类 * 使用继承后,子类将会拥有父类的所有属性和方法 * 通过继承可以将多个类中共有的代写在一个类中,这样只需要写一次可让所有的子类都同时拥有父类中的属性 * 如果希望在子类中添加一些父类中没有的属性或方法直接加就可以 * 如果在子类中添加了和父类相同的方法,则子类方法会覆盖掉父类的方法 * 这种子类覆盖掉父类方法的形式,我们称为重写 */ class Dog extends Animail{ run(){ console.log(`${this.name}在跑`) } sayHello(){ console.log('汪汪汪~~'); } } const dog=new Dog('旺财',5); console.log(dog); dog.sayHello(); dog.run(); //定义一个猫的类 // 使Cat继承Animal类 class Cat extends Animail{ sayHello(){ console.log('喵喵喵~~'); } } const cat=new Cat('咪咪',4); console.log(cat); cat.sayHello();
- 在子类中可以使用super来完成对父类的引用 - 代码实例:
class Animal{ name:string; constructor(name:string){ this.name=name; } sayHello(){ console.log('动物在叫!!'); } } class Dog extends Animal{ //在类的方法中super就表示当前类的父类 // sayHello(){ // super.sayHello(); // } age:number; //如果在子类中写了构造函数,在子类的构造函数中必须对父类的构造函数进行调用 constructor(name:string,age:number){ super(name) //调用父类的构造函数 this.age=age; } } const dog=new Dog('旺财',4); dog.sayHello();
-
-
抽象类(abstract class)
-
抽象类是专门用来被其他类所继承的类,它只能被其他类所继承不能用来创建实例
-
abstract class Animal{ abstract run(): void; bark(){ console.log('动物在叫~'); } } class Dog extends Animals{ run(){ console.log('狗在跑~'); } }
-
使用abstract开头的方法叫做抽象方法,抽象方法没有方法体只能定义在抽象类中,继承抽象类时抽象方法必须要实现
代码实例:
/** \* 以abstract开头的类是抽象类 \* 抽象类和其他类区别不大,只是不是用创建对象 \* 抽象类就是专门用来被继承的类 \* 抽象类可以添加抽象方法 */ abstract class Animal{ name:string; constructor(name:string){ this.name=name; } //定义一个抽象方法 //抽象方法使用abstract开头,没有方法体 //抽象方法只能定义在抽象类中,子类必须对抽象方法进行重写 abstract sayHello():void; } class Dog extends Animal{ sayHello(){ console.log("汪汪汪") } } class Cat extends Animal{ sayHello(): void { console.log("喵喵喵喵"); } } const dog=new Dog('旺财'); dog.sayHello();
-
-
3、接口(Interface)
接口的作用类似于抽象类,不同点在于接口中的所有方法和属性都是没有实值的,换句话说接口中的所有方法都是抽象方法。接口主要负责定义一个类的结构,接口可以去限制一个对象的接口,对象只有包含接口中定义的所有属性和方法时才能匹配接口。同时,可以让一个类去实现接口,实现接口时类中要保护接口中的所有属性。
-
示例(检查对象类型):
-
interface Person{ name: string; sayHello():void; } function fn(per: Person){ per.sayHello(); } fn({name:'孙悟空', sayHello() {console.log(`Hello, 我是 ${this.name}`)}});
-
-
示例(实现)
-
//描述一个对象的类型 type myType={ name:string, age:number }; // const obj:myType{ // name:'sss'; // age:234 // } /** * 接口就是用来定义一个类结构,用来定义一个类中包含那哪些属性和方法 * 同时接口也可以当成类型声明去使用,接口可以重复定义,然后编译会自动的把属性和方法组合在一个接口里面去 * * */ interface myInterface{ name:string; age:number; } const obj:myInterface={ name:"sss", age:22 } /** * 接口可以定义类的时候去限制类的结构 * 接口中的所有的属性都不能有实际值 * 接口只定义对象的结构,而不考虑实际值 * 在接口中所有的方法都是抽象方法 */ interface myInter{ name:string; sayHello():void; } /** * 定义类时,可以使用类去实现一个接口 * 实现接口就是使类满足接口的需求 */ class MyClass implements myInter{ name: string; constructor(name:string){ this.name=name; } sayHello(){ console.log("大家好"); } }
-
4、泛型(Generic)
定义一个函数或类时,有些情况下无法确定其中要使用的具体类型(返回值、参数、属性的类型不能确定),此时泛型便能够发挥作用。
-
举个例子:
-
function test(arg: any): any{ return arg; }
-
上例中,test函数有一个参数类型不确定,但是能确定的时其返回值的类型和参数的类型是相同的,由于类型不确定所以参数和返回值均使用了any,但是很明显这样做是不合适的,首先使用any会关闭TS的类型检查,其次这样设置也不能体现出参数和返回值是相同的类型
-
使用泛型:
-
function test<T>(arg: T): T{ return arg; }
-
这里的
<T>
就是泛型,T是我们给这个类型起的名字(不一定非叫T),设置泛型后即可在函数中使用T来表示该类型。所以泛型其实很好理解,就表示某个类型。 -
那么如何使用上边的函数呢?
-
方式一(直接使用):
-
test(10)
-
使用时可以直接传递参数使用,类型会由TS自动推断出来,但有时编译器无法自动推断时还需要使用下面的方式
-
-
方式二(指定类型):
-
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的子类,不一定非要使用接口类和抽象类同样适用。
-
-
代码实例:
/**
\* 在定义函数或是类的时候,如果遇到类型不明确就可以使用泛型
\*
*/
function fn<T>(a:T):T{
return a;
}
//可以直接调用具有泛型的函数
let result=fn(10); //不指定泛型,TS可以自动对类型进行推断
let result1=fn<string>('hello');
//泛型可以同时指定多个
function fn2<T,K>(a:T,b:K):T{
console.log(b);
return a;
}
fn2<number,string>(123,'hello');
interface Inter{
length:number;
}
//T extends Inter 表示泛型T必须是Inter实现类(子类)
function fn3<T extends Inter>(a:T):number{
return a.length
}
class MyClass<T>{
name:T;
constructor(name:T){
this.name=name;
}
}
const mc=new MyClass<string>('孙悟空');
第三章:综合案例–贪吃蛇
页面设计:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>贪吃蛇</title>
</head>
<body>
<!-- 创建游戏的主容器 -->
<div id="main">
<!-- 设置游戏的舞台 -->
<div id="stage">
<!-- 设置蛇 -->
<div id="snake">
<!-- snake内部的div表示蛇的各部分 -->
<div></div>
</div>
<!-- 设置食物 -->
<div id="food">
<!-- 添加四个小的div来设置食物的样式 -->
<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>
</body>
</html>
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: 40px; //设置圆角
//开启弹性盒模型
display: flex;
//设置主轴的方向
flex-flow: column;
//设置侧轴的对齐方向
align-items: center;
//设置主轴的对齐方向
justify-content: space-around;
//游戏舞台
#stage{
width: 304px;
height: 304px;
border: 2px solid black;
//希望蛇相对于舞台定位
position: relative;
//设置食物
#food{
//开启绝对定位
position: absolute;
left: 40px;
top: 100px;
width: 10px;
height: 10px;
background-color: red;
display: flex; //开启弹性盒
flex-flow: row wrap; //设置横轴为主轴,wrap表示会自动换行
justify-content: space-between; //设置主轴与侧轴的空白分配到元素之间
align-content: space-between; //设置wrap各个行的对齐方式
&>div{
width: 4px;
height: 4px;
background-color: black;
transform: rotate(45deg); //使四个div旋转45度
}
}
//设置蛇的样式
#snake{
&>div{
width: 10px;
height: 10px;
background-color: #000;
border: 1px solid @bg-color;
//开启绝对定位
position: absolute;
}
}
}
//记分牌
#score-panel{
width: 300px;
display: flex;
//设置主轴的对齐方式
justify-content: space-between;
}
}
Food.ts 模块
//定义食物类
class Food{
//定义一个属性表示食物所对应的元素
element:HTMLElement;
constructor(){
//获取页面中的food元素并将其赋值给element
this.element=document.getElementById("food")!; //!非空断言,排除undefined和null的情况
}
//定义一个获取食物x轴的方法
get X(){
return this.element.offsetLeft;
}
//定义一个获取食物y轴的方法
get Y(){
return this.element.offsetTop;
}
//修改食物的位置
change(){
//生成一个随机的位置
//食物的位置最小是0,最大是290
//蛇移动一次就是一格,一格的大小就是10,所有要求食物的坐标必须是整10
let top= Math.round(Math.random()*29)*10;
let left=Math.round(Math.random()*29)*10;
this.element.style.left=left+'px';
this.element.style.top=top+'px';
}
}
//测试代码
// const food=new Food();
// console.log(food.X,food.Y);
// food.change();
// console.log(food.X,food.Y);
export default Food;
scorePanel.ts模块
//定义表示记分牌的类
class ScorePanel{
//score和level用来记录分数和等级
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;
//测试代码
// const scorePanel=new ScorePanel(100,2);
// for(let i=0;i<200;i++){
// scorePanel.addScore();
// }
Snake.ts模块
class Snake{
//表示蛇头的元素
head:HTMLElement;
//蛇的身体(包括蛇头)
bodies:HTMLCollectionOf<HTMLElement>;
//获取蛇的容器
element:HTMLElement;
this.element=document.getElementById('snake')!;
this.head=document.querySelector('#snake > div')!;
this.bodies=this.element.getElementsByTagName('div');
}
//获取蛇的坐标(蛇头坐标)X坐标
get X(){
return this.head.offsetLeft;
}
//获取蛇的y轴坐标
get Y(){
return this.head.offsetTop;
}
//设置蛇头的坐标
set X(value:number){
//如果新值与旧值相同,则直接返回不在修改
if(this.X===value){
return;
}
//X的值的合法范围为0-290之间
if(value<0||value>290){
//进入判断说明蛇撞墙了,抛出一个异常
throw new Error('蛇撞墙了!!!')
}
//修改x时,是在修改水平坐标,蛇在左右移动,蛇在向左移动的时候,不能向右移动,反之亦然
if(this.bodies[1]&&(this.bodies[1] as HTMLElement).offsetLeft===value){
//如果发生了掉头,让蛇向反方向继续移动
if(value>this.X){
//如果新值value大于旧值,则说明蛇向右走,此时发生掉头,应该使蛇继续向左走
value=this.X-10;
}else{
//向左走
value=this.X+10;
}
}
//检查有没有撞到自己
this.checkHeadBody();
}
set Y(value:number){
//如果新值与旧值相同,则直接返回不在修改
if(this.Y===value){
return;
}
//Y的值的合法范围为0-290之间
if(value < 0||value > 290){
//进入判断说明蛇撞墙了
throw new Error('蛇撞墙了!!!')
}
//修改x时,是在修改垂直坐标,蛇在上下移动,蛇在向上移动的时候,不能向下移动,反之亦然
if(this.bodies[1]&&(this.bodies[1] as HTMLElement).offsetTop===value){
//如果发生了掉头,让蛇向反方向继续移动
if(value>this.Y){
//如果新值value大于旧值,则说明蛇向右走,此时发生掉头,应该使蛇继续向左走
value=this.Y-10;
}else{
//向左走
value=this.Y+10;
}
}
this.moveBody(); //移动身体
this.head.style.top=value+"px";
//检查有没有撞到自己
this.checkHeadBody();
}
//蛇增加身体的方法
addBody(){
//向element中添加一个div
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++){
if(this.X===this.bodies[i].offsetLeft&& this.Y===this.bodies[i].offsetTop){
//进入判断说明蛇头撞到了身体,游戏结束
throw new Error("撞到自己了~~~");
}
}
}
}
export default Snake;
GameControl.ts 模块
//引入其他的类
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.scorePanel = new ScorePanel();
this.food = new Food();
this.init();
}
//游戏的初始化方法,调用后游戏即开始
init() {
//绑定键盘按下的事件
document.addEventListener("keydown", this.keydownHandler.bind(this));
//调用run方法,使蛇移动
this.run();
}
/**
\* ArrowUp Up
ArrowDown Down
ArrowRight Left
ArrowLeft Right
\*
*/
//创建一个键盘按下的响应函数
keydownHandler(event: KeyboardEvent) {
//在赋值之前检查event.key的值是否合法(用户是否按了正确的按键)
//修改direction属性
this.direction = event.key;
}
//创建一个控制蛇移动的方法
run() {
/**
\* 根据方向(this.direction)来使蛇的位置改变
\* 向上 top 减少
\* 向下 top 增加
\* 向左 left 减少
\* 向右 left 减少
*/
//获取蛇现在的坐标
let X = this.snake.X;
let Y = this.snake.Y;
//根据按键的方向来修改X值和Y值
switch (this.direction) {
case "ArrowUp":
case "Up": //向上移动 top 减少
Y-=10;
break;
case "ArrowDown":
case "Down":
Y+=10;
break; //向下移动 top值增加
case "ArrowLeft":
case "Left": //向左移动
X-=10;
break;
case "ArrowRight":
case "Right": //向右移动
X+=10;
break;
}
//检查蛇是否吃到了食物
this.checkEat(X,Y);
this.snake.X=X;
this.snake.Y=Y;
} catch (e:any) {
//进入到catch,说明出现了异常,游戏结束,弹出一个提示信息
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;
index.ts模块
import { WebSocketServer } from "webpack-dev-server";
import "./style/index.less"
import GameControl from "./moduls/Gamecontrol";
const gameControl=new GameControl();
tsconfig.json 配置文件:
{
“compilerOptions”: {
“module”:“ES2015”,
“target”:“ES2015”,
“strict”: true,
“noEmitOnError”: true,
“moduleResolution”:“Node”
}
}
package.json 配置文件
{
“name”: “part3”,
“version”: “1.0.0”,
“description”: “”,
“main”: “index.js”,
“scripts”: {
“test”: “echo “Error: no test specified” && exit 1”,
“build”: “webpack --mode development”,
“start”: “webpack serve --open --mode development”
},
“keywords”: [],
“author”: “”,
“license”: “ISC”,
“devDependencies”: {
“@babel/core”: “^7.20.5”,
“@babel/preset-env”: “^7.20.2”,
“babel-loader”: “^9.1.0”,
“clean-webpack-plugin”: “^4.0.0”,
“core-js”: “^3.26.1”,
“css-loader”: “^6.7.2”,
“html-webpack-plugin”: “^4.5.2”,
“less”: “^4.1.3”,
“less-loader”: “^11.1.0”,
“postcss”: “^8.4.19”,
“postcss-loader”: “^7.0.2”,
“postcss-preset-env”: “^7.8.3”,
“style-loader”: “^3.3.1”,
“ts-loader”: “^9.4.2”,
“typescript”: “^4.9.3”,
“webpack”: “^5.75.0”,
“webpack-cli”: “^5.0.0”,
“webpack-dev-server”: “^4.11.1”
},
“dependencies”: {
“source-map”: “^0.7.4”
}
}
webpackconfig.js配置文件在上面的知识点里。
效果图:
知识来源可以学习:
https://www.bilibili.com/video/BV1Xy4y1v7S2/?share_source=copy_web&vd_source=3b378adf67a5c1da50c8a5fd5434a968