This page was saved using WebZIP 7.0.3.1030 offline browser on 12/02/19 14:55:01.
Address: http://www.zhufengpeixun.cn/ahead/html/65.3.typescript.html
Title: 珠峰架构师成长计划  •  Size: 78322  •  Last Modified: Sun, 01 Dec 2019 11:37:09 GMT

1. 初始化项目 #

mkdir react-typesript
cd react-typesript
cnpm init -y
touch .gitignore  //mac或linux
type nul > .gitignore  //window

2.安装依赖 #

cnpm i react react-dom @types/react @types/react-dom react-router-dom @types/react-router-dom react-transition-group @types/react-transition-group react-swipe @types/react-swipe  -S
cnpm i webpack webpack-cli webpack-dev-server html-webpack-plugin -D
cnpm i typescript ts-loader source-map-loader -D
cnpm i redux react-redux @types/react-redux redux-thunk  redux-logger @types/redux-logger -S
cnpm i connected-react-router -S
包名 作用
react @types/react react核心库
react-dom @types/react-dom react操作DOM库
react-router-dom @types/react-router-dom react路由库
react-transition-group @types/react-transition-group react动画库
react-swipe @types/react-swipe react轮播图组件库
webpack webpack核心库
webpack-cli webpack命令行文件
webpack-dev-server webpack开发服务器
html-webpack-plugin webpack用于生成html的插件
redux 全局状态管理库
react-redux @types/react-redux 连接react和redux的库
redux-thunk 可以让store派发一个函数的中间件
redux-logger @types/redux-logger 可以在状态改变前后打印状态的中间件
typescript JavaScript语言扩展
ts-loader 可以让Webpack使用TypeScript的标准配置文件tsconfig.json编译TypeScript代码
source-map-loader 使用任意来自Typescript的sourcemap输出,以此通知webpack何时生成自己的sourcemaps,这让你在调试最终生成的文件时就好像在调试TypeScript源码一样

3.支持typescript #

需要生成一个tsconfig.json文件来告诉ts-loader如何编译代码TypeScript代码

tsc --init
{
  "compilerOptions": {
    "outDir": "./dist",
    "sourceMap": true,
    "noImplicitAny": true,
    "module": "commonjs",
    "target": "es5",
    "jsx": "react"
  },
  "include": [
    "./src/**/*"
  ]
}

4.编写webpack配置文件 #

webpack.config.js

const webpack=require('webpack');
const HtmlWebpackPlugin=require('html-webpack-plugin');
const path=require('path');
module.exports={
    mode: 'development',
    entry: "./src/index.tsx",
    output: {
        filename: "bundle.js",
        path: path.join(__dirname,'dist')
    },
    devtool: "source-map",
    devServer: {
        hot: true,
        contentBase: path.join(__dirname,'dist'),
        historyApiFallback: {
            index:'./index.html'
        }
    },
    resolve: {
        extensions: [".ts", ".tsx", ".js", ".json"]
    },

    module: {
        rules: [{
                test: /\.tsx?$/,
                loader: "ts-loader"
            },
            {
                enforce: "pre",
                test: /\.tsx$/,
                loader: "source-map-loader"
            }
        ]
    },

    plugins: [
        new HtmlWebpackPlugin({
            template:'./src/index.html'
        }),
        new webpack.HotModuleReplacementPlugin()
    ],
};

5.计数器组件 #

5.1 src/index.tsx #

import * as React from 'react';
import * as ReactDOM from 'react-dom';
import Counter from './components/Counter';
ReactDOM.render((
    <Counter number={100}/>
),document.getElementById('root'));

5.2 components/Counter.tsx #

src/components/Counter.tsx

import * as React from 'react';
export interface Props{
    number: number
}
export default class Counter extends React.Component<Props>{
    render() {
        const {number}=this.props;
        return (
            <div>
                <p>{number}</p>
            </div>
        )
    }
}

5.3 TodoInput #

src\components\TodoInput.tsx

import * as React from 'react';
export interface Props{
    number?: number
}
interface State {
    value:string
}
export default class TodoInput extends React.Component<Props,State>{
    public static defaultProps:Props = {
        number:0
    }
    public state = {value:''}
    private handleChange = (event:React.ChangeEvent<HTMLInputElement>)=>{
        this.setState({value:event.target.value});
    }
    public render() {
        return (
            <input value={this.state.value} onChange={(event)=>this.handleChange(event)}/>
        )
    }
}

