Api

请求域名

https://api.shop.eduwork.cn/

域名已开启强制HTTPS,请务必使用HTTPS,否则可能会导致部分行为,如POST, PUT请求失败

用户认证

使用 JWT 认证, 需要认证的 Api, 需要添加请求头:

1
Authorization: Bearer Token

Token 为认证(登录)成功之后, 服务器返回的 Token

默认用户

系统提供了默认用户, 当然, 您也可以自己注册

普通用户:

账号:test@a.com
密码:123123

超级管理员:

账号:super@a.com
密码:123123

umi

定义

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

功能特点

  • 可扩展
  • 开箱即用
  • 企业级
  • 大量自研
  • 完备路由
  • 面向未来

什么时候不用umi

如果你,

  • 需要支持 IE 8 或更低版本的浏览器
  • 需要支持 React 16.8.0 以下的 React
  • 需要跑在 Node 10 以下的环境中
  • 有很强的 webpack 自定义需求和主观意愿
  • 需要选择不同的路由方案

Umi 可能不适合你。

为什么不是?

  • cra:create-react-app 是基于 webpack 的打包层方案,包含 build、dev、lint 等,他在打包层把体验做到了极致,但是不包含路由,不是框架,也不支持配置。所以,如果大家想基于他修改部分配置,或者希望在打包层之外也做技术收敛时,就会遇到困难。
  • next.js:是个很好的选择,Umi 很多功能是参考 next.js 做的。要说有哪些地方不如 Umi,我觉得可能是不够贴近业务,不够接地气。比如 antd、dva 的深度整合,比如国际化、权限、数据流、配置式路由、补丁方案、自动化 external 方面等等一线开发者才会遇到的问题。

快速上手

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//1、目录创建
mkdir myapp && cd myapp

//2、安装yarn
npm install yarn -g

//3、创建项目,默认创建ts版本
yarn create @umijs/umi-app

//4、安装依赖
yarn

//4、运行项目
yarn start
  • 报错 TypeError: Cannot read property 'forEach' of undefined,升级浏览器的react-devtools到最新版本即可(v4)
  • 用vscode打开工程会报错如下,这是因为,ts版本不对,切换到工作区版本即可
image-20210515135943838 image-20210515140039692
代码规范化与提交控制(自定义)
  • 使用Prettier(脚手架自带,不用配置)
1
2
3
4
5
6
7
8
9
10
11
//安装过程
npm install --save-dev --save-exact prettier

//创建prettier配置文件,由于中文电脑环境,使用echo生成的文件编码格式有可能不对,可以在vscode中手动添加
echo {}> .prettierrc.json

//创建一个prettier忽略文件
echo .prettierignore

//现在,可以运行一下代码格式化所有文件(忽略文件除外)
npx prettier --write .

但是这样很不方便,每次都需要手动执行代码格式化。因此有一个插件叫Pre-commit Hook,可以在你提交前执行代码格式化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//首先安装husky和lint-staged(必须安装这个版本)
npm i -D husky@4.2.3
npm i -D lint-staged@10.0.8
//配置package.json,删除gitHooks的配置,添加如下配置
"husky": {
"hooks": {
"pre-commit": "lint-staged"
}
}

//Prettieer要兼容eslint需要安装eslint-config-prettier
npm i -D eslint-config-prettier
//配置package.json让eslint兼容prettier
"eslintConfig": {
"extends": [
"prettier"
]
}

现在执行git commit命令就会先检查代码格式,如果有错误,会不让commit。如果仅仅是格式不标准,会自动进行代码格式化,不需要手动。保持项目组代码风格统一

失败状态

image-20210421135707170

成功状态

image-20210421142011731
  • 因为你git commit -m ‘xxx’ xxx可以随便写,不受控制。无法规范要求。使用commitlint可以对xxx做格式要求,不符合要求的不让提交
1
2
3
4
5
6
7
8
9
10
11
12
13
//安装commitlint
npm install --save-dev @commitlint/config-conventional @commitlint/cli

//生成配置文件,由于中文电脑环境,使用echo生成的文件编码格式有可能不对,可以在vscode中手动添加
echo "module.exports = {extends: ['@commitlint/config-conventional']}" > commitlint.config.js

//配置到package.json中的husky上面
"husky": {
"hooks": {
"pre-commit": "lint-staged",
"commit-msg": "commitlint -E HUSKY_GIT_PARAMS" //add this
}
}

可以看到,再次提交时,如果提交信息不符合规范,commit将会失败

image-20210421144508771

提交信息模板

详见https://github.com/conventional-changelog/commitlint/#what-is-commitlint

image-20210421144752741 image-20210421144810647

type :用于表明我们这次提交的改动类型,是新增了功能?还是修改了测试代码?又或者是更新了文档?总结以下 11 种类型:

  • build:主要目的是修改项目构建系统(例如 glup,webpack,rollup 的配置等)的提交
  • ci:主要目的是修改项目继续集成流程(例如 Travis,Jenkins,GitLab CI,Circle等)的提交
  • docs:文档更新
  • feat:新增功能
  • fix:bug 修复
  • perf:性能优化
  • refactor:重构代码(既没有新增功能,也没有修复 bug)
  • style:不影响程序逻辑的代码修改(修改空白字符,补全缺失的分号等)
  • test:新增测试用例或是更新现有测试
  • revert:回滚某个更早之前的提交
  • chore:不属于以上类型的其他类型(日常事务)

optional scope:一个可选的修改范围。用于标识此次提交主要涉及到代码中哪个模块。

description:一句话描述此次提交的主要内容,做到言简意赅。

例子:

1
2
git commit -m 'feat: 增加 xxx 功能'
git commit -m 'bug: 修复 xxx 功能'

注意 commitlint大小写敏感,:后面要空格

修改配置
  • 开启ant-design-pro布局,编辑.umirc.ts
1
2
3
4
5
6
7
8
import { defineConfig } from 'umi';

export default defineConfig({
+ layout: {},
routes: [
{ path: '/', component: '@/pages/index' },
],
});

不用重启 yarn start,webpack 会在背后增量编译,过一会就可以看到以下界面

image-20210515164833160
部署发布与验证

执行yarn build,默认构建到./dist目录下

验证发布结果:

先全局安装servenpm install serve -g

serve ./dist

umi基础

目录结构

一个基础的 Umi 项目大致是这样的,

1
2
3
4
5
6
7
8
9
10
11
12
13
├── package.json 包含插件和插件集
├── .umirc.ts 配置文件,包含 umi 内置功能和插件的配置。
├── .env 环境变量
├── dist 执行umi build后的产物
├── mock 此目录下所有 js 和 ts 文件会被解析为 mock 文件,遵循mockjs规范
├── public 此目录下所有文件都会被copy到输出路径
└── src 存放源码
├── .umi 临时文件,不要提交到github
├── layouts/index.tsx 约定式路由时的全局布局文件
├── pages 所有路由组件存放在这里
├── index.less
└── index.tsx
└── app.ts 运行时配置文件,可以在这里扩展运行时的能力,比如修改路由、修改 render 方法等
配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import { defineConfig } from 'umi';
import routes from './routes';

export default defineConfig({
//设置路由
routes: routes,
//开启ant-design-pro布局
layout: {},
//开启按需加载
//dynamicImport: {},
//避免浏览器缓存
//hash: true,
//设置路由前缀,通常用于部署到非根目录。
//base: '/',
//配置 webpack 的 publicPath。当打包的时候,webpack 会在静态文件路径前面添加 publicPath 的值
//publicPath: 'http://xxx.com/cdn',
//打包输出目录
//outputPath: 'build',
//标题
//title: 'UmiJs',
//路由方式
//history: {
// type: 'hash',
//},
//兼容设置
//targets: {
// ie: 11,
//},
//开启代理
//proxy: {
// '/api': {},
//},
//配置主题,实际上是配 less 变量。
theme: {
'@primary-color': '#1DA57A',
},
nodeModulesTransform: {
type: 'none',
},
//快速刷新
fastRefresh: {},
});
  • 有两种方式,第一种是在.umirc.ts文件中配置

  • 第二种是在config文件夹中拆分配置,复杂选择第二种,简单选择第一种,这里选择第二种

  • 如果你想在写配置时也有提示,可以通过 umi 的 defineConfig 方法定义配置

  • 新建 .umirc.local.ts,这份配置会和 .umirc.ts 做 deep merge 后形成最终配置。.umirc.local.ts 仅在 umi dev 时有效。umi build 时不会被加载。

