原文地址:https://github.com/dvajs/dva-knowledgemap
官方文档:https://dvajs.com/guide/getting-started.html
用一用dva,写一个例子。
github地址:https://github.com/YueJingGe/dva-demo
看dva源码。
源码解析地址:https://dvajs.com/guide/source-code-explore.html
const DELAY = 1000
let count = 0
count = count + 1
const user = 'world'
console.log(`hello ${user}`)
// 多行
const content = `
hello ${user}
thanks for you ${user}
`
console.log(content)
function logActivity(activity = 'skiing'){
console.log(activity)
}
logActivity() ;// skiing
[1, 2, 3].map(x => x + 1) // [2,3,4]
// 等同于
[1, 2, 3].map((function(x) {
return x + 1;
}).bind(this));
import dva from 'dva'
import {connect} from 'dva'
import * as service from './services'
export default App
export class App extends Component{}
// 对象
const people = {name:'kk',age:12}
const { name , age } = people
console.log(`${name}:${age}`)
// 数组
const arr = [1,2]
const [foo,bar] = arr
console.log(foo)
// 函数
const add = (state,{payload}) => {
return state.concat(payload)
}
// alias别名
const plus = (state,{payload:todo}) => {
return state.concat(todo)
}
const n = 'kk'
const a = 8
const u = {n , a}
// 定义对象的方法时,可省略去function
app.model({
reducers:{
add(){} // <=> add: function() {}
},
effects:{
*addRemote() {} // <=> addRemote: function() {}
}
})
// 组装数组
const array = ['add']
// [...array,'remove']
// 获取部分项
function directions(first,...rest){
console.log(rest)
}
console.log(directions('a','b','c'))
// 代替apply
function fun(x,y,z){
console.log(y)
}
const args = [1,2,3]
fun.apply(null,args)
// 等同于
fun(...args)
// 合成新的object
const old = {
a:1
}
const change = {
b:2
}
const ret = {...old , ...change}
fetch('/api/todos')
.then(res => res.json())
.then(data => ({data}))
.catch(err => ({err}))
// 定义promise
const delay = (timeout) => {
return new Promise(resolve => {
setTimeout(resolve,timeout)
})
}
delay(1000).then(_ => {
console.log('executed')
})
/*
概述:dva 的 effects 是通过 generator 组织的。Generator 返回的是迭代器,通过 yield 关键字实现暂停功能。
这是一个典型的 dva effect,通过 yield 把异步逻辑通过同步的方式组织起来。
*/
app.model({
namespace:'todos',
effects:{
*addRemove({payload:todo},{put,call}){
yield call(addTodo,todo)
yield put({type:'add',payload:todo})
}
}
})
-------------------------------------------------重要内容---------------------------------------------------
函数
类
const attrs = {
href : 'http://exm.org',
target:'_blank'
}
<a {...attrs}>hello</a>
// 概念:由于js是弱类型语言,声明propTypes对props进行校验是有必要的
function App (props){
return <div>{props.name}</div>
}
App.propTypes = {
name:React.PropTypes.string.isRequired
}
.title {
color: red;
}
:global(.title) {
color: green;
}
//然后在引用的时候:
<App className={styles.title} /> // red
<App className="title" /> // green
/*
概念:在一些复杂的场景中,一个元素可能对应多个 className,而每个 className 又基于一些条件来决定是否出现。
这时,classnames 这个库就非常有用。
*/
import classnames from 'classnames'
const App = (props) => {
const cls = (props) => {
btn : true,
btnLarge:props.type === 'submit',
btnSmall:props.type === 'edit'
}
return <div classNames={cls}>
}
//这样传入不同的type给App组件,就会返回不同的className组合
<App type='submit'/>
<App type='edit'/>
// 增删改 以todo为例
app.model({
namespace:'todos',
state:[],
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
}
})
}
}
})
// 嵌套数据的增删改
app1.model({
namespace:'app',
state:{
todos:[],
loading:false,
},
reducers:{
add(state,{payload:todo}){
const todos = state.todos.concat(todo)
return {...state,todos}
}
}
})
app2.model({
namespace:'todos',
effects:{
*addRemove({payload:todo},{put,call}){
yield call (addTodo,todo)
yield put({type:'add',payload:todo})
}
}
})
yield put({ type: 'todos/add', payload: 'Learn Dva' });
// 7.7.2 call 用于调用异步逻辑 支持promise
const result = yield call(fetch, '/todos');
// 7.7.3 select 从state中获取数据
const todos = yield select(state => state.todos)
app.model({
effects: {
*addRemote() {
try {
// Your Code Here
} catch(e) {
console.log(e.message);
}
},
},
});
fetch学习地址: https://github.com/github/fetch
import request from '../util/request'
//get
request('/api/todos')
// post
request ('/api/todos',{
methods:'post',
body:JSON.stringify({a:1})
})
app.model({
subscriptions:{
setup({dispatch,history}){
history.listen(({pathname})=>{
if(pathname === 'users'){
dispatch({
type:'users/fetch'
})
}
})
}
}
})
import {routerRedux} from 'dva/router'
// inside effects
yield put(routerRedux.push('/logout'))
// outside effects
dispatch(routerRedux.push('/logout'))
// with query
routerRedux.push({
pathname:'/logout',
query:{
page:2,
}
})
import createLogger from 'redux-logger'
const app = dva ({
onAction:createLogger() // onAction支持数组,可同时传入多个中间件
})
// history 切换history为browserHistory
import {browserHistory} from 'dva/router'
const ap = dva({
history:browserHistory
})
//去除 hashHistory 下的 _k 查询参数
import { useRouterHistory } from 'dva/router';
import { createHashHistory } from 'history';
const app2 = dva({
history: useRouterHistory(createHashHistory)({ queryKey: false }),
});
$ npm i dva-cli -g # 安装dva-cli
$ dva new myapp # 创建项目
$ cd myapp
$ npm start # 启动项目
-------------------------------------------------华丽的分割线---------------------------------------------------
dva = redux + redux-saga + react-router
redux
https://github.com/reduxjs/redux
关键字:state、 action、 dispatch、 subscribe、type、 createStore
redux-saga
https://github.com/redux-saga/redux-saga
关键字: * 、yield、 call、 put、 middleware、createSagaMiddleware、applyMiddleware
react-router
看源码之前,先去看 package.json 。看看项目的入口文件,翻翻它用了哪些依赖,对项目便有了大致的概念。
"scripts": {
"start": "roadhog server",
},
"devDependencies": {
"roadhog": "^2.0.0" // 和 webpack 相似的库 起的是 webpack 自动打包和热更替的作用
}
运行 ‘roadhog server’命令,启动入口 ‘src/index.js’。
六部曲:
import dva from 'dva'; // 1. 从dva依赖中引入dva
const app = dva({
initialState: {
products: [
{name: 'kangknag', id: 1},
{name: 'xiaoming', id: 2},
]
}
}); // 2. 通过函数创建一个app对象
app.use({}); // 3. 加载插件
app.model(require('./models/products').default); // 4. 注入model
app.router(require('./router'.default)); // 5. 添加路由
app.start('#app'); // 6. 启动
此时的app是什么呢?
model: ƒ ()
replaceModel: ƒ ()
router: ƒ router(router)
start: ƒ start(container)
unmodel: ƒ ()
use: ƒ ()
_getProvider: ƒ ()
_getSaga: ƒ ()
_history: {length: 4, action: "POP", location: {…}, createHref: ƒ, push: ƒ, …}
_models: Array(2)
0: {namespace: "@@dva", state: 0, reducers: {…}}
1: {namespace: "products", state: Array(0), reducers: {…}}
length: 2
__proto__: Array(0)
_plugin: Plugin {_handleActions: null, hooks: {…}}
_router: ƒ RouterConfig(_ref)
_store: {dispatch: ƒ, subscribe: ƒ, getState: ƒ, replaceReducer: ƒ, liftedStore: {…}, …}
__proto__: Object
总结来说,dva就是一个函数,返回了一个app对象。在这6步中dva完成了 使用react解决view层、redux管理model、saga解决异步的主要功能。
前端工程师一直在做的就是 分离动态的data和静态的view 。
目前 dva 的源码核心部分包含两部分,dva 和 dva-core。前者用高阶组件 React-redux 实现了 view 层,后者是用 redux-saga 解决了 model 层。
{
"dependencies": {
"@babel/runtime": "7.0.0-beta.46", // 编译后文件引用的公共库,可以复用工具函数,有效的减少编译后的体积
"@types/isomorphic-fetch": "^0.0.34", // 解决fetch兼容性问题
"@types/react-router-dom": "^4.2.7", // 得到react-router-dom的声明文件
"@types/react-router-redux": "^5.0.13",
"dva-core": "^1.4.0", // dva 另一个核心,用于处理数据层
"global": "^4.3.2", // 用于提供全局函数的引用
"history": "^4.6.3", // browserHistory 或者 hashHistory
"invariant": "^2.2.2", // 一个有趣的断言库
"isomorphic-fetch": "^2.2.1",// 方便请求异步的函数,dva 中的 fetch 来源
"react-redux": "^5.0.5", // 提供了一个高阶组件,方便在各处调用 store
"react-router-dom": "^4.1.2", // router4,终于可以像写组件一样写 router 了
"react-router-redux": "5.0.0-alpha.9", // redux 的中间件,在 provider 里可以嵌套 router
"redux": "^3.7.2" // 提供了 store、dispatch、reducer
},
}
引用依赖很好的说明了 dva 的功能:统一 view 层。
export default function (opts = {}) {
// ...初始化 route ,和添加 route 中间件的方法
/**
* 1. 新建 function ,函数内实例化一个 app 对象。
*
*/
const app = core.create(opts, createOpts);
/**
* 2. 新建变量指向该对象希望代理的方法
*
*/
const oldAppStart = app.start;
app.router = router;
/**
* 4. 令 app.start = start,完成对 app 对象的 start 方法的代理。
* @type {[type]}
*/
app.start = start;
return app;
// router 赋值
/**
* 3.1 新建同名方法 start,
*
*/
function start(container) {
// 合法性检测代码
/**
* 3.2 在其中使用 call,指定 oldStart 的调用者为 app。
*/
oldAppStart.call(app);
// 因为有 3.2 的执行才有现在的 store
const store = app._store;
}
}
实现代理模式一定要用到 call 吗?
不一定,看看有没有用箭头函数或者函数里面有没有用到this。call能改变this的指向并且立即执行函数。
前端还有那里会用到 call ?
实际开发讲,因为已经使用了es6的标准,基本和this没有什么打交道的机会了。使用class类型的组件还会用到this.xxx.bind(this)。
// 使用 querySelector 获得 dom
if (isString(container)) {
container = document.querySelector(container);
invariant(
container,
`[app.start] container ${container} not found`,
);
}
// 其他代码
// 实例化 store
oldAppStart.call(app);
const store = app._store;
// export _getProvider for HMR
// ref: https://github.com/dvajs/dva/issues/469
app._getProvider = getProvider.bind(null, store, app);
// If has container, render; else, return react component
// 如果有真实的 dom 对象就把 react 拍进去
if (container) {
render(container, store, app, app._router);
// 热加载在这里
app._plugin.apply('onHmr')(render.bind(null, container, store, app));
} else {
// 否则就生成一个 react ,供外界调用
return getProvider(store, this, this._router);
}
// 使用高阶组件包裹组件
function getProvider(store, app, router) {
return extraProps => (
<Provider store={store}>
{ router({ app, history: app._history, ...extraProps }) }
</Provider>
);
}
// 真正的 react 在这里
function render(container, store, app, router) {
const ReactDOM = require('react-dom'); // eslint-disable-line
ReactDOM.render(React.createElement(getProvider(store, app, router)), container);
}
React.createElement(getProvider(store, app, router)) 怎么理解?
getProvider返回的不单单是一个函数,而是一个无状态的react组件。
Provider 是个什么东西?
代理模式实现的高阶组件。 接收redux生成的store做参数后,通过上下文context将store传递进被代理组件。
connect 是个什么东西?
代理模式实现的高阶组件。 为被代理的组件 从context中获得store
connect()(MyComponent) 时发生了什么?
结论:对于 connect()(MyComponent)
redux是状态管理的库。router是控制页面跳转的库。但两者无法协同工作。换句话说,当路由变化以后,store 无法感知到。
于是便有了 react-router-redux(redux的一个中间件)。
主要监听history的变化:
history.listen(location => analyticsService.track(location.pathname))
const history = opts.history || createHashHistory();
const createOpts = {
// 初始化 react-router-redux 的 router
initialReducer: {
routing,
},
// 初始化 react-router-redux 添加中间件的方法,放在所有中间件最前面
setupMiddlewares(middlewares) {
return [
routerMiddleware(history),
...middlewares,
];
},
// 使用代理模式为 history 对象增加新功能,并赋给 app
setupApp(app) {
app._history = patchHistory(history);
},
// 使用代理模式扩展 history 对象的 listen 方法,添加了一个回调函数做参数并在路由变化是主动调用
function patchHistory(history) {
const oldListen = history.listen;
history.listen = (callback) => {
callback(history.location);
return oldListen.call(history, callback);
};
return history;
}
src/index.js主要实现了dva的view层,同时传递一些初始化的数据给dva-core所实现的model层。
而dva-core主要解决model的问题,包括state的管理,数据的异步加载,订阅-发布模式的实现。
"dependencies": {
"@babel/runtime": "7.0.0-beta.46",
"flatten": "^1.0.2",
"global": "^4.3.2",
"invariant": "^2.2.1",
"is-plain-object": "^2.0.3", // 判断是否是一个对象
"redux": "^3.7.1",
"redux-saga": "^0.16.0", // // 处理异步数据流
"warning": "^3.0.0" // 同样是个断言库,不过输出的是警告
},
export function create(hooksAndOpts = {}, createOpts = {}) {
const {
initialReducer,
setupApp = noop,
} = createOpts;
const plugin = new Plugin();
plugin.use(filterHooks(hooksAndOpts));
const app = {
_models: [
prefixNamespace({ ...dvaModel }),
],
_store: null,
_plugin: plugin,
use: plugin.use.bind(plugin),
model,
start,
};
return app;
// .... 方法的实现
function model(){
// model 方法
}
functoin start(){
// Start 方法
}
}