主页

框架环境和基本使用

Umi 是蚂蚁金服的底层前端框架, 是可扩展的企业级前端应用框架, 内置了路由、构建、部署、测试, 包含组件打包、文档工具、请求库、hooks 库、数据流等 , 通过框架的方式简化 React 开发

知识结构图

官网:https://v3.umijs.org/zh-CN

环境准备,快速上手

准备工作:由于国内网络和前端的特殊性,在安装依赖等方面可能会失败或导致无法启动,浪费大量的时间,推荐使用 yarn 作为包管理器,并且使用国内镜像,推荐 yrm 这个工具管理 yarn 镜像

# 安装
npm install -g yrm

# 查看 yarn 镜像源
yrm ls

# 切换源
yrm use taobao

项目初始化

新建一个空目录,在空目录里面执行命令

# 使用 yarn 安装 umi 环境
yarn create @umijs/umi-app

# 安装依赖
cd 目录
yarn

# 启动项目
yarn start

在浏览器里打开 http://localhost:8000,能看到以下界面:

目录结构

├── dist                        // 默认的 build 输出目录
├── mock                        // mock 文件所在目录,基于 express
├── config
    ├── config.js               // umi 配置,同 .umirc.js,二选一
├── public                      // 变通的数据资源目录和一些无需打包的资源
└── src                         // 源码目录
    ├── layouts/index.js        // 全局布局
    ├── models                  // 数据流
    ├── wrappers                // 权限管理
    ├── pages                   // 页面目录,里面的文件即路由
        ├── .umi                // dev 临时目录,需添加到 .gitignore
        ├── .umi-production     // build 临时目录,会自动删除
        ├── document.ejs        // HTML 模板
        ├── 404.js              // 404 页面
        ├── page1.js            // 页面 1,任意命名,导出 react 组件
        ├── page1.test.js       // 测试用例文件
        └── page2               // 页面 2,内部可含有
    ├── global.css              // 约定的全局样式文件,自动引入,也可以用 global.less
    ├── global.js               // 可以在这里加入 polyfill
    ├── app.js                  // 运行时配置文件
├── .umirc.js                   // umi 配置,同 config/config.js,二选一
├── .env                        // 环境变量
└── package.json

构建时配置


构建时是对开发环境配置,如果项目的配置不复杂,推荐在 .umirc.ts 中写配置; 如果项目的配置比较复杂,可以将配置写在config/config.ts 中,并把配置的一部分拆分出去,现实往往是复杂的所以推荐 config/config,两种配置方式二选一,.umirc.ts 优先级更高,采用 config 配置时,一般删除 .umirc.ts

import { defineConfig } from 'umi';
import proxy from './proxy';
import routes from './routes';
import theme from './theme'

export default defineConfig({
  nodeModulesTransform: { // node_modules 目录下依赖文件的编译方式
    type: 'none', // all 慢 兼容性好 none 快 兼容性一般
  },
  mfsu: {}, // 打包提速
  fastRefresh: {}, // 快速刷新 可以保持组件状态,同时编辑提供即时反馈
  title:'UMI3', // 配置标题。
  mountElementId: 'app', // 指定 react app 渲染到的 HTML 元素 id
  favicon: '/favicon.ico', // 使用本地的图片,图片请放到 public 目录

  routes: routes,

  proxy:proxy, // 配置反向代理

  // 启用按需加载
  dynamicImport: {
    loading: '@/components/loading', // 按需加载时指定的 loading 组件
  },

  theme, // 配置主题,实际上是配 less 变量
  devServer: {
    port: 8666, // .env 里面权限更高一些
    // https:true, // 启用 https 安全访问,于对应协议服务器通讯
  }
})

模板约定

umi 内部默认没有 html,会自行生成,如果需要修改, 新建 src/pages/document.ejs

<!doctype html>
<html>
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width">
  <title>Your App</title>
</head>
<body>
  <div id="app"></div>
</body>
</html>

antd, antd-mobile 使用


umi 已整合 antd 组件库,可通过 import {Xxx} from 'antd' 使用

使用指定版本组件库,yarn add xx-xx@x.x.x 后,会优先使用指定版本

antd 主题设定

找到 config/theme

export default {
  "@primary-color": "pink" // antd 全局样式 copy 过来统一修改
};
antd 样式变量:https://ant.design/docs/react/customize-theme-cn

antd-mobile

更新 @umijs/preset-react 到最新版本
umi 已整合 antd-mobile 组件库,可通过 import {Xxx} from 'antd-mobile' 使用 v5 版本,可通过 import {Xxx} from 'antd-mobile-v2' 使用 v2 版本,使用指定版本组件库,yarn add xx-xx@x.x.x 后,会优先使用指定版本,推荐使用 v5

使用 v5 版本报错,找不到被使用的组件时尝试:

  • 删除 .umi
  • 更新 @umijs/preset-react
  • 关闭 mfsu
  • 重启

antd-mobile v5 主题修改

/* src/global.less */
:root:root {
  --adm-color-primary: hotpink; // antd-mobile 全局样式 copy 过来统一修改
}

antd-moblid 提供的全局变量:https://mobile.ant.design/zh/guide/theming/

antd-mobile v2 主题修改和 antd 一样

图片和其他资源引入

项目中使用图片有两种方式:

  1. 先把图片传到 cdn,然后在 JS 和 CSS 中使用图片的绝对路径
  2. 把图片放在项目里,然后在 JS 和 CSS 中通过相对路径的方式使用
前者趋向数据图片,后者趋向写死的图片,通过相对路径引入图片的时候,如果图片小于 10K,会被编译为 Base64 嵌入网页
// src/pages/index.tsx
import styles from './index.less';
import { Button } from 'antd';
import { Button as V2Button } from 'antd-mobile-v2';
import { Button as V5Button } from 'antd-mobile';

import user from '../assets/images/userBj.png'

export default function IndexPage() {
  return (
    <div>
      <h1 className={styles.title}>Page index</h1>
      <Button type='primary'>Button</Button>

      <V2Button type='primary' size='small'>V2Button</V2Button>

      <V5Button color='primary'>Purple</V5Button>

      <div>
        <h3>写死</h3>
        <img src={user} />
        <img src={require('../assets/images/userBj.png')} />

        <h3>动态</h3>
        {/*动态图片 丢到服务器 推荐,或临时指向 public 不推荐*/}
        <img src="/img/bg.jpg" alt="" style={{ width: 100 }} />

        <h3>样式内部使用</h3>
        <div className={styles.test} style={{ height: 100 }}></div>
      </div>
    </div>
  );
}
// src/pages/index.less
.title {
  background: rgb(121, 242, 157);
}

