这里写目录标题
一、项目初始化
- 创建并进入目录
mkdir umijs
cd umujs- 创建umi项目
yarn create @umijs/umi-app
npx @umijs/create-umi-app- 安装依赖
yarn- 启动项目
yarn start- 打包,部署发布
yarn build (构建后项目中会多一个dist项目)- 本地验证
yarn global add serve - - 或 - - npm install http-server -g
serve ./dist - - 或 - - http-server ./dist- 获取镜像源
yarn config get registry
二、基本了解
2.1.路由配置
.umirc.ts
// umi的配置项,如果项目复制,可以将配置写在config/config.ts中,并把配置中的路由配置routes.ts拆分出去,但该文件的优先级更高
import { defineConfig } from 'umi';
export default defineConfig({
nodeModulesTransform: {
type: 'none',
},
hash:true, // 默认为false,用于项目build发布时/dist文件夹下包含hash后缀,每次相关文件改变时,hash后缀就会变,为了减少增量发布和避免浏览器加载缓存
// base:'/admin/', // 设置路由前缀,用于部署到非根目录,路由'/'和'/user',设置了base为'/admin/',就可以通过'/admin/'和'/admin/user'来访问之前的路由
// publicPath:'http://xxx.com/cdn/', // 打包时webpack会在静态文件路径前面添加的publicPath值,要修改静态文件地址(publicPath:'CDN值' 或者 publicPath:'./相对路径')
// 默认是'/'根目录,所以dist/index.html中的静态资源就使用'href="/umi.css"'来获取静态资源
// publicPath:'http://xxx.com/cdn/',打包后就需要使用'href="http://xxx.com/cdn/umi.80764e3e.css"'
// outputPath:'build', // 打包后文件输出目录,默认是dist
title:'UmiJS标题', // 就是项目打开的标题,也可以在路由中设置每一页的标题,title:'首页',路由标题优先级比项目标题优先级高
// history:{
// type:'hash' // 三种可选项,browser(默认值),hash(会将所有路由都添加一个#),memory()
// // 之前使用http://192.168.220.1:8000/访问,现在http://192.168.220.1:8000/#
// // 之前使用http://192.168.220.1:8000/a,现在http://192.168.220.1:8000/#/a
// },
// targets:{
// ie:11 // 需要兼容的浏览器最低版本,会自动引入polyfill和做语法转换
// },
// proxy: { // 代理服务器,当浏览器有跨域问题时的配置项
// '/api': { // 要请求的前缀,遇到到/api前缀的请求(如/api/findAllStudent)就会走这个代理服务器
// 'target': 'http://jsonplaceholder.typicode.com/', // 将请求的目标地址换成target中的地址
// 'changeOrigin': true,
// 'pathRewrite': { '^/api' : '' },
// },
// },
// theme: { // 配置主题,实际上是配 less 变量
// '@primary-color': '#1DA57A', // 设置antd的主色调,antd的定制变量,https://ant.design/docs/react/customize-theme-cn
// },
routes: [ // 路由配置,path是访问哪个路径,component是访问哪个组件(component:'@/pages/index'使用的是绝对路径,@相当于src,component:'index'是相对路径,会以 src/pages 为基础路径开始解析)
{ path: '/', component: '@/pages/index',title:'首页' },
{ path: '/list', redirect:'/user/one' }, // redirect,访问http://192.168.220.1:8000/list可以重定向到http://192.168.220.1:8000/user/one目录
// layouts模板路由
{
path:'/user',
component:'@/layouts/index',
// exact: true, // exact: true代表是否严格匹配
// 路由级别的权限校验
wrappers:[ // 访问/user页面之前先进入'@/wrappers/auth'判断用户是否登录,如果登录了就转入真实的user组件,如果没有登录就重定向到登录页面
'@/wrappers/auth',
],
routes:[ // 子路由,多个路由使用同一个component模板
// 路由传参,":参数名?",加问号时代表传参可传可不传,如果不加问号,就必须传值,否则访问不到页面
// 访问时使用http://192.168.220.1:8000/user/one/111,在component:"@/pages/index"页面可以使用props.match.params来接收路由传参的所有参数
// { path: '/user/one/:id?', component: '@/pages/index',title:'组页面'},
{ path: '/user/one', component: '@/pages/user2',title:'用户页面2'},
{ path: '/user/two', component: '@/pages/user',title:'用户页面'},
{ component:'@/pages/404'} // 当没有以上任何子路由就访问404组件
]
},
{ component:'@/pages/404'} // 当没有以上任何子路由就访问404组件
],
fastRefresh: {},
});
layouts.index.tsx
import React from 'react';
import { Link, NavLink } from 'umi';
import './index.less';
// jsx可以不用管props的类型
const Index = (props: any) => {
return (
<div>
<h2>Header</h2>
{/*
<Link to={'/user/one'}>用户1</Link>
<Link to={'/user/two'}>用户2</Link>
*/}
{/* 与上面的Link区别是,会给当前点击的对象自动添加一个class="active",可以在index.css中写入.active的样式,点击后样式会显示出来 */}
<NavLink to={'/user/one'}>用户1</NavLink>
<NavLink to={'/user/two'}>用户2</NavLink>
{/* 配置路由父组件,{props.children}可以获取子组件内的节点元素 */}
{props.children}
<h2>Footer</h2>
</div>
);
};
export default Index;
pages/index.less
.active {
color: red;
font-size: 33px;
}
pages/index.tsx
import styles from './index.less';
import { DatePicker } from 'antd';
export default function IndexPage(props: any) {
// props.match.params可以获取路由上传递的所有参数
console.log('dnlsbd', props.match.params);
console.log('dnlsbd', props.match.params.id);
return (
<div>
<h1 className={styles.title}>Page index</h1>
<DatePicker />
</div>
);
}
pages/user2.tsx
// 使用rrc自动生成类组件模板文件
// 使用rsc自动生成函数式组件模板文件(推荐)
import React from 'react';
const User = () => {
return <div>User Page2</div>;
};
export default User;
pages/user1.tsx
// 使用rrc自动生成类组件模板文件
// 使用rsc自动生成函数式组件模板文件(推荐)
import React from 'react';
import { Button } from 'antd';
// 路由组件中有props这个属性,自定义的其他组件没有props这个属性
const User = (props: any) => {
return (
<div>
<span>User Page1</span>
<Button
onClick={() => {
props.history.push('/');
}}
>
点我回首页
</Button>
</div>
);
};
export default User;
pages/404.tsx
import React from 'react';
const NotFound = () => {
return (
<div>
<h1>404</h1>
</div>
);
};
export default NotFound;
wrappers/auth.tsx
// 可以用来判断用户名是否登录
import { Redirect } from 'umi';
export default (props: any) => {
const isLogin = true;
if (isLogin) {
// 如果登录的话就返回子组件内容
return <div>{props.children}</div>;
} else {
// 如果没有登录的话就重定向到登录页面
return <Redirect to="/login" />;
}
};
2.2.Html模板
在node_modules/@umijs/core/lib/Html/document.ejs中,里面是将组件挂载到页面上
由于模板代码在node_modules中,一般提交版本代码时是不会提交node_modules的,所以不要在模板上进行修改
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no" />
</head>
<body>
<div id="<%= context.config.mountElementId || 'root' %>"></div>
</body>
</html>
新建 src/pages/document.ejs,umi 约定如果这个文件存在,会作为默认模板
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<title>自定义模板</title>
<!-- 引入bootstrap等第三方样式库 -->
<link href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/4.6.1/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<h3>自定义模板</h3>
<button class="btn btn-success">bootstrap</button>
<div id="root"></div>
</body>
</html>
2.3.Mock数据
- Mock数据是前后端分离开发的关键步骤,通过跟服务器预先约定好的接口,模拟请求数据和逻辑
- 在mock文件夹下写的js或ts文件都会被认为是mock文件
- 可以引入Mock.js第三方库提升Mock数据能力
mock:false // 之前请求的api不存在(相当于在浏览器中发起请求了),会返回document.ejs的文档内容
// 提供环境变量Terminal中临时关闭:MOCK=none umi dev
"start:nomock": "MOCK=none umi dev" // 可以在package.json中配置"start:nomock": "MOCK=none umi dev",在terminal中直接执行yarn start:nomock就可以不使用mock启动了
mock/index.ts
// mock默认是打开的,如果想关闭可以在.umirc.json中配置 mock: false,之前请求的api不存在(相当于在浏览器中发起请求了),会返回document.ejs的文档内容
// 以key : value的形式,value支持 Object 和 Array 类型
import mockjs from 'mockjs'
export default {
// "GET/POST"为请求方式,默认不写是GET方式
// "/api/index"为请求接口
'GET /api/index' : {
id: 1,
name: 'Tom',
age: 12
},
'GET /api/person' : {
id: 2,
name: 'LiLi',
age: 21
},
'GET /api/tags' : mockjs.mock({
// @city动态生成城市,'value|1-100'生成1-100的value值,type|0-2生成0-2的type值,默认值为1
'list|100':[{name:'@city','value|1-100':50,'type|0-2':1}]
})
}
pages/index.tsx
import styles from './index.less';
import { DatePicker, Button } from 'antd';
import { useEffect } from 'react';
import { history, request } from 'umi';
export default function IndexPage(props: any) {
// props.match.params可以获取路由上传递的所有参数
// console.log('dnlsbd', props.match.params);
// console.log('dnlsbd', props.match.params.id);
useEffect(() => {
setTimeout(() => {
// 路由跳转:2秒后跳转到/user/one路由中
// 在组件中不在umi中导入history,也可以在props中直接使用history
history.push('/user/one');
}, 2000);
});
// 方法一:使用umi提供的request请求数据,接收两个参数,第一个参数是url,第二个参数是options
const getData = () => {
request('/api/index').then((res) => {
console.log(res);
});
};
// 方法二:使用async与await发送异步请求,从服务端获取数据
const getData2 = async () => {
const data = await request('/api/person');
console.log(data);
};
// 使用mockjs生成数据
const getData3 = async () => {
const data = await request('/api/tags');
console.log(data);
};
return (
<div>
<h1 className={styles.title}>Page index</h1>
<Button onClick={getData3}>点击获取数据</Button>
<DatePicker />
</div>
);
}
2.4.DvaJS
- umi已经给我们提供plugin-dva插件了
- dva的作用就是让我们更方便的使用redux
- 约定式的model文件
src/models下的文件(推荐使用),src/pages/models目录下的文件,src/pages/model.ts文件
会有校验内容是否是有效的dva model
- 1.创建ui组件 src/pages/tags.tsx(记得配置路由)
- 2.创建model src/models/tags.ts
- 3.使用connect将ui组件和model进行连接
.umirc.ts
{path:'/tags',component:'@/pages/tags'},
pages/tags.tsx
import React from 'react';
import { connect } from 'umi';
import { Button } from 'antd';
const Tags = (props) => {
const { dispatch } = props;
const tagslist = props.tags.tagsList.list || [];
const getTagsList = () => {
// 使用model获取数据
dispatch({
type: 'tags/fetchTagsList', // "models的命名空间/方法名"
payload: null, // 如果是表单提交就使用这个参数将数据提交给model,model那边的payload参数就可以获取到对应的值
});
};
return (
<div>
<h3>Dva的使用</h3>
<Button onClick={getTagsList}>点击获取TagsList</Button>
{tagslist.map((item, index) => {
return <p key={index + item}>{item.name}</p>;
})}
</div>
);
};
// tags会作为props传给Dva组件
export default connect(({ tags }) => ({ tags }))(Tags);
// 报错:mapStateToProps() in Connect(Widget) must return a plain object. Instead received undefined.
// 解决:箭头函数返回的是一个对象,所以{ tags }外面要用()括起来
src/models/tags.ts
import {request} from 'umi'
// 通过request获取mock中的tags数据
const getTags = () => {
return request('/api/tags')
}
export default{
// 命名空间,调用model时,通过命名空间调用,不要和其他model同名
namespace:'tags',
// 状态,是通过effects调用后端接口获取的数据,和react中state类似,和redux中保存的state一样
state:{
tagsList:[]
},
// 影响,调用外部函数,在外部函数中请求后端接口,获取数据
effects:{
// payload:ui组件传过来的参数
// callback:回调函数
// put:将数据向下传递给reducers
// call(外部函数,函数所需参数):访问外部的一些方法,参数一般是payload(ui组件接收的参数)
// 由于使用request异步请求数据,使用 * 与 yield 配合达到同步请求的效果
*fetchTagsList({payload,callback},{put,call}){
// 获取tags数据
const tagsList = yield call(getTags)
// 调用put函数将数据传递给reducers
yield put({
type: 'setTagsList', // 类似于 redux 中 action 的 type
payload: tagsList // 传数据给reducer
})
}
},
// 更新state :使用set方法更新state保存的数据
reducers:{
// state:原来的数据
// action:从effects中yield put获取的值
setTagsList(state,action){
console.log("skanbdjad",state)
console.log("skanbdjad",action)
return {...state,tagsList:action.payload}
}
}
}
2.5.运行时配置
运行时配置:约定写在src/app.tsx中
import { history } from 'umi';
// 定义额外路由变量
let extraRoutes;
// patchRoutes:动态添加路由
export function patchRoutes({ routes }) {
// console.log('添加前', routes); // routes是已经在.umirc.ts中注册过的所有路由
routes.unshift({
path: '/foo',
title: 'foo',
// exact: true, // 严格查询
component: require('@/pages/user1').default, // 在.umirc.ts可以直接通过绝对路径获取组件,在app.tsx中需要使用require请求user1文件,再调用user1的default方法返回user1组件
});
// console.log('添加后', routes);
// 遍历后端获得的路由数组
extraRoutes.map((item) => {
// 合并服务端返回的路由
routes.unshift({
path: item.path,
component: require(`@/pages${item.component}`).default,
});
});
}
// render:覆写render(在patchRoutes之前执行)
export function render(oldRender) {
// 从后端请求获得路由
// fetch('/api/routes').then(res=>res.json()).then((res)=>{
// })
// 模拟从后端获取的路由数据
// path是路由,component是要访问的组件
extraRoutes = [{ path: '/server', component: '/user2' }];
// 在渲染之前做权限校验
// 如果登录了就执行patchRoutes中的extraRoutes数组遍历,并将/server路由与原路由进行合并
// 如果没有登录就使用history.push跳转到登录页面
const isLogin = true;
if (!isLogin) {
history.push('/login');
}
// oldRender用于渲染新页面
oldRender();
}
// onRouteChange:在初始加载和路由切换时做一些事情,在patchRoutes之后执行
export function onRouteChange({ location, routes, action, matchedRoutes }) {
console.log(location); // 当前所在的路由
// 路由名称,用于做埋点统计
console.log(location.pathname);
// 设置标题
console.log('jhbad vsd', matchedRoutes);
console.log('jhbad vsd', matchedRoutes.length);
// 将有matchedRoutes
if (matchedRoutes.length) {
// 如果在.umirc.ts中或该页面新添加的路由中定义的有title就将其设置为文档标题,否则设置为''
document.title =
'郑州师范 ' + (matchedRoutes[matchedRoutes.length - 1].route.title || '');
}
console.log(routes); // 所有的路由(包括在patchRoutes中动态新增的路由)
console.log(action); //POP
}
2.6.Umi-UI
- 安装umi-ui的依赖
yarn add @umijs/preset-ui -D- 在项目package.json中配置
linux:“start:umi-ui”: “UMI_UI=1 umi dev”
windows:“start:umi-ui”: “set UMI_UI=1 && umi dev”- 运行:yarn start:umi-ui
打开项目后,点击右下角Umi UI的可视化编程根据,解决以下问题
在项目根目录下创建一个.env文件,里面进行以下配置:HOST=0.0.0.0
通过点击引入相关的Umi-Ui的模板或区块,引入区块中的布局,点击添加到项目中,项目中pages下会出现一个LayoutSide组件
需要进行调整,在src下创建一个layouts文件夹,将LayoutSide组件放到layouts下面,并修改.umirc.ts下的路由配置
import { defineConfig } from 'umi';
export default defineConfig({
nodeModulesTransform: {
type: 'none',
},
routes: [
{
name: '注册页',
path: '/userregister',
component: './UserRegister',
},
{
name: '模板组件',
title: '数字郑州',
path: '/',
component: '@/layouts/LayoutSide/index',
routes: [
{
name: '空白页面',
path: '/user',
component: '@/pages/User/index',
},
{
name: '首页',
path: '/index',
component: '@/pages/index',
},
],
},
],
fastRefresh: {},
dva: { // 添加@umijs/preset-react插件中dva的配置
immer: true,
hmr: false,
},
locale: { // 添加国际化配置 forMessage
default: 'zh-CN',
antd: false,
title: false,
baseNavigator: true,
baseSeparator: '-',
}
});
三、项目优化
3.1.配置项
将原来的.umirc.ts文件的内容拆分为以下几个文件,在项目根目录先新建一个config文件夹,下面放四个文件
3.1.1.config.ts
相当于.umirc.ts下的配置
// umi的配置项,如果项目复制,可以将配置写在config/config.ts中,并把配置中的路由配置routes.ts拆分出去,但该文件的优先级更高
import { defineConfig } from 'umi';
// 引入路由数组
import routes from './routes'
// 引入其他的默认设置
import defaultSetting from './defaultSetting';
// 引入代理配置
import proxy from './proxy'
export default defineConfig({
nodeModulesTransform: {
type: 'none',
},
hash:true, // 默认为false,用于项目build发布时/dist文件夹下包含hash后缀,每次相关文件改变时,hash后缀就会变,为了减少增量发布和避免浏览器加载缓存
// title:'UmiJS标题', // 就是项目打开的标题,也可以在路由中设置每一页的标题,title:'首页',路由标题优先级比项目标题优先级高
routes,
title:defaultSetting.title,
theme: { // 配置主题色,实际上是配 less 变量
'@primary-color': defaultSetting.PrimaryColor, // 设置antd的主色调,antd的定制变量,https://ant.design/docs/react/customize-theme-cn
},
fastRefresh: {},
targets:{
ie:11 // 需要兼容的浏览器最低版本,会自动引入polyfill和做语法转换
},
proxy:proxy['dev'], // 代理配置
locale:{ // 国际化配置
default:'zh-CN',
antd:false,
title:false,
baseNavigator:true,
baseSeparator:'-',
},
dva:{ // 插件的dva配置
immer:true,
hmr:false,
}
});
3.1.2.defaultSetting.ts
一些默认配置
export default {
PrimaryColor: '#1DA57A',
title:'UmiJS标题',
}
3.1.3.proxy.ts
代理配置
// 对不同环境下的代理进行拆分
export default [
// 运行时的代理
dev:{
'/api': { // 要请求的前缀,遇到到/api前缀的请求(如/api/findAllStudent)就会走这个代理服务器
'target': 'http://jsonplaceholder.typicode.com/', // 将请求的目标地址换成target中的地址
'changeOrigin': true,
'pathRewrite': { '^/api' : '' },
},
},
// 测试时的代理
test:{
'/api': { // 要请求的前缀,遇到到/api前缀的请求(如/api/findAllStudent)就会走这个代理服务器
'target': 'http://jsonplaceholder.typicode.com/', // 将请求的目标地址换成target中的地址
'changeOrigin': true,
'pathRewrite': { '^/api' : '' },
},
}
]
3.1.4.routes.ts
路由配置
// 暴露出路由数组
export default [
[ // 路由配置,path是访问哪个路径,component是访问哪个组件(component:'@/pages/index'使用的是绝对路径,@相当于src,component:'index'是相对路径,会以 src/pages 为基础路径开始解析)
{
path: '/',
component: '@/layouts/index', // layouts模板路由
title:'首页',
routes:[
{path:'/',component:'@/pages/index'},
{
path:'/user', // exact: true, // exact: true代表是否严格匹配
wrappers:[ // 路由级别的权限校验,访问/user页面之前先进入'@/wrappers/auth'判断用户是否登录,如果登录了就转入真实的user组件,如果没有登录就重定向到登录页面
'@/wrappers/auth',
],
routes:[ // 子路由,多个路由使用同一个component模板
// { path: '/user/one/:id?', component: '@/pages/index',title:'用户页面1'}, // 路由传参,":参数名?",加问号时代表传参可传可不传,如果不加问号,就必须传值,否则访问不到页面,访问时使用http://192.168.220.1:8000/user/one/111,在component:"@/pages/index"页面可以使用props.match.params来接收路由传参的所有参数
{ path: '/user/one', component: '@/pages/user1',title:'用户页面1'},
{ path: '/user/two', component: '@/pages/user2',title:'用户页面2'},
{ component:'@/pages/404'} // 当没有以上任何子路由就访问404组件
]
},
{path:'/tags',component:'@/pages/tags'},
{path:'/login',component:'@/pages/login'},
]
},
// { path: '/list', redirect:'/user/one' }, // 重定向redirect,访问http://192.168.220.1:8000/list可以重定向到http://192.168.220.1:8000/user/one目录
{ component:'@/pages/404'} // 当没有以上任何子路由就访问404组件
],
]
3.1.5.utils/request.ts
对request进行封装,可以做一些拦截器,方便在services中使用
// 对request进行封装,可以做一些拦截器,方便在services中使用
import request from 'umi-request'
// message用于消息提醒
import {message} from 'antd'
// 请求拦截器
request.interceptors.request.use((url, options) => {
return {
url: `${url}`,
// 可以在这里设置请求头headers
options: { ...options, interceptors: true, headers: {Hello: 'hello'} },
};
});
// 响应拦截器
request.interceptors.response.use(response => {
// 错误状态码的统一处理
if(response.status > 400){
const codeMaps = {
404:'未找到资源。',
502: '网关错误。',
503: '服务不可用,服务器暂时过载或维护。',
504: '网关超时。',
};
message.error(codeMaps[response.status]);
}
return response;
});
// 将定义的request暴露出去
export default request;
3.1.6.services/tags.ts
在src下创建目录,组件叫什么名字,服务就叫什么名字,将对后端或者models中的请求分离出来
// 将request在src/utils下面进行封装,并设定一些拦截器
import request from "../utils/request"
export const getTags = () => {
return request('/api/tags')
}
3.2.目录结构
public:一些静态资源,打包时原样复制出去
src/.umi:缓存,每次运行会重新生成,不会进版本库
src/assets:本地静态资源
src/components:公共组件
src/pages/页面组件/components:页面相关的组件
src/services:对后端数据的请求
src/utils:工具类
四、Antd Pro
4.1.创建Antd Pro项目
创建Antd Pro项目(基于umi配置的)
- yarn create umi
查看yarn的目录
- yarn global bin
查看全局目录
- yarn global dir
清缓存
- yarn cache clean
- yarn config set global-folder “D:\Develop\yarnConfig\yarn_global”
yarn config set cache-folder “D:\Develop\yarnConfig\yarn_cache”