Umi
Umi简介
Umi,中文发音为「乌米」,是可扩展的企业级前端应用框架。Umi 以路由为基础的,同时支持配置式路由和约定式路由,保证路由的功能完备,并以此进行功能扩展。然后配以生命周期完善的插件体系,覆盖从源码到构建产物的每个生命周期,支持各种功能扩展和业务需求。
Umi 是蚂蚁集团的底层前端框架,已直接或间接地服务了 10000+ 应用,包括 Java、Node、H5 无线、离线(Hybrid)应用、纯前端 assets 应用、CMS 应用、Electron 应用、Serverless 应用等。他已经很好地服务了我们的内部用户,同时也服务了不少外部用户,包括淘系、飞猪、阿里云、字节、腾讯、口碑、美团等。在 2021 年字节的调研报告中,Umi 是其中 25.33% 开发者的选择。
Umi 有很多非常有意思的特性,比如。
1、企业级,在安全性、稳定性、最佳实践、约束能力方面会考虑更多
2、插件化,啥都能改,Umi 本身也是由插件构成
3、MFSU,比 Vite 还快的 Webpack 打包方案
4、基于 React Router 6 的完备路由
5、默认最快的请求
6、SSR & SSG
7、稳定白盒性能好的 ESLint 和 Jest
8、React 18 的框架级接入
9、Monorepo 最佳实践
什么时候不用 Umi?
如果你的项目,
1、需要支持 IE 8 或更低版本的浏览器
2、需要支持 React 16.8.0 以下的 React
3、需要跑在 Node 14 以下的环境中
4、有很强的 webpack 自定义需求和主观意愿
5、需要选择不同的路由方案
…
Umi 可能不适合你。
为什么不是?
create-react-app
create-react-app 是脚手架,和 Umi、next.js、remix、ice、modern.js 等元框架不是同一类型。脚手架可以让我们快速启动项目,对于单一的项目够用,但对于团队而言却不够。因为使用脚手架像泼出去的水,一旦启动,无法迭代。同时脚手架所能做的封装和抽象都非常有限。
next.js
如果要做 SSR,next.js 是非常好的选择(当然,Umi 也支持 SSR);而如果只做 CSR,Umi 会是更好的选择。相比之下,Umi 的扩展性会更好;并且 Umi 做了很多更贴地气的功能,比如配置式路由、补丁方案、antd 的接入、微前端、国际化、权限等;同时 Umi 会更稳定,因为他锁了能锁的全部依赖,定期主动更新,某一个子版本的 Umi,不会因为重装依赖之后而跑不起来。
remix
Remix 是我非常喜欢的框架,Umi 4 从中抄(学)了不少东西。但 Remix 是 Server 框架,其内置的 loader 和 action 都是跑在 server 端的,所以会对部署环境会有一定要求。Umi 将 loader、action 以及 remix 的请求机制同时运用到 client 和 server 侧,不仅 server 请求快,纯 CSR 的项目请求也可达到理论的最快值。同时 Remix 基于 esbuild 做打包,可能不适用于对兼容性有要求或者依赖尺寸特别大的项目。
以上均摘自Umi官网,很有意思的介绍与对比。开整
安装脚手架:
- mkdir myapp && cd myapp //空目录
- npx @umijs/create-umi-app
安装项目依赖:
1.创建组件
2.redirect重定向
1 2 3 4 5 6 7 8
| import React from 'react'; import {Redirect} from "umi";
export default function index() { return ( <Redirect to="/film"/> ) }
|
3.404
创建pages/404.tsx 当没有匹配的路由的时候,会默认访问404.tsx的组件内容
4.嵌套路由及重定向
目录层级如下:
pages
- film
- _layout.tsx
- Comingsoon.tsx
- Nowplaying.tsx
例如:pages下新建film文件夹,在film文件夹下创建**_layout.tsx** ,在该文件中编写Film组件对应的页面,在film文件夹下创建子路由组件ComingSoon组件、Nowplaying组件。在_layout.tsx中使用插槽的方式,去渲染二级路由(嵌套路由)
此时,在嵌套路由的重定向,依然与一级路由的使用方式相同,同样是
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| import React from 'react'; import {Redirect,useLocation} from "umi";
export default function Film(props:any) { const location = useLocation() if (location.pathname === "/film" || location.pathname === "/film/") { return <Redirect to="/film/nowplaying" /> } return ( <div> <div style={{background:"yellow",height:"200px"}}>大轮播</div> {props.children} </div> ) }
|
5.路由根页面和声明式路由导航
在项目根目录下创建layouts目录,目录下创建index.tsx为路由的根页面,通过props.children插槽方式来渲染其他已经渲染好的路由组件。如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| import React from 'react'; import {NavLink} from "umi"; import'./index.less' export default function IndexLayout(props:any) { if (props.location.pathname === "/city" || props.location.pathname.includes("/detail")){ return <div>{props.children}</div> } return ( <div> {props.children} <ul> <li> <NavLink to="/film" activeClassName="active">film</NavLink> </li> <li> <NavLink to="/cinema" activeClassName="active">cinema</NavLink> </li> <li> <NavLink to="/center" activeClassName="active">center</NavLink> </li> </ul> </div> ) }
|
8.编程式路由导航
umi中的编程式路由导航与react-router中的相同,无差异
- props.history.push(“/home”)
- 使用useHistory hook,history.push(“/home”)
编程式导航传参:
目录结构如下 :
pages
pages下创建detail目录,detail目录下创建[id].tsx 在该tsx文件下通过props或者useParams来获取参数信息
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| import React from 'react'; import {useParams} from "umi"; interface IParams { id:string } export default function Detail(props:any) { const params = useParams<IParams>() return ( <div> Detail - {params.id} </div> ) }
|
路由拦截
目录解构如下:
src
在项目根目录下的src目录下创建wrappers文件夹,该文件夹下创建auth.tsx,在需要校验的Center组件下,写下如下代码Center.wrappers = ["@/wrappers/Auth"] 对Center组件进行包装,实际上,此时的Auth组件相当于是Center组件的父组件
auth.tsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| import React from 'react'; import {Redirect} from "umi";
export default function Auth(props:any) { if (localStorage.getItem("token")){ return ( <div> {props.children} </div> ) } return <Redirect to="/login" />
}
|
Center.tsx
1 2 3 4 5 6 7 8 9 10 11
| import React from 'react';
function Center() { return ( <div> center </div> ) } Center.wrappers = ["@/wrappers/Auth"] export default Center
|
hash模式与browser模式
.umirc.js下添加 history属性
1 2 3
| history:{ type:"hash" },
|
mock及反向代理
umi集成组件库
当umi自带antd组件库版本过低时,需要重新安装antd最新版,但是同时要关闭umi自身携带的antd,在umirc.js下添加
dva集成
- 按目录约定注册 model,无需手动 app.model
- 文件名即 namespace,可以省去 model 导出的 namespace key
- 无需手写 router.js,交给 umi 处理,支持 model 和component 的按需加载
- 内置 query-string 处理,无需再手动解码和编码
- 内置 dva-loading 和 dva-immer,其中 dva-immer需通过配开启(简化 reducer 编写)
同步获取数据
业务场景:在City组件中点击不同的城市,将城市信息(名称、ID)传递到Cinema组件
目录结构如下:
src
1.src下新建models目录,在目录下创建CityModel.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| export default { namespace:"city", state : { cityName:"北京", cityId:"110100" }, reducers:{ changeCity(prevState:any,action:any){ console.log(action); return { ...prevState, cityName: action.payload.cityName, cityId: action.payload.cityId } } } }
|
2.通过connect组件将City组件封装成高阶组件,通过dispatch方法派发action
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70
| import React, {useEffect, useState} from 'react'; import {IndexBar,List} from 'antd-mobile' import {useHistory,connect} from "umi";
function City(props:any) { const history = useHistory() const [list,setList] = useState<any>([]) const filterCity = (cities:any) => { const letterArr:Array<string> = [] const newList = [] for (let i = 65; i < 91; i++) { letterArr.push(String.fromCharCode(i)) }
for (var m in letterArr) { var cityItems = cities.filter((item:any) => item.pinyin.substring(0,1).toUpperCase() === letterArr[m] ) cityItems.length && newList.push({ title:letterArr[m], items:cityItems }) } return newList } useEffect(() => { fetch("https://m.maizuo.com/gateway?k=5490218", { headers: { "X-Client-Info": '{"a":"3000","ch":"1002","v":"5.2.0","e":"1651225177596601022185473"}', "X-Host": 'mall.film-ticket.city.list' } }).then(res => res.json()).then(res => { setList(filterCity(res.data.cities)) }) }) const changeCity = (item:any) => { props.dispatch({ type:"city/changeCity", payload:{ cityName:item.name, cityId:item.cityId } }) history.push("/cinema") } return ( <div style={{ height: window.innerHeight }}> <IndexBar> {list.map((group:any) => { const { title, items } = group return ( <IndexBar.Panel index={title} title={title} key={title} > <List> {items.map((item:any, index:number) => ( <List.Item onClick={() => changeCity(item)} key={index}>{item.name}</List.Item> ))} </List> </IndexBar.Panel> ) })} </IndexBar> </div> ) } export default connect(() =>({}))(City)
|
3.通过reducer来接收City组件派发过来的数据,并更新state,Cinema组件获取redux中的数据进行页面渲染
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59
| import React, {useEffect} from 'react'; import {NavBar,DotLoading} from "antd-mobile"; import {useHistory} from "umi" import {connect} from "umi"; import {SearchOutline} from "antd-mobile-icons"
function Cinema(props:any) { let history = useHistory() useEffect(() => { if (props.list.length === 0){ props.dispatch({ type:"cinema/getList", payload:{ cityId: props.cityId } }) }else { console.log("缓存") }
},[]) console.log(props); return ( <div> <NavBar onBack={() => { props.dispatch({ type:"cinema/clearList" }) history.push("/city") }} back={props.cityName} backArrow={false} right={<SearchOutline/>}>标题</NavBar>
{ props.loading && <div style={{fontSize:14,textAlign:"center"}}> <DotLoading/> </div> } <ul> { props.list.map((item:any) => <li key={item.cinemaId}>{item.name}</li> ) } </ul> </div> ) }
export default connect((state:any) => { console.log(state); return { a:1, loading:state.loading.global, cityName:state.city.cityName, cityId:state.city.cityId, list:state.cinema.list } })(Cinema)
|
异步获取数据
业务场景:获取各城市影院数据信息展示到Cinema组件中,第一次正常发请求进行获取,后续再次访问时,走缓存。
首先在src/models 下创建CinemaModel.ts,实现异步获取影院数据的方法,此时与dva 的获取方式相同,通过effects副作用函数来进行异步转同步:call方法解决异步,put方法将异步获取的数据派发到reducers,reducers中进行同步处理,更新state,进行页面渲染
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
| export default { namespace:"cinema", state:{ list:[] }, reducers:{ changeList(prevState:any,action:any){ return { ...prevState, list: action.payload } }, clearList(prevState:any,action:any){ return { ...prevState, list:[] } } },
effects:{ *getList(action:any,obj:any):any{ const {put,call} = obj var res = yield call(getListForCinema,action.payload.cityId) console.log(res);
yield put({ type:"changeList", payload:res })
} } }
async function getListForCinema(cityId:string) { console.log(cityId); var res = await fetch(`https://m.maizuo.com/gateway?cityId=${cityId}&ticketFlag=1&k=878555`,{ headers: { 'X-Client-Info': '{"a": "3000", "ch": "1002", "v": "5.2.0", "e": "1646462402616989231939585"}', 'X-Host': 'mall.film-ticket.cinema.list' } }).then(res => res.json()) return res.data.cinemas }
|
Cinema.tsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58
| import React, {useEffect} from 'react'; import {NavBar,DotLoading} from "antd-mobile"; import {useHistory} from "umi" import {connect} from "umi"; import {SearchOutline} from "antd-mobile-icons"
function Cinema(props:any) { let history = useHistory() useEffect(() => { if (props.list.length === 0){ props.dispatch({ type:"cinema/getList", payload:{ cityId: props.cityId } }) }else { console.log("缓存") }
},[]) console.log(props); return ( <div> <NavBar onBack={() => { props.dispatch({ type:"cinema/clearList" }) history.push("/city") }} back={props.cityName} backArrow={false} right={<SearchOutline/>}>标题</NavBar>
{ props.loading && <div style={{fontSize:14,textAlign:"center"}}> <DotLoading/> </div> } <ul> { props.list.map((item:any) => <li key={item.cinemaId}>{item.name}</li> ) } </ul> </div> ) }
export default connect((state:any) => { console.log(state); return { a:1, loading:state.loading.global, cityName:state.city.cityName, cityId:state.city.cityId, list:state.cinema.list } })(Cinema)
|
umi中的loading
umi中的loading是通过connect组件封装后的state参数中获取的,它可以判断在异步处理的时候,副作用函数effects的执行状态,在数据没有返回之前,loading 是false,数据返回后则是true,可以用过它来进行页面的优化,从而提升用户体验
具体代码见dva异步获取数据中的Cinema组件。