.test{
  // background: url('../assets/images/bg.jpg');
  // 别名效果 在 css 里面指向 src 目录需要使用 ~@
  background: url('~@/assets/images/bg.jpg');
  // background: url('cdn 链接');
  // background: url('/img/bg.jpg');
}

组书写风格与页面跳转

Less 变量,混合,嵌套,父选择器

框架 自带了 less,css 及模块化的解析工具 ,推荐模块化使用 lessimport styles from 'xx.less 避免了全局污染 和 选择器复杂

模块化的基本原理很简单,就是对每个类名(非 :global 声明的)按照一定规则进行转换,保证它的唯一性。如果在浏览器里查看这个示例的 dom 结构,你会发现实际渲染出来是这样的:

<div class="title___3TqAx">title</div>

类名被自动添加了一个 hash 值,这保证了它的唯一性。

实际开发中简单的样式我们并不推荐写 css,推荐使用模板组件来进行开发,或者直接写行内 css。css 并没有很好的依赖关系,很多项目都有冗余的 css,但是却不敢删除

全局变量

// src/global.less

{/* title 为 global.less 配置的整体样式 */}
<h2 className="title">全局样式</h2>


@import '~antd/es/style/themes/default.less';
// 使用 default 的变量
.myLink {
  color: @primary-color;
  font-size: @font-size-base;
}

<div className='myLink'>测试</div>

局部变量

@width: 100px;
@height: @width - 80px;

.header {
  width: @width;
  height: @height;
  background: pink;
}

混合

.bordered (@topW: 1px, @bottomW: 2px) {
  border-top: dotted @topW black;
  border-bottom: solid @bottomW black;
}

#a1 {
  color: #111;
  .bordered();
}

.a2 {
  color: red;
  .bordered(2px, 4px);
  // border-bottom: solid 5px black; // 覆盖混合
}

嵌套

.nesting {
  color: blue;
  h3 {
    color: coral;
  }
  p {
    color: aqua;
  }
}

:global

/* 定义多个全局样式 */
.bars_right {
  font-weight: bold;
  :global {
    .ant-btn {
      border: 0;
    }
    .title {
      background: #fafafa;
    }
  }
}

<div className={styles.bars_right}>
    <button className={`ant-btn`}>按钮</button>
</div>

hooks + 函数式编写组件

function 组件(){}
const 组件 = (props) = >{
  
  // 使用 hooks
  
  // 定义 函数 变量
  
  return jsx
}

useContext

组件上下文共享,越级传递数据,响应式

// 创建上下文 context.jsx 
import {createContext} from 'react'
const AppContext = createContext({})
export default AppContext;

// 祖先组件提供上文 parent.jsx
function 父组件() {
  const [msg, setMsg] = useState('hooks 组件数据')
  return (
      <AppContext.Provider value={{ msg, setMsg }}>
      ....
      <子组件 />
      ...
    </AppContext.Provider>
  )
}



// 后代组件接受下文 child.jsx
import { useContext } from "react";
import AppContext from "../context";

function 后代组件(){
  const {msg,setMsg} = useContext(AppContext);
  return (
    <>
      <div>{msg}</div>
      <button onClick={setMsg}>按钮</button>
    </>
  )
}

useMemo

hooks 出来之后,我们能够使用 function 的形式来创建包含内部 state 的组件。但是,使用 function 的形式,失去 shouldComponentUpdate,我们无法通过判断前后状态来决定是否更新。在函数组件中,react 不再区分 mount 和 update 两个状态,函数组件的每一次调用都会执行其内部的所有逻辑,如下:

export default function Xxx() {
  
  const [count, setCount] = useState(1);
  const [value, setValue] = useState('');
  
  function getNum() {
    console.log("getNum");
    return count * 100
  }
     
  return (
    {/* 组件任何一条数据变化,getNum 函数重复调用 */}
    <div>getNum:{getNum()}</div>
    <button onClick={() => setCount(count + 1)}>+1</button>
    <input value={value} onChange={ev => setValue(ev.target.value)} />
  )
}

那么会带来较大的性能损耗。useMemo 可指定依赖的数据变化才渲染,类似 vue 计算属性,返回缓存后的值数据,可拿去渲染

export default function Xxx() {
  
  const [count, setCount] = useState(1);
  const [value, setValue] = useState('');

  const getNumMemo = useMemo(() => {
    // 可执行一些没有副作用的业务,比如同步重新计算 count
    return count * 100
  }, [count])

  return (
    {/* 只有 count 数据变化,getNumMemo 函数才会调用 */}
    <div>getNum:{getNumMemo}</div>
    <button onClick={() => setCount(count + 1)}>+1</button>
    <input value={value} onChange={ev => setValue(ev.target.value)} />
  )
}

memo

react 父组件更新未传递给子的数据,子组件也会更新,如下:

// 修改 count 或者 value 时,child 组件都会更新
<button onClick={() => setCount(count + 1)}>+1</button>
<input value={value} onChange={ev => setValue(ev.target.value)} />
<Child count={count} />

memo 可以协助子组件只依赖传递过来的数据变化时才更新

import {memo} from 'react'

function Child({count}){
  const show = () => console.log('child 组件渲染')
  return (
    <>
      <h3>Child2 组件</h3>
      <div>{show()}</div>
      <div>{count}</div>
    </>
  )
}
// memo 修饰当前组件 依赖父的数据变化时,才渲染
export default memo(Child)

// 不包装的情况下,父任意数据更新子都会更新
// export default Child

useCallback

由于组件内的业务函数传递给子组件时,每次都会是新的引用,会导致子组件无故更新,如下:

父组件

// 修改 count 或者 value 时,child 组件都会更新
<button onClick={() => setCount(count + 1)}>+1</button>
<input value={value} onChange={ev => setValue(ev.target.value)} />
<Child updateCount=(()=>console.log('业务')) />

子组件

import {memo} from 'react'
function Child({updateCount}){
  const show = () => console.log('child 组件渲染')
  return (
    <>
      <h3>Child3 组件</h3>
      <div>{show()}</div>
      <button onClick={updateCount}>测</button>
    </>
  )
}
// memo 修饰当前组件 依赖父的数据变化时,才渲染 但依赖父的是个函数时 memo 无效
export default memo(Child)