注意:

  • config/config.ts 对应的是 config/config.local.ts
  • .local.ts 是本地验证使用的临时配置,请将其添加到 .gitignore务必不要提交到 git 仓库中
  • .local.ts 配置的优先级最高,比 UMI_ENV 指定的配置更高

可以通过环境变量 UMI_ENV 区分不同环境来指定配置。

举个例子,

1
2
3
// .umirc.js 或者 config/config.jsexport default { a: 1, b: 2 };
// .umirc.cloud.js 或者 config/config.cloud.jsexport default { b: 'cloud', c: 'cloud' };
// .umirc.local.js 或者 config/config.local.jsexport default { c: 'local' };

不指定 UMI_ENV 时,拿到的配置是:

1
2
3
4
5
{
a: 1,
b: 2,
c: 'local',
}

指定 UMI_ENV=cloud 时,拿到的配置是:

1
2
3
4
5
{
a: 1,
b: 'cloud',
c: 'local',
}
运行时配置
  • 运行时配置和配置的区别是他跑在浏览器端,基于此,我们可以在这里写函数、jsx、import 浏览器端依赖等等,注意不要引入 node 依赖。
  • 约定 src/app.tsx 为运行时配置。

配置项

  • modifyClientRenderOpts(fn)

    • 微前端动态修改渲染根节点
  • patchRoutes({ routes })

    • 修改路由
    • 可以和render配置结合使用,请求服务端根据响应动态更新路由
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    let extraRoutes;

    export function patchRoutes({ routes }) {
    merge(routes, extraRoutes);
    }

    export function render(oldRender) {
    fetch('/api/routes').then(res=>res.json()).then((res) => {
    extraRoutes = res.routes;
    oldRender();
    })
    }
  • render (oldRender: Function)

覆写 render。

这个render只在打开网站的时候调用1次,路由跳转不会调用

比如用于渲染之前做权限校验,

1
2
3
4
5
6
7
8
9
10
11
import { history } from 'umi';

export function render(oldRender) {
fetch('/api/auth').then(auth => {
if (auth.isLogin) { oldRender() }
else {
history.push('/login');
oldRender()
}
});
}
  • onRouteChange({ routes, matchedRoutes, location, action })

在初始加载和路由切换时做一些事情。例如埋点统计

还可以用来设置标题

  • rootContainer(LastRootContainer, args)

修改交给 react-dom 渲染时的根组件。

比如用于在外面包一个 Provider,

1
2
3
export function rootContainer(container) {
return React.createElement(ThemeProvider, null, container);
}

args 包含:

  • routes,全量路由配置
  • plugin,运行时插件机制
  • history,history 实例

路由

在 Umi 中,应用都是单页应用,页面地址的跳转都是在浏览器端完成的,不会重新请求服务端获取 html,html 只在应用初始化时加载一次。所有页面由不同的组件构成,页面的切换其实就是不同组件的切换,你只需要在配置中把不同的路由路径和对应的组件关联上。umi的路由基于react-router进行了进一步封装,基本使用同react相同

  • 配置路由

在配置文件中通过 routes 进行配置,格式为路由信息的数组

1
2
3
4
5
6
export default {
routes: [
{ exact: true, path: '/', component: 'index' },
{ exact: true, path: '/user', component: 'user' },
],
}
path

路径通配符

component

匹配到路由时的组件路径,推荐用绝对路径@/xxx/xxx

exact

是否严格匹配,即 location 是否和 path 完全对应上

routes

子路由,配置子路由,通常在需要为多个路径增加 layout 组件时使用。

比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
export default {
routes: [
{ path: '/login', component: 'login' },
{
path: '/',
component: '@/layouts/index',
routes: [
{ path: '/list', component: 'list' },
{ path: '/admin', component: 'admin' },
],
},
],
}

然后在 src/layouts/index 中通过 props.children 渲染子路由,

1
2
3
export default (props) => {
return <div style={{ padding: 20 }}>{ props.children }</div>;
}

这样,访问 /list/admin 就会带上 src/layouts/index 这个 layout 组件。

redirect

配置路由跳转,

比如:

1
2
3
4
5
6
export default {
routes: [
{ exact: true, path: '/', redirect: '/list' },
{ exact: true, path: '/list', component: 'list' },
],
}

访问 / 会跳转到 /list,并由 src/pages/list 文件进行渲染。

warppers

配置路由的高阶组件封装。

比如,可以用于路由级别的权限校验:

1
2
3
4
5
6
7
8
9
10
export default {
routes: [
{ path: '/user', component: 'user',
wrappers: [
'@/wrappers/auth',
],
},
{ path: '/login', component: 'login' },
]
}

然后在 src/wrappers/auth 中,

1
2
3
4
5
6
7
8
9
10
import { Redirect } from 'umi'

export default (props) => {
const { isLogin } = useAuth();
if (isLogin) {
return <div>{ props.children }</div>;
} else {
return <Redirect to="/login" />;
}
}

这样,访问 /user,就通过 useAuth 做权限校验,如果通过,渲染 src/pages/user,否则跳转到 /login,由 src/pages/login 进行渲染。

title

路由标题

页面跳转
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { history } from 'umi';

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

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

// 跳转到上一个路由
history.goBack();
hash路由
link组件
1
2
3
4
5
6
7
import { Link } from 'umi';

export default () => (
<div>
<Link to="/users">Users Page</Link>
</div>
);

然后点击 Users Page 就会跳转到 /users 地址。

注意:

  • Link 只用于单页应用的内部跳转,如果是外部地址跳转请使用 a 标签

路由组件可通过 props 获取到以下属性,

  • match,当前路由和 url match 后的对象,包含 paramspathurlisExact 属性
  • location,表示应用当前处于哪个位置,包含 pathnamesearchquery 等属性
  • history,同 api#history 接口
  • route,当前路由配置,包含 pathexactcomponentroutes
  • routes,全部路由信息

约定式路由

除配置式路由外,Umi 也支持约定式路由。约定式路由也叫文件路由,就是不需要手写配置,文件系统即路由,通过目录和文件及其命名分析出路由配置。

如果没有 routes 配置,Umi 会进入约定式路由模式,然后分析 src/pages 目录拿到路由配置。

比如以下文件结构:

1
2
3
4
.
└── pages
├── index.tsx
└── users.tsx

会得到以下路由配置,

1
2
3
4
[
{ exact: true, path: '/', component: '@/pages/index' },
{ exact: true, path: '/users', component: '@/pages/users' },
]

需要注意的是,满足以下任意规则的文件不会被注册为路由,

  • ._ 开头的文件或目录
  • d.ts 结尾的类型定义文件
  • test.tsspec.tse2e.ts 结尾的测试文件(适用于 .js.jsx.tsx 文件)
  • componentscomponent 目录
  • utilsutil 目录
  • 不是 .js.jsx.ts.tsx 文件
  • 文件内容不包含 JSX 元素
动态路由

约定 [] 包裹的文件或文件夹为动态路由。

比如:

  • src/pages/users/[id].tsx 会成为 /users/:id
  • src/pages/users/[id]/settings.tsx 会成为 /users/:id/settings

举个完整的例子,比如以下文件结构,

