7

基于最新版本 dva+react+typescript 的简单Demo


前言: 本文写给想学 ts 却无从下手, 学了 ts 想融入到 react 开发中却找不到练手demo的新手, 一个 测试鼠标和键盘点击速度的App, 本项目虽然有很多相似版本, 但是不符合各种依赖的版本更替,并且没有 typescript 的实现版本, 所以崩崩觉得还是有必要实现以下这个小东西, 给新手一些参考...... 具体效果如下图: 图片描述

Ps: 这是一个测试鼠标点击速度的 App,记录 1 秒内用户能最多点几次。顶部的 Highest Record 纪录最高速度;中间的是当前速度,给予即时反馈,让用户更有参与感;下方是供点击的按钮。


下面和我一起完成这个小demo吧


1. 项目目录结构

图片描述


2. 安装依赖

npm install -g typescript
npm install --g dva-cli@next     // 最新版本 dva脚手架, 目前为 1.0.0-beta.4

npm install --save-dev @types/react @types/react-dom
npm install --save-dev ts-loader tslint-react

npm install --save antd
npm install --save keymaster   // 键盘事件依赖, 后面会用到

3. 创建项目

dva new 07-dva_calculator_count_example & cd 07-dva_...

4. 配置 typescript, tslint

具体相关配置详见 崩崩 的第一篇文章: 链接描述

5. 定义项目路由

由于 dvaumi 通过 umi-plugin-dva 插件相结合, 使用起来非常方便

而我们的项目路由则约定在 ./src/pages 目录下, 所以新建 example 文件夹 ./src/pages/example
接着测试我们的路由, 在 ./src/pages/example 下新建 page.tsx(./src/pages/example/page.tsx),


6. 测试路由

./src/pages/example 新建page.tsx, 写入测试的react组件
import * as React from 'react';

export interface ICounterProps {
    
};

const Counter: React.SFC<ICounterProps> = (): JSX.Element => {
    return (
        <h1>This is example page.</h1>
    );
};

export default Counter;
接着, 命令行键入 npm start, 在Chrome地址栏接上example, 可以看到结果了, 路由成功!

7. 策划model

  • 首先新建 ./src/utils/delay.tsx, 这是model中的延迟函数, 比较简单
// 定义 Promise 接口
interface IDelayPromise {
    Promise: (resolve?: () => {}, reject?: () => {}) => void;
};

const delay = (time: number): IDelayPromise => {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve();
        }, time);
    });
};

export default delay;

  • 现在正式开始书写我们的 model, model 是一个项目的灵魂,
  • 所以我放在第一步来做...... 新建 ./src/pages/example/models/counter.tsx, 代码如下:
import key from 'keymaster';            // 键盘相关事件

import delay from '../../../utils/delay';     // 延迟函数

export default {
    
    namespace: 'counter',
    
    state: {
        current: 0,        // 即时显示的数字
        record: 0,         // 最高纪录
    },
    
    /*
        回想我们的需求
            1. 鼠标点击, 数字增加, 纪录一秒内最高次数
            2. 一秒后, 数字递减, 变为初始数字
            
        而 reducers 是唯一可以改变 state 的地方, 
        
        这个需求里,我们需要定义两个 reducer,                     
        count/add 和 count/minus ,分别用于计数的增和减。要注意的是 count/add 时 record 的 
        逻辑,他只在有更高的记录时才会被记录, 这个用一句 三元判断即可
    */
    
    reducers: {
        add (state) {
            const newCurrent = state.current + 1;
            return {
                ...state,
                record: newCurrent > state.record ? newCurrent: state.record,
                current: newCurrent,
            };
        },
        
        minus (state) {
            const newCurrent = state.current - 1;
            return {
                ...state,
                current: newCurrent,
            };
        },
    },
    
    /*
        接着, 由于我们要实现, 延迟并返回初始状态,
        所以, 我们在 effects 中定义 addRemote 方法
        
        注: 这里的 call, put, 是大佬封装好的方法, 直接使用即可
               call: 用于调用异步处理函数
               put:  调用 reducers 
    */
    
    effects: {
        *addRemote ({ payload }, { call, put }) {
            yield put({ type: 'add' });
            yield call({ delay, 1000 });    // delay 函数, 
            yield put({ type: 'minus' }); 
        },
    },
    
    
    /*
        最后, 我们可以再加一点功能, 通过订阅键盘来获取键盘敲击次数
        当然, 按键也是随意更改的, 通过 subscriptions
    */
    subscriptions: {
        keyboardWather ({ dispatch }) {
            key('space', () => {
                dispatch({ type: 'addRemote' });
            });
        },
    },
};

