The overall graphic
I have been using UMI for some time. Personally, I understand that UMiJS is a combination of a large number of popular React extension solutions. It has built-in plug-ins such as DVA data stream and React-Router, which are all integrated into UMI for developers to use directly.
Two key points in UMI are routing scheme and DVA data flow. Routing configuration is very elegant, and dVA + ANTD has a very good effect.
Here is a data trend for DVA data flow.
fromaspirantzhangDraw sketches
The core idea of DVA is to separate the page from the data layer, which is called Model and contains Reducer, Effect, and Subscription objects.
Pages and data are associated with connect, which is the same thing if you know About Redux.
It’s 2021, and the community has long since moved away from Connect, but since many companies still use higher-level components to pass state, I’ll write a demo with Connect. Write a separate method for hooks.
Here are some concepts
Reducer
It contains the synchronization code, which is the unified socket that formally sends data to the page. We can only return data to the page at this layer.
Its parameters look like this
type Reducer<S, A> = (state: S, action: A) = > S
Copy the code
State is the original data, which is a generic type. The return value of the Reducer function must be the same as state.
The action is the {type,payload} object
This action is the action with dispatch(Action).
Effect
The contents of this object are asynchronous functions, so if we want to send ajax requests or other asynchronous functions, we’ll put them in this layer.
A closer look at the sketch shows that Effect also extends out a service that houses many asynchronous functions. For example, if a user performs an operation that triggers an asynchronous function, these asynchronous functions can be placed in a service and returned to Effect by call.
Effect must be genarator. Async and await are invalid. But we can use async, await in a service.
Another point to note is that all data must be sent to the Reducer page through the reducer, so the data obtained asynchronously needs to be sent to the Reducer through the put call.
So where are the call and put functions? It exists in the function defined by Effect. Let’s look at the parameters of an Effect function.
*(action, effects) = > void
Copy the code
Call and put are in Effects.
Subscription
The business scenario I often use for this is to trigger an asynchronous function in Effect to fetch data through a route change.
Let’s look at its usage parameters
({ dispatch, history }, done) => unlistenFunction
Copy the code
Here is a Model use case
Take a look at some code:
subscriptions: {
setup({ dispatch, history }) {
return history.listen(({ pathname }) = > {
if (pathname === '/') {
dispatch({
type: 'query'}); }}); }},Copy the code
In the use case, you call the Listen method in History directly to listen for changes in the path of the page, or dispatch it if it is /.
The history is actually an object of the History library, which acts as a wrapper around the History native API. See the MDN – history
practice
1. Configure routes
With these basic concepts in mind, we can simulate real development by writing a simple example.
We need to install UMi3 first, please check the official website for installation
When you’re done installing, your directory structure should look like this
... ├ ─ ─ editorconfig ├ ─ ─ gitignore ├ ─ ─ the prettierignore ├ ─ ─ the prettierrc ├ ─ ─ the umirc. Ts ├ ─ ─ the README. Md ├ ─ ─ the mock │ └ ─ ─ . Gitkeep ├ ─ ─ package. Json ├ ─ ─ the SRC │ └ ─ ─ pages │ ├ ─ ─ but less │ └ ─ ─ index. The TSX ├ ─ ─ tsconfig. Json └ ─ ─ typings. Which sCopy the code
We write all our code in Pages, which is associated with routing. There are two types of UMI routes: contract route and configuration route. We use configuration routing directly.
$ mkdir config && cd config
$ touch config.ts routes.ts
Copy the code
By doing this, we created these structures separately
├── bass exercises ─ bass exercises ─ routesCopy the code
Then write the following:
// config/config.ts
import { defineConfig } from 'umi';
import routes from './routes';
export default defineConfig({
routes: routes,
});
Copy the code
// config/routes.ts
export default[{exact: true.path: '/'.component: './index' },
{ exact: true.path: '/users'.component: './users/users'},];Copy the code
Yarn start accesses /users to see if it takes effect.
If it is blank, then congratulations, this is normal, we haven’t written the users file yet.
As an explanation, the above operation is to write the Route. If you are familiar with react-Route, you should be able to read the properties.
Configuration routing is very convenient. It indexes the SRC /pages directory by default, so we just need to write./index under Component instead of./ SRC /page/index.
2. Create a directory
Now we want to create a Users directory to match our routing, we need to create it like this
cd src/pages && mkdir users && cd users
touch Users.tsx model.ts service.ts
Copy the code
This gives you the following directory structure
. ├ ─ ─ model. Ts ├ ─ ─ service. The ts └ ─ ─ the users. The TSXCopy the code
Then write user.tsx
import React from 'react';
const Users = () = > {
return <div>users</div>;
};
export default Users;
Copy the code
If you haven’t deleted.umirc.ts at this point, there is a high probability that it will fail.
Umirc. Ts has a higher priority than config, which is usually used for very simple projects. In real development, it’s important to write code with minimal rules, and configuration files are no exception, so I recommend splitting everything out and deleting.umirc.ts.
Then run YARN Start to access/Users. You should see the letters Users appear on the page.
3. Define the model
The model consists of the following
const model = {
namespace: ' '.state: {},
reducers: {},
effects: {},
subscriptions: {}};export default model;
Copy the code
Namespace is the namespace of Model and is also an attribute of State
State is the initial repository
Reducers are objects that save functions for processing synchronous data and return data to view layer through reducer, which is a unified data transmission and binding interface
Effects are used to hold objects that handle asynchronous functions, and the data that passes through effcets needs to be put to reducers to be returned to the data layer
Subscriptions are used to subscribe to data sources.
It doesn’t matter if you don’t understand it, let’s write a piece of data with the overall diagram. For example, I now need to put data in the Users page. Define a function directly in reducers and return it.
reducers: {
getList() {
return 123; }},Copy the code
When a page enters users, it needs to subscribe to a data, so we write this in Subscriptions
subscriptions: {
setup({ dispatch, history }) {
return history.listen(({ pathname }) = > {
if (pathname === '/users') {
dispatch({
type: 'getList'}); }}); }},Copy the code
History is used to monitor the path. If the path is /users, dispatch an action. The type in the action is the function name in the Reducer, which indicates that the reducer function is triggered.
And finally, remember to write the namespace property, so I’m going to write users here, which is very important, and we’ll use it next.
4. Connect the data layer to the page layer
Let’s use CONNECT to connect the data layer to the page layer.
// Users.tsx
import React from 'react';
import { connect } from 'umi';
const mapStateToProps = (state) = > {
const { users } = state;
return { users };
};
export default connect(mapStateToProps)(Users);
Copy the code
As with Redux, we need to write a mapStateToProps function, pass it to connect, return a function, and pass it to the Users component, which will return a new component.
The Users component can then get the data through props.
const Users = (props) = > {
return <div>users{props.users}</div>;
};
Copy the code
What is props. Users here? It is a namespace write property. The namespace allows you to customize the key in the state and obtain the data in the key.
If it’s okay, it should be on your page
users123
Copy the code
5. Asynchronous operation Effects
In a real web page, usually we jump to a route, we need to get data asynchronously and then render the page, so we need to use Effects.
Since the data is too simple, we’d better use the Table component in Ant-Design to make the Users page look better.
So let’s just let Effects work with us.
(1) First, we define a data
// 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'],},];Copy the code
(2) Modify subscription subscriptions
We need to change the Dispatch from Subscriptions to this
dispatch({
type: 'asyncGetData'});Copy the code
So I’m going to change the name of the function defined in Effects.
This operation means that the asyncGetData function is executed when the route is /users.
(3) Write genarator function in Effects
effects: {
*asyncGetData(action, effects) {
const listData = yield Promise.resolve(data);
// Push the data to the reducers with effects.put and trigger the gitList function
yield effects.put({ type: 'getList'.payload: listData }); }},Copy the code
Put receives an action as a parameter. We directly pass the defined data to the getList method in the Reducers through the payload in the action, and the getList transmits the payload to the page.
Since Reducers was the only place to connect to the front end page, we had to.
(4) The Reducer function receives and sends to the page layer
reducers: {
getList(state, action) {
return { listData: action.payload };
},
Copy the code
(5) Finally modify the page in users.tsx
Here is a copy of the component code from 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>),},];// Focus here
return <Table columns={columns} dataSource={props.users.listData} />;
};
Copy the code
We have crossed the data to the page via props. Users. ListData.
Pay attention to the point
There is a small problem that needs attention. Why did I return an object in the Reducer?
getList(state, action) {
return { listData: action.payload };
},
Copy the code
In the third step of defining the model, we returned 123 directly, and then got the passed data directly through props. Users.
But in this case, it would be logical for me to return the data and get the data directly from props. Users.
Because I ran into a bug here, I can see the state data through redux’s Chrome plugin.
There is indeed an array in the Users property. On the page, however, the users passed to props did not get this data.
So the solution here is to wrap it around an object when you return.
6.service.ts
The service file is mainly used for asynchronous functions, where we can write the logic of asynchronous functions.
Resolve is used to return data. In practice, ajax is used to send a request to return data, so we define a function in the service to get data asynchronously.
Since steps 1-5 used mock data, I used interface data here, so I created a new route.
├── bass Exercises ── ├── bass exercisesCopy the code
If it is convenient, write the new routing component and model data again in steps 1-5 to make it easier to understand.
I omit the process of redoing the component below, because the programs are similar, so I write the service interaction directly.
$ yarn add axios
Copy the code
// 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;
}
Copy the code
Asynchronous functions in the service file support async and await. The above interface is also provided by AspirantZhang and will not have cross-domain issues.
Finally, we need to call in the asynchronous function from the Service
// login/model.ts
effects: {*asyncGetData(action, effects) {
//const listData = yield Promise.resolve(data);
// Get the data from the actual interface
const {data} = yield effects.call(getRemote);
// Push the data to reducers with effects.put
yield effects.put({ type: 'getList'.payload: data }); }},Copy the code
If you’ve written it yourself and still have problems, check out my Github for a comparison
Page interaction with the repository
Let’s try modifying the data in the LOGIN component again. Start by creating a new folder and file
$ mkdir components && touch UseModal.tsx
Copy the code
Introduce components for Ant -d
import { Modal, Form, Input } from 'antd';
Copy the code
Write to the content page
<>
<Table columns={columns} dataSource={props.login.listData} rowKey="id" />
<UseModal showModal={showModal} visible={visible} />
</>
Copy the code
The content of the UseModal file is available on demand from Ant -D, so I won’t show you the code here. If you want to know more, you can get it directly from github library.
What I mainly show here is the process of using UMI for warehouse and page interaction. Knowing this process, the code is normal business logic.
It’s best to look at the code I’ve written together with the presentation of the page, otherwise it might get confusing. I have written comments in the source code, very detailed.
My current page looks like this
So what I want to do is change the name of the page by clicking OK, and the interface in the background looks like this
http://public-api-v1.aspirantzhang.com/users/? id=xx data:{name:' xxx'} method:PUTCopy the code
So I need to trigger a commit when I click OK, and it is recommended to commit using umiJS, which uses Dispatch to make changes to the data in the common repository.
So the flow of data here is
Page Dispath ==> Service ==> Effects ==> Reducers ==> pageCopy the code
The first step is to write the dispatch function on the page
The dispatch function can be obtained from props
Const onSubmit = (values) => {const {id} = record; SetConfirmLoading (true); SetTimeout (() => {// this is the logic of the page dispatch. // This is the logic of the page dispatch. 'login/asyncEditName', payload: { id: id, ... values }, }); // Return to false setConfirmLoading(false); // Pop-ups disappear showModal(); }, 1000); };Copy the code
[namespace]/functionName [namespace]/functionName]
The second step is to write axios code in the Service
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);
});
}
Copy the code
For the ID and data parameters, we can pass the second and third parameters of effect.call.
The third step is to write the logic in Effects
The second argument to call is passed to the function inside the service that takes the first argument
*asyncEditName({ payload }, effects) {
{id: XXX,payload:{name: XXX}}
yield effects.call(EditName, payload.id, payload);
// Put a new request to render the page, and let the page send another request to fetch data
yield effects.put({ type: 'asyncGetData' });
},
Copy the code
The result is something like this
Page Loading state
In a page, we often encounter an interaction that needs to be refreshed after loading, where the state can be obtained through the state passed to the page, which can be obtained in mapStateToProps
const mapStateToProps = (state) = > {
const { login, loading } = state;
// Loading Can obtain the loading state of asynchronism
return { login, userLoading: loading.models.login };
};
export default connect(mapStateToProps)(Login);
Copy the code
Once you get this number, you can use it on the page.
If you don’t want to do this, use the hooks of useSelector to get the values in state.
Annoying the connect
I got bored with Connect after writing a few components, whereas I could have used hooks when using React-Redux alone.
I tried two hooks and found that UMI had already wrapped them so that I could call the hooks directly from the function component without writing connect.
The provider and createStore are no longer needed, as they were done in model time.
Usage:
import { useDispatch, useSelector, Dispatch } from 'dva';
const Login = () = > {
// Don't use connect
const dispatch = useDispatch<Dispatch>();
const { login, loading } = useSelector((state: any) = > {
returnstate; }); ...Copy the code
The latter
Now the basic flow has gone, it is suggested to follow the flow to knock a code to deepen the impression, also can understand umi’s concept of data flow.
As for the business framework, I think UMI has encapsulated a set of best practices very well. The company is currently using this set of development back-end management systems, and the experience is good, and UMI itself is a bit of template writing. If your team thinks the React stack is too flexible, Projects are difficult to maintain and manage, so try this integration solution. I believe it will bring you a different surprise.
I’ll see you next time.