1
2
3
4
5
6
7
8
.
└── pages
└── [post]
├── index.tsx
└── comments.tsx
└── users
└── [id].tsx
└── index.tsx

会生成路由配置,

1
2
3
4
5
6
7
8
9
10
[
{ exact: true, path: '/', component: '@/pages/index' },
{ exact: true, path: '/users/:id', component: '@/pages/users/[id]' },
{ exact: true, path: '/:post/', component: '@/pages/[post]/index' },
{
exact: true,
path: '/:post/comments',
component: '@/pages/[post]/comments',
},
];
动态可选路由

约定 [ $] 包裹的文件或文件夹为动态可选路由。

比如:

  • src/pages/users/[id$].tsx 会成为 /users/:id?
  • src/pages/users/[id$]/settings.tsx 会成为 /users/:id?/settings

举个完整的例子,比如以下文件结构,

1
2
3
4
5
6
7
.
└── pages
└── [post$]
└── comments.tsx
└── users
└── [id$].tsx
└── index.tsx

会生成路由配置,

1
2
3
4
5
6
7
8
9
[
{ exact: true, path: '/', component: '@/pages/index' },
{ exact: true, path: '/users/:id?', component: '@/pages/users/[id$]' },
{
exact: true,
path: '/:post?/comments',
component: '@/pages/[post$]/comments',
},
];
嵌套路由

Umi 里约定目录下有 _layout.tsx 时会生成嵌套路由,以 _layout.tsx 为该目录的 layout。layout 文件需要返回一个 React 组件,并通过 props.children 渲染子组件。

比如以下目录结构,

1
2
3
4
5
6
.
└── pages
└── users
├── _layout.tsx
├── index.tsx
└── list.tsx

会生成路由,

1
2
3
4
5
6
7
8
[
{ exact: false, path: '/users', component: '@/pages/users/_layout',
routes: [
{ exact: true, path: '/users', component: '@/pages/users/index' },
{ exact: true, path: '/users/list', component: '@/pages/users/list' },
]
}
]
全局layout

约定 src/layouts/index.tsx 为全局路由。返回一个 React 组件,并通过 props.children 渲染子组件。

比如以下目录结构,

1
2
3
4
5
6
7
.
└── src
├── layouts
│ └── index.tsx
└── pages
├── index.tsx
└── users.tsx

会生成路由,

1
2
3
4
5
6
7
8
[
{ exact: false, path: '/', component: '@/layouts/index',
routes: [
{ exact: true, path: '/', component: '@/pages/index' },
{ exact: true, path: '/users', component: '@/pages/users' },
],
},
]

一个自定义的全局 layout 如下:

1
2
3
4
5
import { IRouteComponentProps } from 'umi'

export default function Layout({ children, location, route, history, match }: IRouteComponentProps) {
return children
}
不同的全局layout

你可能需要针对不同路由输出不同的全局 layout,Umi 不支持这样的配置,但你仍可以在 src/layouts/index.tsx 中对 location.path 做区分,渲染不同的 layout 。

比如想要针对 /login 输出简单布局,

1
2
3
4
5
6
7
8
9
10
11
12
13
export default function(props) {
if (props.location.pathname === '/login') {
return <SimpleLayout>{ props.children }</SimpleLayout>
}

return (
<>
<Header />
{ props.children }
<Footer />
</>
);
}
404路由

约定 src/pages/404.tsx 为 404 页面,需返回 React 组件。

比如以下目录结构,

1
2
3
4
5
.
└── pages
├── 404.tsx
├── index.tsx
└── users.tsx

会生成路由,

1
2
3
4
5
[
{ exact: true, path: '/', component: '@/pages/index' },
{ exact: true, path: '/users', component: '@/pages/users' },
{ component: '@/pages/404' },
]

这样,如果访问 /foo//users 都不能匹配,会 fallback 到 404 路由,通过 src/pages/404.tsx 进行渲染。

权限路由

通过指定高阶组件 wrappers 达成效果。

如下,src/pages/user

1
2
3
4
5
6
7
8
9
import React from 'react'

function User() {
return <>user profile</>
}

User.wrappers = ['@/wrappers/auth']

export default User

然后在 src/wrappers/auth 中,

1
2
3
4
5
6
7
8
9
10
import { Redirect } from 'umi'

export default (props) => {
const { isLogin } = useAuth();
if (isLogin) {
return <div>{ props.children }</div>;
} else {
return <Redirect to="/login" />;
}
}

这样,访问 /user,就通过 useAuth 做权限校验,如果通过,渲染 src/pages/user,否则跳转到 /login,由 src/pages/login 进行渲染。

拓展路由属性

支持在代码层通过导出静态属性的方式扩展路由。

比如:

1
2
3
4
5
6
7
function HomePage() {
return <h1>Home Page</h1>;
}

HomePage.title = 'Home Page';

export default HomePage;

其中的 title 会附加到路由配置中。

插件

页面跳转

声明式

通过 Link 使用,通常作为 React 组件使用。

1
2
3
4
5
import { Link } from 'umi';

export default () => (
<Link to="/list">Go to list page</Link>
);
命令式

通过 history 使用,通常在事件处理中被调用。

1
2
3
4
5
import { history } from 'umi';

function goToListPage() {
history.push('/list');
}

也可以直接从组件的属性中取得 history

1
2
3
export default (props) => (
<Button onClick={()=>props.history.push('/list');}>Go to list page</Button>
);

HTML模板

修改

umijs将html模板封装到npm包里,如果要改变模板,不要修改npm包,可以新建 src/pages/document.ejs,umi 约定如果这个文件存在,会作为默认模板,比如:

1
2
3
4
5
6
7
8
9
10
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<title>Your App</title>
</head>
<body>
<div id="root"></div>
</body>
</html>
配置模板

模板里可通过 context 来获取到 umi 提供的变量,context 包含:

  • route,路由信息,需要打包出多个静态 HTML 时(即配置了 exportStatic 时)有效
  • config,用户配置信息

比如:

1
<link rel="icon" type="image/x-icon" href="<%= context.config.publicPath %>favicon.png" />

环境变量

两种方式

使用cross-env兼容多平台
1
2
3
4
5
6
7
8
# OS X, Linux
$ PORT=3000 umi dev

# Windows (cmd.exe)
$ set PORT=3000&&umi dev

$ yarn add cross-env --dev
$ cross-env PORT=3000 umi dev
在.env文件中配置

比如:

1
2
PORT=3000
BABEL_CACHE=none

然后执行,

1
$ umi dev

会以 3000 端口启动 dev server,并且禁用 babel 的缓存。

命令行工具

打包:umi build

调试: umi dev

使用css

全局样式

Umi 中约定 src/global.css或者less 为全局样式,如果存在此文件,会被自动引入到入口文件最前面。

css modules
  • create-react-app也内置支持css modules,不过他是通过文件名是否包含.module来识别的
  • umi更加智能,umi会自动识别CSS Modules的使用,你把他当作CSS Modules使用时,他才是。使用import styles引入会被自动识别为css modules
1
2
3
4
5
// CSS Modules,引入,只有通过styles.xxxx赋值才会生效
import styles from './foo.css';

// 非 CSS Modules 引入,会直接对符合条件的选择器生效
import './foo.css';

使用图片

js里使用图片
1
2
3
export default () => <img src={require('./foo.png')} />

export default () => <img src={require('@/foo.png')} />
js里使用svg
  • 组件式引入
1
2
3
4
5
import { ReactComponent as Logo } from './logo.svg'

function Analysis() {
return <Logo width={90} height={120} />
}
  • url式引入
1
2
3
4
5
import logoSrc from './logo.svg'

function Analysis() {
return <img src={logoSrc} alt="logo" />
}
CSS使用图片

通过相对路径引用。

比如,

1
2
3
.logo {
background: url(./foo.png);
}

