创作人 Leo
编辑时间 Fri Jun 5,2020 at 09:12
Umi 是可扩展的企业级前端应用框架。Umi 以路由为基础的,同时支持配置式路由和约定式路由,保证路由的功能完备,并以此进行功能扩展。然后配以生命周期完善的插件体系,覆盖从源码到构建产物的每个生命周期,支持各种功能扩展和业务需求。
学习 UMI 之前建议了解下:
推荐使用 yarn 创建一个 umi app
$ mkdir myapp && cd myapp
$ yarn create @umijs/umi-app
$ yarn
如果你使用 npm,可执行 npx @umijs/create-umi-app,效果一致。
安装 react 和 ant design 插件集(可选)
# 或 npm i @umijs/preset-react -D
$ yarn add @umijs/preset-react -D
路由、组件编写基础知识,参考 Ant Design of React / 项目实战
UMI 官方文档
这里我们实现一个简单的用户管理功能
页面包括:
涉及知识:
在 mock 文件夹下新建 api.ts
export default {
'Get /api/user/list':{
'users':[
{id:1,name:"leo",age:30} ,
{id:2,name:"lucy",age:18} ,
{id:3,name:"jack",age:60} ,
]
}
}
编辑后会自动热编译,如果没有,自行 yarn start 重启
浏览器访问 http://localhost:8003/api/user/list 可看到效果
{
"users": [
{
"id": 1,
"name": "leo",
"age": 30
},
{
"id": 2,
"name": "lucy",
"age": 18
},
{
"id": 3,
"name": "jack",
"age": 60
}
]
}
路由是 umi 框架自动解析,注意不要跟 .umirc 中的路由冲突
在实际工程中,我们经常会需要将公用的内容封装起来,比如页面的公用头部,尾部,菜单等
通过封装组件可以实现公用内容封装
一般情况下,我们把组件放在 components 文件夹
Header.tsx 是页面公用头部
import React from 'react'
import styles from './Header.less'
const logo = require('@/assets/timg.jpg')
class Header extends React.Component<any, any>{
render() {
return (
<div className={styles.header}>
<div className={styles.logo}>
<img src={logo} />
</div>
<div className={styles.title}>
<p className={styles.head_title}>Welcome to my site.</p>
<p className={styles.sub_title}>Believe yourself. You can do anything.</p>
</div>
</div>
);
}
}
export default Header ;
Header.less
.header{
width:100%;
height:200px;
background: wheat;
}
.logo{
float:left;
}
.logo img{
height:180px;
margin-top: 10px;
margin-left: 10px;
}
.title{
float:left;
color: #0f77e5;
margin-top: 10px;
margin-left: 10px;
}
.title .head_title{
font-size: 24px;
margin-bottom: 2px;
}
.title .sub_title{
font-size: 16px;
}
Menu.tsx 是页面的菜单列表
import React from 'react'
import styles from './Menu.less'
class Menu extends React.Component<any, any>{
render() {
return <div className={styles.menu}>
<p>菜单</p>
<ul>
<li><a href="/">用户列表</a></li>
<li><a href="/user/new">新建用户</a></li>
</ul>
</div>;
}
}
export default Menu ;
Menu.less
.menu{
width:200px;
height:800px ;
float:left;
background: aquamarine;
}
Public.tsx 我们将公用头部和菜单合并到这个公用页,然后通过将调用方提供的子内容填充进来,实现公用内容封装
import React from 'react'
import styles from './Public.less'
import Header from '@/components/Header';
import Menu from '@/components/Menu';
import { Button } from 'antd';
import Form from '@/components/RegisterForm';
class Public extends React.Component<any, any>{
constructor(props:any) {
super(props);
}
render() {
return <div className={styles.main}>
<Header />
<div>
<Menu />
<div className={styles.content}>
{this.props.children}
</div>
</div>
</div> ;
}
}
export default Public
Public.less
.public{
color: #e5b00f;
}
.main{
width:1200px;
margin:0 auto;
}
.content{
float:left;
margin-top: 10px;
margin-left: 10px;
}
现在写一个页面测试一下
新建一个页面并配置路由,不熟悉的同学,参考安装章节提到的官方文档
import React from 'react';
import Public from '@/components/Public';
import styles from './index2.less';
export default ()=>{
return <Public>
<h1>this is a test page</h1>
</Public>
}
效果
redux
umi 实现数据驱动基础是 redux
Redux 是 JavaScript 状态容器,提供可预测化的状态管理。
中文文档
redux-saga.js
redux-saga 是一个用于管理应用程序 Side Effect(副作用,例如异步获取数据,访问浏览器缓存等)的 library,它的目标是让副作用管理更容易,执行更高效,测试更简单,在处理故障时更容易。
中文文档
dva
umi 数据驱动框架用的是 dva
dva 首先是一个基于 redux 和 redux-saga 的数据流方案,然后为了简化开发体验,dva 还额外内置了 react-router 和 fetch,所以也可以理解为一个轻量级的应用框架。
中文文档
先看一个例子:
这个例子需要前面的 user/list mock 数据,没有的同学可以参照上一小节创建 user/list 数据
这里会用到 fetch,Promise,js生成器
1. fetch 是用来替代 ajax 访问 http 接口的方法,参考 使用 Fetch
2. Promise 对象用于表示一个异步操作的最终完成 (或失败), 及其结果值. 参考 Promise
3. js生成器 生成器函数提供了一个强大的选择:它允许你定义一个包含自有迭代算法的函数, 同时它可以自动维护自己的状态。参考 迭代器和生成器
我们通过访问 user/list 接口获取用户列表,并渲染到页面
知识点包括:
先上一段代码
function getdata(page:number, limit:number){
return new Promise((resolve, reject) => {
//http://localhost:8003
fetch(`/api/user/list?page=${page}&limit=${limit}`).then((response)=>{
return response.json() ;
}).then((jsonData)=>{
resolve(jsonData)
})
})
}
export default {
namespace: 'users',
state: {
userList:[]
},
effects:{
// @ts-ignore
*getUserList(action, { call, put }) {
console.log("get user list model ...", action.page)
const response = yield call(getdata);
yield put({
type: 'saveUserList',
payload: response,
});
},
},
reducers: {
// delete(state, { payload: id }) {
// return state.filter(item => item.id !== id);
// },
// @ts-ignore
saveUserList(state, action) {
console.log("save user list model ...")
return { ...state, userList: action.payload || [] };
},
},
}
这是一个 model,我们将它放在 models/users.ts,我们首先介绍下这段代码相关基础知识:
getdata 函数通过内部包装了一个 Promise 对象,并在 Promise 中通过 fetch 访问我们刚刚创建的 api /api/user/list
导出的对象是根据 dva 数据模型定义的:
namespace 外部访问需要用的命名空间
state 数据和默认值
effects 定义一些函数,用来处理异步动作,基于 Redux-saga 实现
reducers 定义一些函数,用来处理同步操作,可以看做是 state 的计算器
call(fn, …args)
call 函数是 Redux-saga 定义的
创建一个 Effect 描述信息,用来命令 middleware 以参数 args 调用函数 fn 。
fn: Function - 一个 Generator 函数, 也可以是一个返回 Promise 或任意其它值的普通函数。
args: Array<any> - 传递给 fn 的参数数组。
put(action)
put 函数是 Redux-saga 定义的
创建一个 Effect 描述信息,用来命令 middleware 向 Store 发起一个 action。 这个 effect 是非阻塞型的,并且所有向下游抛出的错误(例如在 reducer 中),都不会冒泡回到 saga 当中。
action: Object - 有关完整信息,请参见 Redux dispatch 的文档
…state
js 的展开语法 参考文章
这里用的是构造字面量对象时使用展开语法
var obj1 = { foo: 'bar', x: 42 };
var obj2 = { foo: 'baz', y: 13 };
var clonedObj = { ...obj1 };
// 克隆后的对象: { foo: "bar", x: 42 }
var mergedObj = { ...obj1, ...obj2 };
// 合并后的对象: { foo: "baz", x: 42, y: 13 }
通过示例代码我们来了解到,后面展开的内容会将前面展开的内容覆盖
好了,涉及到的基础知识介绍完了,下面简单对这个程序进行解读
数据模型定义好了,我们看下将它渲染到 UI
首先修改 index.jsx
import React, { useState } from 'react';
import Public from '@/components/Public';
import { connect } from 'umi';
import styles from './index.less'
class Logic extends React.Component {
constructor(props) {
super(props);
this.prevPageClicked = this.prevPageClicked.bind(this)
this.nextPageClicked = this.nextPageClicked.bind(this)
this.goPageClicked = this.goPageClicked.bind(this)
// this.state = {
// page:1
// } // Using the State Hook
this.page = 1
}
componentDidMount() {
console.log("componentDidMount called")
const { dispatch } = this.props;
console.log(dispatch)
if (dispatch) {
dispatch({
type: 'users/getUserList',
page: this.page,
});
}
}
prevPageClicked(event) {
const { dispatch } = this.props;
if (dispatch) {
dispatch({
type: 'users/getUserList',
page: this.page+1,
});
}
}
nextPageClicked(event) {
const { dispatch } = this.props;
if (dispatch) {
dispatch({
type: 'users/getUserList',
page: this.page-1
});
}
}
goPageClicked(event, p) {
const { dispatch } = this.props;
if (dispatch) {
dispatch({
type: 'users/getUserList',
page: p
});
}
}
render() {
const { userList } = this.props;
console.log("user list ", userList)
let listUsers = [] ;
let listPages = [] ;
if (userList.users && userList.users.length > 0) {
listUsers = userList.users.map((d)=>
<tr key={d.id}>
<td>{d.id}</td>
<td>{d.name}</td>
<td>{d.age}</td>
</tr>
);
// deal page info
this.page = userList.page_info.current ;
let pageTotal = Math.ceil(userList.page_info.amount/userList.page_info.limit) ;
for (let i=1; i<=pageTotal; i++) {
listPages.push(<button onClick={(e)=>{this.goPageClicked(e,i)}} key={i}>{i}</button> )
}
}
return (
<Public>
<div>
<table>
<tbody>
<tr>
<th>id</th>
<th>name</th>
<th>age</th>
</tr>
{listUsers}
</tbody>
</table>
</div>
<div className={styles.page_list}>
<button onClick={this.prevPageClicked}>上一页</button>
{listPages}
<button onClick={this.nextPageClicked}>下一页</button>
</div>
</Public>
);
}
}
// @ts-ignore
export default connect(({ users }) => ({
userList:users.userList,
}))(Logic);
index.less
.normal {
}
.title {
background: rgb(121, 242, 157);
}
.test {
color: wheat;
}
.page_list button {
margin-right:4px;
}
代码解读:
先看最后几行
export default connect(({ users }) => ({
userList:users.userList,
}))(Logic);
users 代表我们的数据模型,他跟 models/users.ts 中我们导出的命名空间 users 相对应
这段代码的意思是 我们将 users.userList 赋值给 userList(实际上可以直接传 users,这里示例是想说可以赋值部分数据)
然后将这些数据传递给 Logic ,也就是我们的渲染组件
其他的代码都是 react 相关的,这里简单说明:
下一步介绍如何接入后端提供的 API
服务端选择 golang 开发,使用 beego 框架
直接上代码
定义路由
package routers
import (
...
)
func init() {
...
apiRoute := &controllers.ApiController{}
beego.Any("/api/user/list", apiRoute.UserList)
...
}
编写程序 controllers/api.go
package controllers
import (
...
)
type ApiController struct {
BaseController
}
...
func (ctrl *ApiController) UserList(ctx *context.Context) {
pageStr := ctx.Input.Query("page")
limitStr := ctx.Input.Query("limit")
logs.Debug("recv page params page %s limit %s", pageStr, limitStr)
var page int = 1
var limit int = 10
p,e := strconv.Atoi(pageStr)
if e==nil && p>0 {
page = p
}
l,e := strconv.Atoi(limitStr)
if e==nil && l>0 {
limit = l
}
type L struct {
Id int32 `json:"id"`
Name string `json:"name"`
Age int32 `json:"age"`
}
result := make([]*L, 0)
total := 40
for i := 0; i < total; i++ {
result = append(result, &L{
Id: int32(i),
Name: "Leo",
Age: 10,
})
}
logs.Debug("%d/%d", total, limit)
pageTotal := int(int64(math.Ceil(float64(total)/float64(limit))))
if page > pageTotal {
page = pageTotal
}
if page<1 {
page=1
}
logs.Debug("recv page params page %v limit %v", page, limit)
start := (page-1)*limit
end := start+limit
logs.Debug("start %v end %v", start, end)
if end>total {
end = total
}
resp := map[string]interface{} {
"users": result[start:end] ,
"page_info":map[string]int{
"current": page,
"limit": limit,
"amount": total ,
} ,
}
err := ctx.Output.JSON(resp, true, true)
if err != nil {
ctx.Output.SetStatus(http.StatusInternalServerError)
}
}
...
接入前端
修改 model 中的函数 models/users.ts
function getdata(page:number, limit:number){
return new Promise((resolve, reject) => {
// http://localhost:8003
// /api/user/list
// http://localhost:8080/api/user/list
// + 修改:换成了 golang 后端的 api
fetch(`http://localhost:8080/api/user/list?page=${page}&limit=${limit}`).then((response)=>{
return response.json() ;
}).then((jsonData)=>{
resolve(jsonData)
})
})
}
export default {
namespace: 'users',
state: {
userList:[]
},
effects:{
// @ts-ignore
*getUserList(action, { call, put }) {
console.log("get user list model ...", action.page)
// +修改:传入分页参数
const response = yield call(getdata, action.page, 5);
yield put({
type: 'saveUserList',
payload: response,
});
},
},
reducers: {
// delete(state, { payload: id }) {
// return state.filter(item => item.id !== id);
// },
// @ts-ignore
saveUserList(state, action) {
console.log("save user list model ...")
return { ...state, userList: action.payload || [] };
},
},
}
修改好后启动 go 服务端,启动 mui 前端,刷新页面,即可看到效果,点一点分页,观察下网络访问输出
本文章示例代码托管在GITHUB:
前端:https://github.com/intogosrc/antd-umi-demo
后端:https://github.com/intogosrc/beego-test
下一篇我们将接入数据库,并对数据进行增删改查
感谢小伙伴阅读(^▽^)