useCallback 可以将函数缓存起来,节省性能,指定某个被依赖的数据变化才更新函数,子组件配合 memo 实现,如下:

export default () => {
  
  const updateCount = useCallback(()=>{
    // 业务
  },[])

  return (
     //修改 count 或者 value 时,child 组件都会更新
    <button onClick={() => setCount(count + 1)}>+1</button>
    <input value={value} onChange={ev => setValue(ev.target.value)} />
    <Child updateCount={updateCount} />
  )
}

useLayoutEffect

useLayoutEffect 早于类组件早于 useEffect

挂载时

类 render → 函数 render → useLayoutEffect→ 类 didmount → useEffect

更新时

类 render 渲染 → 函数 render → useLayoutEffect 销毁→ useLayoutEffect 执行→ 类 didUpdate → useEffect 销毁… → useEffect 执行

路由,权限,动态,约定式

页面地址的跳转都是在浏览器端完成的,不会重新请求服务端获取 html,html 只在应用初始化时加载一次 ,页面由不同的组件构成,页面的切换其实就是不同组件的切换, 只需要在把不同的路由路径和对应的组件关联上 ,实现方式如下两种

  1. 配置型路由(在配置文件写入相关配置代码),配置型存在时,约定式失效
  2. 约定式(约定文件位置名称与格式无需写代码配置
    约定式是理想型方案,实际开发一般会向现实低头,推荐采用配置型路由

配置 config/configroutes 属性,接受数组,一般单独写一个 routes 模块文件如下:

//  config/routes
export default [
  { path: '/less', component: 'less' }, // 不写路径从 src/pages找组件
  { path: '/antd', component: './antd' }, // 当前指向 pages
  { component: '@/pages/404' }, // @ 指向 src
]

export default [
    { path: '/login', component: 'login' },
    { path: '/reg', component: './reg' },

    {
      path: '/',
      component: '@/layouts/layout1',// layout 组件
      routes: [
        { path: '/less', component: 'less' },
        { path: '/antd', component: 'antd' },
        {  path:'/', redirect: '/antd' },
        { component: '@/pages/404' },
      ],
    },

    { component: '@/pages/404' },


  ]

[^routes]: 配置子路由,通常在需要为多个路径增加 layout 组件时使用

// layouts/布局组件

// 可引入一些 components 下的公共组件来完成公共布局
import Nav1 from '../../components/nav1'
import styles from './index.less'

const Layout1 = props => {
  return (
    <>
      {props.children}
      <div className={styles['adm-tab-bar']}><Nav1/></div>
    </>
  )
}

export default Layout1;
{ path: '/user', component: 'user',wrappers:['@/wrappers/auth']}, // 路由守卫
// wrappers/auth
import { Redirect } from 'umi'

export default (props) => {
  if (Math.random() < .5) {
    return <div>{props.children}</div>;
  } else {
    return <Redirect to="/login" />;
  }
}

多级子路由

{ 
  path: '/goods', 
  component: '@/layouts/layout2', // 展示区
  routes:[
    // { path: '/goods', component: 'goods' },
    // { path:'/goods', redirect: '/goods/2' }, // 这里的
    { path: '/goods/:id?', component: 'goods/goods-detail' }, // 动态可选路由
    { path: '/goods/:id/comment', component: 'goods/comment' }, // 不配 routes,占用当前展示区
    { path: '/goods/:id/comment/:cid', component: 'goods/comment/comment-detail' },
    { component: '@/pages/404' },
  ]
}

页面跳转,参数接收

声明式跳转 + 传参

const nav1 = ()=>{
  return (
      <NavLink activeClassName={styles.xx} to="/antd">antd</NavLink>
      <NavLink activeStyle={{color:'#399'}} to="/antd">antd</NavLink>
      <Link to="/antd">antd</Link>
      <Link to={{pathname:'/antd',search:'?a=1',query:{a:1}}}>antd</Link>
  )
}

编程式跳转 + 传参

// history 可以导入或者上下文获取
import { history } from 'umi';
const 组件({history})=>{}

// 跳转到指定路由
history.push('/list');

// 带参数跳转到指定路由
history.push('/list?a=b');
history.push({
  pathname: '/list',
  query: {
    a: 'b',
  },
});

// 跳转到上一个路由
history.go(-1);

参数接收

// 可以从组件上下文获取
const 组件 = ({location,match})=>{}

// 如果没有上下文可以 withRouter 包装组件
import { withRouter } from 'umi';
const withRouter({location,match})=>{}

// 可以直接使用 umi 的 hooks 获取
import { useLocation,useParams,useRouteMatch} from 'umi';
const 组件 = ()=>{
  const params = useParams();
  params.id | params.cid
  const location = useLocation();
  location.search | location.query
}
相关 API:https://v3.umijs.org/zh-CN/api

数据生成与请求

数据模拟 umi-mock

Mock 数据是前端开发过程中必不可少的一环,是分离前后端开发的关键链路。通过预先跟服务器端约定好的接口,模拟请求数据甚至逻辑,能够让前端开发独立自主,不会被服务端的开发所阻塞

UMI3 里约定 mock 文件夹下的文件或者 page(s) 文件夹下的 _mock 文件即 mock 文件,文件导出接口定义,支持基于 require 动态分析的实时刷新,支持 ES6 语法,以及友好的出错提示。

export default {
  // 支持值为 Object 和 Array
  'GET /api/users': { users: [1, 2] },
  
  // GET 可省略
  '/api/users/1': { id: 1 },

  // 支持自定义函数,API 参考 express@4,可完成业务
  'POST /api/users/create': (req, res) => {res.end('OK'); },
};

当客户端(浏览器)发送请求,如:GET /api/users,那么本地启动的 umi dev 会跟此配置文件匹配请求路径以及方法,如果匹配到了,就会将请求通过配置处理,就可以像样例一样,你可以直接返回数据

Mock.js 辅助生成自然且多条数据

import Mock from 'mockjs';

export default {
  // 使用 mockjs 等三方库
  'GET /api/tags': Mock.mock({
    'list|100': [{ name: '@city', 'value|1-100': 50, 'type|0-2': 1 }],
  }),
};

对于整个系统来说,请求接口是复杂并且繁多的,为了处理大量模拟请求的场景,我们通常把每一个数据模型抽象成一个文件,统一放在 mock 的文件夹中,然后他们会自动被引入。

为了更加真实的模拟网络数据请求,往往需要模拟网络延迟时间,可以通过第三方插件来简化这个问题,如:roadhog-api-doc#delay

import { delay } from 'roadhog-api-doc'; // 模拟延时

export default delay(
  {
    // 支持值为 Object 和 Array
    '/umi/goods': [
      { id: 1, name: '韭菜' },
      { id: 2, name: '西红柿' },
    ],
  },
  2000,
); // 延时

鉴权

// mock/auth
export default {
  'POST /umi/login': (req, res) => {
    const { username, password } = req.body;
    if (username === 'joe' && password === 'qqqqqq') {
      res.send({
        err: 0,
        msg: '成功',
        currentAuthority: 'user',
      });
    } else if (username === 'admin' && password === 'admin123') {
      res.send({
        err: 0,
        msg: '成功',
        currentAuthority: 'admin',
      });
    } else {
      res.send({
        err: 1,
        msg: '失败',
      });
    }
  },
};

分页

// 查分页
// 指定页数范围内显示全数据,超过只显示两条
'GET /umi/list': (req, res) => {
  const { _page = 1, _limit = 3 } = req.query;

  const totalPage = 3; // 设定总页数
  const lastPageLimit = 2; // 设定尾页条数
  const total = _limit * (totalPage - 1) + lastPageLimit; // 计算总条数

  res.send({
    code: 0,
    data: {
      _page,
      _limit,
      total,
      // 控制 data 键,后面数组的条数
      ...Mock.mock({
        [`data|${_page >= totalPage ? lastPageLimit : _limit}`]: [
          {
            'id|+1': 1,
            create_at: '@date("yyyy-MM-dd HHss")',
            'type_str|1': [
              '中转费明细',
              '调整单明细',
              '代收到付明细',
              '客户运费明细',
              '导入失败记录',
            ],
            name: function () {
              return [
                Mock.mock('@datetime("MMdd")'),
                Mock.mock('@county()'),
                this.operator,
              ].join('-');
            },
            path: 'http://xxx/shop/quotation/导入失败列表.xlsx',
            operator: '@cname',
            'status|1': ['0', '1', '2', '3'],
          },
        ],
      }),
    },
  });
},

增删改

// 增
'POST /umi/list': (req, res) => {
  console.log(req.body);
  res.send(
    Mock.mock({
      'data|1': [
        {
          code: 0,
          data: { ...req.body, a: 2 },
          msg: '成功',
        },
        {
          code: 1,
          msg: '失败',
        },
      ],
    }).data,
  );
},

// 删
'DELETE /umi/list/:id': (req, res) => {
  console.log(req.params.id);
  res.send(
    Mock.mock({
      'data|1': [
        {
          code: 0,
          data: { task_id: '123' },
          msg: '成功',
        },
        {
          code: 1,
          msg: '失败',
        },
      ],
    }).data,
  );
},
  
// 改
'PATCH /umi/list/:id': (req, res) => {
  console.log(req.body);
  res.send(
    Mock.mock({
      'data|1': [
        {
          code: 0,
          data: { ...req.body },
          msg: '成功',
        },
        {
          code: 1,
          msg: '失败',
        },
      ],
    }).data,
  );
},

数据模拟 json-server

一款第三方模拟服务器和数据的包,支持 json 文件存本地被修改,自动生成 resut 风格可操作的接口,有效的 CURD 操作,对数据要求高时,推荐使用

https://www.npmjs.com/package/json-server?activeTab=readme
// jsonserver/db.js
// 用 mockjs 模拟生成数据
const Mock = require('mockjs')
let mr = Mock.Random // 提取 mock 的随机对象

module.exports = () => 
  Mock.mock({
    'goods|3': [{
      'id|+1': 100,
      title: '@ctitle(6,10)',
      des: 'csentence(10,20)',
      time: 'integer()',
      detail: {
        auth: '@cname()',
        auth_icon: mr.image('50x50',mr.color(),mr.cword(1))
      }
    }]
  })

// 使用 app.js
module.exports = {
  ...Mock.mock({
    'goods|3': [{
      'id|+1': 100,
      title: '@ctitle(6,10)',
      des: 'csentence(10,20)',
      time: 'integer()',
      detail: {
        auth: '@cname()',
        auth_icon: mr.image('50x50',mr.color(),mr.cword(1))
      }
    }]
  })
}

启动 json-server 服务 json-server ./jsonserver/db.js

// jsonserver/app.js
const jsonServer = require('json-server'); // 在 node 里面使用 json-server 包
const db = require('./db.js'); // 引入 mockjs 配置模块  需要暴露一个对象
const path = require('path');
const Mock = require('mockjs');

let port = 3333; // 端口
let mock = '/mock' // 接口别名

// 创建服务器
const server = jsonServer.create(); // 创建 jsonserver 服务对象  ~~ express()


// 配置 jsonserver 服务器 中间件
server.use(jsonServer.defaults({
  static: path.join(__dirname, '/public'), // 静态资源托管
}));

server.use(jsonServer.bodyParser); // 抓取 body 数据使用 json-server 中间件


// 响应
server.use((request, res, next) => { // 可选 统一修改请求方式
  // console.log(1)
  // request.method = 'GET';
  // 校验token
  next();
});

// 登录注册校验 模拟 db.js 接口 , 多出逻辑
let mr = Mock.Random;//提取mock的随机对象
server.post(mock + '/login', (req, res) => {

  let username = req.body.username;
  let password = req.body.password;
  (username === 'joe' && password === 'qqqqqq') ?
    res.jsonp({ // json-server 返回数据的一个 api 而已
      "err": 0,
      "msg": "登录成功",
      "data": {
        "follow": mr.integer(1, 5),
        "fans": mr.integer(1, 5),
        "nikename": mr.cname(),
        "icon": mr.image('20x20', mr.color(), mr.cword(1)),
        "time": mr.integer(13, 13),
        "token": mr.integer(25)
      }
    }) :
    res.jsonp({
      "err": 1,
      "msg": "登录失败",
    })

});
server.post(mock + '/reg', (req, res) => {

  let username = req.body.username;
  (username !== 'alex') ?
    res.jsonp({
      "err": 0,
      "msg": "注册成功",
      "data": {
        "follow": mr.integer(0, 0),
        "fans": mr.integer(0, 0),
        "nikename": mr.cname(),
        "icon": mr.image('20x20', mr.color(), mr.cword(1)),
        "time": mr.integer(13, 13)
      }
    }) :
    res.jsonp({
      "err": 1,
      "msg": "注册失败",
    })

});

// 响应 mock 接口 自定义返回结构 定义 mock 接口别名

const router = jsonServer.router(db); // 创建路由对象 db 为 mock 接口路由配置  db==object


router.render = (req, res) => { // 自定义返回结构
  let len = Object.keys(res.locals.data).length; // 判断数据是不是空数组和空对象
  // console.log(len);

  setTimeout(() => { // 模拟服务器延时
    res.jsonp({
      err: len !== 0 ? 0 : 1,
      msg: len !== 0 ? '成功' : '失败',
      data: res.locals.data
    })
  }, 1000)

  // res.jsonp(res.locals.data)

};


server.use(jsonServer.rewriter({ // 路由自定义别名
  [mock + "/*"]: "/$1",
  "/course_category\\?uid=:id": "/course_category/:id",
  "/posts/:category": "/posts?category=:category",
  "/articles\\?id=:id": "/posts/:id"

}));

server.use(router); // 路由响应



// 开启 jsonserver 服务
server.listen(port, () => {
  console.log('mock server is running')
});

启动 app.js 文件 node ./jsonserver/app.js

反向代理

工作中前后端是分离式开发,需要访问本地或者线上真实服务器时,跨域也成了每个前端工程师都需要了解的基本知识,解决方案有前端或者后端开发人员来解决

在 UMI3 中配置 config/configproxy 键,接受一个对象,可单独做一个模块到 config/proxy,并暴露出来

export default {
  '/api/': {
    // 要代理的真实服务器地址
    target: 'https://localhost:9001',
    // 配置了这个可以从 http 代理到 https
    https:true
    // 依赖 origin 的功能可能需要这个,比如 cookie
    changeOrigin: true,
    pathRewrite: { '^/api': '' }, // 路径替换
  } 
}

fetch 请求

fetch是原生的数据请求方法,返回 promise

const getData = async () => {
    //let res = await fetch('/umi/goods/home?_limit=3'); 
        let res = await fetch(
    '/umi/login',{
      method:'post',
      headers:{"Content-type":"application/x-www-form-urlencoded"},
      body:'username=alex&password=alex123'
    });
    let data = await res.json();
    console.log(data);
};

umi-request 请求

通过 import { request } from 'umi'; 你可以使用内置的请求方法。第一个参数是 url,第二个参数是请求的 options。options 具体格式参考 umi-request,也和 axios 用法基本一致

const getData = async () => {
  let res = await request('/umi/goods',{params:{_limit:1}})
  // let res = await request('/api/goods/home',{params:{_limit:1}})
  console.log(res)
}

useRequest 请求

useRequest 是最佳实践中内置的一个 Hook ,默认对数据要求必须返回一个 data 字段,如果不希望有此限定,可以构建时配置一下 config/config

request: {
  dataField: '',
},

在组件初次加载时, 自动触发该函数执行。同时 useRequest 会自动管理异步请求的 loadingdataerror 等状态。

import {useRequest} from 'umi'

export default function  RequestHooks(){

  // 用法 1
  const { data, error, loading } = useRequest('/umi/goods');

    // 用法 2
  const { data, error, loading } = useRequest({
    url: '/umi/goods',
    params:{_limit:1}
  });

    // 用法 4
  const { data, loading, run } = useRequest((_limit) => ({
    url: '/umi/goods',
    params: { _limit }
  }), {
    manual: true, // 手动通过运行 run 触发
  });
  
  // 轮询
  const { data, loading, run } = useRequest((_limit) => ({
    url: '/umi/goods',
    params: { _limit }
  }), {
    manual: true, // 手动通过运行 run 触发
    pollingInterval:1000, // 轮询 一秒读一次
    pollingWhenHidden:false, // 屏幕不可见时,暂停轮询
  });
  
  if (error) {
    return <div>failed to load</div>
  }

  if (loading) {
    return <div>loading...</div>
  }

  return (
    <div>{JSON.stringify(data)}</div>
    <button onClick={()=>run(1)}>手动</button>
  );
}

状态管理

dva 介绍

dva 首先是一个基于 reduxredux-saga 的数据流方案,被 umi 以插件的方式内置,无需安装直接使用,在原有 redux 使用基础上更加简化和高效

dva 里面有关状态管理(数据流)的角色和redux的对比如下:

reduxdva
状态数据statestate
行为描述actionaction
无副作用业务reducerreducer
有副作用业务creatorseffect
通讯请求修改状态函数dispatchdispatch
通讯请求获取状态函数connectconnect
获取数据subscription
  • [^状态数据]: javascript 对象,存公共状态的仓库
  • [^行为描述]: javascript 对象,必须带有 type 属性指明具体的行为,其它字段可以自定义
  • [^dispatch]: 用于触发 action 的函数
  • [^无副作用业务]: 一个纯函数,处理公共状态时的一些同步业务
  • [^有副作用业务]: 处理公共状态时的一些异步业务

数据流向

数据的改变发生通常是通过用户交互行为或者浏览器行为(如路由跳转等)触发的,当此类行为会改变数据的时候可以通过 dispatch 发起一个 action,如果是同步行为会直接通过 Reducers 改变 State ,如果是异步行为(副作用)会先触发 Effects 然后流向 Reducers 最终改变 State,最后 State 的数据再流回组件页面

定义一个 dva 的 Model 如下:

export default {
  namespace:'Model名', // 省略不写,认定文件名为 Model 名
  state:{公共状态数据},
  reducers:{一堆纯函数},
  effects:{一堆异步副作用函数},
  subscription:{一堆监听函数}
}

全局数据&页面数据获取和修改

全局数据定义在 src/models/XX,所有页面都可以访问,同步业务的处理交给 reducers

import { history, request } from 'umi';
import key from 'keymaster';

export default {
  
  namespace: 'global', // 所有 models 里面的 namespace 不能重名
  
  // 初始化全局数据
  state: {
    title:'全局 title',
    text: '全局 text',
    login: false,
    a:'全局 models aaaa',
  },
  
  // 处理同步业务  接收 dispatch({type:'global/setText',...
  reducers: {
    setText(state) {
      // copy 更新并返回
      return {
        ...state,
        text: '全局设置 后的 text'+Math.random().toFixed(2),
      };
    },
    setTitle(state,action) { // action 接受到的参数
      return {
        ...state,
        title: `全局设置 后的 title${action.payload.a}/${action.payload.b}`,
      };
    },
    signin:(state)=>({
      ...state,
      login: true,
    }),
  },
  
};

组件内部获取和修改全局数据

import {connect} from 'umi'

const 组件 = (props) => {
  return (
    <>
      <h3 className="title">获取全局 state  </h3>

      <div>text:{props.text}</div>
      <div>title:{props.title}</div>
      <div>a:{props.A}</div>
      {
        props.isLogin ? <div>已登录</div> : <div>未登录</div>
      }

      <h3 className="title">修改全局 state</h3>
      <button
        onClick={() => {
          props.dispatch({
            type: 'global/setText',
          });
        }}
      >
        修改全局 text,不传参
      </button>

      <button
        onClick={() => {
          props.dispatch({
            type: 'global/setTitle',
            payload:{a:1,b:2}
          });
        }}
      >
        修改全局 text,传参
      </button>
    </>
  );
}

export default connect(state => ({
  // 抓取全局,重命名
  text: state.global.text,
  title: state.global.title,
  A: state.global.a,
  isLogin: state.global.login,
}))(组件);

页面数据获取和修改

页面数据定义在 pages/页面目录/model.js 或者 pages/ 页面目录/models/*.js,当前页面目录只分配一个数据文件时,使用 model.js,当前页面目录分配多个数据文件时,使用 models/*.js

页面目录 /*.jsx 可访问当前页面目录 /model.js 及当前页面目录 /models/*.js,也可向上访问,但子集页面目录和同级页面目录数据不可访问

// pages/*/model.js
export default {
  namespace: 'dva',
  state: 'bmw', 
  reducers: {
    setStr(state) {
      return 'qq';
    },
  }
};

// pages/*/models/a.js
export default {
  namespace: 'a',
  state: 'page model a',
};

组件内部获取和修改页面数据

import {connect,getDvaApp} from 'umi'
import Child from './child'

const Dva = (props) => {
  return (
    <>
      <h3 className="title">获取页面 models</h3>
      <p>model.js 里面的数据:{props.dva}</p>
      <p>models 目录里面的的数据:{props.a}</p>
      <p>models 目录里面的的数据:{props.b}</p>
      <hr/>
      <h3 className='title'>修改页面 model 数据</h3>
      <button onClick={()=>props.dispatch({type:'dva/setStr'})}>修改</button>
    </>
  );
}

export default connect(state => ({
  // 抓取页面级别
  dva:state.dva,
  a: state.a,
  b: state.b,
}))(Dva);

// connect 不传参不获取数据,dispatch 默认传给组件
// export default connect()(Dva);

异步处理逻辑

effects处理异步等一些有副作用的逻辑,如下:

import { request } from 'umi';
export default {
  namespace: 'global', // 所有 models 里面的 namespace 不能重名
  state: {
    login: false,
  },
  reducers: { // 处理同步 左 key 接收 dispatch({type:key
    signin:(state,{type,payload})=>({
      ...state,
      login: true, // payload 实际数据决定 login 的值
    }),
  },
  effects: {
    // 接收来自 dispatch({type:'global/login'...
    *login(action, { call, put, select }) {
      const data = yield call(request,'/umi/login',{method:'post',data:{username:action.payload.username,password:action.payload.password}})
      yield put({
        type: 'signin',
        payload:data
      });
    },
  }
};
  • [^put]: 发出一个 Action,给 reducers
  • [^select]: 从 state 里获取数据,如 const todos = yield select(state => state.todos)
  • [^yield]: 状态机语法,类似 await,同步书写异步代码
  • [^action]: 可获取发送请求时的类型,携带的负载
export default {
  namespace: 'count', // 命名空间,用于区分不同的模块
  state: 0, // 跨组件共享的数据
  reducers: {
    // 处理同步操作
    increment(state) {
      return state + 1;
    },
    decrement(state) {
      return state - 1;
    },
    incrementStep(state, action) {
      return state + action.payload;
    },
    decrementStep(state, action) {
      return state - action.payload;
    },
  },
  effects: {
    // 处理异步操作
    *incrementAsync(action, { put, call }) {
      yield call(delay, 2000); // 调用异步方法
      yield put({
        // 触发 reducers
        type: 'increment',
      });
    },
    *decrementAsync(action, { put, call }) {
      yield call(delay, 1000);
      yield put({
        type: 'decrement',
      });
    },
  },
};

const delay = (ms) => {
  // 模拟异步请求
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log('delay');
      resolve();
    }, ms);
  });
};
import React from 'react';
import { connect } from 'umi';
import { Button, Space } from 'antd';

