一、创建umi应用
1、新建umi应用并启动
mkdir umi && cd umi
yarn create @umijs/umi-app
yarn
yarn start
2、umi应用的基本目录结构如下:
✓表示创建时已经拥有此文件,✕表示该文件需要手动创建
.
✓ ├── package.json
✓ ├── .umirc.ts.
✕ ├── .env
✕ ├── dist
✓ ├── mock
✕ ├── public
✓ └── src
✓ ├── .umi
✕ ├── layouts/index.tsx
✓ ├── pages
✓ ├── index.less
✓ └── index.tsx
✕ └── app.ts
✕ └── config
✕ ├── config.ts
✕ ├── routes.ts
其中.umirc.ts为配置文件,路由默认在这里配置,但为了不让该文件过于庞大,在根目录下创建 config/config.ts ,并将路由拆分出来成 routes.ts
BUG: 如果config.ts和.umirc.ts同时存在,在修改这两个文件的时候不会更新,且在有路由嵌套的时候父组件刷新不出来(未解决),我目前只能把.umirc.ts删掉来解决这个问题
// config/routes.ts
export default [
{ exact: true, path: '/', component: 'index' },
];
// config/config.ts
import { defineConfig } from 'umi';
import routes from './routes';
export default defineConfig({
routes: routes,
});
二、配置式路由和约定式路由
配置式路由比较简单,好上手,所以在这里只讲配置式路由
1、基本属性
path:路径
component:匹配的组件
exact:是否开启精确匹配
routes:子路由
redirect:重定向
title:标题
wrappers:配置路由的高阶组件封装(该属性我也不太了解)
当前路由结构:
// config/routes.ts
// pages目录下新建count和person文件夹,person文件夹下新建male、female文件夹,文件名均为index.tsx。路由配置如下:
export default [
{ exact: true, path: '/', component: 'index', title: '主页' },
{ exact: true, path: '/count', component: 'count', title: '计数器' },
{
path: '/person', component: 'person', routes: [
{ path: 'male', component: 'person/male', title: '男人' },
{ path: 'female', component: 'person/female', title: '女人' }
], title: '一堆人'
},
];
2、向子路由传参(Person组件向Male组件传参)
1)params
// config/routes.ts
{ path: 'male/:id', component: 'person/male', title: '男人' },
// Person组件
import { history } from 'umi'
const Person = (props:any) => {
const id = 1
const showMale = () => {
// history.push('/person/male')
history.push(`/person/male/${id}`)
}
const showFemale = () => {
history.push('/person/female')
}
return (
<>
<h1>我是Person</h1>
<button onClick={showMale}>show Male</button>
<button onClick={showFemale}>show Female</button>
{ props.children }
</>
)
}
export default Person
在子路由的props.match.params中取
2)search
// config/routes.ts
{ path: 'male', component: 'person/male', title: '男人' },
// Person组件
history.push(`/person/male/?id=${id}`)
在子路由的props.location.search中取
3)query
// config/routes.ts
{ path: 'male', component: 'person/male', title: '男人' },
// Person组件
history.push({pathname: '/person/male', query: { name: String(id) }})
在子路由的props.location.query中取,在umi中search和query特别像,一般不用search
4)state
// config/routes.ts
{ path: 'male', component: 'person/male', title: '男人' },
// Person组件
history.push({pathname: '/person/male', state: { name: id }})
在子路由的props.location.state中取,这样做的好处是参数不会在地址栏中显示出来
5)通过cloneElement( Umi 3 官方推荐)
// Person组件
import React from 'react'
import { history } from 'umi'
const Person = (props:any) => {
const id = 1
const showMale = () => {
history.push('/person/male')
// history.push(`/person/male/${id}`)
// history.push(`/person/male/?id=${id}`)
// history.push({pathname: '/person/male', query: { name: String(id) }})
// history.push({pathname: '/person/male', state: { name: id }})
}
const showFemale = () => {
history.push('/person/female')
}
return (
<>
<h1>我是Person</h1>
<button onClick={showMale}>show Male</button>
<button onClick={showFemale}>show Female</button>
{/* { props.children } */}
{
React.Children.map(props.children, child => {
return React.cloneElement(child, { foo: {name: id} });
})
}
</>
)
}
export default Person
在子路由的props中直接可以取到foo
三、请求Mock数据
//安装mockjs用于模拟数据
yarn add @types/mockjs
//安装axios用于请求数据
yarn add axios
//在mock文件夹下新建mockData.ts
// 引入 Mock
import Mock from 'mockjs'
// 定义数据类型
export default {
'GET /api/tags': Mock.mock({
// 3条数据
"info|3": [{
// 商品种类
"goodsClass": "女装",
// 商品Id
"goodsId|+1": 1,
//商品名称
"goodsName": "@ctitle(10)",
//商品地址
"goodsAddress": "@county(true)",
//商品等级评价★
"goodsStar|1-5": "★",
//商品图片
"goodsImg": "@Image('100x100','@color','小甜甜')",
//商品售价
"goodsSale|30-500": 30,
// 邮箱:
"email": "@email",
// 颜色
"color": "@color",
// name
"name": "@name",
//img,参数1:背景色,参2:前景色,参3:图片的格式,默认png,参4:图片上的文字
"img": "@image('100*100','@color')",
//英文文本(句子)参1:句子的个数,参2:句子的最小个数 参3:句子的最大个数,没有参1时,参2参3才会生效
"Etext": "@paragraph(1,1,3)",
//中文文本(句子)参1:句子的个数,参2:句子的最小个数 参3:句子的最大个数,没有参1时,参2参3才会生效
"Ctext": "@cparagraph(1,1,3)",
//中国大区
"cregion": "@region",
// 省
"cprovince": "@province",
//市
"ccity": "@city",
//省 + 市
"ss": "@city(true)",
//县
"country": "@county",
//省市县
"countrysx": "@county(true)",
//邮政编码
"code": "@zip"
}]
})
}
1、通过axios请求(Male组件)
import axios from 'axios'
const Male = () => {
const getData = () => {
axios.get('/api/tags').then(res => {
//在res.data.info中取到数据
console.log(res.data.info);
})
}
return (
<>
<h2>我是Male</h2>
<button onClick={getData}>axios请求mock数据</button>
</>
)
}
export default Male
2、通过fetch请求(Female组件)
const Female = () => {
const getData = async () => {
try {
const res = await fetch('/api/tags')
const data = await res.json()
//在data.info中取到数据
console.log(data.info);
} catch (error) {
console.log(error);
}
}
return (
<>
<h2>我是Female</h2>
<button onClick={getData}>fetch请求mock数据</button>
</>
)
}
export default Female
四、在umi中使用dva
1、在src下新建models文件夹(这个名字不能随便取,umi约定/src/models下的文件为dva模块),在models下新建store.ts(名字随意且可以有多个文件),我打算在Male组件和Female组件中增加男人和女人,然后在Count组件中可以得到男人和女人的总人数。
// store.ts
export default {
//有多个文件时命名空间不能重复
namespace: 'store',
//state中保存状态
state: {
male: [],
female: [],
count: 0
},
//reducers对比vuex的mutations,用于同步修改
reducers: {
addMale(state: { [propName: string]: any }, action: { [propName: string]: any }) {
//注意:state.male必须通过这种方式修改,通过push方式会导致页面不更新
state.male = [...state.male, action.payload]
state.count++
return { ...state }
},
addFemale(state: { [propName: string]: any }, action: { [propName: string]: any }) {
state.female = [...state.female, action.payload]
state.count++
return { ...state }
},
}
}
//Male组件
import { useRef } from 'react'
import axios from 'axios'
//引入dva中的connect模块
import { connect } from 'dva'
//nanoid用于生成随机字符串
import { nanoid } from 'nanoid'
const Male = (props: any) => {
const nameInput = useRef<HTMLInputElement>(null)
const ageInput = useRef<HTMLInputElement>(null)
const getData = () => {
axios.get('/api/tags').then(res => {
//在res.data.info中取到数据
console.log(res.data.info);
})
}
//点击按钮,若name和age不为空,则调用addMale用于修改state的值
const addPerson = () => {
const name = nameInput.current?.value
const age = ageInput.current?.value
const id = nanoid()
if (name !== '' && age !== '') {
props.addMale({ id, name, age });
(nameInput.current as HTMLInputElement).value = '';
(ageInput.current as HTMLInputElement).value = ''
} else {
alert('姓名和年龄不能为空')
}
}
return (
<>
<h2>我是Male</h2>
<button onClick={getData}>axios请求mock数据</button>
<br />
<div>
<span>姓名:</span>
<input ref={nameInput} type="text" placeholder="请输入姓名" />
</div>
<div>
<span>年龄:</span>
<input ref={ageInput} type="text" placeholder="请输入年龄" />
</div>
<button onClick={addPerson}>添加男人</button>
<ul>
{
props.maleList.map((item: any) => {
return <li key={item.id}>{item.name} : {item.age}</li>
})
}
</ul>
</>
)
}
const actionCreator = {
addMale: (payload: any) => ({ type: 'store/addMale', payload })
}
//connect与react-redux类似
export default connect((state: any) => ({ maleList: state.store.male }), actionCreator)(Male)
Female组件和Male组件代码几乎一样,所以不做展示。
//count组件比较简单
import { history } from 'umi'
import { connect } from 'dva'
const Count = (props: any) => {
const goIndex = () => [
history.push('/')
]
return (
<>
<div>
<h1>我是Count</h1>
<button onClick={goIndex}>GO Index</button>
<p>男女总人数为:{ props.count }</p>
</div>
</>
)
}
export default connect((state: any) => ({count: state.store.count}))(Count)
以上代码即可完成在Male组件与Female组件中添加男(女)人后,去到Count组件可以看到男女总人数,这就已经实现了数据共享。
2、dva中的effect
1)put,用于触发action,我打算在Count组件中添加一个清零按钮,点击后清除已经添加的男人和女人,总人数清零。
//store.ts
effects: {
*clear(_: any, { put }: any) {
yield put({
type: 'clearState',
})
}
}
//在reducers中新增吃clearState
clearState() {
return {
male: [],
female: [],
count: 0
}
}
//Count组件
import { history } from 'umi'
import { connect } from 'dva'
const Count = (props: any) => {
const goIndex = () => [
history.push('/')
]
return (
<>
<div>
<h1>我是Count</h1>
<button onClick={goIndex}>GO Index</button>
<p>男女总人数为:{props.count}</p>
<button onClick={() => { props.clear() }}>清零</button>
</div>
</>
)
}
const actionCreator = {
clear: () => ({ type: 'store/clear' })
}
export default connect((state: any) => ({ count: state.store.count }), actionCreator)(Count)
2)call,用于调用异步逻辑,现在我想在点击清零按钮之后等5秒再将人数清零,那么我们仅需要修改一下effects。注意:call函数的第一个参数需返回一个Promise对象
effects: {
*clear(_: any, { put, call }: any) {
yield call(() => {
return new Promise((resolve,reject) => {
setTimeout(resolve,5000)
})
})
yield put({
type: 'clearState',
})
}
}
3)select,用于从state里获取数据。这里有一个大坑,会报一个错:‘yield’ expression implicitly results in an ‘any’ type because its containing generator lacks a return-type annotation. ts(7057),我们把它翻译过来:“yield”表达式隐式生成“any”类型,因为其包含的生成器缺少返回类型注释。贴一个讲此问题的帖子:点我去看
具体到这个项目来说就是添加一个返回类型限制,代码如下:
effects: {
*clear(_: any, { put, call, select }: any): Generator {
yield call(() => {
return new Promise((resolve, reject) => {
setTimeout(resolve, 5000)
})
})
//就是下面这句话报的错,原因是ts判断不出来*函数的返回类型,所以我们要给*函数加一个Generator返回类型限制
const payload = yield select((state: any) => state.store.male)
//我们就可以在清空数据之前获取一次数据
console.log('会被清除的男人',payload);
yield put({
type: 'clearState',
})
}
}
3、dva中的subscription。subscriptions 是订阅,用于订阅一个数据源,然后根据需要 dispatch 相应的 action。
我想每次进count页面我都在控制台打印当前的男人和女人数据,最终完整的store.ts文件如下
export default {
namespace: 'store',
state: {
male: [],
female: [],
count: 0
},
reducers: {
addMale(state: { [propName: string]: any }, action: { [propName: string]: any }) {
state.male = [...state.male, action.payload]
state.count++
return { ...state }
},
addFemale(state: { [propName: string]: any }, action: { [propName: string]: any }) {
state.female = [...state.female, action.payload]
state.count++
return { ...state }
},
clearState() {
return {
male: [],
female: [],
count: 0
}
},
showPerson(state: { [propName: string]: any }) {
console.log('@',state.male);
console.log('@@',state.female);
return state
}
},
effects: {
*clear(_: any, { put, call, select }: any): Generator {
yield call(() => {
return new Promise((resolve, reject) => {
setTimeout(resolve, 1000)
})
})
//就是下面这句话报的错,原因是ts判断不出来*函数的返回类型,所以我们要给*函数加一个Generator返回类型限制
const payload = yield select((state: any) => state.store.male)
console.log('会被清除的男人',payload);
yield put({
type: 'clearState',
})
}
},
subscriptions: {
setup({ dispatch, history }: any) {
history.listen(({ pathname }: any) => {
if (pathname === '/count') {
dispatch({
type: 'showPerson'
})
}
})
}
}
}
五、总结
至此,一个简要的umi+dva应用就搭建完成了。其中用到了umi的路由、数据模拟,用到了dva的状态管理,掌握了这些知识就可以自己开发项目啦。我把demo放在了git仓库,需要自取:戳我下载