第一部分 Typescript基础 一、typescript 安装
全局安装完成后,我们新建一个hello.ts的ts文件
接下来我们在命令行输入tsc hello.ts来编译这个ts文件,然后会在同级目录生成一个编译好了的hello.js文件
那么我们每次都要输tsc hello.ts命令来编译,这样很麻烦,能否让它自动编译?答案是可以的,使用vscode来开发,需要配置一下vscode就可以。
首先我们在命令行执行tsc --init来生成配置文件,然后我们在目录下看到生成了一个tsconfig.json文件
这个json文件里有很多选项
target是选择编译到什么语法
module则是模块类型
outDir则是输出目录,可以指定这个参数到指定目录
更多细节 https://zhongsp.gitbooks.io/typescript-handbook/content/doc/handbook/tsconfig.json.html
接下来我们需要开启监控了,在vscode任务栏中
Typescript在线编辑器
建议使用在线编辑器练习 http://www.typescriptlang.org/play/index.html
二、数据类型
js是弱类型语言,强弱类语言有什么区别呢?typescript最大的优点就是类型检查,可以帮你检查你定义的类型和赋值的类型。
2.1 布尔类型boolean let isFlag = true ;isFlag = "hello swr" ; let isFlag :boolean = true isFlag = "hello swr"
2.2 数字类型number let age :number = 28 ;age = 29 ;
2.3 字符串类型string let name :string = "poetries" name = "iamswr"
以上boolean、number、string类型有个共性,就是可以通过typeof来获取到是什么类型,是基本数据类型
那么复杂的数据类型是怎么处理的呢?
2.4 数组 Array let persion :string[] = ['poetries' , 'jing' ]let persions :Array <string> = ['poetries' , 'jing' ]let persionObject :Array <object> = [{name :'poetries' ,age :22 }]let persionObjects :object[] = [{name :'poetries' ,age :22 }]let arr :Array <number|object|string|boolean> = [22 , 'test' , true , {name :'poetries' }]let arrAny :Array <any> = ['test' ,12 ,false ]
2.5 元组类型tuple
什么是元组类型?其实元组是数组的一种。
有点类似解构赋值,但是又不完全是解构赋值,比如元组类型必须一一对应上
元组类型是一个不可变的数组,长度、类型是不可变的
let per :[string,number,object] = ['poetries' ,22 ,{love : 'coding' }]
2.6 枚举类型enum
什么是枚举?枚举有点类似一一列举,一个一个数出来。一般用于值是某几个固定的值
enum sex { BOY ='男孩' , GIRL ='女孩' } console .log (sex)
var sex;(function (sex ) { sex["BOY" ] = "\u7537\u5B69" ; sex["GIRL" ] = "\u5973\u5B69" ; })(sex || (sex = {})); console .log (sex);
比如我们实际项目中,特别是商城类,订单会存在很多状态流转,那么非常适合用枚举
enum orderStatus { WAIT_FOR_PAY = "待支付" , UNDELIVERED = "完成支付,待发货" , DELIVERED = "已发货" , COMPLETED = "已确认收货" }
到这里,我们会有一个疑虑,为什么我们不这样写呢?
let orderStatus2 = { WAIT_FOR_PAY : "待支付" , ... }
如果我们直接写对象的键值对方式,是可以在外部修改这个值的,而我们通过enum则不能修改定义好的值了
2.7 任意类型 any
any有好处也有坏处,特别是前端,很多时候写类型的时候,几乎分不清楚类型,任意去写,写起来很爽,但是对于后续的重构、迭代等是非常不友好的,会暴露出很多问题,某种程度来说,any类型就是放弃了类型检查了
比如我们有这样一个场景,就是需要获取某一个dom节点
let btn = document .getElementById ('btn' );btn.style .color = "blue" ;
此时我们发现在ts中会报错
因为我们取这个dom节点,有可能取到,也有可能没取到,当没取到的时候,相当于是null,是没有style这个属性的。
那么我们可以给它添加一个类型为any
let btn :any = document .getElementById ('btn' );btn.style .color = "blue" ;
let person :any = "poetries" person = 22
2.8 null undefined类型 let str :(string | number | null | undefined )str = 'poetries' str = 28 str = null str = undefined
2.9 void类型
void表示没有任何类型,一般是定义函数没有返回值
function say (name:string ):void { console .log ('hello:' , name) return undefined ; return } say ('poetries' )function say1 (name:string ):string { return 'ok' }
2.10 never类型
这个用得很少,一般是用于抛出异常
function error (message:string ):never { throw new Error (message) } error ('errorMsg' )
2.11 我们要搞明白any、never、void
any是任意的值
void是不能有任何值
never永远不会有返回值
any比较好理解,就是任何值都可以
let str :any = "hello poetries" str = 28 str = true
void不能有任何值(返回值)
never则不好理解,什么叫永远不会有返回值?
function loop ( ):never { while (true ){ console .log ("陷入死循环啦" ) } } loop ()function parse (str:string ):(never | any){ return JSON .parse (str) } let json = parse ('{"name":"poetries"}' )let json = parse ("iamswr" )
也就是说,当一个函数执行的时候,被抛出异常打断了,导致没有返回值或者该函数是一个死循环,永远没有返回值,这样叫做永远不会有返回值。
实际开发中,是never和联合类型来一起用,比如
function say ( ):(never | string) { return "ok" }
三、函数 3.1 函数定义 function sayHello (name:string ):void { }
3.2 函数参数处理 function sayHello (name:string,age:number ):void { console .log ('hello' , name, age) } sayHello ('poetries' ,22 )function sayHelloToYou (name:string,age?:number ):void { console .log ('hello' , name, age) } sayHelloToYou ('poetries' )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)
3.3 函数重载 function eating (name: string ):void ;function eating (name: number ):void ;function eating (name:any ): void { console .log (name) } eating ("hello poetries" )eating (22 )
四、类 4.1 定义一个类
如何定义一个类?
class Persion { name : string; age :number; constructor (name: string, age: number ){ this .name = name; this .age = age; } say ():string { return 'hello poetries' } } let p = new Persion ('poetries' , 22 )
var Persion = (function ( ) { function Persion (name, age ) { this .name = name; this .age = age; } Persion .prototype .say = function ( ) { return 'hello poetries' ; }; return Persion ; }()); var p = new Persion ('poetries' , 22 );
4.2 类的继承 class Parent { name : string; age : number; constructor (name:string, age: number ){ this .name = name; this .age = age; } say ():string{ return 'hello poetries' } } 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 ('poetries' , 22 , '静观流叶' )console .log (child)
4.3 类的修饰符
public公开的,可以供自己、子类以及其它类访问
protected受保护的,可以供自己、子类访问,但是其他就访问不了
private私有的,只有自己访问,而子类、其他都访问不了
class Parents { 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 pare = new Parents ('poetries' , 22 , 3000 )console .log (pare.name )
4.4 静态属性、静态方法 跟es6差不多
class Person2 { static name1 = 'poetries' static say ( ) { console .log ('hello poetries' ) } } let per2 = new Person2 ()Person2 .say ()
4.5 抽象类
抽象类和方法,有点类似抽取共性出来,但是又不是具体化,比如说,世界上的动物都需要吃东西,那么会把吃东西这个行为,抽象出来
如果子类继承的是一个抽象类,子类必须实现父类里的抽象方法,不然的话不能实例化,会报错
/ 关键字 abstract抽象 abstract class Animal { abstract eat ():void ; } class Dog extends Animal { eat ( ){ console .log ("吃骨头" ) } }
五、接口
这里的接口,主要是一种规范,规范某些类必须遵守规范,和抽象类有点类似,但是不局限于类,还有属性、函数等
5.1 接口规范对象 function getUserInfo (user:{name:string,age:number} ) { console .log (user.name ,user.age ) } getUserInfo ({name : 'poetries' , age : 22 })function getUserInfo1 (user:{name:string,age:number} ){ console .log (`${user.name} ${user.age} ` ) } function getInfo (user:{name:string,age:number} ){ console .log (`${user.name} ${user.age} ` ) } getUserInfo1 ({name :"poetries" ,age :22 })getInfo ({name :"poetries" ,age :22 })interface infoInterface { name : string, age : number; } function getUserInfo2 (user:infoInterface ) { console .log (user.name ,user.age ) } function getInfo2 (user:infoInterface ) { console .log (user.name ,user.age ) } getUserInfo2 ({name :"poetries" ,age :22 })getInfo2 ({name :"poetries" ,age :22 })interface infoInterface2{ name : string; age : number; city?:string; } function getUserInfo3 (user:infoInterface2 ){ console .log (`${user.name} ${user.age} ${user.city} ` ) } function getInfo3 (user:infoInterface ){ console .log (`${user.name} ${user.age} ` ) } getUserInfo3 ({name :"poetries" ,age :22 ,city :"深圳" })getInfo3 ({name :"iamswr" ,age :22 })
5.2 接口规范函数 interface mytotal { (a :number,b :number):number; } let totalSum :mytotal = function (a:number,b:number ):number { return a + b } console .log (totalSum (10 , 20 ))
5.3 接口规范数组 interface userInterface { [index : number]: string; } let arrTest : userInterface = ['poetries' , '静观流叶' ]console .log (arrTest)
5.4 接口规范类
这个比较重要,因为写react的时候会经常使用到类
interface Animal2 { name :string; eat (any :string):void ; } class Person6 implements Animal2 { name : string; constructor (name: string ) { this .name = name; } eat (any :string):void { console .log (`吃` +any) } } interface Animal3 { name : string; eat (any : string):void ; } interface Animal4 { sleep ():void ; } class Person7 implements Animal3 ,Animal4 { name : string; constructor (name:string ){ this .name = name; } eat (any:string ) { console .log (`吃` +any) } sleep ( ) { console .log ('睡觉' ) } }
5.5 接口继承接口 interface Animal5 { name :string; eat (any :string):void ; } interface Animal6 extends Animal5 { sleep ():void ; } class Person8 implements Animal2 { name : string; constructor (name:string ) { this .name = name; } eat (any :string):void { console .log (`吃${any} ` ) } sleep ( ){ console .log ('睡觉' ) } }
六、泛型 6.1 函数的泛型
泛型可以支持不特定的数据类型,什么叫不特定呢?比如我们有一个方法,里面接收参数,但是参数类型我们是不知道,但是这个类型在方法里面很多地方会用到,参数和返回值要保持一致性
function deal<T>(value :T):T{ return value } console .log (deal<string>("poetries" ))console .log (deal<number>(22 ))
实际上,泛型用得还是比较少,主要是看类的泛型是如何使用的
6.2 类的泛型 class MyMath <T> { private arr : T[] = [] add (value: T ) { this .arr .push (value) } } let mymath = new MyMath <number>()mymath.add (1 ) mymath.add (2 ) mymath.add (3 )
有了接口为什么还需要抽象类?
接口里面只能放定义,抽象类里面可以放普通类、普通类的方法、定义抽象的东西。
第二部分 结合React实践 一、环境配置 1.1 初始化项目
生成一个目录ts_react_demo,输入npm init -y初始化项目
然后在项目里我们需要一个.gitignore来忽略指定目录不传到git上
进入.gitignore输入我们需要忽略的目录,一般是node_modules
// .gitignore node_modules
1.2 安装依赖
接下来我们准备下载相应的依赖包,这里需要了解一个概念,就是类型定义文件
1.2.1 类型定义文件
因为目前主流的第三方库都是以javascript编写的,如果用typescript开发,会导致在编译的时候会出现很多找不到类型的提示,那么如果让这些库也能在ts中使用呢?
1.2.2 相关依赖包 React相关
- 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的核心包 - 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相关
- 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中找到
1.3 Typescript config配置
首先我们要生成一个tsconfig.json来告诉ts-loader怎样去编译这个ts代码
会在项目中生成了一个tsconfig.json文件,接下来进入这个文件,来修改相关配置
{ "compilerOptions" : { "target" : "es5" , "module" : "commonjs" , "outDir" : "./dist" , "sourceMap" : true , "noImplicitAny" : true , "jsx" : "react" , }, "include" : [ "./src/**/*" ] }
1.4 webpack配置
在./src/下创建一个index.html文件,并且添加<div id='app'></div>标签
<!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
const webpack = require ("webpack" );const HtmlWebpackPlugin = require ("html-webpack-plugin" );const path = require ("path" )module .exports = { entry :"./src/index.tsx" , mode :"development" , output :{ path :path.resolve (__dirname,'dist' ), filename :"index.js" }, devtool :"source-map" , resolve :{ extensions :[".ts" ,'.tsx' ,'.js' ,'.json' ] }, module :{ rules :[ { test :/\.tsx?$/ , loader :"ts-loader" }, { enforce :"pre" , test :/\.js$/ , loader :"source-map-loader" } ] }, plugins :[ new HtmlWebpackPlugin ({ template :"./src/index.html" , filename :"index.html" }), new webpack.HotModuleReplacementPlugin () ], devServer :{ 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 poetries")
接下来我们npm run build一下,看看能不能正常编译
编译成功,我们可以看看./dist/下生成了index.html index.js index.js.map三个文件
那么我们在开发过程中,不会每次都npm run build来看修改的结果,那么我们平时开发过程中可以使用npm run dev。这样就启动成功了一个http://localhost:8080/的服务了。
接下来我们看看热更新是否配置正常,在./src/index.tsx中新增一个console.log('hello poetries'),我们发现浏览器的控制台会自动打印出这一个输出,说明配置正常了
二、React组件 2.1 写一个计数器组件
首先我们在./src/下创建一个文件夹components,然后在./src/components/下创建文件Counter.tsx
import * as React from "react" ;export default class CounterComponent extends React.Component { 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中把这个组件导进来
import * as React from "react" ;import * as ReactDom from "react-dom" ;import CounterComponent from "./components/Counter" ;ReactDom .render (<CounterComponent /> ,document .getElementById ("app" ))
这样我们就把这个组件引进来了,接下来我们看下是否能够成功跑起来
到目前为止,感觉用ts写react还是跟以前差不多,没什么区别,要记住,ts最大的特点就是类型检查,可以检验属性的状态类型
假设我们需要在./src/index.tsx中给<CounterComponent />传一个属性name,而CounterComponent组件需要对这个传入的name进行类型校验,比如说只允许传字符串
ReactDom.render(<CounterComponent name="poetries" />,document.getElementById("app"))
然后需要在./src/components/Counter.tsx中写一个接口来对这个name进行类型校验
import * as React from "react" ;interface IProps { name :string, } interface IState { number : number } export default class CounterComponent extends React.Component <IProps ,IState >{ state = { number :0 } render ( ){ return ( <div > <p > {this.state.number}</p > <p > {this.props.name}</p > <button onClick ={() => this.setState({number:this.state.number + 1})}>+</button > </div > ) } }
2.2 结合Redux使用 2.2.1 基础使用
上面state中的number就不放在组件里了,我们放到redux中,接下来我们使用redux
首先在./src/创建store目录,然后在./src/store/创建一个文件index.tsx
import { createStore } from "redux" ;import reducers from "./reducers" ;let store = createStore (reducers);export default store;
然后我们需要创建一个reducers,在./src/store/创建一个目录reducers,该目录下再创建一个文件index.tsx。
但是我们还需要对reducers中的函数参数进行类型校验,而且这个类型校验很多地方需要复用,那么我们需要把这个类型校验单独抽离出一个文件。
那么我们需要在./src/下创建一个types目录,该目录下创建一个文件index.tsx
export interface Store { number :number }
回到./src/store/reducers/index.tsx
import { Store } from "../../types/index" let initState :Store = { number :0 }export default function (state:Store=initState,action ) { }
上面这段代码暂时先这样,因为需要用到action,我们现在去配置一下action相关的,首先我们在./src/store下创建一个actions目录,并且在该目录下创建文件counter.tsx
因为配置./src/store/actions/counter.tsx会用到动作类型,而这个动作类型是属于常量,为了更加规范我们的代码,我们在./src/store/下创建一个action-types.tsx,里面写相应常量
export const ADD = "ADD" ;export const SUBTRACT = "SUBTRACT" ;
回到./src/store/actions/counter.tsx
import * as types from "../action-types" ;export default { add ( ){ return { type : types.ADD } }, subtract ( ){ return { type : types.SUBTRACT } } }
我们可以想一下,上面return { type:types.ADD }实际上是返回一个action对象,将来使用的时候,是会传到./src/store/reducers/index.tsx的action中,那么我们怎么定义这个action的结构呢?
import * as types from "../action-types" ;export interface Add { type :typeof types.ADD } export interface Subtract { type :typeof types.SUBTRACT } export type Action = Add | Subtract export default { add ():Add { return { type : types.ADD } }, subtract ():Subtract { return { type : types.SUBTRACT } } }
接着我们回到./store/reducers/index.tsx
经过上面一系列的配置,我们可以给action使用相应的接口约束了并且根据不同的action动作行为来进行不同的处理
import { Store } from "../../types/index" import { Action } from "../actions/counter" import * as types from "../action-types" let initState :Store = { number :0 }export default function (state:Store=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 ; } }
接下来,我们怎么样把组件和仓库建立起关系呢
首先进入./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" ;ReactDom .render (( <Provider store ={store} > <CounterComponent name ="poetries" /> </Provider > ),document .getElementById ("app" ))
我们到组件内部建立连接,./src/components/Counter.tsx
import * as React from "react" ;import { connect } from "react-redux" ;import actions from "../store/actions/counter" ;import { Store } from "../types" ;interface IProps { number :number, name :string, add :any, subtract :any } interface IState { number : number } class CounterComponent extends React.Component <IProps ,IState >{ state = { number :0 } render ( ){ let { number,add,subtract } = this .props return ( <div > <h1 > {this.props.name}</h1 > <button onClick ={add} > +</button > <br /> <button onClick ={subtract} > -</button > <p > {number}</p > </div > ) } } let mapStateToProps = function (state:Store ) { return state } export default connect ( mapStateToProps, actions )(CounterComponent );
这时候看到成功执行了
其实搞来搞去,跟原来的写法差不多,主要就是ts会进行类型检查。
如果对number进行异步修改,该怎么处理?这就需要我们用到redux-thunk
接着我们回到./src/store/index.tsx
import { createStore, applyMiddleware } from "redux" ;import reducers from "./reducers" ;import thunk from "redux-thunk" ;import logger from "redux-logger" ;let store = createStore (reducers, applyMiddleware (thunk,logger));export default store;
接着我们回来./src/store/actions,新增一个异步的动作行为
import * as types from "../action-types" ;export interface Add { type :typeof types.ADD } export interface Subtract { type :typeof types.SUBTRACT } export type Action = Add | Subtract export default { add ():Add { return { type : types.ADD } }, subtract ():Subtract { return { type : types.SUBTRACT } }, addAsync ():any{ return function (dispatch:any,getState:any ) { setTimeout (function ( ){ dispatch ({type :types.ADD }) }, 1000 ); } } }
到./src/components/Counter.tsx组件内,使用这个异步
2.2.2 合并reducers
假如我们的项目里面,有两个计数器,而且它俩是完全没有关系的,状态也是完全独立的,这个时候就需要用到合并reducers了
首先我们新增action的动作行为类型,在./src/store/action-types.tsx
然后修改接口文件,./src/types/index.tsx
然后把./src/store/actions/counter.tsx文件拷贝在当前目录并且修改名称为counter2.tsx
然后把./src/store/reduces/index.tsx拷贝并且改名为counter.tsx和counter2.tsx
我们多个reducer是通过combineReducers方法,进行合并的,因为我们一个项目当中肯定是存在非常多个reducer,所以统一在这里处理。
import { combineReducers } from "redux" ;import counter from "./counter" ;import counter2 from "./counter2" ;let reducers = combineReducers ({ counter, counter2, }); export default reducers;
最后修改组件,进入./src/components/,其中
到目前为止,我们完成了reducers的合并了,那么我们看看效果如何,首先我们给./src/index.tsc添加Counter2组件,这样的目的是与Counter组件完全独立,互不影响,但是又能够最终合并到readucers
2.3 路由 2.3.1 基本用法
首先进入./src/index.tsx导入我们的路由所需要的依赖包
import * as React from "react" ;import * as ReactDom from "react-dom" ;import { Provider } from "react-redux" ;import { BrowserRouter as Router ,Route ,Link } from "react-router-dom" import store from './store' 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" ))
但是有个很大的问题,就是我们直接访问http://localhost:8080/counter会找不到路由
因为我们的是单页面应用,不管路由怎么变更,实际上都是访问index.html这个文件,所以当我们访问根路径的时候,能够正常访问,因为index.html文件就放在这个目录下,但是当我们通过非根路径的路由访问,则出错了,是因为我们在相应的路径没有这个文件,所以出错了
从这一点也可以衍生出一个实战经验,我们平时项目部署上线的时候,会出现这个问题,一般我们都是用nginx来把访问的路径都是指向index.html文件,这样就能够正常访问了。
那么针对目前我们这个情况,我们可以通过修改webpack配置,让路由不管怎么访问,都是指向我们制定的index.html文件。
进入./webpack.config.js,在devServer的配置对象下新增一些配置
... devServer :{ hot :true , contentBase :path.resolve (__dirname,'dist' ), historyApiFallback :{ index :"./index.html" } } ...
修改webpack配置需要重启服务,然后重启服务,看看浏览器能否正常访问http://localhost:8080/counter
2.3.2 同步路由到redux
路由的路径,如何同步到仓库当中。以前是用一个叫react-router-redux的库,把路由和redux结合到一起的,react-router-redux挺好用的,但是这个库不再维护了,被废弃了,所以现在推荐使用connected-react-router这个库,可以把路由状态映射到仓库当中
首先我们在./src下创建文件history.tsx
假设我有一个需求,就是我不通过Link跳转页面,而是通过编程式导航,触发一个动作,然后这个动作会派发出去,而且把路由信息放到redux中,供我以后查看。
我们进入./src/store/reducers/index.tsx
import { combineReducers } from "redux" ;import counter from "./counter" ;import counter2 from "./counter2" ;import { connectRouter } from "connected-react-router" ;import history from "../../history" ;let reducers = combineReducers ({ counter, counter2, router : connectRouter (history) }); export default reducers;
我们进入./src/store/index.tsx来添加中间件
import { createStore, applyMiddleware } from "redux" ;import reducers from "./reducers" ;import thunk from "redux-thunk" ;import logger from "redux-logger" ;import { routerMiddleware } from "connected-react-router" ;import history from "../history" ;let store = createStore (reducers, applyMiddleware (routerMiddleware (history),thunk,logger));export default store;
我们进入./src/store/actions/counter.tsx加个goto方法用来跳转
import * as types from "../action-types" ;import { push } from "connected-react-router" ;export interface Add { type :typeof types.ADD } export interface Subtract { type :typeof types.SUBTRACT } export type Action = Add | Subtract export default { add ():Add { return { type : types.ADD } }, subtract ():Subtract { return { type : types.SUBTRACT } }, addAsync ():any{ return function (dispatch:any,getState:any ) { setTimeout (function ( ){ dispatch ({type :types.ADD }) }, 1000 ); } }, goto (path:string ){ return push (path) } }
我们进入./src/components/Counter.tsx中加个按钮,当我点击按钮的时候,会向仓库派发action,仓库的action里有中间件,会把我们这个请求拦截到,然后跳转