CSS 里也支持别名,但需要在前面加 ~ 前缀,

1
2
3
.logo {
background: url(~@/foo.png);
}

注意:

  1. 这是 webpack 的规则,如果切到其他打包工具,可能会有变化
  2. less 中同样适用
图片路径问题

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

  1. 先把图片传到 cdn,然后在 JS 和 CSS 中使用图片的绝对路径
  2. 把图片放在项目里,然后在 JS 和 CSS 中通过相对路径的方式使用

前者不会有任何问题;后者,如果在 JS 中引用相对路径的图片时,在发布时会根据 publicPath 引入绝对路径,所以就算没有开启 dynamicImport 时,也需要注意 publicPath 的正确性。

Base64编译小图片

通过相对路径引入图片的时候,如果图片小于 10K,会被编译为 Base64,否则会被构建为独立的图片文件。

10K 这个阈值可以通过 inlineLimit 配置修改。

umi进阶

按需加载

**为什么使用 dynamic**:封装了使用一个异步组件需要做的状态维护工作,开发者可以更专注于自己的业务组件开发,而不必关心 code spliting、async module loading 等等技术细节。

通常搭配 动态 import 语法 使用。

封装一个异步组件

1
2
3
4
5
6
7
8
9
import { dynamic } from 'umi';

export default dynamic({
loader: async function() {
// 这里的注释 webpackChunkName 可以指导 webpack 将该组件 HugeA 以这个名字单独拆出去
const { default: HugeA } = await import(/* webpackChunkName: "external_A" */ './HugeA');
return HugeA;
},
});

使用异步组件

1
2
3
4
5
6
7
8
9
10
11
import React from 'react';
import AsyncHugeA from './AsyncHugeA';

// 像使用普通组件一样即可
// dynamic 为你做:
// 1. 异步加载该模块的 bundle
// 2. 加载期间 显示 loading(可定制)
// 3. 异步组件加载完毕后,显示异步组件
export default () => {
return <AsyncHugeA />;
}
快速刷新

配置文件加上 fastRefresh: {} 即可开启

使用Umi UI

在项目中执行

1
2
$ yarn add @umijs/preset-ui -D
$ UMI_UI=1 umi dev

mock

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

Umi 约定 /mock 文件夹下所有文件为 mock 文件。

比如:

1
2
3
4
5
6
7
.
├── mock
├── api.ts
└── users.ts
└── src
└── pages
└── index.tsx

/mock 下的 api.tsusers.ts 会被解析为 mock 文件。

如果 /mock/api.ts 的内容如下,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
    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.setHeader('Access-Control-Allow-Origin', '*');
res.end('ok');
},
}

然后访问 /api/users 就能得到 { users: [1,2] } 的响应,其他以此类推。

可以通过配置关闭,

1
2
3
export default {
mock: false,
};

也可以通过环境变量临时关闭,

1
$ MOCK=none umi dev

Mock.js 是常用的辅助生成模拟数据的三方库,借助他可以提升我们的 mock 数据能力。

比如:

1
2
3
4
5
6
7
8
import mockjs from 'mockjs';

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

umi中使用dva

dva是基于redux和redux-saga的数据流方案,同时内置了react-router和fetch,是一个轻量级的应用框架

umi中默认已经整合dva,可以直接使用

包含以下功能,

  • 内置 dva,默认版本是 ^2.6.0-beta.20,如果项目中有依赖,会优先使用项目中依赖的版本。
  • 约定式的 model 组织方式,不用手动注册 model
  • 文件名即 namespace,model 内如果没有声明 namespace,会以文件名作为 namespace
  • 内置 dva-loading,直接 connect loading 字段使用即可
  • 支持 immer,通过配置 immer 开启

符合以下规则的文件会被认为是 model 文件,

  • src/models 下的文件
  • src/pages 下,子目录中 models 目录下的文件
  • src/pages 下,所有 model.ts 文件(不区分任何字母大小写)

比如:

1
2
3
4
5
+ src
+ models/a.ts
+ pages
+ foo/models/b.ts
+ bar/model.ts

其中 a.tsb.tsmodel.ts 如果其内容是有效 dva model 写法,则会被认为是 model 文件。

查看项目中包含了哪些 model。

1
$ umi dva list model
DVA DEMO

尽量避免使用any

网络请求服务未抽离到单独的service,按照一般的开发规范应该是需要抽离的

1、展示组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
//pages/dvademo/index.tsx
import React, { memo, useState } from 'react';
import { connect, tagItem } from 'umi';
import { Dispatch } from 'umi';

const DvaDemo = memo(function index({
dispatch,
tags = [],
}: {
dispatch: Dispatch<string>;
tags: tagItem[];
}) {
const [value, setValue] = useState('');

const handleClick = () => {
//dispatch 发送获取tags的action
console.log('触发onclick', value);
dispatch({
type: 'dvademo/fetchTags',
payload: value,
});
};

return (
<div>
<input
value={value}
placeholder="请输入筛选城市"
onChange={(event) => {
// console.log(event.target.value);
setValue(event.target.value);
}}
/>
<button onClick={handleClick}>获取筛选后的tags数据</button>
{tags.map((item) => {
return <div>{item.name}</div>;
})}
</div>
);
});

export default connect((state: { dvademo: { tags: tagItem[] } }) => {
return {
tags: state.dvademo.tags,
};
})(DvaDemo);

2、model,注意对整体使用 as Model来规范化ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
//pages/models/dvademo.ts
import { Model } from 'dva';
import { request } from 'umi';
//暴漏的tagItem会默认保存到umi命名空间里
export type tagItem = { name: string; value: number; type: number };
//实际上getTags这个网络请求方法应该拆分到service文件夹下面
const getTags = () => {
return request('/api/tags');
};
export default {
//model的命名控件
namespace: 'dvademo',
//初始化state
state: {
tags: [],
},
effects: {
*fetchTags({ payload }, { call, put }) {
const res = yield getTags();
//这里直接用输入进行数据筛选,实际上payload应该作为参数传递给getTags方法
//因为mockjs,没法模拟带参
let filterList = [];
if (!payload.trim()) {
//如果筛选条件为空,直接返回所有
filterList = res.list;
} else {
//如果有筛选条件,进行筛选
filterList = res.list.filter((item: tagItem) => {
if (item.name.includes(payload)) {
return true;
}
return false;
});
}
console.log(payload, filterList);
//更新tags数组
yield put({
type: 'setTagList',
payload: filterList,
});
},
},
reducers: {
setTagList(state, action) {
return { ...state, tags: (action as any).payload };
},
},
} as Model;

3、mock文件

1
2
3
4
5
6
7
8
9
//mock/tags.ts
import mockjs from 'mockjs';

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

4、实现效果如下

image-20210516205613648

dva-immer

  • Type: boolean | object
  • Default: false

表示是否启用 immer 以方便修改 reducer。

注:如需兼容 IE11,需配置 { immer: { enableES5: true }}

useModel

useModel 是一个 Hook,提供消费 Model 的能力,使用示例如下:

1
2
3
4
5
6
import { useModel } from 'umi';

export default () => {
const { user, fetchUser } = useModel('user', model => ({ user: model.user, fetchUser: model.fetchUser }));
return <>hello</>
};

useModel 有两个参数,namespaceupdater

  • namespace - 就是 hooks model 文件的文件名,如上面例子里的 useAuthModel
  • updater - 可选参数。在 hooks model 返回多个状态,但使用组件仅引用了其中部分状态,并且希望仅在这几个状态更新时 rerender 时使用(性能相关)。

详见: https://beta-pro.ant.design/docs/simple-model-cn

antd pro

定义

