umi实现todoList

最近新项目用的umi框架,看完官网api后,打算运用umi+antd pro+dva上手实现个简单的todoList练习一下

在网上找到了一个效果示例


src=http___img-blog.csdnimg.cn_20190118101803600.gif&refer=http___img-blog.csdnimg.gif

搭建项目:

前置条件:需要安装node环境及node版本10.13以上
查看node版本:
node -v

先找个地方建个空目录
mkdir myProject-umi&& cd myProject-umi
通过官方工具创建项目
npx @umijs/create-umi-app

项目创建成功,目录结构如下


image.png

package.json

{
  "private": true,
  "scripts": {
    "start": "umi dev",
    "build": "umi build",
    "postinstall": "umi generate tmp",
    "prettier": "prettier --write '**/*.{js,jsx,tsx,ts,less,md,json}'",
    "test": "umi-test",
    "test:coverage": "umi-test --coverage"
  },
  "gitHooks": {
    "pre-commit": "lint-staged"
  },
  "lint-staged": {
    "*.{js,jsx,less,md,json}": [
      "prettier --write"
    ],
    "*.ts?(x)": [
      "prettier --parser=typescript --write"
    ]
  },
  "dependencies": {
    "@ant-design/pro-layout": "^6.5.0",
    "react": "17.x",
    "react-dom": "17.x",
    "umi": "^3.5.20"
  },
  "devDependencies": {
    "@types/react": "^17.0.0",
    "@types/react-dom": "^17.0.0",
    "@umijs/preset-react": "1.x",
    "@umijs/test": "^3.5.20",
    "lint-staged": "^10.0.7",
    "prettier": "^2.2.0",
    "typescript": "^4.1.2",
    "yorkie": "^2.0.0"
  }
}

这里umi是3.x版本,umi3.x官方文档介绍 (umijs.org),默认的脚手架内置了 @umijs/preset-react,包含布局、权限、国际化、dva、简易数据流等常用功能。比如想要 ant-design-pro 的布局,编辑 .umirc.ts 配置 layout: {},并且需要安装 @ant-design/pro-layout。

默认生成的.umirc.ts配置文件

import { defineConfig } from 'umi';

export default defineConfig({
  nodeModulesTransform: {
    type: 'none',
  },
  routes: [
    { path: '/', component: '@/pages/index' },
  ],
  fastRefresh: {},
});

安装依赖:npm install
启动项目:npm start
项目启动后会自动在src下 生成临时文件.umi文件夹,不要提交 .umi 目录到 git 仓库(一般在生成项目时间gitignore文件已经加上了.umi),他们会在 umi dev 和 umi build 时被删除并重新生成。

在浏览器中输入地址打开网页,可以看到默认的page页面


image.png

如果想要antd pro的布局,可以在.umirc.ts中配置layout:{},或新建layout文件夹下的自定义布局

考虑到实现一个todoList 需要状态管理 国际化等,目录结构改造如下

├── package.json
├── .umirc.ts //umi 配置,同 config/config.js,二选一,umi内置功能的配置和插件的配置,由于项目比较简单这里选用.umirc.ts
├── mock //mock数据
  └── todoList.ts
└── src
  ├── .umi //自动生成的临时文件
  ├── components //公共组件
    └── TodoList.tsx
  ├── pages // 所有路由组件在这里
    └── todoList //todoList页面
      ├── index.less
      └── index.tsx
  ├── model //状态管理
    └── model.ts
  ├── locales //国际化
    ├── en-US.ts
    └── zn-CN.ts
  ├── service //请求服务
    └── index.ts

配置

.umirc.ts文件需要配置的项

  • 页面路由:配置routes,格式为路由信息的数组。
{ 
  name: 'dashboard',
  icon: 'dashboard',
  hideInMenu: true,
  hideChildrenInMenu: true,
  hideInBreadcrumb: true,
  authority: ['admin'], 
}
  • name : 当前路由在菜单和面包屑中的名称,注意这里是国际化配置的 key ,具体展示菜单名可以在 /src/locales/zh-CN.js 进行配置。

  • icon : 当前路由在菜单下的图标名。

  • hideInMenu : 当前路由在菜单中不展现,默认 false

  • hideChildrenInMenu : 当前路由的子级在菜单中不展现,默认 false

  • hideInBreadcrumb : 当前路由在面包屑中不展现,默认 false

  • authority : 允许展示的权限,不设则都可见,详见:权限管理

  • 路由用按需引入的方式,配置dynamicImport项

  • 热更新:fastRefresh

  • 国际化配置:locale
      default默认语言
      antd是否支持国际化

  • layout:antd pro布局

