一篇关于Umijs的学习笔记
啥是UmiJs呢!? 甭管他 我们先滚去了解 redux / redux-saga / dvajs
redux
这个网上教程一堆 自己也有博客 就不多说了 反正知道他是react中最火的一个状态管理的方案就行
redux-saga
然后嘞 我们就看看啥是redux-saga,我们先想想redux是怎么写异步action的,哎,同步action是啥,dispatch一个对象,异步呢,就是dispatch一个函数嘛,然后在applyMiddleWare里面加上thunk,具体的实现原理大概就是tunk会帮你在函数action里面再调dispatch返回出原生的action对象,所以就可以在这个函数里面执行异步操作,因为最后返回的都是action对象,不影响reducer对action的处理,然后applyMiddleWare的作用原理是store=>next=>action=>{}这种形式把所有的middleware串联起来再在最后调dispatch,就是enhancer,create Store()的第三个参数(前两个参数分别是reducer和initalState,这里enhancer可以理解为一种HOF,高阶函数,类似react的高阶组件,起到一个包装修饰的作用),这里就不多说了,这篇博客的重点也不是这个。
然后就有大佬觉得,哎呀thunk太麻烦啦,我不同的异步操作要分散在不同的action里面啊,我action形式也不统一一会儿object一会儿function的太丑了啊,你这轮子不行我要自己造。 这就是大佬的思考方式,别人的轮子不好用就自己造,我等凡人要么是硬着头皮用轮子要么是硬着头皮去找自己不喜欢的轮子。
于是,redux-saga出来了,官方一点的介绍是
redux-saga 是一个用于管理 Redux 应用异步操作的中间件(又称异步 action)。 redux-saga 通过创建 Sagas 将所有的异步操作逻辑收集在一个地方集中处理,可以用来代替 redux-thunk 中间件。。
好,有了saga之后,redux的项目逻辑就被分成了两部分
- Reducers 处理由action带过来的state
- Sagas 协调异步操作 (主要是网络请求)
然后saga跟thunk的作用原理也不一样

