Umi.js 学习记录

本文记录了学习 Umi.js 的过程,主要探讨了 Dva 中数据的变更原理,包括 state 的管理、dispatch 的使用、reducers 和 effects 的作用。同时,介绍了 Umi 的路由权限配置,包括在路由定义中添加权限属性以及运行时的权限校验策略。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

创建项目

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
  • 当用户点击按钮之后
    1. 触发唯一更新 state 的方法 dispatch,
    2. dispatch 有两个参数 dispatch({type:'',payload:{}})
      • type: 就相当于给这个 action 起一个描述性的名字。
      • payload: 在异步的行为中用于传递异步请求的参数。
    3. 如果是同步行为,直接通过 reducers 改变 state。
    4. 如果是异步行为,会先触发 effects(中的方法),通过 put,推向 reducers,最终改变 state。
    5. models 中的 state 发生变化之后,使用 connect 方法拿到 model 中的数据从而改变页面。

注意点:

  1. Reducers: 是纯函数,它们接受当前的 state 和接收到的 Action 作为参数,根据 Action 的类型来决定如何计算并返回新的 stateReducers 必须保持纯净,即给定同样的输入,始终产生同样的输出,且不产生任何副作用。
  2. Action: 当需要改变数据时,首先会触发一个 ActionAction 是一个普通的 JavaScript 对象,包含一个必填的 type 字段,用来标识这个 Action 的目的。例如,{ type: 'ADD_ITEM' }
  3. connect: 将组件与 store 中的数据连接起来。这样,组件可以直接访问和 dispatch Actionstore,从而驱动数据变化。
// 通过 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" />;
  }
};

渲染前的权限校验

  1. 运行时配置(在 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();
};
  1. 动态路由读取、添加
// 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" },
    ]);
  },
};
  1. 路由监听
// 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";
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值