// .umirc.ts
import { defineConfig } from 'umi';

export default defineConfig({
  nodeModulesTransform: {
    type: 'none',
  },
  layout:{}, //antdpro布局
  routes: [
    {
        name: 'todo列表', //菜单名称
        path: '/',
        component:'./todoList' 
    }
  ], //路由
  fastRefresh: {}, //热更新
  locale: {
    default: 'zh-CN',
    antd: true,
  }, //国际化
  dynamicImport: {
  } //动态引入
});

mock

todoList主要的操作就是增删改查,
所以这里加了两个接口,查询list的接口,和更新list数据的接口(增删改都调用这个接口),并用setTimeout来模拟异步请求

  • getTodoList:请求初始todolist数据
  • updateTodoList:更新todolist数据
// mock/mock.ts
import { Response, Request } from "umi";

export default {
    'GET /api/getTodoList': (req: Request, res: Response) => {
        setTimeout(() => {
            res.send({
                status: 'ok',
                code: '200',
                data: [
                    {content: '初始任务1', status: '0', key: '初始任务1'},
                    {content: '初始任务2', status: '1', key: '初始任务2'}
                ]
            })
        }, 2000)
    },
    'POST /api/updateTodoList': (req: Request, res: Response) => {
        setTimeout(() => {
            res.send({
                status: 'ok',
                code: '200',
            })
        }, 2000)
    }
}

locales

//locales/en-US.ts
export default {
    'placeholder':'what to do',
    'addTask': 'add task',
    'delete': 'delete',
    'allTask': 'all task',
    'completedTask': 'completed task',
    'uncompletedTask': 'uncompleted task'
};

//locales/zh-CN.ts
export default {
    'placeholder':"你想做点什么",
    'addTask': '添加任务',
    'delete': '删除',
    'allTask': '所有任务',
    'completedTask': '已完成任务',
    'uncompletedTask': '待办任务'
};

model

model 作状态管理,包含

  • namespace: 表示在全局 state 上的 key
  • state:状态数据
  • reducers :管理同步方法,必须是纯函数
  • effects :管理异步操作,采用了 generator 的相关概念
  • subscriptions:订阅数据源

在 umi 中会按照约定的目录来注册 model,且文件名会被识别为 model 的 namespace

model 还分为 src/models/.js 目录下的全局 model,和 src/pages/**/models/.js 下的页面 model

这里model文件建在src目录下,作为全局的model

//models/models.ts
import type {Reducer, Effect} from 'umi';
import {getTodoList, updateTodoList} from '@/services/index'
export interface IList {
    content: string;
    status: string;
    key: string;
}
export type ModelState = {
    list: IList[];
}
export interface PropsFromDva {
    data: ModelState;
} 
export type ModelType = {
    namespace: string;
    state: ModelState;
    reducers: {
        changeTaskList: Reducer<ModelState>;
    };
    effects: {
        updateTodoList: Effect,
        getTodoList: Effect
    }
}

const Model: ModelType = {
    namespace: 'data',
    state: {
        list: []
    },
    reducers: {
        changeTaskList(state, {payload}) {
            return {
                ...state,
                list: payload
            }
        }
    },
    effects: {
        *getTodoList(_,{call, put}) {
            const response = yield call(getTodoList);
            yield put({
                type: 'changeTaskList',
                payload: response.data
            })
        },
        *updateTodoList({payload},{call, put}){
            const response = yield call(updateTodoList,payload)
            return response;
        }
    }
}

export default Model;

service

//services/index.ts
import {request} from "umi";

export function updateTodoList(params:any) {
    return request(`/api/updateTodoList`, {
        method: 'post',
        params
    })
}

export function getTodoList() {
    return request(`/api/getTodoList`, {
        method: 'get',
    })
}

page

为了在做增删改操作时,三个tab同步更新数据,一个方法就是将数据存储在model,页面从state中拿到最新数据

我们知道可以运用两种方式关联state和view
1、connect:需要用 dva 或 umi 中导出 connect 方法,然后将 model 绑定到组件,mapStateToProps,在props中拿到state
2、dva 2.6x 之后,提供的hook: useSelector、useDispatch

这里使用hook的形式,useDispatch useSelector