OK, 到这里, 我们的 model 部分已经完成, 这是一个项目最重要的部分, 对类似 put, call, subscriptions 这些 api 不太熟悉的可以看一下这里: 链接描述

8. 书写 components

  • 现在开始项目组件的编写, 因为所有的状态都被存到了 dva 中,
  • 所以书写 简单美观函数组件(SFC) 是不二之选

1. 在 ./src/pages/example/page.tsx,这是大的路由组件, 里面包含类似 Header, Nav, Footer, 等类似的 容器组件, 在其内写入如下代码:

    import * as React from 'react';
    
    import Counter from './components/Counter';   // 导入 Counter 组件, 这是整个项目的容器
    
    const styles: NodeRequire = require('./index.less');    // 整个项目的样式文件, 等会儿会一一列出
    
    export interface IAppProps {};
    
    const App: React.SFC<IAppProps> = (): JSX.Element => {
        return (
            <div className={styles['app-container']}>
                <div className={styles['app-content']}>
                    <Counter />
                </div>
            </div>
        );
    };
    
    export default App;
    

2. 新建 ./src/pages/example/index.less,写入样式

    .app-container {}
    
    .app-content {
        box-sizing: border-box;
        overflow: hidden;
        width: 400px;
        height: 400px;
        margin: 50px auto;
        border: 1px solid #ccc;
        box-shadow: 0 0 4px 4px #ddd;
    }
    
    .counterbox {
        display: flex;
        flex-direction: column;
        box-sizing: border-box;
        height: 100%;
        padding: 10px;
    }
    
    .counterbox .counter-show {
        flex: 1;
        line-height: 1.5;
        font-size: 20px;
        color: #666;
    }
    
    .counterbox .counter-currentcount {
        line-height: 100px;
        font-size: 30px;
    }
    
    .counterbox .counter-button {
        flex: 3;
        display: flex;
        justify-content: center;
        align-items: center;
        height: 100%;
        margin-top: 10px;
    }

3. 新建 ./src/pages/example/components/Counter.tsx, 这是整个项目的工作地

import * as React from 'react';

import { connect } from 'dva';            // 将dva中的 state 转化为组件的 props
import { Dispatch } from '../../../node_modules/redux';    // 类型限制, 

const styles: NodeRequire = require('../index.less');

import CounterButton from './CounterButton';    // 这是点击的按钮, 我将它分成了一个单独的组件,


// 定义接口
export interface ICounterProps {
    current: number;    // 即时数字
    record?: number;    // 最高纪录
    dispatch: Dispatch<{type: string, payload?: any}>;    // tip: 这里类型定义Dispatch, vscode 会自动帮我引入 Dispatch, 很智能
};


const Counter: React.SFC<ICounterProps> = (props: ICounterProps): JSX.Element => {
    const { current, record, dispatch } = props;
    
    // 按钮点击事件, 处理函数
    const handleClick: React.ReactEventHandler<HTMLButtonElement> = (event: React.MouseEvent<HTMLButtonElement>): void => {
        console.log(event.currentTarget);        // button Element
        dispatch({
            type: 'counter/addRemote',         // 触发 action
        });
    };
    
    return (
        <div className={styles['counterbox']}>
            <p className={styles['counter-show']}>The highest count is: { record }</p>
            <div className={styles['counter-currentcount']}>
                Current count is: { current }
            </div>
            <div className={styles['counter-button']}>
                <CounterButton onBtnClick={handleClick} />
            </div>
        </div>
    );
};

// 将 state => props
const mapStateToProps = (state): {current: number, record?: number} => {
    const { current, record } = state.counter;
    
    return {
        current,
        record,
    };
};


export default connect(mapStateToProps)(Counter);

3. 书写 Button按钮 组件

import * as React from 'react';

import { Button } from 'antd';


export interface ICounterButtonProps {
    onBtnClick: (event: React.MouseEvent<HTMLButtonElement>) => void;
};


const CounterButton: React.SFC<ICounterButtonProps> = (props: ICounterButtonProps): JSX.Element => {
    return (
        <Button
            type = "primary"
            onClick = {props.onBtnClick}
        >
            Please Click Me 
        </Button>
    );
};

export default CounterButton;

9. 大功告成

保存, 刷新浏览器, 应该就OK啦

崩崩结语

    项目不难, 但是对于 新手 来说, 如何使用 ts 进行类型定义? ,以及懂得如何去梳理脉络, 我觉得是至关重要的.

    对代码有不懂的同学可以下方评论,或者加扣: 1766083035, 深入讨论

自学不易, 100% of the effort to change back to the tears of graduation.


转载请注明出处


崩崩
58 声望3 粉丝

大三老猫