最近新項目用的umi框架乳绕,看完官網(wǎng)api后泥栖,打算運用umi+antd pro+dva上手實現(xiàn)個簡單的todoList練習(xí)一下
在網(wǎng)上找到了一個效果示例
搭建項目:
前置條件:需要安裝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)如下
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)厨诸,默認的腳手架內(nèi)置了 @umijs/preset-react,包含布局蜜托、權(quán)限抄囚、國際化、dva橄务、簡易數(shù)據(jù)流等常用功能幔托。比如想要 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文件已經(jīng)加上了.umi)棠涮,他們會在 umi dev 和 umi build 時被刪除并重新生成谬哀。
在瀏覽器中輸入地址打開網(wǎng)頁,可以看到默認的page頁面
如果想要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
: 當前路由在菜單和面包屑中的名稱帘睦,注意這里是國際化配置的key
袍患,具體展示菜單名可以在/src/locales/zh-CN.js
進行配置。icon
: 當前路由在菜單下的圖標名竣付。hideInMenu
: 當前路由在菜單中不展現(xiàn)诡延,默認false
。hideChildrenInMenu
: 當前路由的子級在菜單中不展現(xiàn)卑笨,默認false
孕暇。hideInBreadcrumb
: 當前路由在面包屑中不展現(xiàn),默認false
赤兴。authority
: 允許展示的權(quán)限妖滔,不設(shè)則都可見,詳見:權(quán)限管理路由用按需引入的方式桶良,配置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: {
} //動態(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':"你想做點什么",
'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ù)
我們知道可以運用兩種方式關(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;
最終效果如圖