https://www.bilibili.com/video/BV1JJ411q7iw
umi+ts+antd Pro 珠峰课程
npm install -g cnpm --registry=https://registry.npm.taobao.org
cnpm install -g yarn
yarn create umi
git init
npm i
扩展 window,加了一些属性
umi+dva 项目快速上手指南
在 react 项目中,使用 react+umi+dva+antd 这一阿里系列技术栈的人越来越多,本文就来分享一下 umi 项目的上手教程。在 react 项目中,使用 react+umi+dva+antd 这一阿里系列技术栈的人越来越多,本文就来分享一下 umi 项目的上手教程。
构建项目
node 环境
node 版本 >= 8.0.0
全局安装 umi
npm install -g umi
建议使用 yarn 安装
// 全局安装yarn
npm install -g yarn
// 使用yarn安装umi
yarn global add umi
构建 umi 项目
mkdir myapp && cd myapp
yarn create umi
详细构建请参考 umi 官方项目构建
目录结构
├── config/
├── config.js // umi 配置,同 .umirc.js,二选一
├── dist/ // 默认的 build 输出目录
├── mock/ // mock 文件所在目录,基于 express
├── public/ // 全局相对路径文件
└── src/ // 源码目录,可选 ├── assets/ // 静态文件 ├── components/ // 全局共用组件 ├── layouts/index.js // 全局入口文件 ├── models/ // 全局models文件,存放全局共用数据store ├── pages/ // 页面目录,业务组件 ├── .umi/ // dev 临时目录,需添加到 .gitignore ├── .umi-production/ // build 临时目录,会自动删除 ├── index/ // 首页模块 ├── manager/ // 管理端模块 ├── components/ // 管理端-局部公共组件 ├── models/ // 管理端-局部models,存放manager的store ├── services/ // 管理端-局部services,存放manager的接口 ├── index.js // 业务组件index ├── page.js // 业务组件page ├── _layout.js // 局部入口文件 ├── 404.js // 404 页面 ├── services/ // 全局services文件,存放全局公共接口 ├── utils/ // 全局工具类 ├── global.css // 约定的全局样式文件,自动引入,也可以用 global.less ├── global.js // 约定的全局Js文件,自动引入,可以在这里加入 polyfill ├── app.js // 运行时配置文件 ├── .umirc.js // umi 配置,同 config/config.js,二选一 ├── .env // 环境变量 └── package.json
配置文件
umi 允许在 .umirc.js 或 config/config.js (二选一,.umirc.js 优先)中进行配置,支持 ES6 语法。本文使用 config/config.js
export default {
base: '/web/', //部署到非根目录时才需配置
targets: { //配置浏览器最低版本,比如兼容ie11 ie: 11 }, hash: true, //开启打包文件的hash值后缀 treeShaking: true, //去除那些引用的但却没有使用的代码 plugins: [ [ 'umi-plugin-react', { antd: true, //启用后自动配置 babel-plugin-import,实现antd按需加载 dynamicImport: { //实现路由级的动态加载 webpackChunkName: true //实现有意义的异步文件名 }, dva: { dynamicImport: true, //是否启用按需加载 hmr: true //是否启用 dva 的 热更新 }, //通过 webpack 的 dll 插件预打包一份 dll 文件来达到二次启动提速的目的 dll: { exclude: [], include: ['dva', 'dva/router', 'dva/saga', 'dva/fetch', 'antd/es'] }, //约定式路由时才需引用,用于忽略指定文件夹中自动生成的路由 routes: { exclude: [ /components\//, /model\.(j|t)sx?$/, /components\.(j|t)sx?$/, /service\.(j|t)sx?$/, /models\//, /services\// ], }, } ] ], //配置式路由时,路由文件由此引用(往下会讲到) routes: routes, //代理请求 proxy: { "/api": { "target": "http://jsonplaceholder.typicode.com/", "changeOrigin": true, "pathRewrite": { "^/api" : "" } } }, alias: {'@': resolve(__dirname, '../src'),} //别名,umirc.js为'src' };
pages
+ pages
+ manager
+ components/
+ models/
+ services/
- index.js - _layout.js
业务目录中,标配局部公用组件 components,局部共用数据 models,局部共用接口 services,_layout.js 入口文件需要则引入。
路由
1. 约定式路由
1.1 umi 约定,在 pages 目录下的. js/.jsx 会自动生成路由,除开在配置文件 plugins/routes 中被 exclude 的目录或文件,文件的路径即路由。 1.2 src/layout / 为全局 layout,默认全局入口文件,配置式路由下无效。pages / 下任何文件下的_layout.js 即当前文件夹路由下的入口文件,必须先经过_layout.js 才能进入当前文件夹路由
2. 配置式路由
在配置文件 .umirc.(ts|js) 或者 config/config.(ts|js) 中引入:
export default {
routes: [
{
path: '/',
component: '../layouts/index', routes: [ { path: '/user', redirect: '/user/login' },//redirect,避免只渲染_layout.js { path: '/user/login', component: './user/login' }, { path: '/manager', component: '../pages/management/_layout.js', routes: [ { path: '/manager/system', component: '../pages/management/manager/system', Routes: ['./routes/PrivateRoute.js'] } } ], }, ], };
3. 权限路由
路由通过 Routes 属性来实现权限路由, 如上文:/manager/system 路由。创建一个./routes/PrivateRoute.js 权限文件,props 里会传入 / manager/system 的路由信息。
export default (props) => {
return (
<div> <div>PrivateRoute (routes/PrivateRoute.js)div> { props.children } div> ); }
4. 跳转路由
link 方式
import Link from 'umi/link';
<Link to="/list">Go to list pageLink>
router 方式
import router from 'umi/router';
router.push({
pathname: '/list',
query: {
a: 'b',
},
});
models 层
models/index.js,不能空文件。由于 umi/dva 是基于 redux,redux-saga 的数据流方案,dva 的 connect 即 redux 的 connect,dva 的 model 即 redux-saga 简化版,更易用。
import * as services from '../services';
{
namespace: 'system', //models命名空间,需全局唯一 state: { //models存储的数据store dataList: [] }, reducers: { save(state, { payload }) { //更新store,用新数据合并state的旧数据 return { ...state, ...payload }; } }, effects: { * testFunc({ payload: params }, { call, put, select }) { //dispatch请求的方法 const { dataList } = yield select(state => state.system); //获取models中的state const { data } = yield call(services.testFunc, params); //call,请求services里面的接口以及传参,可继续往后面加参数,跟JavaScript的call一样 if (data && data.code == 0) { const data_ = data.data.content; yield put({ //put,必须发出action save,此action被reducer监听,从而达到更新state数据的目的 type: 'save', payload: { dataList: data_ || [] } }); return data_; //返回response,可选 } }, }, subscriptions: { //订阅,在app.start()即启动项目时被执行 setup({ dispatch, history }) { return history.listen(({ pathname, query }) => { // 进入 '/manager/system' 路由,会发起一个名叫 'save' 的 effect if (pathname === '/manager/system') { //do sth... dispatch({ type: 'save', payload: query }); } }) } } }
注:call 会阻塞,在 call 方法调用结束之前,call 方法之后的语句是无法执行的,使得以同步的方式执行异步,如需无阻塞,则要引入 fork,用 yield fork 代替 yield call。
services 层
请求后台接口的方法,返回一个 promise 对象。
import request from '@utils/request';
export function rightSave(values) { return request(`/authirty/right/save`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(values) }); }
components 层
dispatch
与 effect 中的 put 方法相似,必须触发 action。 在 model 的 subscription 参数中使用。 在组件中,由 connect 过的组件中,组件的 props 中才可以获取到 dispatch。
import { connect } from 'dva';
export default connect()(System);
- 接口请求 方式一 (推荐): 使用 dispatch 调用 models 里面 effects/reducer 声明的方法,
this.props.dispatch({
type: 'system/testFunc', //type,命名空间/effects方法名
payload: params, //payload,参数
}).then(res => {}) //直接赋值更新state this.props.dispatch({ type: 'system/save', //type,命名空间/reducer方法名 payload: { rightDetail: {a:1} }, //payload,参数 }).then(res => {}) //请求全局model this.props.dispatch({ type: 'global/getOrganizationList', //全局namespace/全局effects方法名 payload: params, //payload,参数 }).then(res => {})
方式二:dispatch 带 callback 回调函数作为第三个参数
//组件中
this.props.dispatch({
type: 'system/testFunc',
payload: params,
callback: (res) => {
if (!!res) {
//do sth } } }); //model中 *testFunc({ payload, callback }, { call, put }){ const { data } = yield call(services.rightSave, payload); if (data && data.code == 0) { !!callback && && callback(data); } }
方式三 (少用):如果组件中不需要用到 model 存 store 时,直接引入 services 请求接口
//组件中
//使用new promise请求接口,service返回promise函数,待接口异步请求完才执行.then
import * as service from '../services'; new Promise((resolve) => { const ret = service.rightSave(param); resolve(ret); }).then((ret) => { if (ret && ret.code == 200) { //do sth } }); // 或者 // 现在的ajax框架返回的就是一个promise的话,可以直接方法名跟上.then()即可 import { rightSave } from '../services'; rightSave().then(res => { let data = res.data; if (data && data.code == 200) { //do sth } });
connect
方式一:mapStateToProps,class 继承组件和函数组件都能用这种
接口请求完后,组件中引入 connect,获取 models 的数据放入当前组件 props 中
import { connect } from 'dva';
function mapStateToProps(state) { //state是项目所有的models const { selectList } = state.system; //获取namespace命名空间为system的models数据state const { organizationList } = state.global; //全局获取namespace命名空间为global的models数据state return { selectList, organizationList }; } export default connect(mapStateToProps)(System); //connect组件 //或者直接解构 export default connect(({ system, global: { organizationList } }) => ({ ...system, organizationList }))(System);
方式二:es6 注解方式引入,只能用于 class 继承组件
@connect(({ system, global: {organizationList} }) => ({
...system,
organizationList
}))
class System extends React.Component{render(){return {<>>}}}
withRouter
withRouter 的作用:未经路由跳转的组件,如子组件想拿到路由的相关信息 location、history、match 等时可用,经路由跳转的页面则默认已有路由信息。 FAQ: 1. url 变化了,但页面组件不刷新,是什么原因? layouts/index.js 里如果用了 connect 传数据,需要用 umi/withRouter 高阶一下
import withRouter from 'umi/withRouter';
export default withRouter(connect(mapStateToProps)(LayoutComponent));
- 全局 layout 使用 connect 后路由切换后没有刷新? 需用 withRouter 包一下导出的 react 组件,注意顺序。
import withRouter from 'umi/withRouter';
export default withRouter(connect()(Layout));
mock
export default {
// 支持值为 Object 和 Array
'GET /api/users': { users: [1, 2] }, // 支持自定义函数,API 参考 express@4 'POST /api/users/create': (req, res) => { res.end('OK'); }, }
如请求接口 / api/list,数据返回 {users: [1, 2] }
数据流程
最后给大家总结一下 dva 的数据流向: * View 层 dispatch 操作 action –> 触发 models 层 effect 中相应方法 –> 触发 call 发起 services 层请求,获取接口数据 –> 触发 put 发起 reducer 处理相应的 action 更新数据 –> 更新 model 层中 state –> 触发 view 层的 render 方法进行重新渲染 –> 页面更新
附上数据流向图
参考文档
umi 文档:https://umijs.org/zh/guide/ dva 文档:https://dvajs.com/guide/
ps: 如有纰漏,请留言更正!
构建项目
node 环境
node 版本 >= 8.0.0
全局安装 umi
npm install -g umi
建议使用 yarn 安装
// 全局安装yarn
npm install -g yarn
// 使用yarn安装umi
yarn global add umi
构建 umi 项目
mkdir myapp && cd myapp
yarn create umi
详细构建请参考 umi 官方项目构建
目录结构
├── config/
├── config.js // umi 配置,同 .umirc.js,二选一
├── dist/ // 默认的 build 输出目录
├── mock/ // mock 文件所在目录,基于 express
├── public/ // 全局相对路径文件
└── src/ // 源码目录,可选 ├── assets/ // 静态文件 ├── components/ // 全局共用组件 ├── layouts/index.js // 全局入口文件 ├── models/ // 全局models文件,存放全局共用数据store ├── pages/ // 页面目录,业务组件 ├── .umi/ // dev 临时目录,需添加到 .gitignore ├── .umi-production/ // build 临时目录,会自动删除 ├── index/ // 首页模块 ├── manager/ // 管理端模块 ├── components/ // 管理端-局部公共组件 ├── models/ // 管理端-局部models,存放manager的store ├── services/ // 管理端-局部services,存放manager的接口 ├── index.js // 业务组件index ├── page.js // 业务组件page ├── _layout.js // 局部入口文件 ├── 404.js // 404 页面 ├── services/ // 全局services文件,存放全局公共接口 ├── utils/ // 全局工具类 ├── global.css // 约定的全局样式文件,自动引入,也可以用 global.less ├── global.js // 约定的全局Js文件,自动引入,可以在这里加入 polyfill ├── app.js // 运行时配置文件 ├── .umirc.js // umi 配置,同 config/config.js,二选一 ├── .env // 环境变量 └── package.json
配置文件
umi 允许在 .umirc.js 或 config/config.js (二选一,.umirc.js 优先)中进行配置,支持 ES6 语法。本文使用 config/config.js
export default {
base: '/web/', //部署到非根目录时才需配置
targets: { //配置浏览器最低版本,比如兼容ie11 ie: 11 }, hash: true, //开启打包文件的hash值后缀 treeShaking: true, //去除那些引用的但却没有使用的代码 plugins: [ [ 'umi-plugin-react', { antd: true, //启用后自动配置 babel-plugin-import,实现antd按需加载 dynamicImport: { //实现路由级的动态加载 webpackChunkName: true //实现有意义的异步文件名 }, dva: { dynamicImport: true, //是否启用按需加载 hmr: true //是否启用 dva 的 热更新 }, //通过 webpack 的 dll 插件预打包一份 dll 文件来达到二次启动提速的目的 dll: { exclude: [], include: ['dva', 'dva/router', 'dva/saga', 'dva/fetch', 'antd/es'] }, //约定式路由时才需引用,用于忽略指定文件夹中自动生成的路由 routes: { exclude: [ /components\//, /model\.(j|t)sx?$/, /components\.(j|t)sx?$/, /service\.(j|t)sx?$/, /models\//, /services\// ], }, } ] ], //配置式路由时,路由文件由此引用(往下会讲到) routes: routes, //代理请求 proxy: { "/api": { "target": "http://jsonplaceholder.typicode.com/", "changeOrigin": true, "pathRewrite": { "^/api" : "" } } }, alias: {'@': resolve(__dirname, '../src'),} //别名,umirc.js为'src' };
pages
+ pages
+ manager
+ components/
+ models/
+ services/
- index.js - _layout.js
业务目录中,标配局部公用组件 components,局部共用数据 models,局部共用接口 services,_layout.js 入口文件需要则引入。
路由
1. 约定式路由
1.1 umi 约定,在 pages 目录下的. js/.jsx 会自动生成路由,除开在配置文件 plugins/routes 中被 exclude 的目录或文件,文件的路径即路由。 1.2 src/layout / 为全局 layout,默认全局入口文件,配置式路由下无效。pages / 下任何文件下的_layout.js 即当前文件夹路由下的入口文件,必须先经过_layout.js 才能进入当前文件夹路由
2. 配置式路由
在配置文件 .umirc.(ts|js) 或者 config/config.(ts|js) 中引入:
export default {
routes: [
{
path: '/',
component: '../layouts/index', routes: [ { path: '/user', redirect: '/user/login' },//redirect,避免只渲染_layout.js { path: '/user/login', component: './user/login' }, { path: '/manager', component: '../pages/management/_layout.js', routes: [ { path: '/manager/system', component: '../pages/management/manager/system', Routes: ['./routes/PrivateRoute.js'] } } ], }, ], };
3. 权限路由
路由通过 Routes 属性来实现权限路由, 如上文:/manager/system 路由。创建一个./routes/PrivateRoute.js 权限文件,props 里会传入 / manager/system 的路由信息。
export default (props) => {
return (
<div> <div>PrivateRoute (routes/PrivateRoute.js)div> { props.children } div> ); }
4. 跳转路由
link 方式
import Link from 'umi/link';
<Link to="/list">Go to list pageLink>
router 方式
import router from 'umi/router';
router.push({
pathname: '/list',
query: {
a: 'b',
},
});
models 层
models/index.js,不能空文件。由于 umi/dva 是基于 redux,redux-saga 的数据流方案,dva 的 connect 即 redux 的 connect,dva 的 model 即 redux-saga 简化版,更易用。
import * as services from '../services';
{
namespace: 'system', //models命名空间,需全局唯一 state: { dataList: [] }, //models存储的数据store reducers: { save(state, { payload }) { //更新store,用新数据合并state的旧数据 return { ...state, ...payload }; } }, effects: { * testFunc({ payload: params }, { call, put, select }) { //dispatch请求的方法 const { dataList } = yield select(state => state.system); //获取models中的state const { data } = yield call(services.testFunc, params); //call,请求services里面的接口以及传参,可继续往后面加参数,跟JavaScript的call一样 if (data && data.code == 0) { const data_ = data.data.content; yield put({ //put,必须发出action save,此action被reducer监听,从而达到更新state数据的目的 type: 'save', payload: { dataList: data_ || [] } }); return data_; //返回response,可选 } }, }, subscriptions: { //订阅,在app.start()即启动项目时被执行 setup({ dispatch, history }) { return history.listen(({ pathname, query }) => { // 进入 '/manager/system' 路由,会发起一个名叫 'save' 的 effect if (pathname === '/manager/system') { //do sth... dispatch({ type: 'save', payload: query }); } }) } } }
注:call 会阻塞,在 call 方法调用结束之前,call 方法之后的语句是无法执行的,使得以同步的方式执行异步,如需无阻塞,则要引入 fork,用 yield fork 代替 yield call。
services 层
请求后台接口的方法,返回一个 promise 对象。
import request from '@utils/request';
export function rightSave(values) { return request(`/authirty/right/save`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(values) }); }
components 层
dispatch
与 effect 中的 put 方法相似,必须触发 action。 在 model 的 subscription 参数中使用。 在组件中,由 connect 过的组件中,组件的 props 中可以获取到 dispatch。
import { connect } from 'dva';
export default connect()(System);
- 接口请求 方式一 (推荐): 使用 dispatch 调用 models 里面 effects/reducer 声明的方法,
this.props.dispatch({
type: 'system/testFunc', //type,命名空间/effects方法名
payload: params, //payload,参数
}).then(res => {}) //直接赋值更新state this.props.dispatch({ type: 'system/save', //type,命名空间/reducer方法名 payload: { rightDetail: {a:1} }, //payload,参数 }).then(res => {}) //请求全局model this.props.dispatch({ type: 'global/getOrganizationList', //全局namespace/全局effects方法名 payload: params, //payload,参数 }).then(res => {})
方式二:dispatch 带 callback 回调函数作为第三个参数
//组件中
this.props.dispatch({
type: 'system/testFunc',
payload: params,
callback: (res) => {
if (!!res) {
//do sth } } }); //model中 *testFunc({ payload, callback }, { call, put }){ const { data } = yield call(services.rightSave, payload); if (data && data.code == 0) { !!callback && && callback(data); } }
方式三 (少用):如果组件中不需要用到 model 存 store 时,直接引入 services 请求接口
//组件中
//使用new promise请求接口,service返回promise函数,待接口异步请求完才执行.then
import * as service from '../services'; new Promise((resolve) => { const ret = service.rightSave(param); resolve(ret); }).then((ret) => { if (ret && ret.code == 200) { //do sth } }); // 或者 // 现在的ajax框架返回的就是一个promise的话,可以直接方法名跟上.then()即可 import { rightSave } from '../services'; rightSave().then(res => { let data = res.data; if (data && data.code == 200) { //do sth } });
connect
方式一:mapStateToProps,class 继承组件和函数组件都能用这种
接口请求完后,组件中引入 connect,获取 models 的数据放入当前组件 props 中
import { connect } from 'dva';
function mapStateToProps(state) { //state是项目所有的models const { selectList } = state.system; //获取namespace命名空间为system的models数据state const { organizationList } = state.global; //全局获取namespace命名空间为global的models数据state return { selectList, organizationList }; } export default connect(mapStateToProps)(System); //connect组件 //或者直接解构 export default connect(({ system, global: { organizationList } }) => ({ ...system, organizationList }))(System);
方式二:es6 注解方式引入,只能用于 class 继承组件
@connect(({ system, global: {organizationList} }) => ({
...system,
organizationList
}))
class System extends React.Component{render(){return {<>>}}}
withRouter
withRouter 的作用:未经路由跳转的组件,如子组件想拿到路由的相关信息 location、history、match 等时可用,经路由跳转的页面则默认已有路由信息。
FAQ:
1. url 变化了,但页面组件不刷新,是什么原因? layouts/index.js 里如果用了 connect 传数据,需要用 umi/withRouter 高阶一下
import withRouter from 'umi/withRouter';
export default withRouter(connect(mapStateToProps)(LayoutComponent));
2. 全局 layout 使用 connect 后路由切换后没有刷新? 需用 withRouter 包一下导出的 react 组件,注意顺序。
import withRouter from 'umi/withRouter';
export default withRouter(connect()(Layout));
mock
export default {
// 支持值为 Object 和 Array
'GET /api/users': { users: [1, 2] }, // 支持自定义函数,API 参考 express@4 'POST /api/users/create': (req, res) => { res.end('OK'); }, }
如请求接口 / api/list,数据返回 {users: [1, 2] }
数据流程
最后给大家总结一下 dva 的数据流向: * View 层 dispatch 操作 action –> 触发 models 层 effect 中相应方法 –> 触发 call 发起 services 层请求,获取接口数据 –> 触发 put 发起 reducer 处理相应的 action 更新数据 –> 更新 model 层中 state –> 触发 view 层的 render 方法进行重新渲染 –> 页面更新
附上数据流向图
参考文档
umi 文档:https://umijs.org/zh/guide/
dva 文档:https://dvajs.com/guide/
构建项目node 环境全局安装 umi构建 umi 项目目录结构配置文件pages路由1. 约定式路由2. 配置式路由3. 权限路由4. 跳转路由models 层services 层components 层dispatchconnectwithRoutermock数据流程参考文档构建项目node 环境全局安装 umi构建 umi 项目目录结构配置文件pages路由1. 约定式路由2. 配置式路由3. 权限路由4. 跳转路由models 层services 层components 层dispatchconnectwithRoutermock数据流程参考文档
https://preview.pro.ant.design/
admin
ant.design
没有注册页面,编写一个注册页面
先看:什么是一个 block
以前是全部自动下载,现在需要自己下载 block
umi block list
项目启动起来:
Umi UI 是新的工具
两个用户,admin 和 user
pw 都是 ant.design
user 比admin 权限少,看不到管理页
登录后跳转到之前页面——通过 ?redirect 参数实现
effects:副作用,这里用的 saga 的用法
form:高阶组件用法
可以收集段的值,也可以进行校验
force: true —— 强行校验
payload:携带的数据
userAndRegister 命名空间,在models 里面定义的
...: ES6扩展新语法:
https://blog.csdn.net/qq_30100043/article/details/53391308
这个 interface 一会定义后端的接口需要用到
@ 表示根目录
因为拦截了mock,直接校验通过
JSON Web Token 入门教程- 阮一峰的网络日志
不可以,想知道为什么得用 saga —— 基于 saga 都是 generator 模式
对外umi,对内 bigfish
单点登录,非常复杂,不是一般前端能搞得定的
qiankun:
权限系统怎么处理的:
来个 swagger 生成客户端请求 —— 今天不讲,下节课讲