5.4 函数式组件 #

src\components\TodoItem.tsx

import * as React from 'react';
const todoItemStyle:React.CSSProperties = {
    color:'red',
    backgroundColor:'green'
}
interface Props {
    content:string;
}
const TodoItem:React.SFC<Props> = (props:Props)=>(
    <li style={todoItemStyle}>{props.content}</li>
)
TodoItem.defaultProps;

export default TodoItem;

6. 使用redux #

6.1 src/index.tsx #

src/index.tsx

import * as React from 'react';
import * as ReactDOM from 'react-dom';
import Counter from './components/Counter';
import { Provider } from 'react-redux';
import store from './store';
ReactDOM.render((
    <Provider store={store}>
        <Counter/>
    </Provider>
),document.getElementById('root'));

6.2 components/Counter.tsx #

src/components/Counter.tsx

import * as React from 'react';
import { connect } from 'react-redux';
import { Store } from '../store/types';
import * as actions from '../store/actions';
export interface Props{
    number: number,
    increment: any,
    decrement: any
}
class Counter extends React.Component<Props>{
    render() {
        const {number,increment,decrement}=this.props;
        return (
            <div>
                <p>{number}</p>
                <button onClick={increment}>+</button>
                <button onClick={decrement}>-</button>
            </div>
        )
    }
}

let mapStateToProps=function (state:Store):Store {
    return state;
}
export default connect(mapStateToProps,actions)(Counter);

6.3 src/store/index.tsx #

src/store/index.tsx

import {createStore } from 'redux'
import reducers from './reducers';
let store=createStore(reducers);
export default store;

6.4 store/action-types.tsx #

src/store/action-types.tsx

export const INCREMENT='INCREMENT';
export const DECREMENT='DECREMENT';

6.5 reducers/index.tsx #

src/store/reducers/index.tsx

import * as types from '../action-types';
import { Store } from '../types';
import {Action} from '../actions';
export default function (state: Store={ number: 0 },action: Action): Store {
    switch (action.type) {
        case types.INCREMENT:
            return {...state,number:state.number+1};
        case types.DECREMENT:
            return {...state,number:state.number-1};
        default:
            return state;
    }
}

6.6 actions/index.tsx #

src/store/actions/index.tsx

import {INCREMENT,DECREMENT} from '../action-types';
export interface Increment{
    type:typeof INCREMENT
}
export interface Decrement{
    type:typeof DECREMENT
}
export type Action=Increment|Decrement;

export function increment(): Increment {
    return { type: INCREMENT };
}
export function decrement():Decrement {
    return { type: DECREMENT };
}

6.7 types/index.tsx #

src/store/types/index.tsx

export interface Store{
    number: number
}

7. 合并reducers #

7.1 src/index.tsx #

import * as React from 'react';
import * as ReactDOM from 'react-dom';
import Counter1 from './components/Counter1';
import Counter2 from './components/Counter2';
import { Provider } from 'react-redux';
import store from './store';
ReactDOM.render((
    <Provider store={store}>
        <React.Fragment>
          <Counter1/>
          <Counter2/>
        </React.Fragment>
    </Provider>
),document.getElementById('root'));

7.2 /action-types.tsx #

src/store/action-types.tsx

export const INCREMENT='INCREMENT';
export const DECREMENT='DECREMENT';

export const INCREMENT1='INCREMENT1';
export const DECREMENT1='DECREMENT1';

export const INCREMENT2='INCREMENT2';
export const DECREMENT2='DECREMENT2';

7.3 reducers/index.tsx #

src/store/reducers/index.tsx

import counter1 from './counter1';
import counter2 from './counter2';
import { combineReducers } from 'redux';
let reducers = {
    counter1,
    counter2
};
type ReducersType = typeof reducers;
type RootState = {
    [key in keyof typeof reducers]:ReturnType<typeof reducers[key]>
}
export {RootState}

export default combineReducers(reducers);

7.4 types/index.tsx #

src/store/types/index.tsx

export interface Store{
    counter1: Counter1,
    counter2: Counter2
}
export interface Counter1{
    number: number
}
export interface Counter2{
    number: number
}

7.5 components/Counter1.tsx #

src/components/Counter1.tsx