Ant Design Pro是基于Ant Design和Umi封装的一整套企业级中后台前端/设计解决方案,致力于在设计规范和基础组件的基础上,继续向上构建,提炼出典型模板/业务组件/配套资源设计。

  • 技术栈: es6 react dva umijs g2 antd redux react-router
  • 特点
    • 一整套完整的开发规范,包括文件组织结构,代码规范,功能拆分
    • 基于umi插件完善的支持国际化,状态管理、全局路由、HMR实时预览

安装

新建一个空的文件夹作为项目目录,并在目录下执行:

1
yarn create umi

生成了一个完整的开发框架,提供了涵盖中后台开发的各类功能和坑位,下面是整个项目的目录结构。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
├── config                   # umi 配置,包含路由,构建等配置
├── mock # 本地模拟数据
├── public
│ └── favicon.png # Favicon
├── src
│ ├── assets # 本地静态资源
│ ├── components # 业务通用组件
│ ├── e2e # 集成测试用例
│ ├── layouts # 通用布局
│ ├── models # 全局 dva model
│ ├── pages # 业务页面入口和常用模板
│ ├── services # 后台接口服务
│ ├── utils # 工具库
│ ├── locales # 国际化资源
│ ├── global.less # 全局样式
│ └── global.ts # 全局 JS
├── tests # 测试工具
├── README.md
└── package.json

安装依赖。

1
yarn install

运行项目

1
2
3
4
5
//默认使用mock
yarn start

//不适用mock
yarn start:no-mock

框架逐级提升,create-react-app –> dva –> umi –> antd pro

ui框架: ant-design –> ProComponents

越往后抽象程度越高,是基于业务的进一步封装,使用起来方便

开发

布局

通常布局是和路由系统紧密结合的,Ant Design Pro 的路由使用了 Umi 的路由方案,我们将配置信息统一抽离到 config/config.ts 下,通过如下配置定义每个页面的布局:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
routers: [
{
path: '/',
component: '../layouts/BasicLayout', // 指定以下页面的布局
routes: [
// dashboard
{ path: '/', redirect: '/dashboard/analysis' },
{
path: '/dashboard',
name: 'dashboard',
icon: 'dashboard',
routes: [
{ path: '/dashboard/analysis', name: 'analysis', component: './Dashboard/Analysis' },
{ path: '/dashboard/monitor', name: 'monitor', component: './Dashboard/Monitor' },
{ path: '/dashboard/workplace', name: 'workplace', component: './Dashboard/Workplace' },
],
},
],
},
];

映射路由和页面布局(组件)的关系如代码所示,完整映射转换实现可以参看 config.ts

更多 Umi 的路由配置方式可以参考:Umi 配置式路由

我们在 config.ts 扩展了一些关于 pro 全局菜单的配置。

1
2
3
4
5
6
7
8
{
name: 'dashboard',
icon: 'dashboard',
hideInMenu: true,
hideChildrenInMenu: true,
hideInBreadcrumb: true,
authority: ['admin'],
}
  • name: 当前路由在菜单和面包屑中的名称,注意这里是国际化配置的 key,具体展示菜单名可以在 /src/locales/zh-CN.ts 进行配置。
  • icon: 当前路由在菜单下的图标名。
  • hideInMenu: 当前路由在菜单中不展现,默认 false
  • hideChildrenInMenu: 当前路由的子级在菜单中不展现,默认 false
  • hideInBreadcrumb: 当前路由在面包屑中不展现,默认 false
  • authority: 允许展示的权限,不设则都可见,详见:权限管理

ProLayout 组件 是 Pro v4 中新增的组件,与一般的组件不同,它非常重型,在其中集成了菜单,布局,页头,面包屑,设置抽屉等多种功能。ProLayout来自ProComponents。

ProComponents 是基于 Ant Design 而开发的模板组件,提供了更高级别的抽象支持,开箱即用。可以显著的提升制作 CRUD 页面的效率,更加专注于页面。

  • 对于标题和 logo,Layout 提供了 titlelogo 属性来自定,如果你有更强的定制需求,可以试试 menuHeaderRender: (logo,title) => ReactNode 属性,onMenuHeaderClick 允许你覆盖默认的点击事件。

  • 如果你需要自定义的 menu ,siderWidth 属性可以控制右侧菜单的宽度,menuRendermenuItemRender 可以让你完成自定义整个菜单。menuDataRender 可以用于自定义菜单数据,你可以将其替换为从服务器获取的数据。默认菜单时基于路由配置而生成的

  • ProLayout默认包括面包屑,如果要在新建的页面中显示面包屑,必须要在根标签中使用PageContainer标签包裹

  • PageHeaderWrapper 封装了 ant design 的 PageHeader 组件,增加了 tabList,和 content。 根据当前的路由填入 title 和 breadcrumb。它依赖 Layout 的 route 属性。当然你可以传入参数来复写默认值。 PageHeaderWrapper 支持 TabsPageHeader 的所有属性。

    PageHeaderWrapper 必须要被 ProLayout 包裹才能自动生成面包屑和标题。

路由和菜单

路由和菜单是组织起一个应用的关键骨架,pro 中的路由为了方便管理,使用了中心化的方式,在 config.ts

目前脚手架中所有的路由都通过 config.ts381,iconhideChildrenInMenuauthority,来辅助生成菜单。

菜单根据 config.ts 生成。

如果你的项目并不需要菜单,你可以在 src/layouts/BasicLayout.tsx 中设置 menuRender={false}

面包屑由 PageHeaderWrapper 实现,Layout 将 根据 MenuData 生成的 breadcrumb,并通过 PageHeaderWrapper 将其展现。 PageHeaderWrapper 封装至 Ant Design 的 PageHeader,api 完全相同。

在脚手架中我们通过嵌套路由来实现布局模板。config.ts的布局,如果你需要新增布局可以再直接增加一个新的一级数据。

由于 umi 的限制,在 config.ts,Pro 中暂时支持使用 ant.design img 的 url。只需要直接在 icon 属性上配置即可,如果是个 url,Pro 会自动处理为一个 img 标签。

新增页面

1、在 src / pages 下创建新的 js,less 文件。 如果有多个相关页面,您可以创建一个新文件夹来放置相关文件。

2、将文件加入菜单和路由

3、新增 model、service

新增组件

对于一些可能被多处引用的功能模块,建议提炼成业务组件统一管理。这些组件一般有以下特征:

  • 只负责一块相对独立,稳定的功能;
  • 没有单独的路由配置;
  • 可能是纯静态的,也可能包含自己的 state,但不涉及 dva 的数据流,仅受父组件(通常是一个页面)传递的参数控制。

下面以一个简单的静态组件为例进行介绍。假设你的应用中经常需要展现图片,这些图片宽度固定,有一个灰色的背景和一定的内边距,有文字介绍,就像下图这样:

img

你可以用一个组件来实现这一功能,它有默认的样式,同时可以接收父组件传递的参数进行展示。

1、在 src/components 下新建一个以组件名命名的文件夹,注意首字母大写,命名尽量体现组件的功能,这里就叫 ImageWrapper。在此文件夹下新增 js 文件及样式文件(如果需要),命名为 index.tsindex.less

2、在要使用这个组件的地方,按照组件定义的 API 传入参数,直接使用就好,不过别忘了先引入

修改样式
  • Ant Design Pro 默认使用 less 作为样式语言,建议在使用前或者遇到疑问时学习一下 less 的相关特性。

  • 在样式开发过程中,有两个问题比较突出:

    • 全局污染 —— CSS 文件中的选择器是全局生效的,不同文件中的同名选择器,根据 build 后生成文件中的先后顺序,后面的样式会将前面的覆盖;
    • 选择器复杂 —— 为了避免上面的问题,我们在编写样式的时候不得不小心翼翼,类名里会带上限制范围的标识,变得越来越长,多人开发时还很容易导致命名风格混乱,一个元素上使用的选择器个数也可能越来越多。

    为了解决上述问题,我们的脚手架默认使用 CSS Modules 模块化方案

    导入:以对象形式导入会默认开启css modules

    1
    2
    3
    4
    5
    //开启css modules,需要在html标签使用style={styles.xxx}来应用,不应用不会生效
    import styles from '../index.less'

    //当作普通样式导入,选择器会全部生效
    import '../index.less'
  • 如果如果要保持主题一致,可导入antd less变量在less文件中
