- 支付宝前端应用架构的发展和选择 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. Initialize
const 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. Plugins
app.use(createLoading());
// 3. Router
app.router(require('./router').default);
// 4. Start
app.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.user
namespace: 'user',
state: { // 初始的 state
data: [],
loading: false,
},
reducers: {
save(state, action) {
return { ...state, ...action.payload }
}
},
// saga里的effects,里面的各种处理异步操作的saga
effects: {
*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,里面的各种处理异步操作的saga
effects: {
*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的节点注入到与此视图数据相关的组件上