总体图示
我对 umi 的使用有一段时间了,个人理解umijs 是大量React 流行的扩展方案的结合体,内部自带 dva 数据流、React-Router 等插件,全部整合到 umi 中,方便开发者直接使用。
umi 中两个比较重要的是关键点是路由方案和 dva数据流,路由配置非常优雅,搭配 dva+antd有非常好的效果。
下面是关于 dva 数据流的一个数据走向。
摘自 aspirantzhang画的草图
解释一下:dva核心思想是将页面和数据层分离,数据层的名字取名为 model,内部包含 Reducer、Effect、Subscription 三个对象。
页面和数据之间采用 connect 进行关联,如果了解 Redux,就会知道这是一样的东西。
不过现在已经2021年了,社区也早就不采用 connect 了,不过由于很多公司依然使用高阶组件传递state,所以我这里先用 connect 写一遍demo。后面单独写一个 hooks 的方法。
下面是一些概念介绍
Reducer
内部包含同步代码,它是将数据正式发送给页面的统一扎口,我们只能在这一层返回数据给页面。
它的参数是这样的
type Reducer<S, A> = (state: S, action: A) => S
state 就是原来的数据,它是一个泛型,Reducer 函数的返回值必须跟state类型一致。
action 是{type,payload}对象
这个 action 就是 dispatch(action)的那个 action。
Effect
这个对象里面包含的内容是异步函数,也就是我们如果要发送 ajax 请求或者其他异步函数,会放到这一层里面。
仔细看看草图,我们会发现 effect 还往外延伸出一个 service,这个 service 是用来存放很多异步函数的。比如用户做一些操作,触发异步函数,那么这些异步函数都可以放在 service 中,通过 call 来返回给 effect。
effect里必须是 genarator 函数,使用 async、await 会失效。不过我们可以在 service 中使用 async、await。
还有一点需要注意的是,由于所有数据都必须通过 reducer 传给页面,所以异步获取到的数据需要调用 put 发送给 reducer。
那么 call 函数和 put 函数到底在哪呢?它存在于 Effect定义的函数中。我们来看 Effect 中的函数的参数都有哪些。
*(action, effects) => void
call跟put 就放在 effects 中。
Subscription
对于这个,我经常用的业务场景是通过路由的改变,来触发 effect 中的异步函数获取数据。
我们来看一下它的使用参数
({ dispatch, history }, done) => unlistenFunction
这里有一个model用例
截取一些代码看看:
subscriptions: {
setup({ dispatch, history }) {
return history.listen(({ pathname }) => {
if (pathname === '/') {
dispatch({
type: 'query',
});
}
});
},
},
在用例中,是直接调用 history 中的 listen 方法,监听页面的路径变化,如果是/的话,就 dispatch 一下。
这个 history 实际是一个 history 库的对象,它相当于对 history 原生 api 的一次封装。参见mdn-history
实践
1.路由配置
知道了上面的基础概念后,我们可以通过写一个简单的例子来模拟真实开发了。
我们需要首先需要安装 umi3,请查看官网安装
当你安装完后,你的目录结构应该是这样的
.
├── .editorconfig
├── .gitignore
├── .prettierignore
├── .prettierrc
├── .umirc.ts
├── README.md
├── mock
│ └── .gitkeep
├── package.json
├── src
│ └── pages
│ ├── index.less
│ └── index.tsx
├── tsconfig.json
└── typings.d.ts
我们写代码都是在 pages 中的,这个页面关联的是路由。在 umi 中,有两种路由分别是约定式路由,还一种是配置型路由。我们直接使用配置路由。
$ mkdir config && cd config
$ touch config.ts routes.ts
通过上面操作,我们分别创建了这样的结构
└── config
├── config.ts
├── routes.ts
然后分别写入以下内容:
// config/config.ts
import { defineConfig } from 'umi';
import routes from './routes';
export default defineConfig({
routes: routes,
});
// config/routes.ts
export default [
{ exact: true, path: '/', component: './index' },
{ exact: true, path: '/users', component: './users/users' },
];
这时候yarn start访问/users看看有没有生效。
如果显示空白,那么恭喜你,这是正常的,我们还没有编写 users 的文件呢。
这里解释一下,上面的操作就是编写了路由,如果你对 React-Route 很熟悉,那么你应该看得懂属性。
配置型路由非常方便。它默认索引 src/pages 目录,所以我们只需要在 component 下面写./index,而不是./src/page/index。
2.创建目录
现在我们要创建一个 users目录来配合我们的路由,我们需要这样创建
cd src/pages && mkdir users && cd users
touch Users.tsx model.ts service.ts
这样你就可以获得以下目录结构
.
├── model.ts
├── service.ts
└── users.tsx
然后编写 user.tsx
import React from 'react';
const Users = () => {
return <div>users</div>;
};
export default Users;
如果你此时还没有删除.umirc.ts,那么大概率会出现失效的情况。
官方文档介绍.umirc.ts优先级要比 config 更高,我们一般在做非常简单的项目时需要会用到.umirc.ts。真实开发时,需要着重按照最小原则来编写代码,配置文件也不例外,所以这里我推荐将所有配置都拆分出来,那么删除.umirc.ts即可。
然后再运行yarn start访问/users,你应该看到页面中出现了 users 几个字母。
3.定义 model
model 由以下内容组成
const model = {
namespace: '',
state: {},
reducers: {},
effects: {},
subscriptions: {},
};
export default model;
namespace 是 model 的命名空间,同时也是state的属性
state 是初始仓库
reducers 是保存处理同步数据的函数的对象,通过 reducer 返回数据给 view 层,它是统一的数据传导扎口
effects 则是用来保存处理异步函数的对象,通过 effcets 的数据需要 put 给 reducers 来返回给数据层
subscriptions就是订阅数据源用的。
看不懂没关系,结合 总体图示 我们来编写一段数据。比如我现在需要在 users 页面放入数据。直接在 reducers 里面定义一个函数,返回即可。
reducers: {
getList() {
return 123;
},
},
当页面进入 users时,就需要订阅一个数据,所以我们在 subscriptions 中这样写
subscriptions: {
setup({ dispatch, history }) {
return history.listen(({ pathname }) => {
if (pathname === '/users') {
dispatch({
type: 'getList',
});
}
});
},
}
上面使用 history 监听路径,当路径为/users 时,dispatch 一个action,action里面的 type 是 reducer 中的函数名,表示触发这个 reducer 函数。
最后一点,要记得写 namespace 的属性,这里我写的是users,这个很重要,下面会用到。
4.数据层和页面层连接
下面使用 connect 连接数据层和页面层。
// Users.tsx
import React from 'react';
import { connect } from 'umi';
const mapStateToProps = (state) => {
const { users } = state;
return { users };
};
export default connect(mapStateToProps)(Users);
跟 Redux 使用方法一样,我们需要写一段mapStateToProps函数,然后传给 connect 函数,返回一个函数后,再传入 Users 组件,这样会返回一个新的组件。
然后Users 组件就可以通过 props 获得数据了。
const Users = (props) => {
return <div>users{props.users}</div>;
};
这里的props.users是什么呢? 它是 namespace写的属性。通过 namespace 我们可以自定义 state 内的 key,获得 key 内的数据。
如果没问题,你的页面上应该会出现
users123
5.异步操作effects
真实的网页,往往我们跳转到某一个路由中,会需要异步获取数据,再渲染页面,所以我们需要用到 effects。
由于数据过于简单,这里我们最好再用 ant-design 里的 table 组件帮我们把 users 页面画的更好看一点。
我们先让 effects 搭配我们操作。
(1)首先,我们定义一个数据
// model.ts
const data = [
{
key: '1',
name: 'John Brown',
age: 32,
address: 'New York No. 1 Lake Park',
tags: ['nice', 'developer'],
},
{
key: '2',
name: 'Jim Green',
age: 42,
address: 'London No. 1 Lake Park',
tags: ['loser'],
},
];
(2)修改订阅subscriptions
我们需要将 subscriptions 中的 dispatch改成这样
dispatch({
type: 'asyncGetData',
});
也就是改成定义在 effects 里面的函数名。
这个操作表示当路由为/users时,执行 asyncGetData函数。
(3)effects中写 genarator 函数
effects: {
*asyncGetData(action, effects) {
const listData = yield Promise.resolve(data);
//用 effects.put 把数据 推给 reducers,并触发 gitList 函数
yield effects.put({ type: 'getList', payload: listData });
},
},
put 接收一个action作为参数,我们直接将定义好的 data 通过 action 中的 payload 传递给 reducers 中的 getList 方法,由 getList 将 payload 传递到给页面。
由于 reducers 是唯一连接前端页面的地方,所以我们不得不这么做。
(4)reducer 函数接收并发送给页面层
reducers: {
getList(state, action) {
return { listData: action.payload };
},
(5)最后修改Users.tsx中的页面
这里是拷贝了table-ant-design中的组件代码。
const Users = (props) => {
const columns = [
{
title: 'Name',
dataIndex: 'name',
key: 'name',
render: (text) => <a>{text}</a>,
},
{
title: 'Age',
dataIndex: 'age',
key: 'age',
},
{
title: 'Address',
dataIndex: 'address',
key: 'address',
},
{
title: 'Tags',
key: 'tags',
dataIndex: 'tags',
render: (tags) => (
<>
{tags.map((tag) => {
let color = tag.length > 5 ? 'geekblue' : 'green';
if (tag === 'loser') {
color = 'volcano';
}
return (
<Tag color={color} key={tag}>
{tag.toUpperCase()}
</Tag>
);
})}
</>
),
},
{
title: 'Action',
key: 'action',
render: (text, record) => (
<Space size="middle">
<a>Delete</a>
</Space>
),
},
];
//重点看这里
return <Table columns={columns} dataSource={props.users.listData} />;
};
我们已经将数据通过 props.users.listData划到页面中来了。
注意点
这里有个小问题需要注意,为什么我在 Reducer 中会返回一个对象呢?
getList(state, action) {
return { listData: action.payload };
},
在第三步定义 model 中,我们是直接返回了数据123,然后直接通过 props.users 获取到传递的这个数据。
但是在这里,按照道理来说我也应当返回data 数据,然后通过 props.users 直接获取到 data 数据才对。
因为我在这里遇到了 bug,通过 redux 的chrome 插件,我可以看到 state 的数据。
的确 users 属性内有个数组。但是到页面上,却发现传递给 props 中的 users 没有获取到这个 data。
所以这里的解决方法就是返回时,用一个对象把它包起来。
6.service.ts
service 这个文件主要用于异步函数,我们可以将异步函数的逻辑写到这里来。
上面的步骤我主要使用的是 promise.resolve 来返回一个数据,实际开发中,肯定是用 ajax 发送请求来返回数据的,所以我们在 service 中定义一下异步获取数据的函数。
由于第1-5步用的是 mock 数据,这里用的是接口数据,所以我重新新建了一个路由。
login
├── Login.tsx
├── model.ts
└── service.ts
如果方便的话,请重新按照1-5步再写一遍新的路由组件和 model数据,以便于理解。
我在下面省略重新做组件的过程,因为程序都差不多,所以这里按照差别直接写 service 的交互。
$ yarn add axios
// service.ts
import axios from 'axios';
export async function getRemote(params) {
const { data } = await axios
.get('http://public-api-v1.aspirantzhang.com/users')
.then(null, (error) => {
throw new Error(error);
});
return data;
}
service 文件中的异步函数支持async、await。上面的接口也是aspirantzhang提供的,不会有跨域问题。
最后我们需要将service中的异步函数 call 过来
// login/model.ts
effects: {
*asyncGetData(action, effects) {
//const listData = yield Promise.resolve(data);
//这里的语法改一下,根据实际接口拿到数据
const {data} = yield effects.call(getRemote);
//用 effects.put 把数据 推给 reducers
yield effects.put({ type: 'getList', payload: data });
},
},
如果你自己写过一遍还是发现有问题,可以看我的 github对比一下
页面与仓库的交互
下面我们再尝试修改 login 组件中的数据。 首先新建新的文件夹和文件
$ mkdir components && touch UseModal.tsx
引入 ant-d 的组件
import { Modal, Form, Input } from 'antd';
写入内容页面中
<>
<Table columns={columns} dataSource={props.login.listData} rowKey="id" />
<UseModal showModal={showModal} visible={visible} />
</>
关于 UseModal 的文件内容,都是从 ant-d 中随拿随用的,这里就不展示具体代码,想要知道的直接从github库里面拿即可。
我在这里主要展示的是使用 umi 进行仓库与页面交互的过程,知道了这个过程,代码就是正常的业务逻辑了。
最好根据我写好的代码和页面展示效果一起看,否则可能会懵逼。我已经在源码中写好了注释,非常详细。
目前我的页面是这样的
那我希望的效果是通过点击 ok 后让页面修改 name,后台的接口是这样的
http://public-api-v1.aspirantzhang.com/users/?id=xx
data:{name:' xxx'}
method:PUT
所以我需要在点击 ok 的时候触发一次提交,这里建议采用 umijs 的用法来提交,也就是使用 dispatch 来让公共仓库的数据发生改变。
所以这里的数据流向就是
页面 dispath ==> Service ==> Effects ==> Reducers ==> 页面
第一步我们在页面写 dispatch 函数
dispatch 这个函数可以通过 props 获取
const onSubmit = (values) => {
//不用管这是啥,只需要看 dispatch 那个代码
const { id } = record;
//控制 model框 的 ok 键旋转效果
setConfirmLoading(true);
//异步只是为了让 ok 键旋转效果明显一点
setTimeout(() => {
//这里才是页面 dispatch 的逻辑
//主要看这里就行
props.dispatch({
type: 'login/asyncEditName',
payload: { id: id, ...values },
});
//设置回 false
setConfirmLoading(false);
//弹框消失
showModal();
}, 1000);
};
需要注意的是这里的写法一定要写成[namespace]/functionName的形式,否则就失效啦。
第二步我们在 Service 中写 axios 代码
export async function EditName(id, data) {
await axios({
url: `http://public-api-v1.aspirantzhang.com/users/${id}`,
method: 'PUT',
data: data,
}).then(null, (error) => {
throw new Error(error);
});
}
对于 id 和 data 两个参数,我们可以通过 effect.call的第二、三个参数进行传递。
第三步在 Effects 中写逻辑
call 的第二个参数开始,会传递给作为第一个参数的service 内的函数
*asyncEditName({ payload }, effects) {
//这里的 payload 是页面传过来的数据{id:xxx,payload:{name:xxx}}
yield effects.call(EditName, payload.id, payload);
//为了让页面渲染所以重新 put 一个请求,让页面再发一次请求获取数据
yield effects.put({ type: 'asyncGetData' });
},
最后呈现的效果是这样的
页面loading 状态
在页面中,我们经常会遇到需要刷新后有个 loading 中的一种交互,这里的状态我们可以通过传递给页面的 state 取得,也就是在 mapStateToProps 中可以获取
const mapStateToProps = (state) => {
const { login, loading } = state;
//loading 可以获取异步的 loading 状态
return { login, userLoading: loading.models.login };
};
export default connect(mapStateToProps)(Login);
取到这个数字后,就可以在页面中使用啦。
如果不想用这种方式取,可以使用 useSelector 这个 hooks 取 state 里面的值。
烦人的 connect
我在写了几个组件后就觉得 connect 非常烦,而之前单独使用 react-redux 的时候明明还是可以用 hooks 的。
于是尝试了一下两个 hooks,发现umi 已经做好封装,可以不用写 connect ,直接在函数组件中调用两个 hooks 就行。
也不再需要 provider,createStore ,因为这些在 model 时就已经完成。
用法:
import { useDispatch, useSelector, Dispatch } from 'dva';
const Login = () => {
//不用 connect 的写法
const dispatch = useDispatch<Dispatch>();
const { login, loading } = useSelector((state: any) => {
return state;
});
……
后话
现在基本的流程已经走完了,建议亲手跟着流程敲一遍代码加深印象,同时也能理解 umi 的数据流概念。
对于业务框架来说,我觉得 umi 将一套最佳实践方案封装得非常棒,目前公司正在使用这一套开发后台管理系统,体验还不错,而且 umi 本身就带有一点模板写法的味道,如果你的团队认为React 系列技术栈一直被诟病的写法过于灵活,项目不好维护管理,那么尝试一下这套整合方案吧。我相信会带给你不一样的惊喜。
我们下期再见。