1
@import '~antd/lib/style/themes/default.less';

这样可以轻松获取 antd 样式变量并在文件中使用它们,这可以保持保持页面的一致性,并有助于实现自定义主题。

和服务端交互

在 Ant Design Pro 中,一个完整的前端 UI 交互到服务端处理流程是这样的:

  1. UI 组件交互操作;
  2. 调用 model 的 effect;
  3. 调用统一管理的 service 请求函数;
  4. 使用封装的 request.ts 发送请求;
  5. 获取服务端返回;
  6. 然后调用 reducer 改变 state;
  7. 更新 model。

从上面的流程可以看出,为了方便管理维护,统一的请求处理都放在 services 文件夹中,并且一般按照 model 维度进行拆分文件,如:

1
2
3
4
services/
user.ts
api.ts
...

其中,utils/request.ts 是基于 fetch 的封装,便于统一处理 POST,GET 等请求参数,请求头,以及错误提示信息等。具体可以参看 request.ts

例如在 services 中的一个请求用户信息的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// services/user.ts
import request from '../utils/request';

export async function query() {
return request('/api/users');
}

export async function queryCurrent() {
return request('/api/currentUser');
}

// models/user.ts
import { queryCurrent } from '../services/user';
...
effects: {
*fetch({ payload }, { call, put }) {
...
const response = yield call(queryCurrent);
...
},
}
  • 处理异步请求

在处理复杂的异步请求的时候,很容易让逻辑混乱,陷入嵌套陷阱,所以 Ant Design Pro 的底层基础框架 dva 使用 effect 的方式来管理同步化异步请求:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
effects: {
*fetch({ payload }, { call, put }) {
yield put({
type: 'changeLoading',
payload: true,
});
// 异步请求 1
const response = yield call(queryFakeList, payload);
yield put({
type: 'save',
payload: response,
});
// 异步请求 2
const response2 = yield call(queryFakeList2, payload);
yield put({
type: 'save2',
payload: response2,
});
yield put({
type: 'changeLoading',
payload: false,
});
},
},

通过 generatoryield 使得异步调用的逻辑处理跟同步一样,更多可参看 dva async logic

引入外部模块

除了 antd 组件以及脚手架内置的业务组件,有时我们还需要引入其他外部模块,这里以引入富文本组件 react-quill 为例进行介绍。

  • 引入依赖

在终端输入下面的命令完成安装:

1
npm install react-quill --save

加上 --save 参数会自动添加依赖到 package.json 中去。

  • 使用

直接贴代码了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import React from 'react';
import { Button, notification, Card } from 'antd';
import ReactQuill from 'react-quill';
import 'react-quill/dist/quill.snow.css';

export default class NewPage extends React.Component {
state = {
value: 'test',
};

handleChange = (value) => {
this.setState({
value,
});
};

prompt = () => {
notification.open({
message: 'We got value:',
description: <span dangerouslySetInnerHTML={{ __html: this.state.value }} />,
});
};

render() {
return (
<Card title="富文本编辑器">
<ReactQuill value={this.state.value} onChange={this.handleChange} />
<Button style={{ marginTop: 16 }} onClick={this.prompt}>
Prompt
</Button>
</Card>
);
}
}

富文本

这样就成功引入了一个富文本组件。有几点值得注意:

  • import 时需要注意组件暴露的数据结构;
  • 有一些组件需要额外引入样式,比如本例。
mock和联调

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

在 Ant Design Pro 中,因为我们的底层框架是 umi,而它自带了代理请求功能,通过代理请求就能够轻松处理数据模拟的功能。

umi 里约定 mock 文件夹下的文件即 mock 文件,文件导出接口定义,支持基于 require 动态分析的实时刷新,支持 ES6 语法,以及友好的出错提示,详情参看 umijs.org

