likes
comments
collection
share

UmiJS快速介绍

作者站长头像
站长
· 阅读数 20

官网:umijs.org/

开发环境

第一步,安装nvm

% sudo curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.38.0/install.sh | bash

% nvm -v
0.39.1

第二步,安装Node.js

% nvm install 16
% nvm use 16

% node -v
v16.20.0

第三步,安装pnpm。Umi.js推荐使用pnpm来管理依赖

% curl -fsSL https://get.pnpm.io/install.sh | sh -

% pnpm -v
8.1.0

运行脚手架

第一步,创建空目录

% mkdir myapp
% cd myapp

第二步,通过官方工具创建项目

这个命令会安装 create-umi 脚手架并自动运行。国内建议选 pnpm + taobao 源,速度提升明显。这一步会自动安装依赖,同时安装成功后会自动执行 umi setup 做一些文件预处理等工作。

% pnpm dlx create-umi@latest

过程中:
○  Pick Umi App Template
│  Simple App
○  Pick Npm Client
│  pnpm
○  Pick Npm Registry
│  taobao

成功后显示:Done in 14.9s

第三步,pnpm dev启动项目

% pnpm dev

App listening at: 
>   Local: http://localhost:8000 

第四步,在浏览器里打开 http://localhost:8000/ 能看到欢迎界面

目录结构

项目开发过程中,请按如下官方推荐的目录结构进行开发

.
├── config
│   └── config.ts  // 【配置文件】
├── dist // 执行umi build后产物的默认输出文件夹
├── mock
│   └── app.ts|tsx  // mock文件
├── src
│   ├── .umi    // dev时的临时文件目录,比如入口文件、路由等
│   ├── .umi-production   // build 时的临时文件目录,比如入口文件、路由等
│   ├── layouts  // 全局布局,默认会在所有路由下生效
│   │   ├── BasicLayout.tsx
│   │   ├── index.less
│   ├── models
│   │   ├── global.ts
│   │   └── index.ts
│   ├── pages // 页面
│   │   ├── index.less
│   │   └── index.tsx
│   ├── utils // 推荐目录
│   │   └── index.ts
│   ├── services // 推荐目录
│   │   └── api.ts
│   ├── app.(ts|tsx)   // 运行时配置文件
│   ├── global.ts  // 全局前置脚本文件
│   ├── global.(css|less|sass|scss)  // 全局样式文件
│   ├── overrides.(css|less|sass|scss)  // 高优先级全局样式文件
│   ├── favicon.(ico|gif|png|jpg|jpeg|svg|avif|webp) // 站点favicon图标文件
│   └── loading.(tsx|jsx)  // 全局加载组件
├── node_modules
│   └── .cache
│       ├── bundler-webpack
│       ├── mfsu
│       └── mfsu-deps
├── .env // 【环境变量】
├── plugin.ts  //  项目级 Umi 插件 
├── .umirc.ts  // 【配置文件】与config/config.ts文件二选一
├── package.json
├── tsconfig.json
└── typings.d.ts

.umirc.ts

注意:此文件与config/config.ts文件功能相同,2选1 。且.umirc.ts文件优先级较高

配置文件,包含Umi所有非运行时配置(运行时配置一般定义于app.ts)

public目录

存放固定的静态资源,如存放public/image.png,则开发时可以通过/image.png访问到,构建后会被拷贝到输出文件夹

对于svg资源:
import SmileUrl, { ReactComponent as SvgSmile } from './smile.svg';
// <SvgSmile />

对于图片等资源:
import imgUrl from './image.png'
// <img src={imgUrl} />>

src目录/app.[ts|tsx]

运行时配置文件,可以在这里扩展运行时的能力,比如修改路由、修改render方法等

src目录/layouts/index.tsx

全局布局,默认会在所有路由下生效,比如有以下路由关系:

[
	{ path: '/', component: '@/pages/index' },
	{ path: '/users', component: '@/pages/users' },
]

输出为:

<Layout>
	<Page>index</Page>
	<Page>users</Page>
</Layout>

当需要关闭layout时可以使用layout: false,当需要更多层layout时,可以考虑使用wrappers,仅在配置式路由可用:

routes: [
	{ path: '/', component: './index', layout: false },
	{ 
		path: '/users', 
		component: './users', 
		wrappers: ['@/wrappers/auth']
	}
]

src目录/pages目录

约定式路由默认以 pages/ 文件夹的文件层级结构来生成路由表