看这张图,thunk作用在Middleware那里,用来帮你给action函数调disapatch,是在action创建后才起作用的。
然后saga,是在你应用启动后就被调用了,他的层级和reducer,store,view是一样的,就可以把他理解为一个被创建的进程,reducer处理action,store管理action,view渲染action带来的state,saga就负责在后台监听action,判断你是同步还是异步,然后帮你调其他API,比如发起异步请求啦,发起同步action啦,甚至是调用其他的saga。
大概了解了saga的原理后,我们就来看一下相关的API
相应API
先看两个辅助函数
takeEvery()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| import { call, put } from 'redux-saga/effects'
export function* fetchUser(action) { try { const data = yield call(Api.fetchUser, action.payload.url); yield put({type: "FETCH_SUCCEEDED", data}); } catch (error) { yield put({type: "FETCH_FAILED", error}); } }
function* watchFetchUser() { yield takeEvery('USER_REQUESTED', fetchUser) }
|
这怎么理解? 就假设有一个按钮,点一次可以加载列表,然后有人手欠就一直点点点点点点, 所以’USER_REQUESTED’就不断被分发(算是允许并发,处理多个相同的action),也就是会一直调用fetchUser()这个saga。列表就会不断加载更新。So,如何解决有人喜欢显摆自己手速的问题呢?
takeLatest()
1 2 3 4 5 6 7 8 9 10
| import { takeLatest } from `redux-saga/effects`
function* fetchUser(action) { ... }
function* watchLastFetchUser() { yield takeLatest('USER_REQUESTED', fetchUser) }
|
这个辅助函数的意思就是说,当一个USER_REQUESTED
action 被发起时,使用 takeLatest
来启动一个新的 fetchUser
任务。 由于 takeLatest
取消了所有之前启动且未完成的任务,这样便可以保证:即使用户以极快的速度连续多次触发 USER_REQUESTED
action,我们都只会以最后的一个结束。
辅助函数介绍完了,我们来看看saga里面比较重要一个部分 Effect
Effect函数
saga里面有很多执行Effect的函数,而这些是saga实现作用的关键,看一下常用的吧,为什么叫Effect呢,就是和纯函数对应吗,你异步操作肯定会有副作用产生,所以管你叫Effect没啥问题对吧
take函数可以理解为监听未来的action,它创建了一个命令对象,告诉middleware等待一个特定的action, Generator会暂停,直到一个与pattern匹配的action被发起,才会继续执行下面的语句,也就是说,take是一个阻塞的 effect
1 2 3 4 5 6 7
| function* watchFetchData() { while(true) { yield take('FETCH_REQUESTED'); yield fork(fetchData); } }
|
put函数是用来发送action的 effect,你可以简单的把它理解成为redux框架中的dispatch函数,当put一个action后,reducer中就会计算新的state并返回,注意: put 也是阻塞 effect
1 2 3 4 5 6 7 8
| export function* toggleItemFlow() { let list = [] yield put({ type: actionTypes.UPDATE_DATA, data: list }) }
|
call函数你可以把它简单的理解为就是可以调用其他函数的函数,它命令 middleware 来调用fn 函数, args为函数的参数,注意: fn 函数可以是一个 Generator 函数,也可以是一个返回 Promise 的普通函数,call 函数也是阻塞 effect
1 2 3 4 5 6 7 8 9 10
| export const delay = ms => new Promise(resolve => setTimeout(resolve, ms))
export function* removeItem() { try { return yield call(delay, 500) } catch (err) { yield put({type: actionTypes.ERROR}) } }
|
fork 函数和 call 函数很像,都是用来调用其他函数的,但是fork函数是非阻塞函数,也就是说,程序执行完 yield fork(fn, args)
这一行代码后,会立即接着执行下一行代码语句,而不会等待fn函数返回结果后,在执行下面的语句 (takeEvery就是利用take和fork实现的高级API)
1 2 3 4 5 6 7 8 9
| import { fork } from 'redux-saga/effects'
export default function* rootSaga() { yield fork(addItemFlow) yield fork(removeItemFlow) yield fork(toggleItemFlow) yield fork(modifyItem) }
|
select 函数是用来指示 middleware调用提供的选择器获取Store上的state数据,你也可以简单的把它理解为redux框架中获取store上的 state数据一样的功能 :store.getState()
1 2 3 4
| export function* toggleItemFlow() { let tempList = yield select(state => state.getTodoList.list) }
|
createSagaMiddleware 函数是用来创建一个 Redux 中间件,将 Sagas 与 Redux Store 链接起来
sagas 中的每个函数都必须返回一个 Generator 对象,middleware 会迭代这个 Generator 并执行所有 yield 后的 Effect(Effect 可以看作是 redux-saga 的任务单元)
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
| import {createStore, applyMiddleware} from 'redux' import createSagaMiddleware from 'redux-saga' import reducers from './reducers' import rootSaga from './rootSaga'
const sagaMiddleware = createSagaMiddleware()
const store = createStore( reducers, 将sagaMiddleware 中间件传入到 applyMiddleware 函数中 applyMiddleware(sagaMiddleware) )
sagaMiddleware.run(rootSaga)
export default store
|
大致就是这些吧,我们目前停留用轮子的阶段也别想着深入探讨原理了,react都还没玩明白呢TnT,会常用 API开发就行,等成为开发大佬后再去走源码看底层比较好吧。
dvaJs
好嘞,了解了saga后,我们接下来就来看dvaJs,dva 首先是一个基于 redux 和 redux-saga 的数据流方案,然后为了简化开发体验,dva 还额外内置了 react-router 和 fetch,所以也可以理解为一个轻量级的应用框架。
- 易学易用,仅有 6 个 api,对 redux 用户尤其友好,配合 umi 使用后更是降低为 0 API
- elm 概念,通过 reducers, effects 和 subscriptions 组织 model
- 插件机制,比如 dva-loading 可以自动处理 loading 状态,不用一遍遍地写 showLoading 和 hideLoading
- 支持 HMR,基于 babel-plugin-dva-hmr 实现 components、routes 和 models 的 HMR
翻了官网文档,好像dvaJS的思想其实就是多了一个model,把state/reducer/saga统一到了model中,然后用connect把model和page连接起来,而不是和react- redux一样直接连接store了。直接看代码
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
| app.model({ namespace: 'todos', state: [], effects: { *addRemote({ payload: todo }, { put, call }) { yield call(addTodo, todo); yield put({ type: 'add', payload: todo }); }, }, reducers: { add(state, { payload: todo }) { return state.concat(todo); }, remove(state, { payload: id }) { return state.filter(todo => todo.id !== id); }, update(state, { payload: updatedTodo }) { return state.map(todo => { if (todo.id === updatedTodo.id) { return { ...todo, ...updatedTodo }; } else { return todo; } }); }, }, subscriptions: { setup({ dispatch, history }) { history.listen(({ pathname }) => { if (pathname === '/users') { dispatch({ type: 'users/fetch', }); } }); }, }, });
|
subscriptions
是订阅,用于订阅一个数据源,然后根据需要 dispatch 相应的 action。数据源可以是当前的时间、服务器的 websocket 连接、keyboard 输入、geolocation 变化、history 路由变化等等。格式为 ({ dispatch, history }) => unsubscribe
。

