umi實現(xiàn)todoList

最近新項目用的umi框架,看完官網(wǎng)api后,打算運(yùn)用umi+antd pro+dva上手實現(xiàn)個簡單的todoList練習(xí)一下

在網(wǎng)上找到了一個效果示例


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

搭建項目:

前置條件:需要安裝node環(huán)境及node版本10.13以上
查看node版本:
node -v

先找個地方建個空目錄
mkdir myProject-umi&& cd myProject-umi
通過官方工具創(chuàng)建項目
npx @umijs/create-umi-app

項目創(chuàng)建成功,目錄結(jié)構(gòu)如下


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),默認(rèn)的腳手架內(nèi)置了 @umijs/preset-react,包含布局、權(quán)限、國際化、dva、簡易數(shù)據(jù)流等常用功能。比如想要 ant-design-pro 的布局,編輯 .umirc.ts 配置 layout: {},并且需要安裝 @ant-design/pro-layout。

默認(rèn)生成的.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文件已經(jīng)加上了.umi),他們會在 umi dev 和 umi build 時被刪除并重新生成。

在瀏覽器中輸入地址打開網(wǎng)頁,可以看到默認(rèn)的page頁面


image.png

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

考慮到實現(xiàn)一個todoList 需要狀態(tài)管理 國際化等,目錄結(jié)構(gòu)改造如下

├── package.json
├── .umirc.ts //umi 配置,同 config/config.js,二選一,umi內(nèi)置功能的配置和插件的配置,由于項目比較簡單這里選用.umirc.ts
├── mock //mock數(shù)據(jù)
? └── todoList.ts
└── src
? ├── .umi //自動生成的臨時文件
? ├── components //公共組件
? ? └── TodoList.tsx
? ├── pages // 所有路由組件在這里
? ? └── todoList //todoList頁面
? ? ? ├── index.less
? ? ? └── index.tsx
? ├── model //狀態(tài)管理
? ? └── model.ts
? ├── locales //國際化
? ? ├── en-US.ts
? ? └── zn-CN.ts
? ├── service //請求服務(wù)
? ? └── index.ts

配置

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

  • 頁面路由:配置routes,格式為路由信息的數(shù)組。
{ 
  name: 'dashboard',
  icon: 'dashboard',
  hideInMenu: true,
  hideChildrenInMenu: true,
  hideInBreadcrumb: true,
  authority: ['admin'], 
}
  • name : 當(dāng)前路由在菜單和面包屑中的名稱,注意這里是國際化配置的 key ,具體展示菜單名可以在 /src/locales/zh-CN.js 進(jìn)行配置。

  • icon : 當(dāng)前路由在菜單下的圖標(biāo)名。

  • hideInMenu : 當(dāng)前路由在菜單中不展現(xiàn),默認(rèn) false 。

  • hideChildrenInMenu : 當(dāng)前路由的子級在菜單中不展現(xiàn),默認(rèn) false 。

  • hideInBreadcrumb : 當(dāng)前路由在面包屑中不展現(xiàn),默認(rèn) false

  • authority : 允許展示的權(quán)限,不設(shè)則都可見,詳見:權(quán)限管理

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

  • 熱更新:fastRefresh

  • 國際化配置:locale
    ? default默認(rèn)語言
    ? 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: {
  } //動態(tài)引入
});

mock

todoList主要的操作就是增刪改查,
所以這里加了兩個接口,查詢list的接口,和更新list數(shù)據(jù)的接口(增刪改都調(diào)用這個接口),并用setTimeout來模擬異步請求

  • getTodoList:請求初始todolist數(shù)據(jù)
  • updateTodoList:更新todolist數(shù)據(jù)
// 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: '初始任務(wù)1', status: '0', key: '初始任務(wù)1'},
                    {content: '初始任務(wù)2', status: '1', key: '初始任務(wù)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':"你想做點(diǎn)什么",
    'addTask': '添加任務(wù)',
    'delete': '刪除',
    'allTask': '所有任務(wù)',
    'completedTask': '已完成任務(wù)',
    'uncompletedTask': '待辦任務(wù)'
};

model

model 作狀態(tài)管理,包含

  • namespace: 表示在全局 state 上的 key
  • state:狀態(tài)數(shù)據(jù)
  • reducers :管理同步方法,必須是純函數(shù)
  • effects :管理異步操作,采用了 generator 的相關(guān)概念
  • subscriptions:訂閱數(shù)據(jù)源

在 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同步更新數(shù)據(jù),一個方法就是將數(shù)據(jù)存儲在model,頁面從state中拿到最新數(shù)據(jù)

我們知道可以運(yùn)用兩種方式關(guān)聯(lián)state和view
1、connect:需要用 dva 或 umi 中導(dǎo)出 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
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容