umi 框架学习笔记(一)

创作人 Leo


编辑时间 Sun Jun 21,2020 at 13:30


?

Umi 是可扩展的企业级前端应用框架。Umi 以路由为基础的,同时支持配置式路由和约定式路由,保证路由的功能完备,并以此进行功能扩展。然后配以生命周期完善的插件体系,覆盖从源码到构建产物的每个生命周期,支持各种功能扩展和业务需求。

学习 UMI 之前建议了解下:

  1. react
  2. redux
  3. redux-saga
  4. dva
  5. mock

安装

推荐使用 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 官方文档

简单教程

这里我们实现一个简单的用户管理功能

页面包括:

  1. 提交用户信息页面
  2. 查看用户信息页面

涉及知识:

  1. 基本 umi 使用
  2. 使用 mock 前后端分离开发
  3. 编写组件
  4. 拆分公用框架,Header,Footer,Menu 等
  5. 组合多个组件为完整页面

mock 数据编写

在 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>
}

效果

alt 测试页面

数据驱动

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 接口获取用户列表,并渲染到页面
知识点包括:

  1. 调用 Mock 定义数据
  2. 定义数据模型 Model
  3. 访问线上 api

先上一段代码

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 }

通过示例代码我们来了解到,后面展开的内容会将前面展开的内容覆盖

好了,涉及到的基础知识介绍完了,下面简单对这个程序进行解读

  1. 首先我们定义了 getdata 函数用来访问api并返回数据
  2. 我们导出了一个 dva 对象,用来描述模型相关信息
  3. 我们定义了 users.userList,它是 users 命名空间下代表一个用户列表
  4. 在 getUserList 异步函数中,我们通过调用 getdata 获取用户列表,并传递给 saveUserList 同步函数
  5. saveUserList 返回的值会修改 state 中对应的值,这里我们把 userList 改成了 api 返回的数据

UI 端数据调用

数据模型定义好了,我们看下将它渲染到 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 相关的,这里简单说明:

  1. model 数据会绑定到组件的 props
  2. 我们在组件生命周期函数 componentDidMount 中对 State 进行 dispatch,这里调用 users/getUserList ,即为 users 命名空间的数据模型中 getUserList 异步函数,其中 page 为传给 getUserList 的参数。这样就更新了数据模型 users.userList 的数据
  3. prevPageClicked 等函数是分页的点击事件,在构造函数中我们将这些函数进行 bind(this) 操作
  4. 当绑定到组建的 props 更新,react 会执行对应的 render 渲染页面,通过 userList.users.map 对返回的用户列表进行迭代,并放到数组,在需要显示处插入数组,即可实现列表渲染,这部分不明白需要复习 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
下一篇我们将接入数据库,并对数据进行增删改查
感谢小伙伴阅读(^▽^)


阅读:1608
搜索
  • Linux 高性能网络编程库 Libevent 简介和示例 2679
  • web rtc 学习笔记(一) 2591
  • react 学习笔记(一) 2490
  • Mac系统编译PHP7【20190929更新】 2388
  • zksync 和 layer2 2370
  • Hadoop Map Reduce 案例:好友推荐 2288
  • Hadoop 高可用集群搭建 (Hadoop HA) 2275
  • 小白鼠问题 2213
  • Linux 常用命令 2178
  • 安徽黄山游 2153
简介
不定期分享软件开发经验,生活经验