然后翻到一个贼清晰的博客介绍,就四张图,这里我就不放了,放链接https://yuque.com/flying.ni/the-tower/tvzasn
基于此,应该对dvaJs有了个大概的了解吧,就是用了一个model(重装机甲嘛)把redux+saga同一联合,直接在一个model里面就能完成action的dispatch和reducer加工,不需要一个个什么./src/redux/action/todo.js啥的。
好嘞,接下来我们就看这篇博客的真正的主角——umiJs!!
。。为什么到现在才开始真正的主角,我觉得是不是有点长有点久了。。。
UmiJs
那么那么那么,什么是umiJs呢
官方介绍Umi,中文可发音为乌米,是可扩展的企业级前端应用框架。Umi 以路由为基础的,同时支持配置式路由和约定式路由,保证路由的功能完备,并以此进行功能扩展。然后配以生命周期完善的插件体系,覆盖从源码到构建产物的每个生命周期,支持各种功能扩展和业务需求。
说白了就是大佬写的脚手架。
比如我们写react项目的时候,要干嘛?
yarn caeate-react-app
是吧,这就是官方提供给我们的一个基于react的脚手架,但这是官方提供哈,一般用于基本需求,那就有大佬不乐意咯,官方不行,官方一般,我自己写一个脚手架
yarn create @umijs/umi-app
所以,umiJs其实就是一个基于react的脚手架,官方文档也说了为什么不用前者
- create-react-app 是基于 webpack 的打包层方案,包含 build、dev、lint 等,他在打包层把体验做到了极致,但是不包含路由,不是框架,也不支持配置。所以,如果大家想基于他修改部分配置,或者希望在打包层之外也做技术收敛时,就会遇到困难。
- 然后我们看看umiJs收敛了哪些依赖

!!离谱不离谱!!他连antd都直接装好了。
仔细一想umi作者好像是阿里的,antd也是阿里的,啊,那没事了。
然后嘞,咱们就看看这个新脚手架的项目结构吧