在配置式路由中,component若写为相对路径,将从该文件夹为起点开始寻找文件:

routes: [
	// `./index` === `@/pages/index`
	{ path: '/', component: './index' }
]

基础路由

假设 pages 目录结构如下:
+ pages/
  + users/
    - index.tsx
  - index.tsx

会自动生成路由配置如下:

[
  { path: '/', component: '@/pages/index.tsx' },
  { path: '/users/', component: '@/pages/users/index.tsx' },
]

动态路由

约定带 $ 前缀的目录或文件为动态路由。若 $ 后不指定参数名,则代表 * 通配

比如以下目录结构:
+ pages/
  + foo/
    - $slug.tsx
  + $bar/
    - $.tsx
  - index.tsx

会生成路由配置如下:

[
  { path: '/', component: '@/pages/index.tsx' },
  { path: '/foo/:slug', component: '@/pages/foo/$slug.tsx' },
  { path: '/:bar/*', component: '@/pages/$bar/$.tsx' },
]

pages/404.tsx

在使用约定式路由时,该文件会自动被注册为全局 404 的 fallback 页面。若使用配置式路由,需要自行配置兜底路由到路由表最后一个:

routes: [
	// other routes ...
	{ path: '/*', component: '@/pages/404.tsx' }
]

路由

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

配置路由

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

(Umi4默认按页拆包,从而有更快的页面加载速度,由于加载过程是异步的,所以需要编写 loading.tsx 来给项目添加加载样式,提升体验)

// .umirc.ts
export default {
	routes: [
		{ path: '/', component: 'index' },
		{ path: '/user', component: 'user' },
	],
}

path

String类型。path只支持两种占位符配置,第一种是 动态参数 :id 的形式,第二种是 *通配符 ,通配符只能出现路由字符串的最后。 如:

