使用umi快速搭建项目以及如何在umi中使用dva进行状态管理

一、创建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>&nbsp;&nbsp;
      <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>&nbsp;&nbsp;
      <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仓库,需要自取:戳我下载

  • 4
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值