function ComponentA(props) {
  return (
    <div>
      ComponentA --- {props.count}
      <br />
      <Space>
        <Button
          type="primary"
          onClick={() => props.dispatch({ type: 'count/increment' })}
        >
          + 1
        </Button>
        <Button
          type="primary"
          onClick={() =>
            props.dispatch({ type: 'count/incrementStep', payload: 5 })
          }
        >
          + 5
        </Button>
        <Button
          type="primary"
          onClick={() => props.dispatch({ type: 'count/incrementAsync' })}
        >
          async + 1
        </Button>
      </Space>
    </div>
  );
}

const mapStateToProps = (state) => {
  return {
    count: state.count,
  };
};
export default connect(mapStateToProps)(ComponentA);

丢弃 connect 高级组件,转投 hooks

import { useDispatch, useSelector } from 'umi';

const 组件 = () => {
  const dispatch = useDispatch();
  const { dva } = useSelector((state) => ({ dva: state.dva }));
  return (
    <>
      <h3 className="title">子组件3</h3>
      <div>{dva}</div>
      <div>
        <button
          onClick={() => {
            dispatch({ type: 'global/setTitle', payload: { a: 11, b: 22 } });
          }}
        >
          修改全局 model
        </button>
      </div>
    </>
  );
};

export default 组件;