/groups
/groups/admin
/users/:id
/users/:id/messages
/files/*
/files/:id/*

component

配置location和path匹配后用于渲染的React组件路径。可以是绝对路径,也可以是相对路径,如果是相对路径,会从src/pages开始寻找。

如果指向src目录的文件,可以用 @ ,比如 component: '@/layouts/basic',推荐使用@组织路由文件位置。

routes

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

export default {
  routes: [
    { path: '/login', component: 'login' },
    {
      path: '/',
      component: '@/layouts/index',
      routes: [
        { path: '/list', component: 'list' },
        { path: '/admin', component: 'admin' },
      ],
    }, 
  ],
}

在全局布局 src/layouts/index 中,通过 <Outlet/> 来渲染子路由:

import { Outlet } from 'umi'
 
export default function Page() {
  return (
    <div style={{ padding: 20 }}> 
      <Outlet/> 
    </div>
  )
}

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

redirect

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

export default {
  routes: [
    { path: '/', redirect: '/list' },
    { path: '/list', component: 'list' },
  ],
}

wrappers

配置路由组件的包装组件,通过包装组件可以为当前的路由组件组合进更多的功能。 比如,可以用于路由级别的权限校验

export default {
  routes: [
    { path: '/user', component: 'user',
      wrappers: [
        '@/wrappers/auth',
      ],
    },
    { path: '/login', component: 'login' },
  ]
}

然后在 src/wrappers/auth 中进行权限判断。访问 /user,就通过 auth 组件做权限校验,如果通过,渲染 src/pages/user,否则跳转到 /login。

import { Navigate, Outlet } from 'umi'
 
export default (props) => {
  const { isLogin } = useAuth();
  if (isLogin) {
    return <Outlet />;
  } else{
    return <Navigate to="/login" />;
  }
}

约定式路由

除配置式路由外,Umi也支持约定式路由。约定式路由也叫文件路由,就是不需要手写配置,文件系统即路由,通过目录和文件及其命名分析出路由配置。如果没有routes配置,Umi会进入约定式路由模式,然后分析src/pages 目录拿到路由配置。 如:

.
  └── pages
    ├── index.tsx
    └── users.tsx

会得到以下路由配置:
[
  { path: '/', component: '@/pages/index' },
  { path: '/users', component: '@/pages/users' },
]

动态路由

约定,带 $ 前缀的目录或文件为动态路由。若 $ 后不指定参数名,则代表 * 通配,比如以下目录结构:

src/pages/users/$id.tsx   会成为   /users/:id
src/pages/users/$id/settings.tsx   会成为   /users/:id/settings
+ pages/
  + foo/
    - $slug.tsx
  + $bar/
    - $.tsx
  - index.tsx

会生成路由配置如下:
[
  { path: '/', component: '@/pages/index.tsx' },
  { path: '/foo/:slug', component: '@/pages/foo/$slug.tsx' },
  { path: '/:bar/*', component: '@/pages/$bar/$.tsx' },
];

全局layout

约定 src/layouts/index.tsx 为全局路由。返回一个 React 组件,并通过 <Outlet /> 渲染嵌套路由。

.
└── src
    ├── layouts
    │   └── index.tsx
    └── pages
        ├── index.tsx
        └── users.tsx

会生成如下路由:
[
  { 
    path: '/', 
    component: '@/layouts/index',
    routes: [
      { path: '', component: '@/pages/index' },
      { path: 'users', component: '@/pages/users' },
    ],
  },
]

可以通过 layout: false 来细粒度关闭某个路由的全局布局显示,该选项只在一级生效:

routes: [
	{ 
		path: '/', 
		component: './index', 
		// OK
		layout: false 
	},
	{
		path: '/users',
		routes: [
			// 不生效,此时该路由的 layout 并不是全局布局,而是 `/users`
			{ layout: false }
		]
	}
]

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

import { Outlet } from 'umi'
 
export default function Layout() {
  return <Outlet />
}

不同的全局layout

如果需要针对不同路由输出不同的全局layout,可以在 src/layouts/index.tsx 中对 location.path 做区分,渲染不同的layout

import { useLocation, Outlet } from 'umi';
 
export default function() {
  const location = useLocation();
  if (location.pathname === '/login') {
    return <SimpleLayout><Outlet /></SimpleLayout>
  }
 
  // 使用 `useAppData` / `useSelectedRoutes` 可以获得更多路由信息
  // const { clientRoutes } = useAppData()
  // const routes = useSelectedRoutes()
 
  return (
    <>
      <Header />
      <Outlet />
      <Footer />
    </>
  );
}

404路由

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

404只有约定式路由会自动生效,如果使用配置式路由,需要自行配置404的通配路由。

.
└── pages
    ├── 404.tsx
    ├── index.tsx
    └── users.tsx

会生成路由:
[
  { path: '/', component: '@/pages/index' },
  { path: '/users', component: '@/pages/users' },
  { path: '/*', component: '@/pages/404' },
]

页面跳转

命令式跳转使用 history API,组件内还可以使用 useNavigate hook

Link组件

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

import { Link } from 'umi';
 
export default function Page() {
  return (
    <div>
      <Link to="/users">Users Page</Link>
    </div>
  )
}

路由组件参数

Umi4使用react-router@6作为路由组件,路由参数的获取使其hooks

match信息-useMatch

const match = useMatch('/comp/:id')
// match 
{
	"params": {
		"id": "paramId"
	},
	"pathname": "/comp/paramId/",
		"pathnameBase": "/comp/paramId",
		"pattern": {
		"path": "/comp/:id",
			"caseSensitive": false,
			"end": true
	}
}

location信息-useLocation

推荐使用 useLocation, 而不是直接访问 history.location. 两者的区别是 pathname 的部分。 history.location.pathname 是完整的浏览器的路径名;而 useLocation 中返回的 pathname 是相对项目配置的base的路径。

举例:项目如果配置 base: '/testbase', 当前浏览器地址为 https://localhost:8000/testbase/page/apple

history.location.pathname 为 /testbase/page/apple

useLocation().pathname 为 /page/apple

const location  = useLocation();
// location
{
  "pathname": "/path/",
  "search": "",
  "hash": "",
  "state": null,
  "key": "default"
}

路由动态参数-useParams

// 路由配置 /comp/:id
// 当前 location /comp/paramId
 
const params  = useParams();
// params
{
  "id": "paramId"
}

query信息-useSearchParams

// 当前 location /comp?a=b;
const [searchParams, setSearchParams] = useSearchParams();
searchParams.get('a')  // b
searchParams.toString()  // a=b
 
setSearchParams({a:'c',d:'e'}) // location 变成 /comp?a=c&d=e

Mock

/mock 目录下的所有文件为Mock文件

Mock文件

Mock文件默认导出一个对象,对象的每个Key对应了一个Mock接口,值则是这个接口所对应的返回数据。当Http的请求方法是GET时,可以省略方法部分,只需要路径即可

// ./mock/users.ts
export default {
  // 返回值可以是数组形式
  'GET /api/users': [
    { id: 1, name: 'foo' },
    { id: 2, name: 'bar' }
  ],
  // 返回值也可以是对象形式
  'GET /api/users/1': { id: 1, name: 'foo' },
	// 省略请求方法
	'/api/users/2': { id: 1, name: 'foo' },
	// POST方法
	'POST /api/users/3': { result: 'true' },
	// PUT方法
	'PUT /api/users/4': { id: 1, name: 'new-foo' },

	// 可以用函数的方式来声明如何计算返回值
	'POST /api/users/create': (req, res) => {
    // 添加跨域请求头
    res.setHeader('Access-Control-Allow-Origin', '*');
    res.end('ok');
  }
}

引入Mock.js

在Mock中我们经常使用Mock.js来帮我们方便的生成随机的模拟数据

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

代理

在配置文件中使用proxy配置。

将 /api 前缀的请求,代理到 jsonplaceholder.typicode.com/,替换请求地址中的 /api 为 '',并且将请求来源修改为目标url。如请求 /api/a,实际上是请求 jsonplaceholder.typicode.com/a

export default {
  proxy: {
    '/api': {
      'target': 'http://jsonplaceholder.typicode.com/',
      'changeOrigin': true,
      'pathRewrite': { '^/api' : '' },
    },
  },
}

一般我们使用这个能力来解开发中的跨域访问问题。由于浏览器(或者 webview)存在同源策略,之前我们会让服务端配合使用 Cross-Origin Resource Sharing (CORS) 策略来绕过跨域访问问题。现在有了本地的 node 服务,我们就可以使用代理来解决这个问题。我们请求同源的本地服务,然后让本地服务去请求非同源的远程服务。需要注意的是,请求代理,代理的是请求的服务,不会直接修改发起的请求 url。它只是将目标服务器返回的数据传递到前端。所以你在浏览器上看到的请求地址还是 http://localhost:8000/api/a

值得注意的是 proxy暂时只能解开发时(dev)的跨域访问问题,可以在部署时使用同源部署。如果在生产上(build)的发生跨域问题的话,可以将类似的配置转移到Nginx容器上。


常用API

详情见:umijs.org/docs/api/ap…

createBrowserHistory

创建使用浏览器内置history来跟踪应用的BrowserHistory

// create a BrowserHistory
import { createBrowserHistory } from 'umi';
const history = createBrowserHistory();

createHashHistory

返回一个HashHistory实例,HashHistory将当前位置存储在URL的哈希部分中,这意味着它在路由切换时不会发送请求到服务器

// create a HashHistory
import { createHashHistory } from 'umi';
const history = createHashHistory();

createMemoryHistory

MemoryHistory不会在地址栏被操作或读取

const history = createMemoryHistory(location)

createSearchParams

包装 new URLSearchParams(init) 的工具函数,支持使用数组和对象创建

import { createSearchParams } from 'umi';
 
// 假设路径 http://a.com?foo=1&bar=2
createSearchParams(location.search);
createSearchParams("foo=1&bar=2");
createSearchParams("?foo=1&bar=2");
 
// 键值对对象
createSearchParams({ foo: 'bar', qux: 'qoo'}).toString()
// foo=bar&qux=qoo
 
// 键值元组数组
createSearchParams([["foo", "1"], ["bar", "2"]]).toString()
// foo=1&bar=2

generatePath

使用给定的带参数的path和对应的params生成实际要访问的路由。

import { generatePath } from 'umi';
 
generatePath("/users/:id", { id: "42" }); // "/users/42"
generatePath("/files/:type/*", {
  type: "img",
  "*": "cat.jpg",
}); // "/files/img/cat.jpg"

Helmet

用于在页面中动态配置head中的标签,例如title

import { Helmet } from 'umi';
 
export default function Page() {
  return (
    <Helmet>
      <title>Hello World</title>
    </Helmet>
  );
}

history

和history相关的操作,用于获取当前路由信息、执行路由跳转、监听路由变更

// 建议组件或 hooks 里用 useLocation 取
import { useLocation } from 'umi';
export default function Page() {
  let location = useLocation();
  return (
    <div>
     { location.pathname }
     { location.search }
     { location.hash }
    </div>
  );
}

命令式路由跳转

import { history } from 'umi';
 
// 跳转到指定路由
history.push('/list');
 
// 带参数跳转到指定路由
history.push('/list?a=b&c=d#anchor', state);
history.push({
    pathname: '/list',
    search: '?a=b&c=d',
    hash: 'anchor',
  },
  {
    some: 'state-data',
  }
);
 
// 跳转当前路径,并刷新 state
history.push({}, state)
 
// 跳转到上一个路由
history.back();
history.go(-1);

路由监听

import { history } from 'umi';
 
const unlisten = history.listen(({ location, action }) => {
  console.log(location.pathname);
});
unlisten();

Link

<Link>是React组件,是带路由跳转功能的<a>元素

import { Link } from 'umi';
 
function IndexPage({ user }) {
  return <Link to={user.id}>{user.name}</Link>;
}

matchPath

matchPath可以将给定的路径以及一个已知的路由格式进行匹配,并且返回匹配结果

import { matchPath } from 'umi';
const match = matchPath(
  { path: "/users/:id" },
  "/users/123",
);

matchRoutes

可以将给定的路径以及多个可能的路由选择进行匹配,并且返回匹配结果

import { matchRoutes } from 'umi';
const match = matchRoutes(
  [
    {
      path: "/users/:id",
    },
    {
      path: "/users/:id/posts/:postId",
    },
  ],
  "/users/123/posts/456",
);

NavLink

是否为路由激活状态,用于显示当前的选中状态

import { NavLink } from 'umi';
 
function Navs() {
  return <ul>
    <li><NavLink to="message" style={({ isActive }) => isActive ? { color: 'red' } : undefined}>Messages</NavLink></li>
    <li><NavLink to="tasks" className={({ isActive }) => isActive ? 'active' : undefined}>Tasks</NavLink></li>
    <li><NavLink to="blog">{({ isActive }) => <span className={isActive ? 'active' : undefined}>Blog</span>}</NavLink></li>
  </ul>;
}

Outlet

用于渲染父路由中渲染子路由。如果父路由被严格匹配,会渲染子路由中的 index 路由(如有)

resolvePath

用于在客户端解析前端路由跳转路径

terminal

在开发阶段在浏览器向node终端输出日志的工具

useAppData

返回全局的应用数据

useLocation

返回当前location对象

// 在 location change 时做一些 side effect 操作,比如 page view 统计
import { useLocation } from 'umi';
 
function App() {
  const location = useLocation();
  React.useEffect(() => {
    ga('send', 'pageview');
  }, [location]);
  // ...
}

useMatch

useMatch返回传入path的匹配信息;如果匹配失败将返回null

useNavigate

useNavigate钩子函数返回一个可以控制跳转的函数;比如可以用在提交完表单后跳转到其他页面

// 跳转路径
import { useNavigate } from 'umi';
 
let navigate = useNavigate();
navigate("../success", { replace: true });
// 返回上一页
import { useNavigate } from 'umi';
 
let navigate = useNavigate();
navigate(-1);

useOutlet

useOutlet返回当前匹配的子路由元素,内部使用的就是此hook

useOutletContext

useOutletContext用于返回Outlet组件上挂载的context

useParams

返回动态路由的匹配参数键值对对象;子路由中会集成父路由的动态参数

import { useParams } from 'umi';
 
// 假设有路由配置  user/:uId/repo/:rId
// 当前路径       user/abc/repo/def
const params = useParams()
/* params
{ uId: 'abc', rId: 'def'}
*/