import * as React from 'react';
import { connect } from 'react-redux';
import {RootState} from '../store/reducers/index';
import * as types from '../store/types';
import * as actions from '../store/actions/counter1';
type StateProps = ReturnType<typeof mapStateToProps>;
type DispatchProps =typeof actions;
type Props = StateProps & DispatchProps;

/* export interface Props{
    number: number,
    increment1: any,
    decrement1: any
} */
class Counter1 extends React.Component<Props>{
    render() {
        const {number,increment1,decrement1}=this.props;
        return (
            <div>
                <p>{number}</p>
                <button onClick={increment1}>+</button>
         <button onClick={()=>incrementAmount(5)}>+5</button>
                <button onClick={decrement1}>-</button>
            </div>
        )
    }
}

let mapStateToProps=function (state:RootState):types.Counter1 {
    return state.counter1;
}

export default connect(mapStateToProps,actions)(Counter1);

7.6 src/components/Counter2.tsx #

src/components/Counter2.tsx

import * as React from 'react';
import { connect } from 'react-redux';
import * as types from '../store/types';
import * as actions from '../store/actions/counter2';
export interface Props{
    number: number,
    increment2: any,
    decrement2: any
}
class Counter2 extends React.Component<Props>{
    render() {
        const {number,increment2,decrement2}=this.props;
        return (
            <div>
                <p>{number}</p>
                <button onClick={increment2}>+</button>
                <button onClick={decrement2}>-</button>
            </div>
        )
    }
}

let mapStateToProps=function (state:types.Store):types.Counter2 {
    return state.counter2;
}

export default connect(mapStateToProps,actions)(Counter2);

7.7 actions/counter1.tsx #

src/store/actions/counter1.tsx

import {INCREMENT1,DECREMENT1} from '../action-types';
export interface Increment1{
    type:typeof INCREMENT1
}
export interface IncrementAmount{
    type:typeof INCREMENT_AMOUNT,
    payload:number
}
export interface Decrement1{
    type:typeof DECREMENT1
}
export type Action=Increment1|Decrement1;

export function increment1(): Increment1 {
    return { type: INCREMENT1 };
}
export function incrementAmount(amount:number):IncrementAmount {
    return { type: INCREMENT_AMOUNT,payload:amount };
}
export function decrement1():Decrement1 {
    return { type: DECREMENT1 };
}

7.8 actions/counter2.tsx #

src/store/actions/counter2.tsx

import {INCREMENT2,DECREMENT2} from '../action-types';
export interface Increment2{
    type:typeof INCREMENT2
}
export interface Decrement2{
    type:typeof DECREMENT2
}
export type Action=Increment2|Decrement2;

export function increment2(): Increment2 {
    return { type: INCREMENT2 };
}
export function decrement2():Decrement2 {
    return { type: DECREMENT2 };
}

7.9 reducers/counter1.tsx #

src/store/reducers/counter1.tsx

import * as types from '../action-types';
import { Counter1 } from '../types';
import {Action} from '../actions/counter1';
export default function (state: Counter1={ number: 0 },action: Action): Counter1 {
    switch (action.type) {
        case types.INCREMENT1:
            return {...state,number:state.number+1};
        case types.DECREMENT1:
            return {...state,number:state.number-1};
        default:
            return state;
    }
}

7.10 reducers/counter2.tsx #

src/store/reducers/counter2.tsx

import * as types from '../action-types';
import { Counter2 } from '../types';
import {Action} from '../actions/counter2';
export default function (state: Counter2={ number: 0 },action: Action): Counter2 {
    switch (action.type) {
        case types.INCREMENT2:
            return {...state,number:state.number+1};
        case types.DECREMENT2:
            return {...state,number:state.number-1};
        default:
            return state;
    }
}

8.配置路由 #

8.1 src/index.tsx #

import * as React from 'react';
import * as ReactDOM from 'react-dom';
import Counter1 from './components/Counter1';
import Counter2 from './components/Counter2';
import { Provider } from 'react-redux';
import store from './store';
import {BrowserRouter as Router, Route,Link } from 'react-router-dom';
ReactDOM.render((
    <Provider store={store}>
        <Router >
            <React.Fragment>
                <Link to="/counter1">counter1</Link>
                <Link to="/counter2">counter2</Link>
                <Route path="/counter1" component={Counter1} />
                  <Route path="/counter2" component={Counter2}/>
         </React.Fragment>
        </Router>

    </Provider>
),document.getElementById('root'));

9. connected-react-router #

9.1 components/Counter1.tsx #

