- 支付宝前端应用架构的发展和选择 https://github.com/sorrycc/blog/issues/6
- React + Redux 最佳实践 https://github.com/sorrycc/blog/issues/1
dva2.x的主要缺点
- react-router,不支持 hooks,hooks重构项目要注意;不是最新的 react-router-dom5.x
- hooks memo 和dva connect使用 bug
- hooks 不能使用 useHistory
- history路由报错
- less报错,less版本必须是2.x, less-loader必须是 4.x,否则报 less编译的错误
dva更新文档 https://github.com/dvajs/dva/issues/2208{"less": "^2.7.3","less-loader": "^4.1.0",}
项目结构
├── .editorconfig #├── .eslintrc # Eslint config├── .gitignore #├── .roadhogrc # Roadhog config├── mock # 数据mock的接口文件├── package-lock.json├── package.json├── public│ └── index.html└── src # 项目源码目录├── assets├── components # 项目组件├── index.css # css入口├── index.js # 项目入口文件├── models # 数据模型├── router.js # 路由配置├── routes # 页面路由组件├── services # API接口管理└── utils # 工具函数/src/.├── assets│ └── yay.jpg├── components│ └── Example.js├── index.css├── index.js├── models│ └── example.js├── router.js├── routes│ ├── IndexPage.css│ └── IndexPage.js├── services│ └── example.js└── utils└── request.js
src/index.js
import dva from 'dva';import createLoading from 'dva-loading';import createHistory from 'history/createBrowserHistory';import { persistStore, persistReducer } from 'redux-persist'import storage from 'redux-persist/lib/storage'import 'moment/locale/zh-cn';import { name } fom './package.json';import './polyfill';import './index.less';// redux持久化配置const persistConfig = {key: 'root',storage: storage,blacklist: []}// 1. Initializeconst app = dva({history: createHistory({basename: name,}),onError(error) {// 捕捉effect中执行的报错,subscriptions 中通过done触发的错误console.error(error.message);},onAction() {return (next) => (action) => next(action)},// 可监听state状态的变化onStateChange() { },onReducer(reducer) {return persistReducer(persistConfig, reducer)}});// 2. Pluginsapp.use(createLoading());// 3. Routerapp.router(require('./router').default);// 4. Startapp.start('#root');window.onload = () => persistStore(app._store)
models
每个路由都对应一个 model层
在model定义好这个路由的initialstate、reducers、sagas、subscriptions
然后组件里面,connect()(组件)
在组件里 dispatch(action) 发起调用
- 同步action时,type写成’namespace/reducer’ ,dva就调用 reducers下,对应名字的reducer更新state
- 异步action,type就写成’namespace/saga’,dva就调用 effects下,对应名字的 saga,
然后再 effects里面,put(action),同步更新state
export default {// 分割的路由,对应 combine到root Reducer里的名字,这里是state.usernamespace: 'user',state: { // 初始的 statedata: [],loading: false,},reducers: {save(state, action) {return { ...state, ...action.payload }}},// saga里的effects,里面的各种处理异步操作的sagaeffects: {*asyncGetUser(action, effect) {const { payload } = action;const { put, call, select } = effect;const res = yield call(axios.post('url'), payload);yield put({ type: 'save', payload: { data: res.data } });}},// 监听路径变化,当路由为 user时,dispatch一个获取数据的请求subscriptions: {// 当监听有变化的时候就会依次执行这的变化setup({ dispatch, history }) {// 当浏览器的页面的大小变化时,触发里面的dispatch方法,save就是reducers中的方法名window.onresize = () => {dispatch({ type: "save" })}},onClick({ dispatch }) {//当鼠标点击时就会触发里面的dispatch命令,这里的save就是reducers中的方法名document.addEventListener('click', () => {dispatch({ type: "save" })})},setupHistory({history, dispatch}) {history.listen(({pathname, query}) => {if(pathname !== '/user') return;dispatch({type: 'asyncGetUser', payload: {query}});})},},}
subscriptions 中,只能 dispatch 当前 model 中的 reducer 和 effects
- subscriptions 中配置的函数只会执行一次,也就是在调用 app.start() 的时候,会遍历所有 model 中的 subscriptions 执行一遍
- subscriptions 中配置的函数需要返回一个函数,该函数应该用来取消订阅的该数据源
- subscriptions 中配置的key的名称没有任何约束,而且只有在app.unmodel的时候才有用
subscriptions
subscription相当于一个监听器,可以监听
- 路由变化
- 鼠标
- 键盘变化
- 服务器连接变化
- 状态变化等
- 可以根据不同的变化做出相应的处理,根据需要dispatch相应的action
subscription格式: ({ dispatch, history }) => unsubscribe
subscriptions中的方法名是随意定的,每次变化都会一次去调用里面的所有方法,所以要加上相应的判断
subscriptions是订阅,用于订阅一个数据源
- 可以在这里面监听路由变化,比如当路由跳转到本页面时,发起请求来获取初始数据,数据源可以是
- 当前的时间
- 服务器的websocket连接
- keyboard输入
- geolocation变化
history路由变化等
export default {namespace: 'user',state: {data: [],},reducers: {save(state, {payload}) {return { ...state, ...payload }}},// saga里的effects,里面的各种处理异步操作的sagaeffects: {*asyncGetUser({ payload }, { put, call, select }) {const res = yield call(axios.post('url'), payload);yield put({ type: 'save', payload: { data: res.data } });}},subscriptions: {setupHistory({history, dispatch}) {history.listen(({pathname, query}) => {if(pathname !== '/user') return;dispatch({type: 'user/asyncGetUser', payload: {query}});// dispatch({ type: 'asyncGetUser', payload: query });})},}}
path-to-regexp
如果 subscriptions,监听的 url规则比较复杂,比如: /users/:userId/search,使用 path-to-regexp来匹配路由
import pathToRegexp from 'path-to-regexp';subscriptions: {setupHistory({history, dispatch}) {history.listen(({pathname, query}) => {const match = pathToRegexp('/users/:userId/search').exec(pathname);if(match) {const userId = match[1];}dispatch({type: 'user/asyncGetUser', payload: {userId}});})},}
component
组件 dispatch(action) 触发异步更新,不需要关心数据的处理逻辑,数据处理逻辑都在 models里面
组件只关心从 props中获取对应的数据
function App({dispatch}) {useEffect(() => {const action = {type: 'nampsace/saga', payload: {page: 1, limit: 10}}dispatch(action);}, [])}function mapStateToProps(state) {return {loading: state.loading.effects['user/fetch'],data: state.user.data,}}function mapDispatchToProps(dispatch){}// 依赖注入export default connect(mapDispatchToProps, null)(App);
- 只要这个组件关心的数据没变,就不会重新渲染
- 依赖注入:将需要的state的节点注入到与此视图数据相关的组件上