1
2
3
4
5
6
7
8
9
10
11
12
export default {
// 支持值为 Object 和 Array
'GET /api/users': { users: [1, 2] },

// GET POST 可省略
'/api/users/1': { id: 1 },

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

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

比如定义如下映射规则:

1
2
3
4
5
6
'GET /api/currentUser': {
name: 'momo.zxy',
avatar: imgMap.user,
userid: '00000001',
notifyCount: 12,
},

访问的本地 /api/currentUser 接口:

请求头

img

返回的数据

img
  • 引入mockjs
    • mockjs是常用的辅助生成模拟数据的第三方库,当然你可以用你喜欢的任意库来结合 umi 构建数据模拟功能。
1
2
3
4
5
6
7
8
import mockjs from 'mockjs';

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

设置 response 的请求头即可:

1
2
3
4
5
'POST /api/users/create': (req, res) => {
...
res.setHeader('Access-Control-Allow-Origin', '*');
...
},

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

为了更加真实地模拟网络数据请求,往往需要模拟网络延迟时间。

  • 使用setTimeout模拟延迟,
  • 如果要对全部接口使用模拟延迟,可以使用roadhog-api-doc模拟延迟

ProComponents

简介

ProComponents 是基于 Ant Design 而开发的模板组件,提供了更高级别的抽象支持,开箱即用。可以显著的提升制作 CRUD 页面的效率,更加专注于页面。

  • ProLayout 解决布局的问题,提供开箱即用的菜单和面包屑功能
  • ProTable 表格模板组件,抽象网络请求和表格格式化
  • ProForm 表单模板组件,预设常见布局和行为
  • ProCard 提供卡片切分以及栅格布局能力
  • ProDescriptions 定义列表模板组件,ProTable 的配套组件
  • ProSkeleton 页面级别的骨架屏

在使用之前可以查看一下典型的 Demo 来判断组件是否适合你们的业务。ProComponents 专注于中后台的 CRUD, 预设了相当多的样式和行为。这些行为和样式更改起来会比较困难,如果你的业务需要丰富的自定义建议直接使用 Ant Design。

antd pro框架自带ProComponents,无需单独安装

组件

ProLayout

ProLayout 可以提供一个标准又不失灵活的中后台标准布局,同时提供一键切换布局形态,自动生成菜单等功能。与 PageContainer 配合使用可以自动生成面包屑,页面标题,并且提供低成本方案接入页脚工具栏。

页面中需要承载内容时,可以使用 ProLayout 来减少布局成本。

ProLayout 默认不提供页脚,要是和 Pro 官网相同的样式,需要自己引入一下页脚。

PageContainer

PageContainer 是为了减少繁杂的面包屑配置和标题,很多页面都需要面包屑和标题的配置。当然也可以关掉自动生成的,而使用自己的配置。

PageContainer 封装了 antd 的 PageHeader 组件,增加了 tabList 和 content。 根据当前的路由填入 title 和 breadcrumb。它依赖 Layout 的 route 属性。当然你可以传入参数来复写默认值。 PageContainer 支持 Tabs 和 PageHeader 的所有属性。

ProCard

页内容器卡片,提供标准卡片样式,卡片切分以及栅格布局能力。ProCard 创造性地将 Col, Row, Card, Tabs 等组件实现结合在一起,让你仅用一个组件就能够完成卡片相关的各种布局。

  • 如果你还需要结合图表一起使用,可以参考 StatisticCard 指标卡组件,他是 ProCard 的进一步封装。
  • 若您也需要封装 ProCard,注意需要透出 isProCard=true 的静态属性让 ProCard 可以识别为同一个元素。
  • 响应式布局容器可以使用ProCard
image-20210519153342951 image-20210519153404683
StatisticCard

指标卡结合统计数值用于展示某主题的核心指标,结合 Ant Design Charts 图表库丰富数值内容,满足大多数数值展示的场景。

image-20210519153427584
ProForm - 高级表单

ProForm 在原来的 Form 的基础上增加一些语法糖和更多的布局设置,帮助我们快速的开发一个表单。同时添加一些默认行为,让我们的表单默认好用。

分步表单,Modal 表单,Drawer 表单,查询表单,轻量筛选等多种 layout 可以覆盖大部分的使用场景,脱离复杂而且繁琐的表单布局工作,更少的代码完成更多的功能。

  • 如果你想要设置默认值,请使用 initialValues,任何直接使用组件 valueonChange 的方式都有可能导致值绑定失效。
  • 如果想要表单联动或者做一些依赖,可以使用 render props 模式, ProFormDependency 绝对是最好的选择
  • ProForm 的 onFinish 与 antd 的 Form 不同,是个 Promise,如果你正常返回会自动为你设置按钮的加载效果
  • 如果想要监听某个值,建议使用 onValuesChange。保持单向的数据流无论对开发者还是维护者都大有脾益
  • ProForm 没有黑科技,只是 antd 的 Form 的封装,如果要使用自定义的组件可以用 Form.Item 包裹后使用,支持混用
ProTable - 高级表格

ProTable 的诞生是为了解决项目中需要写很多 table 的样板代码的问题,所以在其中做了封装了很多常用的逻辑。这些封装可以简单的分类为预设行为与预设逻辑。

当你的表格需要与服务端进行交互或者需要多种单元格样式时,ProTable 是不二选择。

image-20210519153723343
EditableProTable - 可编辑表格

可编辑表格 EditableProTable 与 ProTable 的功能基本相同,为了方便使用 EditableProTable 增加了一些预设,关掉了查询表单和操作栏,同时修改了 value 和 onChange 使其可以方便的继承到 antd 的 Form 中。

image-20210519153746684
ProList - 高级列表

基于 ProTable 实现,可以认为是 ProTable 的一个特例,在完成一个标准的列表时即可使用。

ProList 与 antd 的 List 相比,API 设计上更像 Table,使得可以通过配置化的方式快速定义数据项的展现形式。也使得 Table 和 List 的切换变得更加容易。另外 ProList 基于 ProTable 实现,除了 Table 相关的 API 以外 ProList 支持大部分 ProTable 的 API。

image-20210519153806610

ahooks

ahooks 是一个 React Hooks 库,致力提供常用且高质量的 Hooks。

Demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import React from "react";
import { useToggle } from "ahooks";

export default () => {
const [ state, { toggle } ] = useToggle();

return (
<div>
<p>Current Boolean: {String(state)}</p>
<p>
<button onClick={() => toggle()}>Toggle</button>
</p>
</div>
);
};
image-20210519154302781

API

useRequest - 一个强大的管理异步数据请求的 Hook.
  • 自动请求/手动请求
  • SWR(stale-while-revalidate)
  • 缓存/预加载
  • 屏幕聚焦重新请求
  • 轮询
  • 防抖
  • 节流
  • 并行请求
  • 依赖请求
  • loading delay
  • 分页
  • 加载更多,数据恢复 + 滚动位置恢复

在这个例子中, useRequest 接收了一个异步函数 getUsername ,在组件初次加载时, 自动触发该函数执行。同时 useRequest 会自动管理异步请求的 loading , data , error 等状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import { useRequest } from 'ahooks';
import Mock from 'mockjs';
import React from 'react';

function getUsername(): Promise<string> {
return new Promise((resolve) => {
setTimeout(() => {
resolve(Mock.mock('@name'));
}, 1000);
});
}

export default () => {
const { data, error, loading } = useRequest(getUsername);

if (error) {
return <div>failed to load</div>;
}
if (loading) {
return <div>loading...</div>;
}
return <div>Username: {data}</div>;
};
useAntdTable – 优先使用ProTable

封装了常用的 antd Form 与 antd Table 联动逻辑,并且同时支持 antd V3 和 V4。

useDrag 和useDrop

拖拽

useDynamicList

一个帮助你管理列表状态,并能生成唯一 key 的 Hook

const { list, remove, getKey, push } = useDynamicList(['David', 'Jack']);

useSelections

常见联动 checkbox 逻辑封装,支持多选,单选,全选逻辑,还提供了是否选择,是否全选,是否半选的状态。

const result: Result= useSelections<T>(items: T[], defaultSelected?: T[]);

useDebounce

用来处理防抖值的 Hook。

1
2
3
4
const debouncedValue = useDebounce(
value: any,
options?: Options
);
useDebounceFn

用来处理防抖函数的 Hook。

1
2
3
4
5
6
7
const {
run,
cancel
} = useDebounceFn(
fn: (...args: any[]) => any,
options?: Options
);
useInterval

一个可以处理 setInterval 的 Hook。

1
useInterval(fn: () => void, interval: number, options?: Options);
useThrottle

用来处理节流值的 Hook。

useThrottleFn

用来处理节流函数的 Hook。

1
2
3
4
5
6
7
const {
run,
cancel
} = useThrottleFn(
fn: (...args: any[]) => any,
options?: Options
);
useTimeout

一个可以处理 setTimeout 计时器函数的 Hook。

1
useTimeout(fn: () => void, delay: number | undefined | null);
useDebounceEffect

useEffect 增加防抖的能力。

1
2
3
4
5
useDebounceEffect(
effect: () => (void | (() => void | undefined)),
deps?: any[],
options?: Options
);
useMount

只在组件 mount 时执行的 hook。

等价于

1
2
3
useEffect(()=>{
//
},[])
useThrottleEffect

useEffect 增加节流的能力。

useTrackedEffect

useEffect 的基础上,追踪触发 effect 的依赖变化。

1
2
3
4
useTrackedEffect(
effect: (changes:[], previousDeps:[], currentDeps:[]) => (void | (() => void | undefined)),
deps?: deps,
)
useUnmount

只在组件 unmount 时执行的 hook。

1
useUnmount(fn: () => void);
useUnmountedRef

获取当前组件是否已经卸载的 hook,用于避免因组件卸载后更新状态而导致的内存泄漏

1
const unmountRef: { current: boolean } = useUnmountedRef;
useUpdate

强制组件重新渲染的 hook。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import React from 'react';
import { useUpdate } from 'ahooks';

export default () => {
const update = useUpdate();

return (
<>
<div>Time: {Date.now()}</div>
<button type="button" onClick={update} style={{ marginTop: 8 }}>
update
</button>
</>
);
};
useUpdateEffect

一个只在依赖更新时执行的 useEffect hook。

useUpdateLayoutEffect

一个只在依赖更新时执行的useLayoutEffect hook

useBoolean

优雅的管理 boolean 值的 Hook。

1
2
3
const [ state, { toggle, setTrue, setFalse }] = useBoolean(
defaultValue?: boolean,
);
useCookieState

一个可以将状态持久化存储在cookie中的hook

useCountDown

一个用于管理倒计时的Hook。

1
2
3
4
5
6
7
const [countdown, setTargetDate, formattedRes] = useCountDown(
{
targetDate,
interval,
onEnd
}
);
useCounter

一个可以管理 count 的 Hook。

1
2
3
4
5
6
const [current, {
inc,
dec,
set,
reset
}] = useCounter(initialValue, {min, max});
参数 说明 类型
current 当前值 number
inc 加,默认加 1 (delta?:number) => void
dec 减,默认减 1 (delta?:number) => void
set 设置 current (value: number
reset 重置为默认值 () => void
useHistoryTravel

优雅的管理状态变化历史,可以快速在状态变化历史中穿梭 - 前进跟后退。

useLocalStorageState

一个可以将状态持久化存储在 localStorage 中的 Hook 。

1
2
3
4
const [state, setState] = useLocalStorageState<T>(
key: string,
defaultValue?: T | (() => T),
): [T?, (value?: T | ((previousState: T) => T)) => void]

如果想从 localStorage 中删除这条数据,可以使用 setState()setState(undefined)

useMap

一个可以管理 Map 类型状态的 Hook。

1
2
3
4
5
6
7
8
9
10
const [
map,
{
set,
setAll,
remove,
reset,
get
}
] = useMap(initialValue?: Iterable<[any, any]>);
usePrevious

保存上一次渲染时状态的 Hook。

1
2
3
4
const previousState: T = usePrevious<T>(
state: T,
compareFunction: (prev: T | undefined, next: T) => boolean
);
useSessionStorageState

一个可以将状态持久化存储在 sessionStorage 中的 Hook。

useSet

一个可以管理 Set 类型状态的 Hook。

useSetState

管理 object 类型 state 的 Hooks,用法与 class 组件的 this.setState 基本一致。

useToggle

用于在两个状态值间切换的 Hook。

useClickAway

优雅的管理目标元素外点击事件的 Hook。

useDocumentVisibility

可以获取页面可见状态的 Hook。

useEventListener

优雅使用 addEventListener 的 Hook。

useEventTarget

常见表单控件(通过 e.target.value 获取表单值) 的 onChange 跟 value 逻辑封装,支持自定义值转换和重置功能。

useFavicon

用于设置与切换页面 favicon。

useFullscreen

一个用于处理 dom 全屏的 Hook。

useHover

一个用于追踪 dom 元素是否有鼠标悬停的 Hook。

useInViewport

一个用于判断 dom 元素是否在可视范围之内的 Hook。

useKeyPress

一个优雅的管理 keyup 和 keydown 键盘事件的 Hook,支持键盘组合键,定义键盘事件的 key 和 keyCode 别名输入 。

useMouse

一个跟踪鼠标位置的 Hook

useScroll

获取元素的滚动状态。

useSize

一个用于监听 dom 节点尺寸变化的 Hook。

useTextSelection

实时获取用户当前选取的文本内容及位置。

useTitle

用于设置页面标题的 Hook。

useCreation

useCreationuseMemouseRef 的替代品。

因为 useMemo 不能保证被 memo 的值一定不会被重计算,而 useCreation 可以保证这一点。以下为 React 官方文档中的介绍:

You may rely on useMemo as a performance optimization, not as a semantic guarantee. In the future, React may choose to “forget” some previously memoized values and recalculate them on next render, e.g. to free memory for offscreen components. Write your code so that it still works without useMemo — and then add it to optimize performance.

而相比于 useRef,你可以使用 useCreation 创建一些常量,这些常量和 useRef 创建出来的 ref 有很多使用场景上的相似,但对于复杂常量的创建,useRef 却容易出现潜在的性能隐患。

1
2
const a = useRef(new Subject()) // 每次重渲染,都会执行实例化 Subject 的过程,即便这个实例立刻就被扔掉了
const b = useCreation(() => new Subject(), []) // 通过 factory 函数,可以避免性能隐患
useLockFn

用于给一个异步函数增加竞态锁,防止并发执行。

usePersistFn

持久化 function 的 Hook。

参考 如何从 useCallback 读取一个经常变化的值?

在某些场景中,你可能会需要用 useCallback 记住一个回调,但由于内部函数必须经常重新创建,记忆效果不是很好,导致子组件重复 render。对于超级复杂的子组件,重新渲染会对性能造成影响。通过 usePersistFn,可以保证函数地址永远不会变化。

useReactive

提供一种数据响应式的操作体验,定义数据状态不需要写useState , 直接修改属性即可刷新视图。

useSafeState

用法与 React.useState 完全一样,但是在组件卸载后异步回调内的 setState 不再执行,避免因组件卸载后更新状态而导致的内存泄漏

项目开发

初始化

安装Demo
1
2
3
4
5
6
yarn create umi admin_real
? Select the boilerplate type ant-design-pro
? 🧙 Be the first to experience the new umi@3 ? Pro V4
? 🤓 Which language do you want to use? TypeScript
? 🚀 Do you need all the blocks or a simple scaffold? simple
? 🦄 Time to use better, faster and latest antd@4! antd@4
移除无关页面和菜单
  • 删除routes.ts中的无关路由
  • 删除src/locales中的国家化配置
  • 删除src/pages下的无关页面
  • pages目录结构调整,对应路由配置和文件导入也需要调整
image-20210519203634585
  • 404/index.tsx页面国际化配置不好,添加国际化配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import { Button, Result } from 'antd';
import React from 'react';
import { history, useIntl } from 'umi';

const NoFoundPage: React.FC = () => {
const intl = useIntl();
return (
<Result
status="404"
title="404"
subTitle={intl.formatMessage({
id: 'pages.404.subTitle',
defaultMessage: 'Sorry, the page you visited does not exist.',
})}
extra={
<Button type="primary" onClick={() => history.push('/')}>
{intl.formatMessage({ id: 'pages.404.backhome', defaultMessage: 'Back Home' })}
</Button>
}
/>
);
};

export default NoFoundPage;
  • 登录界面调整,移除手机验证码登录,图标和文字调整
  • 去除代理相关配置,本项目直接使用线上开发完毕的接口。在src/utils/request.ts中定义基础地址,并且修改错误状态码对应提示
  • 本项目中所有网络请求均使用umi-request,他是网络请求库,基于 fetch 封装, 兼具 fetch 与 axios 的特点, 旨在为开发者提供一个统一的 api 调用方式, 简化使用, 并提供诸如缓存, 超时, 字符编码处理, 错误处理等常用功能.
  • 登录和退出登录功能开发
    • 登陆时首先访问/login接口获取token,将token保存在localstorage中,然后携带token去获取用户信息,用户信息存储在redux中。退出登录时移除这俩,并且重定向到首页即可。退出登录重定向到首页时携带一个redirect参数,登录成功后直接跳转到redirect的页面
  • 首页统计页面开发
  • 用户管理页面开发
  • 商品管理页面开发

image-20210522132834366

image-20210522153904082

  • antd加载中可以用Spin标签,也可以用骨架屏

  • 上传图片使用阿里云的oss对象存储,先用token去服务端请求获取签名,然后带签名去上传文件到阿里云获取图片在线地址

  • 上传图片用Upload标签,受控组件onChange只回调一次的解决方法 https://github.com/ant-design/ant-design/issues/2423

  • 填写内容过多时可使用分布表单,使用起来很方便

  • 高级表单和分步表单要使用自定义组件或者第三方组件时,对该组件使用ProForm.Item进行包裹,然后再使用useForm()获取到表单的实例,调用实例为ProForm.Item即可实现内外的必填项一致性控制

1
2
3
4
5
6
7
8
9
10
11
<ProForm.Item
name="cover"
label="商品主图"
rules={[{ required: true, message: '请上传商品主图' }]}
>
<div>
<AliyunOssUpload type="image" updateFile={updateFile}>
<Button icon={<UploadOutlined />}>上传封面图</Button>
</AliyunOssUpload>
</div>
</ProForm.Item>
  • 详情填写界面使用富文本组件braft-editor,官网 https://braft.margox.cn/
  • token过期需要跳转到登录界面,非路由组件中可以使用 import {history} from ‘umi’
  • 上传组件进行统一封装
  • umi-request中通过params和data传参的区别,get一般用params,提交(post,put,patch)一般用data

image-20210524222951157

  • 分类管理,包括二级菜单

使用table组件视线,table组件如果数据有children会自动生成折叠菜单

image-20210524231827029 image-20210524231931828
  • 订单管理

image-20210525192852015

image-20210525192916762

已支付的订单才可以发货

image-20210525193041530
  • 轮播管理模块

image-20210526082233330

image-20210526082342491