subscriptions 源*获取

订阅一个数据 “源” 的变化,使用场景如:服务器的 websocket 连接、keyboard 输入、geolocation 变化、history 路由变化

import key from 'keymaster';

export default {
  namespace: 'global',
  state: {
  },
  subscriptions: {
    listenRoute({ dispatch,history}) {
      history.listen(({ pathname, query }) => {
        console.log('global subscriptions',pathname,query);
      });
    },
    listenKeyboard({dispatch}) { // 监听键盘
      key('⌘+i, ctrl+i', () => { dispatch({type:'setText'}) });
    },
    listenResize({dispatch}) { // 监听窗口变化
      window.onresize = function(){
        console.log('onresize')
      }
    },
    listenScroll({dispatch,history}){
      window.onscroll=function () {
        console.log('onscroll')
      }
    }
  },
};

运行时配置

构建时配置 config/config 能覆盖大量场景,但有一些却是编译时很难触及的。比如:

  • 在出错时显示个 message 提示用户
  • 在加载和路由切换时显示个 loading
  • 页面载入完成时请求后端,根据响应动态修改路由
    运行时配置和构建时配置的区别是他跑在浏览器端,基于此,我们可以在这里写函数、jsx、import 浏览器端依赖等等,注意不要引入 node 依赖。