src/components/Counter1.tsx

import * as React from 'react';
import { connect } from 'react-redux';
import * as types from '../store/types';
import * as actions from '../store/actions/counter1';
export interface Props{
    number: number,
    increment1: any,
    decrement1: any,
    goCounter2: any
}
class Counter1 extends React.Component<Props>{
    render() {
        const {number,increment1,decrement1,goCounter2}=this.props;
        return (
            <div>
                <p>{number}</p>
                <button onClick={increment1}>+</button>
                <button onClick={decrement1}>-</button>
                <button onClick={goCounter2}>goCounter2</button>
            </div>
        )
    }
}

let mapStateToProps=function (state:types.Store):types.Counter1 {
    return state.counter1;
}

export default connect(mapStateToProps,actions)(Counter1);

9.2 src/index.tsx #

src/index.tsx

import * as React from 'react';
import * as ReactDOM from 'react-dom';
import Counter1 from './components/Counter1';
import Counter2 from './components/Counter2';
import { Provider } from 'react-redux';
import store from './store';
import {Route,Link } from 'react-router-dom';
import { ConnectedRouter } from 'connected-react-router'
import history from './store/history';
ReactDOM.render((
    <Provider store={store}>
        <ConnectedRouter history={history}>
            <React.Fragment>
                <Link to="/counter1">counter1</Link>
                <Link to="/counter2">counter2</Link>
            <Route path="/counter1" component={Counter1} />
              <Route path="/counter2" component={Counter2}/>
         </React.Fragment>
        </ConnectedRouter>
    </Provider>
),document.getElementById('root'));

9.3 actions/counter1.tsx #

src/store/actions/counter1.tsx

import {INCREMENT1,DECREMENT1} from '../action-types';
import { push } from 'connected-react-router';

export interface Increment1{
    type:typeof INCREMENT1
}
export interface Decrement1{
    type:typeof DECREMENT1
}
export type Action=Increment1|Decrement1;

export function increment1(): any {
    return function (dispatch:any,getState:any) {
        setTimeout(function () {
            dispatch({
                type:INCREMENT1
            })
        },1000);
    }
}
export function decrement1():Decrement1 {
    return { type: DECREMENT1 };
}
export function goCounter2():any {
    return push('/counter2');
}

9.4 src/store/index.tsx #

src/store/index.tsx

import {createStore,applyMiddleware} from 'redux'
import reducers from './reducers';
import { routerMiddleware } from 'connected-react-router'
import history from './history';
import thunk from 'redux-thunk';
import logger from 'redux-logger';
let router = routerMiddleware(history);
let store=createStore(reducers,applyMiddleware(router,thunk,logger));
export default store;

9.5 reducers/index.tsx #

src/store/reducers/index.tsx

import counter1 from './counter1';
import counter2 from './counter2';
import { combineReducers } from 'redux';
import history from '../history';
import { connectRouter } from 'connected-react-router'
let reducers=combineReducers({
    counter1,
    counter2,
    router: connectRouter(history)
});
export default reducers;

9.6 store/history.tsx #

src/store/history.tsx

import {createBrowserHistory} from 'history'
const history=createBrowserHistory()
export default history;

10. 编写单元测试 #

10.1 安装单元测试模块 #

cnpm i jest @types/jest ts-jest enzyme @types/enzyme enzyme-adapter-react-16 @types/enzyme-adapter-react-16 -D

10.2 配置package.json #

"scripts": {
    "dev": "webpack-dev-server",
    "test": "jest"
  },
  "jest": {
    "moduleFileExtensions": [
      "js",
      "ts",
      "tsx"
    ],
    "transform": {
      "^.+\\.tsx?$": "ts-jest"
    },
    "testMatch": [
      "<rootDir>/test/**/*.(spec|test).tsx"
    ]
  }

10.3 index.test.tsx #

test\index.test.tsx

test('测试运行', () => {
   expect(1+1).toBe(2);
})

10.4 counter.spec.tsx #

test\counter.spec.tsx

import * as React from 'react';
import { shallow, configure } from 'enzyme'
import * as  Adapter from 'enzyme-adapter-react-16'
import Counter from '../src/components/Counter';
configure({ adapter: new Adapter() })
test('Jest-React-TypeScript运行', () => {
    const renderer = shallow(<Counter name="计数器"/>);
  expect(renderer.text()).toContain('计数器');
})