前言:你们好,我叫邵威儒,你们都喜欢喊我小邵,学的金融专业却凭借兴趣爱好入了程序猿的坑,从大学买的第一本vb和自学vb,我就与编程结下不解之缘,随后自学易语言写游戏辅助、交易软件,至今进入了前端领域,看到很多朋友都写文章分享,本身也弄一个玩玩,如下文章纯属我的理解,便于记录学习,确定有理解错误或理解不到位的地方,意在站在前辈的肩膀,分享我的对技术的通俗理解,共同成长!javascript
后续我会陆陆续续更新javascript方面,尽可能把javascript这个学习路径体系都写一下
包括前端所经常使用的es六、angular、react、vue、nodejs、koa、express、公众号等等
都会从浅到深,从入门开始逐步写,但愿能让你们有所收获,也但愿你们关注我~html
文章列表:juejin.im/user/5a84f8…前端
Author: 邵威儒
Email: 166661688@qq.com
Wechat: 166661688
github: github.com/iamswr/vue
在本文我将和你们一块儿玩转Typescript,目前angular、deno已经开始使用typescript,而且咱们熟知的vue,在3.0也即将会使用typescript,能够说,前端领域,typescript会逐渐变为必备的技能,那么,为何typescript变得愈来愈火呢?html5
网上有各类typescript和javascript的对比,那么在个人角度的理解,javascript是解释型(动态)语言,能够说是从上到下执行,在咱们开发过程当中,好比有语法错误等等,须要执行到这一行代码才能知道,而typescript则像写易语言那样生成exe时,须要静态编译,而静态编译这个过程,会把代码都检查一遍,看是否经过检测,最终才生成exe,typescript最终是也是编译成javascript原生代码的,只是在这个生成过程当中,会进行各类检测,来检查代码是否符合语法啊规则啊,符合的话最终再编译成javascript,规范了咱们代码的编写,同时也提升了代码的复用以及组件化,在runtime阶段为咱们提早找到错误。java
typescript支持es5/es6的语法,而且扩展了javascript语法,更像java、c#、swift这种语言了。node
在前端nodejs很火,可是为何在后端却不火,很大程度也是由于nodejs也是解释型(动态)语言,优点就是解释型语言比较灵活,可是缺点也很明显,用node开发后台程序,开发一直爽,重构火葬场=。=!一旦重构了,就会出现不少问题,像Java、c#这类语言,很是严谨,类型检查等很是严谨,而javascript呢,通常是靠咱们用肉眼去排查,很麻烦,typescript就是解决这一类问题的。react
总而言之,typescript是将来的趋势,也是谷歌推荐的框架,我也是刚学typescript,不少都是站在前辈的肩膀总结的,废话很少说,咱们开始进入正题吧!webpack
首先咱们全局安装ios
npm i typescript -g
全局安装完成后,咱们新建一个hello.ts
的ts文件
// hello.ts内容
let a = "邵威儒"
复制代码
接下来咱们在命令行输入tsc hello.ts
来编译这个ts文件,而后会在同级目录生成一个编译好了的hello.js
文件
// hello.js内容
var = "邵威儒"
复制代码
那么咱们每次都要输tsc hello.ts
命令来编译,这样很麻烦,可否让它自动编译?答案是能够的,我平时使用vscode来开发,须要配置一下vscode就能够。
首先咱们在命令行执行tsc --init
来生成配置文件,而后咱们在目录下看到生成了一个tsconfig.json
文件
这个json文件里有不少选项
target
是选择编译到什么语法module
则是模块类型outDir
则是输出目录,能够指定这个参数到指定目录接下来咱们须要开启监控了,在vscode任务栏中
此时就会开启监控了,会监听ts的变化,而后自动去编译。
java、c#是强类型语言,而js是弱类型语言,强弱类语言有什么区别呢?typescript最大的优势就是类型检查,能够帮你检查你定义的类型和赋值的类型。
// 在js中,定义isFlag为true,为布尔类型boolean
let isFlag = true;
// 可是咱们也能够从新给它赋值为字符串
isFlag = "hello swr";
// 在ts中,定义isFlag为true,为布尔类型boolean
// 在变量名后加冒号和类型,如 :boolean
let isFlag:boolean = true
// 从新赋值到字符串类型会报错
isFlag = "hello swr"
// 在java中,通常是这样定义,要写变量名也要写类型名
// int a = 10;
// string name = "邵威儒"
复制代码
let age:number = 28;
age = 29;
复制代码
let name:string = "邵威儒"
name = "iamswr"
复制代码
以上boolean、number、string
类型有个共性,就是能够经过typeof
来获取到是什么类型,是基本数据类型。
那么复杂的数据类型是怎么处理的呢?
// 在js中
let pets = ["旺财","小黑"];
// 在ts中
// 须要注意的是,这个是一个字符串类型的数组
// 只能往里面写字符串,写别的类型会报错
let pets:string[] = ["旺财","小黑"];
// 另一种ts写法
let pets:Array<string> = ["旺财","小黑"];
// 那么若是想在数组里放对象呢?
let pets:Array<object> = [{name:"旺财"},{name:"小黑"}];
// 那么怎样在一个数组中,随意放string、number、boolean类型呢?
// 这里的 | 至关于 或 的意思
let arr:Array<string|number|boolean> = ["hello swr",28];
// 想在数组中听任意类型
let arr:Array<any> = ["hello swr",28,true]
复制代码
什么是元组类型?其实元组是数组的一种。
let person:[string,number] = ['邵威儒',28]
复制代码
有点相似解构赋值,可是又不彻底是解构赋值,好比元组类型必须一一对应上,多了少了或者类型不对都会报错。
元组类型是一个不可变的数组,长度、类型是不可变的。
枚举在java中是从6.0才引入的一种类型,在java和ts中的关键字都是enum
。
什么是枚举?枚举有点相似一一列举,一个一个数出来,在易语言中,咱们会常常枚举窗口,来找到本身想要的,通常用于值是某几个固定的值,好比生肖(有12种)、星座(有12种)、性别(男女)等,这些值是固定的,能够一个一个数出来。
为何咱们要用枚举呢?咱们能够定义一些值,定义完了后能够直接拿来用了,用的时候也不会赋错值。
好比咱们普通赋值
// 咱们给性别赋值一个boy,可是咱们有时候手误,可能输成boy一、boy2了
// 这样就会致使咱们赋值错误了
let sex = "boy"
复制代码
既然这样容易致使手误赋错值,那么咱们能够定义一个枚举
// 定义一个枚举类型的值
enum sex {
BOY,
GIRL
}
console.log(sex)
console.log(`邵威儒是${sex.BOY}`)
复制代码
咱们看看转为es5语法是怎样的
// 转为es5语法
"use strict";
var sex;
(function (sex) {
sex[sex["BOY"] = 0] = "BOY";
sex[sex["GIRL"] = 1] = "GIRL";
})(sex || (sex = {}));
console.log(sex); // 打印输出{ '0': 'BOY', '1': 'GIRL', BOY: 0, GIRL: 1 }
console.log("\u90B5\u5A01\u5112\u662F" + sex.BOY); // 打印输出 邵威儒是0
复制代码
是否是感受有点像给对象添加各类属性,而后这个属性又有点像常量,而后经过对象去取这个属性?
上面这样写,不是很友好,那么咱们还能够给BOY
GIRL
赋值
enum sex{
BOY="男",
GIRL="女"
}
复制代码
// 转化为es5语法
// 咱们顺便看看实现的原理
"use strict";
var sex;
// 首先这里是一个自执行函数
// 而且把sex定义为对象,传参进给自执行函数
// 而后给sex对象添加属性而且赋值
(function (sex) {
sex["BOY"] = "\u7537";
sex["GIRL"] = "\u5973";
})(sex || (sex = {}));
console.log(sex); // 打印输出 { BOY: '男', GIRL: '女' }
console.log("\u90B5\u5A01\u5112\u662F" + sex.BOY); // 打印输出 邵威儒是男
复制代码
好比咱们实际项目中,特别是商城类,订单会存在不少状态流转,那么很是适合用枚举
enum orderStatus {
WAIT_FOR_PAY = "待支付",
UNDELIVERED = "完成支付,待发货",
DELIVERED = "已发货",
COMPLETED = "已确认收货"
}
复制代码
到这里,咱们会有一个疑虑,为何咱们不这样写呢?
let orderStatus2 = {
WAIT_FOR_PAY : "待支付",
...
}
复制代码
若是咱们直接写对象的键值对方式,是能够在外部修改这个值的,而咱们经过enum
则不能修改定义好的值了,更加严谨。
any
有好处也有坏处,特别是前端,不少时候写类型的时候,几乎分不清楚类型,任意去写,写起来很爽,可是对于后续的重构、迭代等是很是不友好的,会暴露出不少问题,某种程度来讲,any类型就是放弃了类型检查了。。。
好比咱们有这样一个场景,就是须要获取某一个dom节点
let btn = document.getElementById('btn');
btn.style.color = "blue";
复制代码
此时咱们发如今ts中会报错
由于咱们取这个dom节点,有可能取到,也有可能没取到,当没取到的时候,至关因而null,是没有style这个属性的。
那么咱们能够给它添加一个类型为any
// 添加一个any类型,此时就不会报错了,可是也至关于放弃了类型检查了
let btn:any = document.getElementById('btn');
btn.style.color = "blue";
// 固然也有粗暴一些的方式,利用 ! 强制断言
let btn = document.getElementById("btn");
btn!.style!.color = "blue";
// 能够赋值任何类型的值
// 跟之前咱们var let声明的如出一辙的
let person:any = "邵威儒"
person = 28
复制代码
这个也没什么好说的,不过能够看下下面的例子
// (string | number | null | undefined) 至关于这几种类型
// 是 string 或 number 或 null 或 undefined
let str:(string | number | null | undefined)
str = "hello swr"
str = 28
str = null
str = undefined
复制代码
void表示没有任何类型,通常是定义函数没有返回值。
// ts写法
function say(name:string):void {
console.log("hello",name)
}
say("swr")
复制代码
// 转为es5
"use strict";
function say(name) {
console.log("hello", name);
}
say("swr");
复制代码
怎么理解叫没有返回值呢?此时咱们给函数return一个值
function say(name:string):void {
console.log("hello",name)
// return "ok" 会报错
return "ok"
// return undefined 不会报错
// return 不会报错
}
say("swr")
复制代码
那么此时咱们但愿这个函数返回一个字符串类型怎么办?
function say(name:string):string {
console.log("hello",name)
return "ok"
}
say("swr")
复制代码
这个用得不多,通常是用于抛出异常。
let xx:never;
function error(message: string): never {
throw new Error(message);
}
error("error")
复制代码
any
比较好理解,就是任何值均可以
let str:any = "hello swr"
str = 28
str = true
复制代码
void
不能有任何值(返回值)
function say():void {
}
复制代码
never
则很差理解,什么叫永远不会有返回值?
// 除了上面举例的抛出异常之外,咱们看一下这个例子
// 这个loop函数,一旦开始执行,就永远不会结束
// 能够看出在while中,是死循环,永远都不会有返回值,包括undefined
function loop():never {
while(true){
console.log("陷入死循环啦")
}
}
loop()
// 包括好比JSON.parse也是使用这种 never | any
function parse(str:string):(never | any){
return JSON.parse(str)
}
// 首先在正常状况下,咱们传一个JSON格式的字符串,是能够正常获得一个JSON对象的
let json = parse('{"name":"邵威儒"}')
// 可是有时候,传进去的不必定是JSON格式的字符串,那么就会抛出异常
// 此时就须要never了
let json = parse("iamswr")
复制代码
也就是说,当一个函数执行的时候,被抛出异常打断了,致使没有返回值或者该函数是一个死循环,永远没有返回值,这样叫作永远不会有返回值。
实际开发中,是never和联合类型来一块儿用,好比
function say():(never | string) {
return "ok"
}
复制代码
函数是这样定义的
function say(name:string):void {
console.log("hello",name)
}
say("邵威儒")
复制代码
形参和实参要彻底同样,如想不同,则须要配置可选参数,可选参数放在后面
// 形参和实参一一对应,彻底同样
function say(name:string,age:number):void {
console.log("hello",name,age)
}
say("邵威儒",28)
复制代码
// 可选参数,用 ? 处理,只能放在后面
function say(name:string,age?:number):void {
console.log("hello",name,age)
}
say("邵威儒")
复制代码
那么如何设置默认参数呢?
// 在js中咱们是这样写的
function ajax(url,method="get"){
console.log(url,method)
}
// 在ts中咱们是这样写的
function ajax(url:string,method:string = "GET") {
console.log(url,method)
}
复制代码
那么如何设置剩余参数呢?能够利用扩展运算符
function sum(...args:Array<number>):number {
return eval(args.join("+"))
}
let total:number = sum(1,2,3,4,5)
console.log(total)
复制代码
那么如何实现函数重载呢?函数重载是java中很是有名的,在java中函数的重载,是指两个或者两个以上的同名函数,参数的个数和类型不同
// 好比说咱们如今有2个同名函数
function say(name:string){
}
function say(name:string,age:number){
}
// 那么我想达到一个效果
// 当我传参数name时,执行name:string这个函数
// 当我传参数name和age时,执行name:string,age:number这个函数
// 此时该怎么办?
复制代码
接下来看一下typescript中的函数重载
// 首先声明两个函数名同样的函数
function say(val: string): void; // 函数的声明
function say(val: number): void; // 函数的声明
// 函数的实现,注意是在这里是有函数体的
// 其实下面的say()不管怎么执行,实际上就是执行下面的函数
function say(val: any):void {
console.log(val)
}
say("hello swr")
say(28)
复制代码
在typescript中主要体现是同一个同名函数提供多个函数类型定义,函数实际上就只有一个,就是拥有函数体那个,若是想根据传入值类型的不同执行不一样逻辑,则须要在这个函数里面进行一个类型判断。
那么这个函数重载有什么做用呢?其实在ts中,函数重载只是用来限制参数的个数和类型,用来检查类型的,并且重载不能拆开几个函数,这一点和java的处理是不同的,须要注意。
// ts写法
// 其实跟es6很是像,没太大的区别
class Person{
// 这里声明的变量,是实例上的属性
name:string
age:number
constructor(name:string,age:number){
// this.name和this.age必须在前面先声明好类型
// name:string age:number
this.name = name
this.age = age
}
// 原型方法
say():string{
return "hello swr"
}
}
let p = new Person("邵威儒",28)
复制代码
// 那么转为es5呢?
"use strict";
var Person = /** @class */ (function () {
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.say = function () {
return "hello swr";
};
return Person;
}());
var p = new Person("邵威儒", 28);
复制代码
// 类的继承和es6也是差很少
class Parent{
// 这里声明的变量,是实例上的属性
name:string
age:number
constructor(name:string,age:number){
// this.name和this.age必须在前面先声明好类型
// name:string age:number
this.name = name
this.age = age
}
// 原型方法
say():string{
return "hello swr"
}
}
class Child extends Parent{
childName:string
constructor(name:string,age:number,childName:string){
super(name,age)
this.childName = childName
}
childSay():string{
return this.childName
}
}
let child = new Child("邵威儒",28,"bb")
console.log(child)
复制代码
public
公开的,能够供本身、子类以及其它类访问protected
受保护的,能够供本身、子类访问,可是其余就访问不了private
私有的,只有本身访问,而子类、其余都访问不了class Parent{
public name:string
protected age:number
private money:number
/**
* 也能够简写为
* constructor(public name:string,protected age:number,private money:number)
*/
constructor(name:string,age:number,money:number){
this.name = name
this.age = age
this.money = money
}
getName():string{
return this.name
}
getAge():number{
return this.age
}
getMoney():number{
return this.money
}
}
let p = new Parent("邵威儒",28,10)
console.log(p.name)
console.log(p.age) // 报错
console.log(p.money) // 报错
复制代码
class Person{
// 这是类的静态属性
static name = "邵威儒"
// 这是类的静态方法,须要经过这个类去调用
static say(){
console.log("hello swr")
}
}
let p = new Person()
Person.say() // hello swr
p.say() // 报错
复制代码
抽象类和方法,有点相似抽取共性出来,可是又不是具体化,好比说,世界上的动物都须要吃东西,那么会把吃东西这个行为,抽象出来。
若是子类继承的是一个抽象类,子类必须实现父类里的抽象方法,否则的话不能实例化,会报错。
// 关键字 abstract 抽象的意思
// 首先定义个抽象类Animal
// Animal类有一个抽象方法eat
abstract class Animal{
// 其实是使用了public修饰符
// 若是添加private修饰符则会报错
abstract eat():void;
}
// 须要注意的是,这个Animal类是不能实例化的
let animal = new Animal() // 报错
// 抽象类的抽象方法,意思就是,须要在继承这个抽象类的子类中
// 实现这个抽象方法,否则会报错
// 报错,由于在子类中没有实现eat抽象方法
class Person extends Animal{
eat1(){
console.log("吃米饭")
}
}
// Dog类继承Animal类后而且实现了抽象方法eat,因此不会报错
class Dog extends Animal{
eat(){
console.log("吃骨头")
}
}
复制代码
这里的接口,主要是一种规范,规范某些类必须遵照规范,和抽象类有点相似,可是不局限于类,还有属性、函数等。
// 假设我须要获取用户信息
// 咱们经过这样的方式,规范必须传name和age的值
function getUserInfo(user:{name:string,age:number}){
console.log(`${user.name} ${user.age}`)
}
getUserInfo({name:"邵威儒",age:28})
复制代码
这样看,仍是挺完美的,那么问题就出现了,若是我另外还有一个方法,也是须要这个规范呢?
function getUserInfo(user:{name:string,age:number}){
console.log(`${user.name} ${user.age}`)
}
function getInfo(user:{name:string,age:number}){
console.log(`${user.name} ${user.age}`)
}
getUserInfo({name:"邵威儒",age:28})
getInfo({name:"iamswr",age:28})
复制代码
能够看出,函数getUserInfo
和getInfo
都遵循同一个规范,那么咱们有办法对这个规范复用吗?
// 首先把须要复用的规范,写到接口中 关键字 interface
interface infoInterface{
name:string,
age:number
}
// 而后把这个接口,替换到咱们须要复用的地方
function getUserInfo(user:infoInterface){
console.log(`${user.name} ${user.age}`)
}
function getInfo(user:infoInterface){
console.log(`${user.name} ${user.age}`)
}
getUserInfo({name:"邵威儒",age:28})
getInfo({name:"iamswr",age:28})
复制代码
那么有些参数可传可不传,该怎么处理呢?
interface infoInterface{
name:string,
age:number,
city?:string // 该参数为可选参数
}
function getUserInfo(user:infoInterface){
console.log(`${user.name} ${user.age} ${user.city}`)
}
function getInfo(user:infoInterface){
console.log(`${user.name} ${user.age}`)
}
getUserInfo({name:"邵威儒",age:28,city:"深圳"})
getInfo({name:"iamswr",age:28})
复制代码
// 对一个函数的参数和返回值进行规范
interface mytotal {
// 左侧是函数的参数,右侧是函数的返回类型
(a:number,b:number) : number
}
let total:mytotal = function (a:number,b:number):number {
return a + b
}
console.log(total(10,20))
复制代码
interface userInterface {
// index为数组的索引,类型是number
// 右边是数组里为字符串的数组成员
[index: number]: string
}
let arr: userInterface = ['邵威儒', 'iamswr'];
console.log(arr);
复制代码
这个比较重要,由于写react的时候会常用到类
// 首先实现一个接口
interface Animal{
// 这个类必须有name
name:string,
// 这个类必须有eat方法
// 规定eat方法的参数类型以及返回值类型
eat(any:string):void
}
// 关键字 implements 实现
// 由于接口是抽象的,须要经过子类去实现它
class Person implements Animal{
name:string
constructor(name:string){
this.name = name
}
eat(any:string):void{
console.log(`吃${any}`)
}
}
复制代码
那么若是想遵循多个接口呢?
interface Animal{
name:string,
eat(any:string):void
}
// 新增一个接口
interface Animal2{
sleep():void
}
// 能够在implements后面经过逗号添加,和java是同样的
// 一个类只能继承一个父类,可是却能遵循多个接口
class Person implements Animal,Animal2{
name:string
constructor(name:string){
this.name = name
}
eat(any:string):void{
console.log(`吃${any}`)
}
sleep(){
console.log('睡觉')
}
}
复制代码
interface Animal{
name:string,
eat(any:string):void
}
// 像类同样,经过extends继承
interface Animal2 extends Animal{
sleep():void
}
// 由于Animal2类继承了Animal
// 因此这里遵循Animal2就至关于把Animal也继承了
class Person implements Animal2{
name:string
constructor(name:string){
this.name = name
}
eat(any:string):void{
console.log(`吃${any}`)
}
sleep(){
console.log('睡觉')
}
}
复制代码
泛型能够支持不特定的数据类型,什么叫不特定呢?好比咱们有一个方法,里面接收参数,可是参数类型咱们是不知道,可是这个类型在方法里面不少地方会用到,参数和返回值要保持一致性
// 假设咱们有一个需求,咱们不知道函数接收什么类型的参数,也不知道返回值的类型
// 而咱们又须要传进去的参数类型和返回值的类型保持一致,那么咱们就须要用到泛型
// <T>的意思是泛型,即generic type
// 能够看出value的类型也为T,返回值的类型也为T
function deal<T>(value:T):T{
return value
}
// 下面的<string>、<number>实际上用的时候再传给上面的<T>
console.log(deal<string>("邵威儒"))
console.log(deal<number>(28))
复制代码
class MyMath<T>{
// 定义一个私有属性
private arr:T[] = []
// 规定传参类型
add(value:T){
this.arr.push(value)
}
// 规定返回值的类型
max():T{
return Math.max.apply(null,this.arr)
}
}
// 这里规定了类型为number
// 至关于把T都替换成number
let mymath = new MyMath<number>()
mymath.add(1)
mymath.add(2)
mymath.add(3)
console.log(mymath.max())
// 假设咱们传个字符串呢?
// 则会报错:类型“"邵威儒"”的参数不能赋给类型“number”的参数。
mymath.add("邵威儒")
复制代码
那么咱们会思考,有了接口为何还须要抽象类?
接口里面只能放定义,抽象类里面能够放普通类、普通类的方法、定义抽象的东西。
好比说,咱们父类有10个方法,其中9个是实现过的方法,有1个是抽象的方法,那么子类继承过来,只须要实现这一个抽象的方法就能够了,可是接口的话,则是全是抽象的,子类都要实现这些方法,简而言之,接口里面不能够放实现,而抽象类能够放实现。
这部分代码我传到了github地址 github.com/iamswr/ts_r… ,你们能够结合来看
咱们用ts来搭建一下ts版的react版全家桶脚手架,接下来这部分须要webpack和react的相关基础,我尽可能把注释写全,最好结合git代码一块儿看或者跟着敲一遍,这样更好理解~
首先,咱们生成一个目录ts_react_demo
,输入npm init -y
初始化项目
而后在项目里咱们须要一个.gitignore
来忽略指定目录不传到git上
进入.gitignore
输入咱们须要忽略的目录,通常是node_modules
// .gitignore
node_modules
复制代码
接下来咱们准备下载相应的依赖包,这里须要了解一个概念,就是类型定义文件
------------------------插入开始-------------------------
由于目前主流的第三方库都是以javascript编写的,若是用typescript开发,会致使在编译的时候会出现不少找不到类型的提示,那么若是让这些库也能在ts中使用呢?
咱们在ios开发的时候,会遇到swift、co混合开发,为了解决两种语法混合开发,是经过一个.h
格式的桥接头来把二者联系起来,在js和ts,也存在这样的概念。
类型定义文件(*.d.ts)
就是可以让编辑器或者插件来检测到第三方库中js的静态类型,这个文件是以.d.ts
结尾。
好比说react
的类型定义文件:github.com/DefinitelyT…
在typescript2.0中,是使用@type来进行类型定义,当咱们使用@type进行类型定义,typescript会默认查看./node_modules/@types
文件夹,能够经过这样来安装这个库的定义库npm install @types/react --save
------------------------插入结束-------------------------
接下来,咱们须要下载相关依赖包,涉及到如下几个包
------------------------安装依赖包开始-------------------------
这部分代码已传到 github.com/iamswr/ts_r… 分支:webpack_done
- react // react的核心文件
- @types/react // 声明文件
- react-dom // react dom的操做包
- @types/react-dom
- react-router-dom // react路由包
- @types/react-router-dom
- react-redux
- @types/react-redux
- redux-thunk // 中间件
- @types/redux-logger
- redux-logger // 中间件
- connected-react-router
复制代码
执行安装依赖包npm i react react-dom @types/react @types/react-dom react-router-dom @types/react-router-dom react-redux @types/react-redux redux-thunk redux-logger @types/redux-logger connected-react-router -S
- webpack // webpack的核心包
- webpack-cli // webapck的工具包
- webpack-dev-server // webpack的开发服务
- html-webpack-plugin // webpack的插件,能够生成index.html文件
复制代码
执行安装依赖包npm i webpack webpack-cli webpack-dev-server html-webpack-plugin -D
,这里的-D
至关于--save-dev
的缩写,下载开发环境的依赖包
- typescript // ts的核心包
- ts-loader // 把ts编译成指定语法好比es5 es6等的工具,有了它,基本不须要babel了,由于它会把咱们的代码编译成es5
- source-map-loader // 用于开发环境中调试ts代码
复制代码
执行安装依赖包npm i typescript ts-loader source-map-loader -D
从上面能够看出,基本都是模块和声明文件都是一对对出现的,有一些不是一对对出现,就是由于都集成到一块儿去了。
声明文件能够在node_modules/@types/xx/xx
中找到。
------------------------安装依赖包结束-------------------------
---------------------Typescript config配置开始----------------------
首先咱们要生成一个tsconfig.json
来告诉ts-loader
怎样去编译这个ts代码
tsc --init
复制代码
会在项目中生成了一个tsconfig.json
文件,接下来进入这个文件,来修改相关配置
// tsconfig.json
{
// 编译选项
"compilerOptions": {
"target": "es5", // 编译成es5语法
"module": "commonjs", // 模块的类型
"outDir": "./dist", // 编译后的文件目录
"sourceMap": true, // 生成sourceMap方便咱们在开发过程当中调试
"noImplicitAny": true, // 每一个变量都要标明类型
"jsx": "react", // jsx的版本,使用这个就不须要额外使用babel了,会编译成React.createElement
},
// 为了加快整个编译过程,咱们指定相应的路径
"include": [
"./src/**/*"
]
}
复制代码
---------------------Typescript config配置结束----------------------
---------------------webpack配置开始----------------------
./src/
下建立一个index.html
文件,而且添加<div id='app'></div>
标签// ./src/index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<div id='app'></div>
</body>
</html>
复制代码
./
下建立一个webpack配置文件webpack.config.js
// ./webpack.config.js
// 引入webpack
const webpack = require("webpack");
// 引入webpack插件 生成index.html文件
const HtmlWebpackPlugin = require("html-webpack-plugin");
const path = require("path")
// 把模块导出
module.exports = {
// 之前是jsx,由于咱们用typescript写,因此这里后缀是tsx
entry:"./src/index.tsx",
// 指定模式为开发模式
mode:"development",
// 输出配置
output:{
// 输出目录为当前目录下的dist目录
path:path.resolve(__dirname,'dist'),
// 输出文件名
filename:"index.js"
},
// 为了方便调试,还要配置一下调试工具
devtool:"source-map",
// 解析路径,查找模块的时候使用
resolve:{
// 通常写模块不会写后缀,在这里配置好相应的后缀,那么当咱们不写后缀时,会按照这个后缀优先查找
extensions:[".ts",'.tsx','.js','.json']
},
// 解析处理模块的转化
module:{
// 遵循的规则
rules:[
{
// 若是这个模块是.ts或者.tsx,则会使用ts-loader把代码转成es5
test:/\.tsx?$/,
loader:"ts-loader"
},
{
// 使用sourcemap调试
// enforce:pre表示这个loader要在别的loader执行前执行
enforce:"pre",
test:/\.js$/,
loader:"source-map-loader"
}
]
},
// 插件的配置
plugins:[
// 这个插件是生成index.html
new HtmlWebpackPlugin({
// 以哪一个文件为模板,模板路径
template:"./src/index.html",
// 编译后的文件名
filename:"index.html"
}),
new webpack.HotModuleReplacementPlugin()
],
// 开发环境服务配置
devServer:{
// 启动热更新,当模块、组件有变化,不会刷新整个页面,而是局部刷新
// 须要和插件webpack.HotModuleReplacementPlugin配合使用
hot:true,
// 静态资源目录
contentBase:path.resolve(__dirname,'dist')
}
}
复制代码
webpack.config.js
呢?这就须要咱们在package.json
配置一下脚本package.json
里的script
,添加build
和dev
的配置{
"name": "ts_react_demo",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"build": "webpack",
"dev":"webpack-dev-server"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"@types/react": "^16.7.13",
"@types/react-dom": "^16.0.11",
"@types/react-redux": "^6.0.10",
"@types/react-router-dom": "^4.3.1",
"connected-react-router": "^5.0.1",
"react": "^16.6.3",
"react-dom": "^16.6.3",
"react-redux": "^6.0.0",
"react-router-dom": "^4.3.1",
"redux-logger": "^3.0.6",
"redux-thunk": "^2.3.0"
},
"devDependencies": {
"html-webpack-plugin": "^3.2.0",
"source-map-loader": "^0.2.4",
"ts-loader": "^5.3.1",
"typescript": "^3.2.1",
"webpack": "^4.27.1",
"webpack-cli": "^3.1.2",
"webpack-dev-server": "^3.1.10"
}
}
复制代码
index.tsx
,那么咱们在./src/
下建立一个index.tsx
,而且在里面写入一段代码,看看webpack是否可以正常编译webpack.config.js
中entry
设置的入口文件是index.tsx
,而且在module
中的rules
会识别到.tsx
格式的文件,而后执行相应的ts-loader
// ./src/index.tsx
console.log("hello swr")
复制代码
npm run build
一下,看看能不能正常编译npm run build
复制代码
嗯,很好,编译成功了,咱们能够看看./dist/
下生成了index.html
index.js
index.js.map
三个文件
那么咱们在开发过程当中,不会每次都npm run build
来看修改的结果,那么咱们平时开发过程当中可使用npm run dev
npm run dev
复制代码
这样就启动成功了一个http://localhost:8080/
的服务了。
接下来咱们修改webpack.config.js
的配置,新增一个devServer
配置对象,代码更新在上面webpack.config.js
中,配置开发环境的服务以及热更新。
接下来咱们看看热更新是否配置正常,在./src/index.tsx
中新增一个console.log('hello 邵威儒')
,咱们发现浏览器的控制台会自动打印出这一个输出,说明配置正常了。
---------------------webpack配置结束----------------------
---------------------计数器组件开始----------------------
这部分代码已传到 github.com/iamswr/ts_r… 分支:CounterComponent_1
接下来咱们开始写react,咱们按照官方文档( redux.js.org/ ),写一个计数器来演示。
首先咱们在./src/
下建立一个文件夹components
,而后在./src/components/
下建立文件Counter.tsx
// ./src/components/Counter.tsx
// import React from "react"; // 以前的写法
// 在ts中引入的写法
import * as React from "react";
export default class CounterComponent extends React.Component{
// 状态state
state = {
number:0
}
render(){
return(
<div>
<p>{this.state.number}</p>
<button onClick={()=>this.setState({number:this.state.number + 1})}>+</button>
</div>
)
}
}
复制代码
咱们发现,其实除了引入import * as React from "react"
之外,其他的和以前的写法没什么不一样。
接下来咱们到./src/index.tsx
中把这个组件导进来
// ./src/index.tsx
import * as React from "react";
import * as ReactDom from "react-dom";
import CounterComponent from "./components/Counter";
// 把咱们的CounterComponent组件渲染到id为app的标签内
ReactDom.render(<CounterComponent />,document.getElementById("app"))
复制代码
这样咱们就把这个组件引进来了,接下来咱们看下是否可以成功跑起来
到目前为止,感受用ts写react仍是跟之前差很少,没什么区别,要记住,ts最大的特色就是类型检查,能够检验属性的状态类型。
假设咱们须要在./src/index.tsx
中给<CounterComponent />
传一个属性name
,而CounterComponent
组件须要对这个传入的name
进行类型校验,好比说只容许传字符串。
./src/index.tsx
中修改一下
ReactDom.render(<CounterComponent name="邵威儒" />,document.getElementById("app"))
复制代码
而后须要在./src/components/Counter.tsx
中写一个接口来对这个name
进行类型校验
// ./src/components/Counter.tsx
// import React from "react"; // 以前的写法
// 在ts中引入的写法
import * as React from "react";
// 写一个接口对name进行类型校验
// 若是咱们不写校验的话,在外部传name进来会报错的
interface IProps{
name:string,
}
// 咱们还能够用接口约束state的状态
interface IState{
number: number
}
// 把接口约束的规则写在这里
// 若是传入的name不符合类型会报错
// 若是state的number属性不符合类型也会报错
export default class CounterComponent extends React.Component<IProps, IState>{
// 状态state
state = {
number:0
}
render(){
return(
<div>
<p>{this.state.number}</p>
<p>{this.props}</p>
<button onClick={()=>this.setState({number:this.state.number + 1})}>+</button>
</div>
)
}
}
复制代码
接下来看看如何在redux中使用ts呢?
---------------------计数器组件结束----------------------
---------------------Redux开始----------------------
这部分代码已传到 github.com/iamswr/ts_r…
分支:redux_thunk
上面state中的number就不放在组件里了,咱们放到redux中,接下来咱们使用redux。
首先在./src/
建立store
目录,而后在./src/store/
建立一个文件index.tsx
// .src/store/index.tsx
import { createStore } from "redux";
// 引入reducers
import reducers from "./reducers";
// 接着建立仓库
let store = createStore(reducers);
// 导出store仓库
export default store;
复制代码
而后咱们须要建立一个reducers
,在./src/store/
建立一个目录reducers
,该目录下再建立一个文件index.tsx
。
可是咱们还须要对reducers
中的函数参数进行类型校验,并且这个类型校验不少地方须要复用,那么咱们须要把这个类型校验单独抽离出一个文件。
那么咱们须要在./src/
下建立一个types
目录,该目录下建立一个文件index.tsx
// ./src/types/index.tsx
// 导出一个接口
export interface Store{
// 咱们须要约束的属性和类型
number:number
}
复制代码
回到./src/store/reducers/index.tsx
// ./src/store/reducers/index.tsx
// 导入类型校验的接口
// 用来约束state的
import { Store } from "../../types/index"
// 咱们须要给number赋予默认值
let initState:Store = { number:0 }
// 把接口写在state:Store
export default function (state:Store=initState,action) {
// 拿到老的状态state和新的状态action
// action是一个动做行为,而这个动做行为,在计数器中是具有 加 或 减 两个功能
}
复制代码
上面这段代码暂时先这样,由于须要用到action
,咱们如今去配置一下action
相关的,首先咱们在./src/store
下建立一个actions
目录,而且在该目录下建立文件counter.tsx
。
由于配置./src/store/actions/counter.tsx
会用到动做类型,而这个动做类型是属于常量,为了更加规范咱们的代码,咱们在./src/store/
下建立一个action-types.tsx
,里面写相应常量
// ./src/store/action-types.tsx
export const ADD = "ADD";
export const SUBTRACT = "SUBTRACT";
复制代码
回到./src/store/actions/counter.tsx
// ./src/store/actions/counter.tsx
import * as types from "../action-types";
export default {
add(){
// 须要返回一个action对象
// type为动做的类型
return { type: types.ADD}
},
subtract(){
// 须要返回一个action对象
// type为动做的类型
return { type: types.SUBTRACT}
}
}
复制代码
咱们能够想一下,上面return { type:types.ADD }
其实是返回一个action对象
,未来使用的时候,是会传到./src/store/reducers/index.tsx
的action
中,那么咱们怎么定义这个action
的结构呢?
// ./src/store/actions/counter.tsx
import * as types from "../action-types";
// 定义两个接口,分别约束add和subtract的type类型
export interface Add{
type:typeof types.ADD
}
export interface Subtract{
type:typeof types.SUBTRACT
}
// 再导出一个type
// type是用来给类型起别名的
// 这个actions里是一个对象,会有不少函数,每一个函数都会返回一个action
// 而 ./store/reducers/index.tsx中的action会是下面某一个函数的返回值
export type Action = Add | Subtract
// 把上面定义好的接口做用于下面
// 约束返回值的类型
export default {
add():Add{
// 须要返回一个action对象
// type为动做的类型
return { type: types.ADD}
},
subtract():Subtract{
// 须要返回一个action对象
// type为动做的类型
return { type: types.SUBTRACT}
}
}
复制代码
接着咱们回到./store/reducers/index.tsx
通过上面一系列的配置,咱们能够给action
使用相应的接口约束了而且根据不一样的action
动做行为来进行不一样的处理
// ./store/reducers/index.tsx
// 导入类型校验的接口
// 用来约束state的
import { Store } from "../../types/index"
// 导入约束action的接口
import { Action } from "../actions/counter"
// 引入action动做行为的常量
import * as types from "../action-types"
// 咱们须要给number赋予默认值
let initState:Store = { number:0 }
// 把接口写在state:Store
export default function (state:Store=initState,action:Action) {
// 拿到老的状态state和新的状态action
// action是一个动做行为,而这个动做行为,在计数器中是具有 加 或 减 两个功能
// 判断action的行为类型
switch (action.type) {
case types.ADD:
// 当action动做行为是ADD的时候,给number加1
return { number:state.number + 1 }
break;
case types.SUBTRACT:
// 当action动做行为是SUBTRACT的时候,给number减1
return { number:state.number - 1 }
break;
default:
// 当没有匹配到则返回本来的state
return state
break;
}
}
复制代码
接下来,咱们怎么样把组件和仓库创建起关系呢?
首先进入./src/index.tsx
// ./src/index.tsx
import * as React from "react";
import * as ReactDom from "react-dom";
// 引入redux这个库的Provider组件
import { Provider } from "react-redux";
// 引入仓库
import store from './store'
import CounterComponent from "./components/Counter";
// 用Provider包裹CounterComponent组件
// 而且把store传给Provider
// 这样Provider能够向它的子组件提供store
ReactDom.render((
<Provider store={store}>
<CounterComponent />
</Provider>
),document.getElementById("app"))
复制代码
咱们到组件内部创建链接,./src/components/Counter.tsx
// ./src/components/Counter.tsx
import * as React from "react";
// 引入connect,让组件和仓库创建链接
import { connect } from "react-redux";
// 引入actions,用于传给connect
import actions from "../store/actions/counter";
// 引入接口约束
import { Store } from "../types";
// 接口约束
interface IProps{
number:number,
// add是一个函数
add:any,
// subtract是一个函数
subtract:any
}
class CounterComponent extends React.Component<IProps>{
render(){
// 利用解构赋值取出
// 这里好比和IProps保持一致,不对应则会报错,由于接口约束了必须这样
let { number,add,subtract } = this.props
return(
<div>
<p>{number}</p><br/>
<button onClick={add}>+</button><br />
<button onClick={subtract}>-</button>
</div>
)
}
}
// 这个connect须要执行两次,第二次须要咱们把这个组件CounterComponent传进去
// connect第一次执行,须要两个参数,
// 须要传给connect的函数
let mapStateToProps = function (state:Store) {
return state
}
export default connect(
mapStateToProps,
actions
)(CounterComponent);
复制代码
接下来咱们看下是否配置成功
成功了,能够经过加减按钮对number
进行控制
其实搞来搞去,跟原来的写法差很少,主要就是ts会进行类型检查。
若是对number
进行异步修改,该怎么处理?这就须要咱们用到redux-thunk
接着咱们回到./src/store/index.tsx
// ./src/store/index.tsx
// 须要使用到thunk,因此引入中间件applyMiddleware
import { createStore, applyMiddleware } from "redux";
// 引入reducers
import reducers from "./reducers";
// 引入redux-thunk,处理异步
// 如今主流处理异步的是saga和thunk
import thunk from "redux-thunk";
// 引入日志
import logger from "redux-logger";
// 接着建立仓库和中间件
let store = createStore(reducers, applyMiddleware(thunk,logger));
// 导出store仓库
export default store;
复制代码
接着咱们回来./src/store/actions
,新增一个异步的动做行为
// ./src/store/actions
import * as types from "../action-types";
// 定义两个接口,分别约束add和subtract的type类型
export interface Add{
type:typeof types.ADD
}
export interface Subtract{
type:typeof types.SUBTRACT
}
// 再导出一个type
// type是用来给类型起别名的
// 这个actions里是一个对象,会有不少函数,每一个函数都会返回一个action
// 而 ./store/reducers/index.tsx中的action会是下面某一个函数的返回值
export type Action = Add | Subtract
// 把上面定义好的接口做用于下面
// 约束返回值的类型
export default {
add():Add{
// 须要返回一个action对象
// type为动做的类型
return { type: types.ADD}
},
subtract():Subtract{
// 须要返回一个action对象
// type为动做的类型
return { type: types.SUBTRACT}
},
// 一秒后才执行这个行为
addAsync():any{
return function (dispatch:any,getState:any) {
setTimeout(function(){
// 当1秒事后,会执行dispatch,派发出去,而后改变仓库的状态
dispatch({type:types.ADD})
}, 1000);
}
}
}
复制代码
到./src/components/Counter.tsx
组件内,使用这个异步
// ./src/components/Counter.tsx
import * as React from "react";
// 引入connect,让组件和仓库创建链接
import { connect } from "react-redux";
// 引入actions,用于传给connect
import actions from "../store/actions/counter";
// 引入接口约束
import { Store } from "../types";
// 接口约束
interface IProps{
number:number,
// add是一个函数
add:any,
// subtract是一个函数
subtract:any,
addAsync:any
}
class CounterComponent extends React.Component<IProps>{
render(){
// 利用解构赋值取出
// 这里好比和IProps保持一致,不对应则会报错,由于接口约束了必须这样
let { number, add, subtract, addAsync } = this.props
return(
<div>
<p>{number}</p><br/>
<button onClick={add}>+</button><br/>
<button onClick={subtract}>-</button><br/>
<button onClick={addAsync}>异步加1</button>
</div>
)
}
}
// 这个connect须要执行两次,第二次须要咱们把这个组件CounterComponent传进去
// connect第一次执行,须要两个参数,
// 须要传给connect的函数
let mapStateToProps = function (state:Store) {
return state
}
export default connect(
mapStateToProps,
actions
)(CounterComponent);
复制代码
接下来到浏览器看看可否成功
完美~ 可以正常执行
---------------------Redux结束----------------------
---------------------合并reducers开始----------------------
这部分代码已传到 github.com/iamswr/ts_r…
分支:reducers_combineReducers
假如咱们的项目里面,有两个计数器,并且它俩是彻底没有关系的,状态也是彻底独立的,这个时候就须要用到合并reducers了。
下面这些步骤,其实有时间的话能够本身实现一次,由于在实现的过程当中,你会发现,由于有了ts的类型校验,咱们在修改的过程当中,会给咱们很是明确的报错,而不像之前,写一段,运行一下,再看看哪里报错,而ts是直接在编辑器中就提示报错了,让咱们能够很是舒服地去根据报错和提示,去相应的地方修改。
首先咱们把涉及到计数器组件的代码拷贝两份,由于改动太多了,能够在git上看,改动后的目录如图
首先咱们新增action
的动做行为类型,在./src/store/action-types.tsx
export const ADD = "ADD";
export const SUBTRACT = "SUBTRACT";
// 新增做为Counter2.tsx中的actions动做行为类型
export const ADD2 = "ADD2";
export const SUBTRACT2 = "SUBTRACT2";
复制代码
而后修改接口文件,./src/types/index.tsx
// ./src/types/index.tsx
// 把Counter/Counter2组件汇总到一块儿
export interface Store {
counter: Counter,
counter2: Counter2
}
// 分别对应Counter组件
export interface Counter {
number: number
}
// 分别对应Counter2组件
export interface Counter2 {
number: number
}
复制代码
而后把./src/store/actions/counter.tsx
文件拷贝在当前目录而且修更名称为counter2.tsx
// ./src/store/actions/counter2.tsx
import * as types from "../action-types";
export interface Add{
type:typeof types.ADD2
}
export interface Subtract{
type:typeof types.SUBTRACT2
}
export type Action = Add | Subtract
export default {
add():Add{
return { type: types.ADD2}
},
subtract():Subtract{
return { type: types.SUBTRACT2}
},
addAsync():any{
return function (dispatch:any,getState:any) {
setTimeout(function(){
dispatch({type:types.ADD2})
}, 1000);
}
}
}
复制代码
而后把./src/store/reduces/index.tsx
拷贝而且更名为counter.tsx
和counter2.tsx
counter.tsx
import { Counter } from "../../types"
import { Action } from "../actions/counter"
import * as types from "../action-types"
let initState: Counter = { number:0 }
export default function (state: Counter=initState,action:Action) {
switch (action.type) {
case types.ADD:
return { number:state.number + 1 }
break;
case types.SUBTRACT:
return { number:state.number - 1 }
break;
default:
return state
break;
}
}
复制代码
counter2.tsx
import { Counter2 } from "../../types"
import { Action } from "../actions/counter2"
import * as types from "../action-types"
let initState:Counter2 = { number:0 }
export default function (state:Counter2=initState,action:Action) {
switch (action.type) {
case types.ADD2:
return { number:state.number + 1 }
break;
case types.SUBTRACT2:
return { number:state.number - 1 }
break;
default:
return state
break;
}
}
复制代码
index.tsc
combineReducers
方法,进行合并的,由于咱们一个项目当中确定是存在很是多个reducer,因此统一在这里处理。// 引入合并方法
import { combineReducers } from "redux";
// 引入须要合并的reducer
import counter from "./counter";
// 引入须要合并的reducer
import counter2 from "./counter2";
// 合并
let reducers = combineReducers({
counter,
counter2,
});
export default reducers;
复制代码
最后修改组件,进入./src/components/
,其中
// ./src/components/Counter.tsx
import * as React from "react";
import { connect } from "react-redux";
import actions from "../store/actions/counter";
import { Store, Counter } from "../types";
interface IProps{
number:number,
add:any,
subtract:any,
addAsync:any
}
class CounterComponent extends React.Component<IProps>{
render(){
let { number, add, subtract, addAsync } = this.props
return(
<div>
<p>{number}</p><br/>
<button onClick={add}>+</button><br/>
<button onClick={subtract}>-</button><br/>
<button onClick={addAsync}>异步加1</button>
</div>
)
}
}
let mapStateToProps = function (state: Store): Counter {
return state.counter;
}
export default connect(
mapStateToProps,
actions
)(CounterComponent);
复制代码
// ./src/components/Counter2.tsx
import * as React from "react";
// 引入connect,让组件和仓库创建链接
import { connect } from "react-redux";
// 引入actions,用于传给connect
import actions from "../store/actions/counter2";
// 引入接口约束
import { Store, Counter2 } from "../types";
// 接口约束
interface IProps{
number:number,
// add是一个函数
add:any,
// subtract是一个函数
subtract:any,
addAsync:any
}
class CounterComponent1 extends React.Component<IProps>{
render(){
// 利用解构赋值取出
// 这里好比和IProps保持一致,不对应则会报错,由于接口约束了必须这样
let { number, add, subtract, addAsync } = this.props
return(
<div>
<p>{number}</p><br/>
<button onClick={add}>+</button><br/>
<button onClick={subtract}>-</button><br/>
<button onClick={addAsync}>异步加1</button>
</div>
)
}
}
// 这个connect须要执行两次,第二次须要咱们把这个组件CounterComponent传进去
// connect第一次执行,须要两个参数,
// 须要传给connect的函数
let mapStateToProps = function (state: Store): Counter2 {
return state.counter2;
}
export default connect(
mapStateToProps,
actions
)(CounterComponent1);
复制代码
到目前为止,咱们完成了reducers的合并了,那么咱们看看效果如何,首先咱们给./src/index.tsc
添加Counter2
组件,这样的目的是与Counter
组件彻底独立,互不影响,可是又可以最终合并到readucers
// ./src/index.tsx
import * as React from "react";
import * as ReactDom from "react-dom";
import { Provider } from "react-redux";
import store from './store'
import CounterComponent from "./components/Counter";
import CounterComponent2 from "./components/Counter2";
ReactDom.render((
<Provider store={store}>
<CounterComponent />
<br/>
<CounterComponent2 />
</Provider>
),document.getElementById("app"))
复制代码
而后到浏览器看看效果~
完美,这样咱们就处理完reducers的合并了,在这个过程当中,经过ts的类型检测,我再也不像之前那样,写一段代码,运行看看是否报错,再定位错误,而是根据ts在编辑器的报错信息,直接定位,修改,把错误扼杀在摇篮。
---------------------合并reducers结束----------------------
---------------------路由开始----------------------
这部分代码已传到 github.com/iamswr/ts_r…
分支:HashRouter
首先进入./src/index.tsx
导入咱们的路由所须要的依赖包
// ./src/index.tsx
import * as React from "react";
import * as ReactDom from "react-dom";
import { Provider } from "react-redux";
import store from './store'
// 引入路由
// 路由的容器:HashRouter as Router
// 路由的规格:Route
// Link组件
import { HashRouter as Router,Route,Link } from "react-router-dom"
import CounterComponent from "./components/Counter";
import CounterComponent2 from "./components/Counter2";
import Counter from "./components/Counter";
function Home() {
return <div>home</div>
}
ReactDom.render((
<Provider store={store}>
{/* 路由组件 */}
<Router>
{/* 放两个路由规则须要在外层套个React.Fragment */}
<React.Fragment>
{/* 增长导航 */}
<ul>
<li><Link to="/">Home</Link></li>
<li><Link to="/counter">Counter</Link></li>
<li><Link to="/counter2">Counter2</Link></li>
</ul>
{/* 当路径为 / 时是home组件 */}
{/* 为了不home组件一直渲染,咱们能够添加属性exact */}
<Route exact path="/" component={Home}/>
<Route path="/counter" component={CounterComponent}/>
<Route path="/counter2" component={CounterComponent2} />
</React.Fragment>
</Router>
</Provider>
),document.getElementById("app"))
复制代码
接下来看看路由是否配置成功
完美,成功了,也能够看出Counter
Counter2
组件是互相独立的。
可是咱们发现了一个问题,http://localhost:8080/#/counter
中有个#
的符号,很是不美观,那么咱们如何变成http://localhost:8080/counter
这样呢?
这部分代码已传到 github.com/iamswr/ts_r…
分支:connected-react-router
咱们仍是进入./src/index.tsx
,
把import { HashRouter as Router,Route,Link } from "react-router-dom"
中的HashRouter
更改成BrowserRouter
再从浏览器访问http://localhost:8080/
再跳转到http://localhost:8080/counter
发现仍是很完美
可是有个很大的问题,就是咱们直接访问http://localhost:8080/counter
会找不到路由
这是怎么回事?由于咱们的是单页面应用,无论路由怎么变动,实际上都是访问index.html
这个文件,因此当咱们访问根路径的时候,可以正常访问,由于index.html
文件就放在这个目录下,可是当咱们经过非根路径的路由访问,则出错了,是由于咱们在相应的路径没有这个文件,因此出错了。
从这一点也能够衍生出一个实战经验,咱们平时项目部署上线的时候,会出现这个问题,通常咱们都是用ngxin
来把访问的路径都是指向index.html
文件,这样就可以正常访问了。
那么针对目前咱们这个状况,咱们能够经过修改webpack
配置,让路由无论怎么访问,都是指向咱们制定的index.html
文件。
进入./webpack.config.js
,在devServer
的配置对象下新增一些配置
// ./webpack.config.js
...
// 开发环境服务配置
devServer:{
// 启动热更新,当模块、组件有变化,不会刷新整个页面,而是局部刷新
// 须要和插件webpack.HotModuleReplacementPlugin配合使用
hot:true,
// 静态资源目录
contentBase:path.resolve(__dirname,'dist'),
// 无论访问什么路径,都重定向到index.html
historyApiFallback:{
index:"./index.html"
}
}
...
复制代码
修改webpack
配置须要重启服务,而后重启服务,看看浏览器可否正常访问http://localhost:8080/counter
完美,无论访问什么路径,都可以正常重定向到index.html
了
接下来,完美这个路由的路径,如何同步到仓库当中呢?
之前是用一个叫react-router-redux
的库,把路由和redux
结合到一块儿的,react-router-redux
挺好用的,可是这个库再也不维护了,被废弃了,因此如今推荐使用connected-react-router
这个库,能够把路由状态映射到仓库当中。
首先咱们在./src
下建立文件history.tsx
,
// ./src/history.tsx
// 引入一个基于html5 api的history的createBrowserHistory
import { createBrowserHistory } from "history";
// 建立一个history
let history = createBrowserHistory();
// 导出
export default history;
复制代码
假设我有一个需求,就是我不经过Link
跳转页面,而是经过编程式导航,触发一个动做,而后这个动做会派发出去,并且把路由信息放到redux中,供我之后查看。
咱们进入./src/store/reducers/index.tsx
// ./src/store/reducers/index.tsx
import { combineReducers } from "redux";
import counter from "./counter";
import counter2 from "./counter2";
// 引入connectRouter
import { connectRouter } from "connected-react-router";
import history from "../../history";
let reducers = combineReducers({
counter,
counter2,
// 把history传到connectRouter函数中
router: connectRouter(history)
});
export default reducers;
复制代码
咱们进入./src/store/index.tsx
来添加中间件
// ./src/store/index.tsx
复制代码
咱们进入./src/store/actions/counter.tsx
加个goto
方法用来跳转。
// ./src/store/actions/counter.tsx
复制代码
咱们进入./src/components/Counter.tsx
中加个按钮,当我点击按钮的时候,会向仓库派发action,仓库的action里有中间件,会把咱们这个请求拦截到,而后跳转。
// ./src/components/Counter.tsx
import * as React from "react";
import { connect } from "react-redux";
import actions from "../store/actions/counter";
import { Store, Counter } from "../types";
interface IProps{
number:number,
add:any,
subtract:any,
addAsync:any,
goto:any
}
class CounterComponent extends React.Component<IProps>{
render(){
let { number, add, subtract, addAsync,goto } = this.props
return(
<div>
<p>{number}</p><br/>
<button onClick={add}>+</button><br/>
<button onClick={subtract}>-</button><br/>
<button onClick={addAsync}>异步加1</button>
{/* 增长一个按钮,而且点击的时候执行goto方法实现跳转 */}
<button onClick={()=>goto('/counter2')}>跳转到/counter2</button>
</div>
)
}
}
let mapStateToProps = function (state: Store): Counter {
return state.counter;
}
export default connect(
mapStateToProps,
actions
)(CounterComponent);
复制代码
---------------------路由结束----------------------
到此为止,用typesript把react全家桶简单过了一遍,之因此写typesript版react全家桶,是为了让你们知道这个typesript在实际项目中,是怎么使用的,可是涉及到各个文件跳来跳去,有时候很简单的几句话能够带过,可是为了你们明白,写得也啰里啰嗦的,刚开始使用typesript,感受效率也没怎么提升,可是在慢慢使用当中,会发现,确实不少错误,可以提早帮咱们发现,这对之后项目的维护、重构显得很是重要,不然未来项目大了,哪里出现错误了,估计也须要排查很是久的时间,typesript未来或许会成为趋势,做为前端,总要不断学习的嘛