useResolvedPath

根据当前路径将目标地址解析出完整的路由信息

import { useResolvedPath } from 'umi';
 
const path = useResolvedPath('docs')
/* path
{ pathname: '/a/new/page/docs', search: '', hash: '' }
*/

useRouteData

返回当前匹配路由的数据的钩子函数

useRoutes

渲染路由的钩子函数,传入路由配置和可选参数location, 即可得到渲染结果;如果没有匹配的路由,结果为null

useRouteProps

读取当前路由在路由配置里的props属性,可以用此hook来获取路由配置中的额外信息。(同样适用于约定式路由)

useSelectedRoutes

用于读取当前路径命中的所有路由信息

useSearchParams

useSearchParams 用于读取和修改当前 URL 的 query string。类似 React 的 useState,其返回包含两个值的数组,当前 URL 的 search 参数和用于更新 search 参数的函数。

import React from 'react';
import { useSearchParams } from 'umi';
 
function App() {
  let [searchParams, setSearchParams] = useSearchParams();
  function handleSubmit(event) {
    event.preventDefault();
    setSearchParams(serializeFormQuery(event.target));
  }
  return <form onSubmit={handleSubmit}>{/* ... */}</form>;
}
转载自:https://juejin.cn/post/7216914115260858423
评论
请登录