配置方式

约定 src/app.jsx 定义并暴露一些固定名称的函数,他们会在浏览器端择机运行,全局执行一些业务

渲染前的权限校验

render 函数, 用于改写把整个应用 render 到 dom 树里, 覆写 render,接受一个 oldRender 函数,最后用 oldRender 来渲染 dom, 需至少被调用一次

export const render = async (oldRender) => {
  const { isLogin} = await request('/umi/auth');
  if (!isLogin) {
    history.push('/login');
  }
  // oldRender, Function,原始 render 方法,需至少被调用一次
  oldRender();
}  
// mock/auth  
'GET /umi/auth': (req, res) => {
  res.send({
    isLogin: true,
  });
},

动态路由读取、添加

patchRoutes 函数提供了在运行时,动态修改路由的入口,一般可以和 render 函数配合, 请求服务端根据响应动态更新路由

export function patchRoutes({ routes }) {
    // routes 为当前路由
  routes.unshift({
    path: '/foo',
    exact: true,
    component: require('@/pages/foo').default,
  });
}

需要注意的地方是:

  • 动态路由的 compnent 要的是一个组件不是一段地址,可通过 require 引入
  • 动态路由读取后,跳转后不显示,需要关闭 mfsu: {}
  • 子路由不跳转,除了 layout 组件,其他需要添加 exact,构建时的配置在编译后都会自动加,而动态路由如果路由数据没有 exact 会导致不跳转
  • 数据数据里面不可以有 require,数据需要过滤,require(非空字符拼接+变量)
  • document.ejs 报错,需要 require 拼接时找到 index.jsx 目前 umi3 有这个问题

