1-1,背景发展描述
(1)Web 1.0:静态网页,纯内容展示
从web诞生到2005年, 那时的网站采用后端 MVC 模式,前端只是后端 MVC 的 V,这种局面我们称为前后端不分离的时代;
Model(模型层):提供/保存数据
Controller(控制层):数据处理,实现业务逻辑
View(视图层):展示数据,提供用户界面
(2)Web 2.0:动态网页,富交互,前端数据处理
从2005年后ajax被关注,通过大量使用ajax技术,不需要刷新页面,就可以使前端与服务器进行网络通信。前端也能独立的得到数据,然后处理数据,并展示出来,这种局面我们称为前后端分离的时代;
ajax技术促成了 Web 2.0 的诞生,在当时ajax是一项革命性的技术,颠覆了用户体验。
(3)需求使然,前端框架相应产生
为了解决浏览器兼容问题,jQuery,Dojo,ExtJS,Mootools等前端框架相继诞生,我们可以用这些框架频繁发送 AJAX 请求到后台,在得到数据后,再用这些 框架更新 DOM 树。前端能读写数据,切换视图, 用户交互, 既然要做这么多事,必然的网页的代码体积越来越大。
(4)DOM操作问题引发的进步
对于jQuery来说,DOM操作需要反复地操作文档,并触发网页的重绘和重排,这会严重影响网页的性能,当项目中大量的代码去操作DOM时,最严重的可能导致页面卡死,所以jQuery无法应对越来越复杂的前端开发工作。
(5)MVVM流行
为了解决上述问题,获得更优秀的用户体验,组件化开发和MVVM的开始兴起。随着越来越多的框架出现,网页逐渐由web site演变成web app,使得复杂的单页面应用的出现。
1-2,react特性
React是用于构建用户界面的JavaScript库, 起源于Facebook的内部项目,因为对市场上MVC框架都不满意,于是开发了React用来架设Instagram的网站。2013年5月29日上线。
特点:
它采用声明范式来描述应用,建立虚拟dom,支持JSX语法,通过react构建组件,能够很好的去复用代码;
缺点:
react抽离了dom,使我们构建页面变得简单,但是对于一个大型复杂应用来说,只有dom层的便捷是不够的,如何组织、管理应用的代码,如何在组件间进行有效通信,这些它都没有解决。所以在我们需要的时候可以借助状态管理架构redux & dva。
2-1,redux
Redux:状态管理器。可以分模块或集中对数据状态进行管理。
它并不是只针对react结合,它可以在各种框架中结合使用,但是在react项目中使用较广泛。
2-2,redux核心概念
store: 仓库,用来存储状态state
state: 状态
reducer: 是一个函数,该函数接收两个参数state和action,负责对状态进行同步管理
action: 是一个对象,且必须有一个属性type,该对象通过store.dispatch来触发,作为reducer接收的参数来实现对state的操作
// 创建仓库
import {
createStore,//创建仓库函数
} from 'redux';
const store = createStore(reducer);
// store中存放状态,
const state = store.getState();
// 操作状态
import {
createStore
} from 'redux';
const initState = {
name:'橘子',
price: '4',
}
function reducer(state=initState,action){
console.log('执行reducer',action)
if(action.type === 'EDIT'){
state.price = action.payload.price;
}
return state;
}
const store = createStore(reducer);
const state = store.getState();
store.dispatch({
type:'EDIT',
payload: {
price: '9'
}
})
// 可以发现当执行dispatch时会触发reducer
console.log('dispatch调用后',state)
// 工厂模式批量返回action
function change(num){
return {
type: 'EDIT',
payload: {
price:num,
}
}
}
store.dispatch(change('10'))
console.log('调用后',state)
2-3,在项目中使用redux
在store仓库下,我们通常会建4个文件夹,分别是
- state:最初的状态,
- reducer:官方建议不要在reducer中写逻辑,逻辑集中在action中
- actionType:在执行时使用,存放type,可以对请求的type进行集中管理
- actionCreator:action制造者,存放所有的action(同步/异步action)
使用详见项目。
3-1,什么是dva
dva 是基于现有应用架构 (redux + react-router + redux-saga + fetch)的一层轻量封装,没有引入任何新概念。
dva是基于react+redux最佳实践上实现的封装方案,简化了redux和redux-saga使用上的诸多繁琐操作。
目录结构:
├── /mock/ # 数据mock的接口文件
├── /src/ # 项目源码目录
│ ├── /components/ # 项目组件
│ ├── /routes/ # 路由组件(页面维度)
│ ├── /models/ # 数据模型
│ ├── /services/ # 数据接口
│ ├── /utils/ # 工具函数
│ ├── route.js # 路由配置
│ ├── index.js # 入口文件
│ ├── index.less
│ └── index.html
├── package.json # 定义依赖的pkg文件
└── proxy.config.js # 数据mock配置文件
在 dva 中,通常需要 connect Model的组件都是 Route Components,组织在/routes/目录下;而/components/目录下则是纯组件基本不需要用到state。
3-2,dva的特性
- 易学易用:仅有 6 个 api,对 redux 用户尤其友好
- elm 概念:通过 reducers, effects 和 subscriptions 组织 model
- 插件机制: dva-loading 可以自动处理 loading 状态,不用一遍遍地写 showLoading 和 hideLoading
elm概念
elm是一门专注web前端的纯函数式编程语言。(redux的核心reducer就是受到了elm的启发)
插件 dva-loading
用于监听页面的请求是否完成(主要是ajax请求是否完成),是通过直接监听异步的effect,在异步请求发出的那一刻开始持续监听该请求方法的状态,在异步请求结束时,loading的状态改为false并停止监听。
const isLoading = loading.effects['user/query']
,其中 user/query
是 model 中的异步请求方法。
在dva中依赖于redux-saga,将每个路由和redux-saga库相连,进入页面则路由对应的组件会自动关联redux-saga,触发相应的reducer方法,从而更新loading中的相应bool。
3-3,dva的基础saga
redux-saga作为react的一个中间件对数据进行处理,主要集中处理react框架中的异步action,采用监听的形式进行工作。在action被传递到reducer之前进行了一次拦截,然后启动异步任务,等异步任务执行完成后再发送一个新的action,调用reducer去修改状态数据。
蓝色的圈圈内可以看做是一堆saga,他们需要和外部进行I/O交互,等交互完成后再修改store中的状态数据。redux-saga的主要功能就是管理这一堆saga。
redux-saga的api : (redux-saga的api有很多)
- call:函数调用
- select:获取Store中的数据
- put:向Store发送action
- takeEveny:在Store上等待指定action
yield call & yield select
*collectionProject({ payload }, { call, put, select }) {
const res = yield call(collectionProject, payload);
const myState = yield select(state => state.projectManage);
if (res && res.code === 0) {
yield put({
type: 'projectList',
// 分页的查询参数
payload: {
current: 1,
name: '',
size: myState.projectPageSize,
},
});
}
},
yield takeEvery
*rootSaga() {
yield takeEvery(actionTypes.collectionProject, goAge)
// 有对应的type是collectionProject的action发起时,执行goAge()
}
*goAge(){
console.log('123');
}
yield put直接调用reducer是同步的,但是调用非reducer的方法是异步的。
function onHandlePress () {
dispatch({
type: 'formAndstepForm/getData',
payload: 'demo',
});
}
// 所有后续的操作都通过 saga 来管理。
effects: {
* getData({payload}, { put, select, call }) {
console.log('start');
yield put(
{
type: 'upState',
payload,
},
);
console.log('end')
},
},
reducers: {
upState: (state, { data: data }) => {
for (let i = 0; i < 8; i++) {
console.log(i);
}
return { ...state, data: data };
},
};
function onHandlePress () {
dispatch({
type: 'formAndstepForm/getData',
payload: 'demo'
});
}
effects: {
* getData({payload}, { put, select, call }) {
console.log('start');
yield put(
{
type: 'testAsync',
payload,
},
);
console.log('end')
},
*testAsync({},{put}){
yield put(
{
type: 'upState',
data: 'this is 2 demo',
},
);
}
},
reducers: {
upState: (state, { data: data }) => {
for (let i = 0; i < 8; i++) {
console.log(i);
}
return { ...state, data: data };
},
};
// j结果 start 0-8 end
// 结果start end 0-8
所有业务代码都存于 saga 中,不再散落在各处,可以证明对异步处理友好;
3-4,dva的api
import dva from 'dva';
import { Component } from 'react';
import createLoading from 'dva-loading';
import history from '@tmp/history';
let app = null;
export function _onCreate() {
const plugins = require('umi/_runtimePlugin');
const runtimeDva = plugins.mergeConfig('dva');
// 1,创建一个 dva 实例
app = dva({
history,
...(runtimeDva.config || {}),
...(window.g_useSSR ? { initialState: window.g_initialData } : {}),
});
// 2,装载插件dva-loading,可以用于配置hook以及其他插件
app.use(createLoading());
(runtimeDva.plugins || []).forEach(plugin => {
app.use(plugin);
});
// 3,注册Model
app.model({ namespace: 'global', ...(require('E:/project/src/models/global.js').default) });
app.model({ namespace: 'list', ...(require('E:/project/src/models/list.js').default) });
app.model({ namespace: 'login', ...(require('E:/project/src/models/login.js').default) });
app.model({ namespace: 'menu', ...(require('E:/project/src/models/menu.js').default) });
app.model({ namespace: 'project', ...(require('E:/project/src/models/project.js').default) });
app.model({ namespace: 'setting', ...(require('E:/project/src/models/setting.js').default) });
app.model({ namespace: 'user', ...(require('E:/project/src/models/user.js').default) });
return app;
}
export function getApp() {
return app;
}
export class _DvaContainer extends Component {
render() {
const app = getApp();
// 4,配置路由
app.router(() => this.props.children);
// 5,启动应用
return app.start()();
}
}
还有一个取消Model注册,app.unmodel(namespace),可清理对应命名空间的model层。
app._models.forEach((model:any) => {
app.unmodel(model.namespace);
});
app._models = [];
3-5,dva与传统redux的区别
对于dva,通过model层,只需要一个文件就可以实现对数据的集中管理, 数据流程如下图:
export default {
namespace: 'example', //表示对于整个应用不同的命名空间,以便通过this.props.example访问,和当前model文件名相同就好之前的reducer名字相同,是全局state的属性,只能为字符串
state: {initText:"hello"}, //表示当前的example中的state状态,这里可以给初始值
subscriptions: {
setup({ dispatch,history }) { // 订阅,可以监听服务器连接,键盘输入,路由,状态等的变化
return history.listen(({ pathname, search }) => {
if (pathname === '/user/login') {
dispatch({
type: 'login/getEncrypt',
});
}
});
},
},
effects: {
*fetch({ payload }, { call, put }) {
//payload是从组件router传递过来的参数,
//这里的call方法可以使用payload参数传递给后台程序进行处理这里可以调用service层的方法进行调用后端程序,
//这里的put表示存储在当前命名空间example中,通过save方法存在当前state中
yield put({ type: 'save' });
},
},
//用来保存更新state值 上面的put方法调用这里的方法
reducers: {
save(state, action) {
return { ...state, ...action.payload };
},
},
};
上面的model是需要注册到src/index.js中的app.router(require(‘./router’).default)否则后面和route组件无法用connect传递数据,如果定义了多个model,那么需要使用多个app.model()来传递model数据;