// pages/todoList/index.tsx
import styles from './index.less';
import TaskList from '@/components/TodoList';
import { useSelector, useDispatch } from 'dva';
import { PropsFromDva } from '@/models/model';
import { Tabs } from 'antd';
import { useEffect } from 'react';
import { useIntl } from 'umi';
const { TabPane } = Tabs;

const TodoList = () => {
    const dispatch = useDispatch();
    const intl = useIntl();
    const { list } = useSelector((state:PropsFromDva) => state.data);
    const completedList = list.filter(item => item.status === '1');
    const uncompletedList = list.filter(item => item.status === '0');
    const selectedKey = completedList.map(item => item.key);
    const tabList = [
        {tab: intl.formatMessage({id: 'allTask'}), key: '1', list: list},
        {tab: intl.formatMessage({id: 'completedTask'}), key: '2', list: uncompletedList},
        {tab: intl.formatMessage({id: 'uncompletedTask'}), key: '3', list: completedList},
    ];

    useEffect(() => {
        dispatch({type: 'data/getTodoList'});
    }, [])

    return (
        <div className={styles.main}>
            <div className={styles.page}>
                <Tabs defaultActiveKey='1' className={styles.tab}>
                    {tabList.map(item => 
                        <TabPane tab={item.tab} key={item.key}>
                            <TaskList list={item.list} selectedKey={selectedKey} />
                        </TabPane>
                    )}
                </Tabs>
            </div>
        </div>
    );
};

export default TodoList;

component

list组件 包含增删改操作

// components/TodoList/index.tsx
import styles from './index.less';
import { useState } from 'react';
import { Button, Checkbox, Input } from 'antd';
import { useDispatch } from 'dva';
import { IList } from '@/models/model';
import { useIntl } from 'umi';
const CheckboxGroup = Checkbox.Group;

interface IProps {
    list: IList[];
    selectedKey: string[];
}
interface IResponse {
    status: string;
    code: string;
}
const TaskList = (props: IProps) => {
    const dispatch = useDispatch();
    const intl = useIntl();
    const {list, selectedKey} = props;
    const [taskContent, setTaskContent] = useState<string>('');

    const updateTodoList= async (list: IList[]) => {
        const res: IResponse = await dispatch({
            type: 'data/updateTodoList',
            payload: list
        })
    }
    const deleteItem = (key: string) => {
        const newList = list.filter((item: IList) => item.key!==key);
        updateTodoList(newList);
        dispatch({
            type: 'data/changeTaskList',
            payload: newList
        });
    };
    
    const onChangeStatus = (checkedValues:any) => {
        const newList:any[] = [];
        list.forEach((item: IList) => {
            if(checkedValues.includes(item.key)) {
                newList.push({...item, status: '1'});
            }
            else{
                newList.push({...item, status: '0'});
            }
        })
        updateTodoList(newList);
        dispatch({
            type: 'data/changeTaskList',
            payload: newList
        });
    };

    const addItem = () => {
        const newList = [...list, {content: taskContent, status: '0', key: taskContent}];
        updateTodoList(newList);
        dispatch({
            type: 'data/changeTaskList',
            payload: newList
        });
        setTaskContent('');
    };

    return (
        <>
            <CheckboxGroup
                onChange={onChangeStatus}
                value={selectedKey}
            >
                {list?.map((item: IList) => (
                    <div key={item.key} className={styles.row}>
                        <Checkbox value={item.key} className={styles.checkbox}>
                            {item.content}
                        </Checkbox>
                        <Button
                            className={styles.btn}
                            type='text'
                            onClick={() => deleteItem(item.key)}
                        >
                            {intl.formatMessage({id: 'delete'})}
                        </Button>
                    </div>
                ))}
            </CheckboxGroup>
            <div className={styles.add_box}>
                <Input
                    className={styles.add_box_input}
                    value={taskContent}
                    placeholder={intl.formatMessage({id:'placeholder'})}
                    onChange={e => setTaskContent(e.target.value)}
                />
                <Button
                    className={styles.add_box_btn}
                    type="primary"
                    onClick={() => addItem()}
                >
                    {intl.formatMessage({id: 'addTask'})}
                </Button>
            </div>
        </>
    );
};

export default TaskList;

最终效果如图


image.png
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,616评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,020评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,078评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,040评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,154评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,265评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,298评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,072评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,491评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,795评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,970评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,654评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,272评论 3 318
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,985评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,223评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,815评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,852评论 2 351

推荐阅读更多精彩内容