模拟路由数据

// mock/auth

'GET /umi/menus': (req, res) => {
    res.send([
      {
        path: '/',
        component: 'layouts/layout1',
        routes: [
          {
            title: '资源引入',
            path: '/resources',
            component: 'pages/css-img',
          },
          { path: '/less', component: 'pages/less' },
          {
            path: '/goods',
            component: 'layouts/layout2',
            routes: [
              { path: '/goods/:id?', component: 'pages/goods/goods-detail' },
              {
                path: '/goods/:id/comment',
                component: 'pages/goods/comment',
              },
              {
                path: '/goods/:id/comment/:cid',
                component: 'pages/goods/comment/comment-detail',
              },
              { component: 'pages/404' },
            ],
          },
          { path: '/data-interaction', component: 'pages/data-interaction' },
          { path: '/dva', component: 'pages/dva' },
          { path: '/antd', component: 'pages/antd' },
          { path: '/hooks', component: 'pages/hooks' },
          {
            path: '/user',
            component: 'pages/user',
            wrappers: ['wrappers/auth'],
          },

          { path: '/', redirect: '/antd' },
          { component: 'pages/404' },
        ],
      },
      { component: 'pages/404' },
    ]);
  },

读取路由数据并添加路由

// src/app

let routesData = []; // 模块变量用来存储路由数据

// render 函数里面读取路由数据
export const render = async (oldRender) => {
  const { isLogin } = await http('/umi/auth');
  if (isLogin) {
    // 获取路由数据
    routesData = await http('/umi/menus');
  } else {
    history.push('/login');
  }
  oldRender();
};

export function patchRoutes({ routes }) {
  filterRoutes(routesData); // 处理数据,添加 exact,指定 index.js,拼接 require
  routesData.map((item) => routes.push(item)); // 动态添加路由
}

const filterRoutes = (routesData) => {
  routesData.map((item) => {
    
    // exact 处理
    if (item.routes && item.routes.length > 0) {
      filterRoutes(item.routes); // 含 routes 键的需要递归处理
    } else {
      item.exact = true; // 不含 routes 键的都添加 exact
    }
    
    // component 地址拼接处理
    if (!item.redirect) { // 不处理带有 redirect 字段
      if (item.component.includes('404')) { // 404 没有 index 文件结构
        item.component = require('@/' + item.component + '.jsx').default;
      } else {
        // 其他页面都指向 index 结构,避免 umi3 的 document.ejs 报错
        item.component = require('@/' + item.component + '/index.jsx').default;
      }
         // 部分需要授权路由的拼接
      if (item.wrappers && item.wrappers.length > 0) {
        item.wrappers.map((str, index) => {
          item.wrappers[index] = require('@/' + str + '.jsx').default;
        });
      }
    }
  });
};
// 动态路由地址要 require 引入
// require(需要字符拼接+变量)
// document.ejs报错,需要找到index.jsx

const filterRoutes = (routesData) => {
  routesData.map((item) => {
    if (item.routes && item.routes.length > 0) {
      filterRoutes(item.routes);
    } else {
      item.exact = true;
    }
    if (!item.redirect) {
      if (item.component.includes('404')) {
        item.component = require('@/' + item.component + '.jsx').default;
      } else {
        item.component = require('@/' + item.component + '/index.jsx').default;
      }
      if (item.wrappers && item.wrappers.length > 0) {
        item.wrappers.map((str, index) => {
          item.wrappers[index] = require('@/' + str + '.jsx').default;
        });
      }
    }
  });
};
export { filterRoutes };
import { request as http, history } from 'umi';
import { filterRoutes } from './utils/filterRoutes';

