创建项目
yarn create @umijs/umi-app
目录结构
.
├── dist/ # 默认的 build 输出目录,生产环境的静态资源会在这里生成
│
├── mock/ # mock 文件所在目录,基于 express,用于模拟后端接口
│
├── config/ # 配置文件目录
│ ├── config.[ts|js] # Umi 配置文件,可以根据环境使用不同的配置文件,如 config.prod.ts、config.test.ts 等
│
├── public/ # 公共静态资源目录,放置不会被 Umi 处理的静态资源,如图片、字体文件等,构建时会被直接复制到输出目录
│
├── src/ # 源代码目录
│ ├── assets/ # 静态资源目录,如图片、字体文件等
│ ├── layouts/ # 页面布局组件目录
│ ├── pages/ # 页面组件目录,每个子目录通常对应一个路由页面
│ ├── pages/document.ejs # HTML 模板文件
│ ├── components/ # 可复用UI组件目录
│ ├── models/ # 数据模型目录(如果使用了 dva 等状态管理库)
│ ├── services/ # API 请求服务目录
│ ├── wrappers/ # 权限管理模块目录
│ ├── utils/ # 工具函数目录
│ ├── global.[css|less] # 全局样式文件
│ └── app.tsx # 应用程序入口文件(运行时配置文件)
│
├── .umirc.ts # Umi 配置文件的另一种形式,适用于 TypeScript 项目,与 config/ 目录下的配置文件二选一(构建时配置文件)
│
├── .env # 环境变量配置文件,可以用来设置不同的环境变量
│
└── package.json # 项目依赖和脚本配置文件
配置文件
import { defineConfig } from "umi";
import routes from "./routes";
import theme from "./theme";
export default defineConfig({
// node_modules 目录下依赖文件的编译方式
nodeModulesTransform: {
type: "none",
},
// 路由
routes,
// 快速刷新。可以保持组件状态
fastRefresh: {},
// 端口设置
devServer: {
port: 8083, // .env里面的权限更高
https: true, // 开启 https
},
// 页面标题
title: "UMI3_DEMO",
// 页面图标。注意:如果使用本地图片,需要放到public目录下
favicon: "./favicon.ico",
// 启用按需加载(分包)
dynamicImport: {
loading: "@/components/Loading", // 按需加载时,页面展示的loading
},
// 指定HTML挂载点(必须放在pages文件夹下)
// mountElementId: 'root',
// 主题颜色
theme,
});
Dva 是怎么改变数据的?
- 定义
models
(约定式文件夹)。数据是存储在 models 中 state 里面的,通过namespace
来区分(namespace 不可重复)。 - 定义全局数据时,将文件写在
models
文件夹下,根据功能命名(如:global.js|goods.js
)。定义组件数据时,可以在对应pages
下新建一个models
文件,然后根据模块对文件命名,数据不多的情况下可直接定义一个index.js
。 - 当用户点击按钮之后
- 触发唯一更新 state 的方法 dispatch,
dispatch
有两个参数dispatch({type:'',payload:{}})
- type: 就相当于给这个 action 起一个描述性的名字。
- payload: 在异步的行为中用于传递异步请求的参数。
- 如果是同步行为,直接通过 reducers 改变 state。
- 如果是异步行为,会先触发 effects(中的方法),通过 put,推向 reducers,最终改变 state。
models
中的 state 发生变化之后,使用 connect 方法拿到 model 中的数据从而改变页面。
注意点:
Reducers
: 是纯函数,它们接受当前的state
和接收到的Action
作为参数,根据 Action 的类型来决定如何计算并返回新的state
。Reducers
必须保持纯净,即给定同样的输入,始终产生同样的输出,且不产生任何副作用。Action
: 当需要改变数据时,首先会触发一个Action
。Action
是一个普通的 JavaScript 对象,包含一个必填的type
字段,用来标识这个Action
的目的。例如,{ type: 'ADD_ITEM' }
。connect
: 将组件与store
中的数据连接起来。这样,组件可以直接访问和dispatch Action
到store
,从而驱动数据变化。
// 通过 useDispatch 和 useSelector 读取和修改全局数据
import { useDispatch, useSelector } from "umi";
export default function Home() {
const dispatch = useDispatch();
const global = useSelector((state) => state.global);
const changeTitle = () => {
dispatch({
type: "global/setTitle",
payload: "UMI3_DEMO",
});
};
return (
<>
<p>全局title: {global.title}</p>
<button onClick={changeTitle}>change global title</button>
</>
);
}
路由权鉴
- Umi 中路由权限可以在配置路由的时候,添加
wrappers
属性,wrappers
属性是一个数组,数组中的元素可以是组件,也可以是函数,函数的参数是组件,返回值是组件。
{
path: '/',
component: '@/layouts/BaseLayouts',
routes: [
{ path: '/login', component: '@/pages/Login' },
{
// 对goods路由设置权限,需要做授权路由,在umi中配置wrappers属性即可,然后在对应的组件中添加对应逻辑
path: '/goods',
wrappers: ['@/wrappers/auth'],
component: '@/pages/goods'
},
]
},
- 这里演示元素为组件的情况,根据登录接口返回的用户角色信息,来判断是否可以访问该路由。(即使用户登录,但是角色权限不够,可以重定向到首页或者指定页面)
import { Redirect } from "umi";
export default (props) => {
const { authority } = props.route;
// 获取当前用户的权限列表
const currentUserAuthority = ["admin"]; // 此变量可根据登录接口将值存到本地或者存到dva中
// 判断当前路由是否可以渲染
if (authority.some((item) => currentUserAuthority.includes(item))) {
return <div>{props.children}</div>;
} else {
return <Redirect to="/home" />;
}
};
渲染前的权限校验
- 运行时配置(在
src
下新建app.[tsx|jsx]
文件)
场景:根据接口返回状态,在没有登录的情况下无法访问登录/注册以外的页面,需要做权限校验。
// app.js
import { request, history } from "umi";
// 运行时配置
export const render = async (oldRender) => {
// 权限校验业务,如果没有登录,则直接跳转到登录页
const { isLogin } = await request("/mock/auth");
console.log("isLogin", isLogin);
if (!isLogin) {
history.push("/login");
} else {
// 获取路由数据
routesData = await request("/mock/menus");
}
// oldRender 至少要被调用一次
oldRender();
};
- 动态路由读取、添加
// app.js
import { request, history } from "umi";
let routesData = []; // 动态读取的路由集合
// 处理接口拿到的路由数据
// 动态路由的 compnent 要的是一个组件不是一段地址,可通过require引入
// 动态路由读取后,跳转后不显示,需要关闭mfsu: {}
// 子路由不跳转,除了layout组件,其他需要添加exact: true,
//
const filterRoutes = (routesData) => {
routesData.map((item) => {
// 添加exact
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 function patchRoutes({ routes }) {
// 动态添加路由
// routes:原本的路由 (登录/ 注册),也就是不需要动态获取的路由
// console.log('routes', routes);
// console.log('routesData', routesData);
// 手动添加一条死路由
// routes.push({
// exact: true,
// component: require('@/pages/404').default,
// });
// 根据接口数据,添加多条,并给给一条路由添加exact,require添加component地址
filterRoutes(routesData);
routesData.map((item) => routes.push(item));
}
// 运行时配置
export const render = async (oldRender) => {
// 权限校验业务,如果没有登录,则直接跳转到登录页
const { isLogin } = await request("/mock/auth");
console.log("isLogin", isLogin);
if (!isLogin) {
history.push("/login");
} else {
// 如果是登录了,那么获取路由数据
routesData = await request("/mock/menus");
}
// oldRender 至少要被调用一次
oldRender();
};
// mock/login.js 模拟数据
export default {
// 动态返回当前用户的路由,切记,component不能包含@符和它后面的斜线
"GET /mock/menus": (req, res) => {
res.send([
{
path: "/",
component: "layouts/BaseLayouts",
routes: [
{ path: "/", redirect: "/home" },
{ path: "/home", component: "pages/Home" },
{
path: "/goods",
wrappers: ["wrappers/auth"],
component: "layouts/AsideLayouts",
routes: [
{
path: "/goods",
component: "pages/goods",
},
{
path: "/goods/:id",
component: "pages/goods/GoodsDetail",
},
{
path: "/goods/:id/comment",
component: "pages/goods/Comment",
},
],
},
{ component: "pages/404" },
],
},
{ component: "pages/404" },
]);
},
};
- 路由监听
// app.jsx
export function onRouteChange({ matchedRoutes, location, routes, action }) {
console.log(routes, 1); // 路由集合
console.log(matchedRoutes, 2); // 当前匹配的路由及子路由
console.log(location, 3); // location及参数
console.log(action, 4); // 当前跳转执行的操作
document.title =
matchedRoutes[matchedRoutes.length - 1].route.title || "hehe";
}