根目录
包含插件和插件集,以 @umijs/preset-
、@umijs/plugin-
、umi-preset-
和 umi-plugin-
开头的依赖会被自动注册为插件或插件集。
配置文件,包含 umi 内置功能和插件的配置。如果项目的配置不复杂,推荐在 .umirc.ts
中写配置; 如果项目的配置比较复杂,可以将配置写在 config/config.ts
中,并把配置的一部分拆分出去,比如路由配置可以拆分成单独的 routes.ts
:
环境变量。
比如:
执行 umi build
后,产物默认会存放在这里。
存储 mock 文件,此目录下所有 js 和 ts 文件会被解析为 mock 文件。
此目录下所有文件会被 copy 到输出路径。
临时文件目录,比如入口文件、路由等,都会被临时生成到这里。不要提交 .umi 目录到 git 仓库,他们会在 umi dev 和 umi build 时被删除并重新生成。
约定式路由时的全局布局文件。
所有路由组件存放在这里。
运行时配置文件,可以在这里扩展运行时的能力,比如修改路由、修改 render 方法等。
然后umi主要就靠插件和配置来完成你的各种依赖需求,状态管理的主要方式就还是dvaJS
约定式路由
这里就是看看umiJs有哪些比较牛逼的点值得记一下,第一个就是约定式路由。不同于传统的配置式路由,都需要注册,umiJs要在.umirc.ts里写,react要通过route注册,
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
| routes: [ { path: '/', component: '@/layouts/index', routes: [ { path: '/', component: '@/pages/hero' }, { path: '/hero', component: '@/pages/hero' }, { path: '/item', component: '@/pages/item' }, { path: '/summoner', component: '@/pages/summoner' }, ] }, ],
<Switch> <Route path='/login' component={Login}></Route> <Route path='/home' component={Home}></Route> <Route path='/send' component={Send}></Route> <Route path='/detail' component={Detail}></Route> <Route path='/wish/:tag' component={Wishes}></Route> <Route path='/mywish' component={MyWish}></Route> <Redirect to='/login'></Redirect> </Switch>
|
哎,那什么是约定路由呢https://umijs.org/zh-CN/docs/convention-routing
官方介绍是:约定式路由也叫文件路由,就是不需要手写配置,文件系统即路由,通过目录和文件及其命名分析出路由配置。
这个很牛逼,不需要手写配置给你自动生成,但感觉不太好管理和debug
然后还有一些很牛的地方就是umiJs的插件集,之前也说了umiJs是靠插件集来对应解决相应组件,这部分有点杂,配置文件是最难看也是最难理解的,等以后牛逼了再说吧,明日复明日了哎。
所以我们就先拿umi框架搭写一个简单的王者荣耀官网!
初始化项目
就命令行就完事了呗 这里不贴了。真想写官网去看配置!(暴躁 卧槽我写了好几天了我受不了了卧槽我在干什么!!!)
然后初始化好了后基本的目录结构就是上面的图嘛,这咱就不管了,完整项目的umiJs框架大家可以去看菁程的代码哈,我这里就是简单介绍一下。
基本结构出来后,我们想一想我们要干嘛,懂不懂前端架构的含金量?
状态管理要用dva,组件模式用antd
于是乎我们来到.umirc.ts下,写上
1 2 3 4 5 6 7 8 9
| import { defineConfig } from 'umi';
export default defineConfig({ dva: { immer: true, hmr: false, }, antd: {}, nodeModulesTransform: { type: 'none', }, })
|
哎,这样就可以用dva和antd了!
好,继续,就假设我们是很牛逼的前端架构师,我们制定了要用的技术栈和布局规范,手底下的开发人员要干嘛?先把大致的布局写出来是吧。
哎这就有了src/layouts/index.tsx
注意 我们采用的是约定式路由 这就会让我们的路由变成
1 2 3 4 5
| <Layouts> <Route/> <Route/> </Layouts>
|
也就是在外面包了一层
然后直接利用antd给的Layouts开始布局页面嘛 代码如下
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
| import React from 'react'; import { Layout, Menu } from 'antd' import { Link } from 'umi' import styles from './index.less'
const { Header, Content, Footer } = Layout;
const menuData = [ { route: '/hero', name: '英雄' }, { route: '/item', name: '局内道具' }, { route: '/summoner', name: '召唤师技能' }, ];
function BasicLayout(props: any) {
const { location: { pathname }, children, } = props;
return ( <Layout> <Header> <div className={styles.logo}>王者荣耀资料库 </div> <Menu theme="dark" mode="horizontal" defaultSelectedKeys={[pathname]} style={{ lineHeight: '64px' }} > {menuData.map(menu => ( <Menu.Item key= {`/${menu.route}`}> <Link to={menu.route}>{menu.name}</Link> </Menu.Item> ))} </Menu> </Header> <Content style={{ padding: '0 50px' }}> <div style={{ background: '#fff', padding: 24, minHeight: 280 }}>{children}</div> </Content> <Footer style={{ textAlign: 'center' }}>今天是乐爷开始学umi的第一天</Footer> </Layout> ); }
export default BasicLayout;
|
然后基本的布局页面我们就弄好咯,静态结束后要干嘛,哎,搞数据,搞活的
dva这是就来咯,记住什么是dva,重机甲,放到代码里面呢?model对象,So,我们就搞一个models文件夹,里面就放这些model对象就好了。(菁程里面好像是把model和需要这个model的页面放在了一个文件夹里面,我也不知道哪种方式牛逼一点)
因为我们就看一个hero页面嘛,就直接上src/models/hero.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 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108
| import { Effect, Reducer, Subscription, request } from 'umi';
export interface HeroProps { ename: number; cname: string; title: string; new_type: number; hero_type: number; skin_name: string; } export interface HeroModelState { name: string; heros: HeroProps[]; freeheros: HeroProps[]; filterKey: number; itemHover: number; }
export interface HeroModelType { namespace: 'hero'; state: HeroModelState; effects: { query: Effect; fetch: Effect; }; reducers: { save: Reducer<HeroModelState>; }; subscriptions: { setup: Subscription }; }
const HeroModel: HeroModelType = { namespace: 'hero',
state: { name: 'hero', heros: [], freeheros: [], filterKey: 0, itemHover: 0 },
effects: { *fetch({ type, payload }, { put, call, select }) { const data = yield request('/web201605/js/herolist.json'); const freeheros = yield request('mock/freeheros.json', { method: 'POST', headers: { Accept: 'application/json', 'Content-Type': 'application/json; charset=utf-8', }, body: JSON.stringify({ number: 10, }), }); const localData = [ { ename: 105, cname: '廉颇', title: '正义爆轰', new_type: 0, hero_type: 3, skin_name: '正义爆轰|地狱岩魂', }, { ename: 106, cname: '小乔', title: '恋之微风', new_type: 0, hero_type: 2, skin_name: '恋之微风|万圣前夜|天鹅之梦|纯白花嫁|缤纷独角兽', }, ]; yield put({ type: 'save', payload: { heros: data || localData, freeheros }, }); }, *query({ payload }, { call, put }) {
}, }, reducers: { save(state, action) { return { ...state, ...action.payload, }; }, }, subscriptions: { setup({ dispatch, history }) { return history.listen(({ pathname }) => { if (pathname === '/hero') { dispatch({ type: 'fetch' }) } }); } }, };
export default HeroModel;
|
这应该都可以看懂吧大家 不懂我现场讲 我写不动了 真的写不动了 知识产出太累了 更何况我还没有知识
这里我们插一嘴,可以看到网络请求直接调用了request这个方法,哎,想想看他是手写的封装fetch还是很pont一样牛逼直接根据后端给的json直接生成?哈哈哈我也不知道 我只知道要在app.ts那里配置一句话就好了
1 2 3 4 5 6 7 8 9 10
| import { ResponseError } from 'umi-request';
export const request = { prefix: '/api', errorHandler: (error: ResponseError) => { console.log(error); }, };
|
🤔 这也就是给所有的请求加了一个前缀和错误处理吗,request的底层。。。再说吧再说吧
接下来还要注意一点,就是在没有mock本地测试数据的话,一般都会产生跨域问题https://juejin.cn/post/6844903882083024910详细的看这个哈 我反正没看完
然后react里有两种解决的跨域方案,简单点的就是写个在packag.json写proxy属性,缺点就是只能访问单一地址,复杂点的就是自己在src下写setupProxy中间件(用node的语法,什么moudle.export),然后umi自然也是有方法解决跨域的啦