let routesData = [];

export function patchRoutes ({ routes }) {
  // 动态添加路由
  console.log('pathroutes', routes);

  /*   routes.push({
    path: '/',
    component: require('@/layouts/layout1').default,
    routes: [
      {
        exact: true,
        title: '资源引入',
        path: '/resources',
        component: require('@/pages/css-img').default,
      },
      {
        exact: true,
        path: '/less',
        component: require('@/pages/less').default,
      },
      {
        path: '/goods',
        component: require('@/layouts/layout2').default, //展示区
        routes: [
          {
            exact: true,
            path: '/goods/:id?',
            component: require('@/pages/goods/goods-detail').default,
          },
          {
            exact: true,
            path: '/goods/:id/comment',
            component: require('@/pages/goods/comment').default,
          },
          {
            exact: true,
            path: '/goods/:id/comment/:cid',
            component: require('@/pages/goods/comment/comment-detail').default,
          },
          { exact: true, component: require('@/pages/404').default },
        ],
      },
      {
        exact: true,
        path: '/data-interaction',
        component: require('@/pages/data-interaction').default,
      },
      {
        exact: true,
        path: '/dva',
        component: require('@/pages/dva').default,
      },
      {
        exact: true,
        path: '/antd',
        component: require('@/pages/antd').default,
      },
      {
        exact: true,
        path: '/hooks',
        component: require('@/pages/hooks').default,
      },
      {
        exact: true,
        path: '/user',
        component: 'user',
        wrappers: [require('@/wrappers/auth').default],
      }, // 路由守卫

      { exact: true, path: '/', redirect: '/antd' },
      { exact: true, component: require('@/pages/404').default },
    ],
  });
  routes.push({ exact: true, component: require('@/pages/404').default }); */
  filterRoutes(routesData);
  routesData.map((item) => routes.push(item));
  console.log('patchRoutes', routes);
}

export const render = async (oldRender) => {
  //只运行首次
  /* console.log(
    'render 渲染之前做一些权限校验,读取路由数据,在patchRoutes之前运行,',
  ); */

  const { isLogin } = await http('/umi/auth');
  if (isLogin) {
    // 获取路由数据
    routesData = await http('/umi/menus');
    // console.log(111, routesData);
  } else {
    history.push('/login');
  }

  // oldRender, Function,原始 render 方法,需至少被调用一次
  oldRender();

  /* fetch('/api/auth').then(auth => {
    if (auth.isLogin) { 获取路由数据 let routesData = auth.routes;oldRender() }
    else { history.push('/login'); }
  }); */
};

export function onRouteChange ({ matchedRoutes, location, routes, action }) {
  console.log('routes', routes); // 路由集合
  console.log('matchedRoutes', matchedRoutes); // 当前匹配的路由及其子路由
  console.log('location', location); // location及其参数
  console.log('action', action); // 当前跳转执行的操作
  // console.log('初始加载和路由切换时的逻辑')
  // 初始加载和路由切换时的逻辑,用于路由监听, action 是路由切换方式如:push
  // 用于做埋点统计
  // 动态设置标题
  document.title =
    matchedRoutes[matchedRoutes.length - 1].route.title || 'heheda';
}
export const request = {
  // timeout: 1000,
  // errorConfig: {},
  // middlewares: [],
  requestInterceptors: [
    (url, options) => {
      // 请求地址 配置项
      options.headers = {
        token:
          'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFsZXgiLCJfaWQiOiI1ZThhMGQ2MzczNDg2MDIzYTRmZDY4ZGYiLCJpYXQiOjE1ODkxMDQ4NjcsImV4cCI6MTU4OTE5MTI2N30.e0GWBDhYILXOuCpoCXq75T4PeBiNFgSab54sMe6yTk4',
      };
      // console.log('请求地址 配置项');
      return { url, options };
    },
  ],
  responseInterceptors: [
    (response, options) => {
      // 响应体 请求时的配置项
      // console.log('响应体 请求时的配置项');
      return response;
    },
  ],
};

路由监听,埋点统计

onRouteChange 函数内部可以设置,在初始加载和路由切换时做一些事情,比如埋点,设置动态标题等操作

export function onRouteChange({ matchedRoutes, location, routes, action }) {
  // 动态设置标题
  document.title = matchedRoutes[matchedRoutes.length - 1].route.title || '默认标题'
}
  • [^location]: history 提供的 location 对象
  • [^routes]: 路由集合
  • [^action]: PUSH|POP|REPLACE|undefined,初次加载时为 undefined
export function onRouteChange({ matchedRoutes, location, routes, action }) {
  console.log('routes', routes) // 路由集合
  console.log('matchedRoutes', matchedRoutes) // 当前匹配的路由及其子路由
  console.log('location', location) // location 及其参数
  console.log('action', action) // 当前跳转执行的操作

  // 动态设置标题
  document.title = matchedRoutes[matchedRoutes.length - 1].route.title || '默认标题'
}

拦截器

umi 内置的 request 和 useRequest 在发送请求之前和数据返回后,可以做一些通用的配置业务,这个时候考虑配置拦截器,参考插件配置

export const request = {
  requestInterceptors: [
    (url, options) => {
      // 请求地址 配置项
      options.headers = {
        token:'..',
      };
      return { url, options };
    },
  ],
  responseInterceptors: [],
};
// src/app.js
export const request = {
  // timeout: 1000, // 延时
  // errorConfig: {}, // 错误处理
  // middlewares: [], // 使用中间件
  requestInterceptors: [ // 请求拦截器,数组里面是函数
    // 参数是请求地址和配置项
    (url, options) => {
      options.headers = {token: 'xxx'}
      return {url, options}
    }
  ], 
  responseInterceptors: [ // 响应拦截器
    // 参数是响应体和请求时配置项
    (response, options) => {
      return response
    }
  ] 
}
umi3 一般适合开发一些 h5 端的各类 web 应用,如果考虑开发中台管理系统,可以去看看蚂蚁系提供的 antd-pro 框架

Umi3

版权属于:Joe
作品采用:本作品采用 知识共享署名-相同方式共享 4.0 国际许可协议 进行许可。
0

目录

来自 《Umi3》
评论

qiaofugui

博主很懒,啥都没有
188 文章数
14 评论量
3 分类数
191 页面数
已在风雨中度过 2年142天2小时24分