快速了解UmiJS
官网: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>;
}