还是去我们的./.umirc.js
然后
1 2 3 4 5 6 7
| "proxy": { "/api/": { "target": "https://pvp.qq.com/", "changeOrigin": true, "pathRewrite": { "^/api": "" } } }
|
顺便一提 代理只是请求服务代理,不是请求地址
我们打开控制台,可以看到我们的请求地址是 http://localhost:8000/api/web201605/js/herolist.json
,响应200,并返回了真实数据。
你不会在浏览器的控制台中查看到我们真实代理的地址,这里需要注意,代理只是将请求服务做了中转,设置proxy不会修改请求地址。
弄了请求 弄了布局 就开始英雄列表页嗷
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 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87
| import React, { FC } from 'react'; import { connect, HeroModelState, ConnectProps } from 'umi'; import { Row, Col, Radio, Card } from 'antd'; import { RadioChangeEvent } from 'antd/es/radio/interface'; import FreeHeroItem from '@/components/FreeHeroItem';
import styles from './hero.less';
const RadioGroup = Radio.Group;
interface PageProps extends ConnectProps { hero: HeroModelState; }
const heroType = [ { key: 0, value: '全部' }, { key: 1, value: '战士' }, { key: 2, value: '法师' }, { key: 3, value: '坦克' }, { key: 4, value: '刺客' }, { key: 5, value: '射手' }, { key: 6, value: '辅助' }, ];
const Hero: FC<PageProps> = ({ hero, dispatch }) => { const { heros = [], filterKey = 0, freeheros = [], itemHover = 0 } = hero; console.log(freeheros) const onChange = (e: RadioChangeEvent) => { dispatch!({ type: "hero/save", payload: { filterKey: e.target.value } }) }; const onItemHover = (index: number) => { dispatch!({ type: 'hero/save', payload: { itemHover: index }, }); } return ( <div className={styles.normal}> <div className={styles.info}> <Row className={styles.freehero}> <Col span={24}> <p>周免英雄</p> <div> { freeheros.map((data, index) => ( <FreeHeroItem data={data} itemHover={itemHover} onItemHover={onItemHover} thisIndex={index} key={index} /> )) } </div> </Col> </Row> </div> <Card className={styles.radioPanel}> <RadioGroup onChange={onChange} value={filterKey}> {heroType.map(data => ( <Radio value={data.key} key={`hero-rodio-${data.key}`}> {data.value} </Radio> ))} </RadioGroup> </Card> <Row> {heros.filter(item => filterKey === 0 || item.hero_type === filterKey).reverse().map(item => ( <Col key={item.ename} span={3} className={styles.heroitem}> <img src={`https://game.gtimg.cn/images/yxzj/img201606/heroimg/${item.ename}/${item.ename}.jpg`} /> <p>{item.cname}</p> </Col> ))} </Row> </div> ); }
export default connect(({ hero }: { hero: HeroModelState }) => ({ hero }))(Hero);
|
好嘞 效果就这样
我还是彩笔写了这么多都没讲明白自己也没学明白呜呜呜