- https://umijs.org/zh/guide/
- https://umijs.org/zh-CN
- github: umi
- github: create-umi
- umi-plugin-react文档
- umi-plugin-react源码
- umi-plugin-dva
- dva-immer
- immer
- umi-blocks
- pro-blocks
简述
Umi,中文可发音为乌米,是可扩展的企业级前端应用框架。Umi 以路由为基础的,同时支持配置式路由和约定式路由,保证路由的功能完备,并以此进行功能扩展。然后配以生命周期完善的插件体系,覆盖从源码到构建产物的每个生命周期,支持各种功能扩展和业务需求。
- UmiJS 是一个类 Next.js 的react开发框架。
- 他基于一个约定,即 pages 目录下的文件即路由,而文件则导出 react 组件。
- 然后打通从源码到产物的每个阶段,并配以完善的插件体系,让我们能把 umi 的产物部署到各种场景里。
快速上手
mkdir myapp && cd myapp
# 通过官方工具创建项目
yarn create @umijs/umi-app # 或 npx @umijs/create-umi-app
yarn install # 安装所有依赖
yarn start # 启动
yarn build # 打包。构建产物默认生成到 ./dist 下,然后通过 “tree ./dist” 命令查看,
# 本地验证,类似于使用 http-server ./dist -p 5000
yarn global add server
serve ./dist
目录约定
.
├── dist/ // 默认的 build 输出目录
├── mock/ // mock 文件所在目录,基于 express
├── public/ // 此目录下所有文件会被 copy 到输出路径。
├── config/
├── config.js // umi 配置,同 .umirc.js,二选一
└── src/ // 源码目录,可选
├── layouts/index.js // 全局布局
├── pages/ // 页面目录,里面的文件即路由
├── .umi/ // dev 临时目录,需添加到 .gitignore
├── .umi-production/ // build 临时目录,会自动删除
├── document.ejs // HTML 模板
├── 404.js // 404 页面
├── page1.js // 页面 1,任意命名,导出 react 组件
├── page1.test.js // 用例文件,umi test 会匹配所有 .test.js 和 .e2e.js 结尾的文件
└── page2.js // 页面 2,任意命名
├── global.css // 约定的全局样式文件,自动引入,也可以用 global.less
├── global.js // 可以在这里加入 polyfill
├── .umirc.js // umi 配置,同 config/config.js,二选一
├── .env // 环境变量
└── package.json
命令行工具
- https://umijs.org/zh-CN/docs/cli
```shell"scripts": {
"dev": "umi dev", // 本地启动
"build": "umi build" // 打包
},
查看当前使用的 umi 的版本号,可以使用别名 -v 调用。
$ umi version$ umi -v
查看帮助
$ umi help
$ umi help
查看 webpack 的配置
$ umi webpack $ umi webpack [options]
默认会打印 development 的配置,如需查看 production 配置,需要指定环境变量:
$ NODE_ENV=production umi webpack
快速查看当前项目使用到的所有的 umi 插件
$ umi plugin
当前支持的 type 是 list,可选参数 key。
$ umi plugin list $ umi plugin list —key
<a name="Txonx"></a>
## umi build 打包
编译构建 web 产物。通常需要针对部署环境,做特定的配置和环境变量修改。相关详情,请查阅[部署](https://umijs.org/zh-CN/docs/deployment)。
```shell
$ umi build
默认产物输出到项目的 dist 文件夹,你可以通过修改配置 outputPath 指定产物输出目录。 默认编译时会将 public 文件夹内的所有文件,原样拷贝到 dist 目录,如果你不需要这个特性,可以通过配置 chainWebpack 来删除它。
export default {
chainWebpack(memo, { env, webpack }) {
// 删除 umi 内置插件
memo.plugins.delete('copy');
}
}
注意:如果 public 里面存在产物同名文件,如 index.html,将会导致产物文件被覆盖。
umi dev 启动服务器
启动本地开发服务器进行项目的开发调试
$ umi dev
启动在浏览器中运行的开发服务器,并监视源文件变化,自动热加载。
默认使用 8000 端口,如果 8000 端口被占用,将会使用 8001 端口,以此类推。 你可以通过设置环境变量 PORT 来指定开发端口号。更多环境变量配置,请查阅环境变量。
开启开发服务还会同时提供一个 Network 的链接,你可以在能访问到你当前运行设备的其他设备中预览页面。
注意:如果是在开启了VPN,或者虚拟机等复杂的网络环境中,这个地址很可能会错误。你可以通过访问你真实可用 ip的对应端口号来访问开发页面。
umi generate 生成器
内置的生成器功能,内置的类型有 page
,用于生成最简页面。支持别名调用 umi g
。
$ umi generate <type> <name> [options]
这个命令支持扩展,通过 api.registerGenerator
注册,你可以通过插件来实现自己常用的生成器。
import { Generator, IApi } from 'umi';
const createPagesGenerator = function ({ api }: { api: IApi }) {
return class PageGenerator extends Generator {
constructor(opts: any) {
super(opts);
}
async writing() {}
};
}
api.registerGenerator({
key: 'pages',
Generator: createPageGenerator({ api }),
});
umi generate page pageName
umi generate page pageName --typescript
umi generate page pageName --less
更多使用类型和参数,请查阅提供生成器扩展的插件的文档。
环境变量
- https://umijs.org/zh-CN/docs/env-variables
命令行添加
```shellOS X, Linux
$ PORT=3000 umi dev
Windows (cmd.exe)
$ set PORT=3000&&umi dev
如果要同时考虑 OS X 和 Windows,可借助三方工具 cross-env
$ cnpm i -D cross-env $ cross-env PORT=3000 umi dev
<a name="GGYT3"></a>
##### 在 .env 文件中定义
Umi 中约定根目录下的 .env 为环境变量配置文件。<br />比如:
```shell
PORT=3000
BABEL_CACHE=none
然后执行,
$ umi dev
会以 3000 端口启动 dev server,并且禁用 babel 的缓存。
以下面demo为例-介绍umi基础
初始化
cnpm i -g umi
mkdir 9.umi
cd mkdir 9.umi
cnpm init -y
# 新建pages目录
mkdir src/pages
约定式路由
除配置式路由外,Umi 也支持约定式路由。约定式路由也叫文件路由,就是不需要手写配置,文件系统即路由,通过目录和文件及其命名分析出路由配置。
如果没有 routes 配置,Umi 会进入约定式路由模式,然后分析 src/pages 目录拿到路由配置。
比如以下文件结构:
.
└── pages
├── index.tsx
└── users.tsx
会得到以下路由配置:
[
{ exact: true, path: '/', component: '@/pages/index' },
{ exact: true, path: '/users', component: '@/pages/users' },
]
注意:满足以下任意规则的文件不会被注册为路由。
- 以 . 或 _ 开头的文件或目录
- 以 d.ts 结尾的类型定义文件
- 以 test.ts、spec.ts、e2e.ts 结尾的测试文件(适用于 .js、.jsx 和 .tsx 文件)
- components 和 component 目录
- utils 和 util 目录
- 不是 .js、.jsx、.ts 或 .tsx 文件
- 文件内容不包含 JSX 元素
创建 首页、用户管理、个人中心3个页面。
首页
❯❯ ~ umi g page index # 通过umi创建一个新的页面,名字叫index(首页)
Write: pages\index.js
Write: pages\index.css
❯❯ ~ umi g page user # 创建 user 页面
Write: pages\user.js
Write: pages\user.css
❯❯ ~ umi g page profile # 创建 profile 页面
Write: pages\profile.js
Write: pages\profile.css
pages/index.js
import React from 'react';
import {Link} from 'umi';
export default function Page() {
return (
<div>
<h1>首页</h1>
<Link to="/profile">个人中心</Link>
</div>
);
}
pages/profile.js
import React from 'react';
import {history} from 'umi';
export default function Page() {
return (
<div>
<h1>个人中心</h1>
<button onClick={() => history.goBack()}>返回</button>
</div>
);
}
全局 layout
约定 src/layuots/index.js
为全局路由,返回一个Reat组件,通过 props.children
渲染子组件(pages下文件)。
比如以下目录结构:
.
└── src
├── layouts
│ └── index.tsx
└── pages
├── index.tsx
└── users.tsx
会生成路由:
[
{
exact: false,
path: '/',
component: '@/layouts/index',
routes: [
{ exact: true, path: '/', component: '@/pages/index' },
{ exact: true, path: '/users', component: '@/pages/users' },
],
},
]
src/layouts/index.js
import React from 'react';
import {Link} from 'umi';
export default class Layout extends React.Component{
render(){
return (
<div>
<ul>
<li><Link to="/">首页</Link></li>
<li><Link to="/user">用户管理</Link></li>
<li><Link to="/profile">个人中心</Link></li>
</ul>
<div>{this.props.children}</div>
</div>
)
}
}
嵌套路由
Umi 里约定目录下有 _layout.tsx
时会生成嵌套路由,以 _layout.tsx
为该目录的 layout。layout 文件需要返回一个 React 组件,并通过 props.children
渲染子组件。
比如以下目录结构:
.
└── pages
└── users
├── _layout.tsx
├── index.tsx
└── list.tsx
会生成路由:
[
{
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' },
]
}
]
src/layouts/index.js 修改
<li><Link to="/user/list">用户管理</Link></li>
src/pages/user/_layout.js (user页面的新入口)
如果有 pages/user/_layout.js, pages/user.js 就自动失效了。
import React from 'react';
import {Link} from 'umi';
export default class Layout extends React.Component{
render(){
return (
<div>
<ul>
<li><Link to="/user/list">用户列表</Link></li>
<li><Link to="/user/add">新增用户</Link></li>
</ul>
<div>{this.props.children}</div>
</div>
)
}
}
src/pages/user/list.js
import React from 'react';
import {Link} from 'umi';
export default function List() {
return (
<ul>
<li><Link to="/user/detail/1">张三</Link></li>
<li><Link to="/user/detail/2">李四</Link></li>
</ul>
);
}
src/pages/user/add.js
import React from 'react';
export default function Add() {
return (
<form>
<input type="text" />
<button type="submit">提交</button>
</form>
);
}
动态路由
约定 []
包裹的文件或文件夹为动态路由。
比如:
src/pages/users/[id].tsx
会成为/users/:id
src/pages/users/[id]/settings.tsx
会成为/users/:id/settings
比如以下文件结构:
.
└── pages
└── [post]
├── index.tsx
└── comments.tsx
└── users
└── [id].tsx
└── index.tsx
会生成路由配置:
[
{ 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/user/detail/[id].js
import React from 'react';
import {Link} from 'umi';
export default function Detail(props) {
console.log(props);
return (
<div>
<div>ID: {props.match.params.id}</div>
</div>
);
}
动态可选路由
约定 [ $]
包裹的文件或文件夹为动态可选路由。
比如:
src/pages/users/[id$].tsx
会成为/users/:id?
src/pages/users/[id$]/settings.tsx
会成为/users/:id?/settings
比如以下文件结构:
.
└── pages
└── [post$]
└── comments.tsx
└── users
└── [id$].tsx
└── index.tsx
会生成路由配置:
[
{ exact: true, path: '/', component: '@/pages/index' },
{ exact: true, path: '/users/:id?', component: '@/pages/users/[id$]' },
{ exact: true, path: '/:post?/comments', component: '@/pages/[post$]/comments' },
];
权限路由
- 通过指定高阶组件
wrappers
达成效果。src/pages/profile.js
```jsx import React from ‘react’; import {history} from ‘umi’;
function Profile() { return (
个人中心
// 用 auth 高阶组件包裹 Profile。 Profile.wrappers = [‘@/wrappers/auth’]; export default Profile;
<a name="QD3Ie"></a>
##### src/wrappers/auth.js
```jsx
import {Redirect} from 'umi';
function auth(props){
const isLogin = localStorage.getItem('isLogin');
if (isLogin){
return props.children;
} else {
return <Redirect to={{pathname: '/login', state: {from: '/profile'}}} />
}
}
export default auth;
src/pages/login.js
import React from 'react';
import {history} from 'umi';
function Login(props){
let toLogin = () => {
localStorage.setItem('isLogin', 'true');
if (props.location.state && props.location.state.from){
history.push(props.location.state.from);
}
}
return (
<div>
<h1>登录</h1>
<button onClick={toLogin}>登录</button>
</div>
)
}
export default Login;
动态路由
- 运行时配置和配置的区别是他跑在浏览器端,基于此,我们可以在这里写函数、jsx、import 浏览器端依赖等等,注意不要引入 node 依赖。
- 约定
src/app.tsx
为运行时配置。
前台运行时
src/components/Foo.js
import React from 'react';
export default function Foo(){
return <div>Foo</div>
}
src/app.js
// 修改路由。
// 比如在最前面添加一个 /foo 路由
export function patchRoutes({routes}){
routes.unshift({
path: '/foo',
exact: true,
component: require('@/components/Foo').default,
});
}
接口返回
项目目录/mock/routes.js (umi 约定 /mock 文件夹下所有文件为 mock 文件。)
添加后 http://localhost:8001/api/routes 就可以直接访问了,返回 [{path: ‘/foo’, component: ‘Foo.js’}]
export default {
// GET 可忽略
'GET /api/routes': [
{
path: '/foo',
component: 'Foo.js',
}
],
// 支持自定义函数,API 参考 express@4
'POST /api/users/create': (req, res) => {
// 添加跨域请求头
res.setHeader('Access-Control-Allow-Origin', '*');
res.end('ok');
},
}
src/app.js
let extraRoutes;
// 修改 clientRender 参数。
export function modifyClientRenderOpts(memo){
memo.routes.unshift(...extraRoutes);
return memo;
}
// 覆写 render。
export function render(oldRender){
fetch('/api/routes').then(res => res.json()).then(res => {
extraRoutes = res.map(item => {
const component = require(`./components/${item.component}`).default;
return {...item, component};
})
oldRender();
})
}