蚂蚁金服开源的企业级React框架,并不是UI框架
- 特性
- 开箱即用,内置
react、react-router ...
- 类似
next.js
且功能完备的路由约定,同时也支持手动配置路由的方式; - 完善的插件体系,高性能,通过插件支持PWA、以路由为单元的code splitting等等;
- 支持静态页面导出,适配各种环境,如中台业务、无线业务、egg、支付宝钱包
- 开发启动快,支持一键开启dll
- 一键兼容IE9、基于
umi-plugin-polyfills
- 支持TypeScript
- 与
dva
数据流的深入融合,支持duck directory、model的自动加载、code splitting等等
- 开箱即用,内置
-
dva
是React应用框架,封装了Redux、Redux-saga、React-router
三个React工具库,目前React最流行的数据流解决放案;
1. State:一个对象,保存整个应用状态;
2. View:React组件构成的视图层;
3. Action:一个对象,描述事件
4. connect():绑定```State```到```View```
5. dispatch():发送```Action```到```State```
-
dva
与umi
的约定-
src
源码:pages(页面)
、components(组件)
、layout(布局)
、model(数据模型)
-
config
配置 -
mock
数据模拟 -
test
测试
-
- 全局安装脚手架:
npm i umi -g
初体验
- 创建一个项目目录:umidemo
-
cd umidemo -> npm init
:生成package.json
"scripts": { "start": "umi dev", "build": "umi build" }
- 创建src目录,生成
pages
目录,默认使用约定式路由;cd src umi g page index // index.js和index.css umi g page about // bout.js和about.css
- 运行项目:
npm start
,自动编译生成页面配置/src/pages/.umi目录
,且项目是热部署;http://localhost:8000/ --> index.js http://localhost:8000/about --> about.js
约定式路由嵌套
- 当出现
_layout.js
页面时默认为父组件页面,通过{props.children}
显示子组件内容; - 嵌套路由:
/users
,创建pages/users
目录umi g page users/_layout // pages/users/_layout.js、_layout.css
-
pages/users/_layout.js
import styles from './_layout.css' export default function(props) { return ( <div className={styles.normal}> <h1>Page users _layout</h1> <div>{props.children}</div> //引入子组件,如果没有创建子组件,则会报错 </div> ) }
- 为
_layout.js
创建子组件,users
的首页index.js
umi g page users/index // pages/users/index.js、index.css
- 访问嵌套路由:
http://localhost:8000/users
-
- 动态路由文件的命名以
$
为开头,users/$name.js
对应路由为/users/:name
- 在
users
目录中,再创建$name.js、$name.css
- 访问
$name.js
:http://localhost:8000/users/xxx
- 在
- 路由跳转
users/index.js
import Link from 'umi/Link' export default function() { const userList = [ { id:1, name:'Tim' }, { id:2, name:'Jerry' } ] return (<div> <ul> { userList.map(item=>( <li key={item.id}> <Link to={`/users/${item.name}`}>{item.name}</Link> </li> )) } </ul> </div>) }
users/$name.js
export default function(props) { return ( <div> <h2>{props.match.params.name}</h2> <button onClick={()=>props.history.goBack()}>返回</button> </div> ) }
-
src/layout
目录中的index.js
将成为项目的定级布局页面,使用{props.children}
显示src/pages
目录中的组件。import styles from './index.css' export default function(props) { return ( <div className={styles.normal}> <p>我是项目的Layout布局</p> <div>{props.children}</div> </div> ) }
配置式路由
- 配置式路由一旦创建,约定式路由自动失效,umi不会再自动创建路由;
- 在项目根目录下创建
config
目录,并创建config.js
文件;export default { //路由配置:路径相对于src/pages routes: [ { path: "/", component: "./index" }, { path: "/about", component: "./about" }, { path: "/users", component: "./users/_layout", routes: [ { path: "/users", component: "./users/index" }, { path: "/users/:name", component: "./users/$name" }, ] }, { component: "./notfound" } // 404页面,上面的所有路由都没有匹配时,则匹配404页面 ] }
- 相应地,创建404组件:
umi g page notfound
- 路由守卫
- 路由守卫组件的路径相对于项目根目录,且后缀名不能省略;
- 在项目根目录下创建
routes
目录,用于存放路由守卫组件; routes/PriviteRoute.js
import Redirect from 'umi/redirect' export default function(props) { if(Math.random()>0.5) { return <Redirect to="/login" /> //没有登录时,重定向到登录页 } //登录成功时,显示子路由的页面组件 return <div>{props.children}</div> }
- 创建登录页
umi g page login
- 在
config/config.js
中配置/login
,并守卫/about
routes: [ { path: "/login", component: "./login" }, { path: "/about", component: "./about", Routes: ["./routes/PriviteRoute.js"] //路由守卫 }, ...... ]
- 引入ant design UI库
npm i antd -S npm i umi-plugin-react -D
config/config.js
export default { //路由配置 routes: [...], //插件配置 plugins: [ [ "umi-plugin-react", { antd: true } ] ] }
- 使用时需要导入组件,因为是按需加载
import {Button} from 'antd' <Button type="primary">Primary</Button>
引入dva
-
dva
主要是软件分层的概念-
Page
负责与用户直接交互:渲染页面、接收用户的操作输入,侧重于展示型和交互逻辑; -
Model
负责处理业务逻辑,可以理解成一个维护页面数据状态的对象,为Page
做数据、状态的读写等操作;
export default { namespace: 'goods', // model的命名空间,区分多个model state: [], //初始状态 effects: { //异步操作 }, reducers: {} }
-
Service
主要负责与HTTP做接口对接,跟后端做数据交互,读写数据;
-
-
dva
已经融合进了umi
,在config/config.js
中打开dva的开关plugins: [ [ "umi-plugin-react", { antd: true, dva: true } ] ]
基本用法
umi g page goods
npm i axios -S
- 路由配置:
config/config.js
routes: [ { path: "/goods", component: "./goods" }, ]
- 在项目根目录下创建
mock/goods.js
,模拟接收请求,响应数据let data = [ { title: '单页面' }, { title: '管理项目' }, ], export default { //method url "get /api/goods": function(req, res) { setTimeout(()=>{ res.json({result: data}) }, 1000); } }
-
Model
:src/models/goods.js
import axios from 'axios' //调接口的逻辑应该放在 Service 层 function getGoods() { return axios.get('/api/goods') } export default { namespace: 'goods', // 命名空间,如果省略,则以文件名作为命名空间 state: [], effects: { *getList(action, {call, put}) { //异步操作 //发起请求 const res = yield call(getGoods) //派发异步action: initGoods yield put({type:'initGoods', payload:res.data.result}) } }, reducers: { initGoods(state, action) { return action.payload }, addGood(state, action) { // 添加函数,返回一个新的state return [...state, {title: action.plyload.title}] } } }
-
pages/goods.js
import {connect} from 'dva' import React, {Component} from 'react' @connect( state=>({ goodList: state.goods, // 从指定命名空间内获取state loading: state.loading, // 通过loading命名空间获取加载的状态 }), { getList: ()=>{ {type: 'goods/getList'} // action的type需要以命名空间为前缀,后跟reducer }, addGood: title=>({ type:'goods/addGood', payload: {title} }) } ) export default class extends Component { componentDidMount() { this.props.getList() //触发事件,发起请求,获取数据 } render() { if(this.props.loading.models.goods) { //命名空间goods 的请求在加载中 return <div>loading</div> } return ( <div> <ul> { this.props.goodList.map((good, index)=>{ return <li key={index}>{good.index}</li> }) } </ul> <button onClick={()=>this.props.addGood("商品3")}>添加</button> </div> ) } }
升级路由守卫
- 在项目根目录下创建
mock/login.js
export default { "post /api/login": function(req, res) { const {username, password} = req.body if(username==="xxx"&&password==="xxx") { return res.json({code: 200, data: {token:"xxxx", name:"xxx"}}) } return res.json({code: 404, info:"登录失败"}) } }
-
Model
:src/models/login.js
import axios from 'axios' import router from 'umi/router' //初始的state const initUserInfo = { token: "", name: "" } //调接口的逻辑应该放在 Service 层 function login(data) { return axios.post('/api/login', data) } export default { namespace: 'login', state: initUserInfo, effects: { //异步操作 *login(action, {call, put}) { //发起请求 const res = yield call(login, action.payload) if(res.data.code===200) { //登录成功 //派发异步action yield put({type:'init', payload:res.data.data}) //登录成功,跳转去首页 router.push('/') } else { console.log("登陆失败!") } } }, reducers: { init(state, action) { return action.payload } } }
-
pages/login.js
import {connect} from 'dva' import React, {Component} from 'react' @connect() export default class extends Component { onSubmit() { //派发action,发起请求 this.props.dispatch({type:"login/login", payload:{username:"xxx",password:"xxx"}}) } render() { return ( <div> <button onClick={onSubmit}>登录</button> </div> ) } }
-
routes/PriviteRoute.js
import React,{Component} from 'react' import Redirect from 'umi/redirect' import {connect} from 'dva' @connect( state => ({token:state.login.token}) //从指定命名空间内获取state ) export default class extends Component { render() { if(this.props.token) { //登录成功时,显示子路由的页面组件 return <div>{this.props.children}</div> } //没有登录时,重定向到登录页 return <Redirect to="/login" /> } }