找回密码
 会员注册
查看: 19|回复: 0

一文彻底搞懂DvaJS原理

[复制链接]

17

主题

0

回帖

52

积分

注册会员

积分
52
发表于 2024-9-30 10:47:41 | 显示全部楼层 |阅读模式
Dva 是什么dva 首先是一个基于redux[1]和redux-saga[2]的数据流方案,然后为了简化开发体验,dva 还额外内置了react-router[3]和fetch[4],所以也可以理解为一个轻量级的应用框架。Dva 解决的问题经过一段时间的自学或培训,大家应该都能理解 redux 的概念,并认可这种数据流的控制可以让应用更可控,以及让逻辑更清晰。但随之而来通常会有这样的疑问:概念太多,并且 reducer, saga, action 都是分离的(分文件)。文件切换问题。redux 的项目通常要分 reducer, action, saga, component 等等,他们的分目录存放造成的文件切换成本较大。不便于组织业务模型 (或者叫 domain model) 。比如我们写了一个 userlist 之后,要写一个 productlist,需要复制很多文件。saga 创建麻烦,每监听一个 action 都需要走 fork -> watcher -> worker 的流程entry 创建麻烦。可以看下这个redux entry[5]的例子,除了 redux store 的创建,中间件的配置,路由的初始化,Provider 的 store 的绑定,saga 的初始化,还要处理 reducer, component, saga 的 HMR 。这就是真实的项目应用 redux 的例子,看起来比较复杂。Dva 的优势易学易用,仅有 6 个 api,对 redux 用户尤其友好,配合 umi 使用[6]后更是降低为 0 APIelm 概念,通过 reducers, effects 和 subscriptions 组织 model插件机制,比如dva-loading[7]可以自动处理 loading 状态,不用一遍遍地写 showLoading 和 hideLoading支持 HMR,基于babel-plugin-dva-hmr[8]实现 components、routes 和 models 的 HMRDva 的劣势未来不确定性高。dva\@3 前年提出计划后,官方几乎不再维护[9]。对于绝大多数不是特别复杂的场景来说,目前可以被 Hooks 取代Dva 的适用场景业务场景:组件间通信多,业务复杂,需要引入状态管理的项目技术场景:使用 React Class Component 写的项目Dva 核心概念基于 Redux 理念的数据流向。用户的交互或浏览器行为通过 dispatch 发起一个 action,如果是同步行为会直接通过 Reducers 改变 State,如果是异步行为(可以称为副作用)会先触发 Effects 然后流向 Reducers 最终改变 State。基于 Redux 的基本概念。包括:State 数据,通常为一个 JavaScript 对象,操作的时候每次都要当作不可变数据(immutable data)来对待,保证每次都是全新对象,没有引用关系,这样才能保证 State 的独立性,便于测试和追踪变化。Action 行为,一个普通 JavaScript 对象,它是改变 State 的唯一途径。dispatch,一个用于触发 action 改变 State 的函数。Reducer 描述如何改变数据的纯函数,接受两个参数:已有结果和 action 传入的数据,通过运算得到新的 state。Effects(Side Effects) 副作用,常见的表现为异步操作。dva 为了控制副作用的操作,底层引入了redux-sagas[10]做异步流程控制,由于采用了generator 的相关概念[11],所以将异步转成同步写法,从而将 effects 转为纯函数。Connect 一个函数,绑定 State 到 View其他概念Subscription,订阅,从源头获取数据,然后根据条件 dispatch 需要的 action,概念来源于elm[12]。数据源可以是当前的时间、服务器的 websocket 连接、keyboard 输入、geolocation 变化、history 路由变化等等。Router,前端路由,dva 实例提供了 router 方法来控制路由,使用的是react-router[13]。Route Components,跟数据逻辑无关的组件。通常需要 connect Model 的组件都是 Route Components,组织在/routes/目录下,而/components/目录下则是纯组件(Presentational Components,详见组件设计方法[14])Dva 应用最简结构不带 Modelimportdvafrom'dva';constApp=()=>Hellodva;//创建应用constapp=dva();//注册视图app.router(()=>);//启动应用app.start('#root');带 Model//创建应用constapp=dva();app.use(createLoading())//使用插件//注册Modelapp.model({namespace:'count',state:0,reducers:{add(state){returnstate+1},},effects:{*addAfter1Second(action,{call,put}){yieldcall(delay,1000);yieldput({type:'add'});},},});//注册视图app.router(()=>);//启动应用app.start('#root');Dva底层原理和部分关键实现背景介绍整个 dva 项目使用 lerna 管理的,在每个 package 的 package.json 中找到模块对应的入口文件,然后查看对应源码。dva 是个函数,返回一了个 app 的对象。目前 dva 的源码核心部分包含两部分,dva 和 dva-core。前者用高阶组件 React-redux 实现了 view 层,后者是用 redux-saga 解决了 model 层。dva[15]dva 做了三件比较重要的事情:代理 router 和 start 方法,实例化 app 对象调用 dva-core 的 start 方法,同时渲染视图使用 react-redux 完成了 react 到 redux 的连接。//dva/src/index.jsexportdefaultfunction(opts={}){//1.使用connect-react-router和history初始化router和history//通过添加redux的中间件react-redux-router,强化了history对象的功能consthistory=opts.history||createHashHistory();constcreateOpts={initialReducer:{router:connectRouter(history),},setupMiddlewares(middlewares){return[routerMiddleware(history),...middlewares];},setupApp(app){app._history=patchHistory(history);},};// 2. 调用 dva-core 里的 create 方法,函数内实例化一个 app 对象。constapp=create(opts,createOpts);constoldAppStart=app.start;//3.用自定义的router和start方法代理app.router=router;app.start=start;returnapp;//3.1绑定用户传递的router到app._routerfunctionrouter(router){invariant(isFunction(router),`[app.router]routershouldbefunction,butgot${typeofrouter}`,);app._router=router;}//3.2调用dva-core的start方法,并渲染视图functionstart(container){//对container做一系列检查,并根据container找到对应的DOM节点if(!app._store){oldAppStart.call(app);}conststore=app._store;//为HMR暴露_getProvider接口//ref:https://github.com/dvajs/dva/issues/469app._getProvider=getProvider.bind(null,store,app);//渲染视图if(container){render(container,store,app,app._router);app._plugin.apply('onHmr')(render.bind(null,container,store,app));}else{returngetProvider(store,this,this._router);}}}functiongetProvider(store,app,router){constDvaRoot=extraProps=>( {router({app,history:app._history,...extraProps})});returnDvaRoot;}functionrender(container,store,app,router){constReactDOM=require('react-dom');//eslint-disable-lineReactDOM.render(React.createElement(getProvider(store,app,router)),container);}我们同时可以发现 app 是通过 create(opts, createOpts)进行初始化的,其中 opts 是暴露给使用者的配置,createOpts 是暴露给开发者的配置,真实的 create 方法在 dva-core 中实现dva-core[16]dva-core 则完成了核心功能:通过 create 方法完成 app 实例的构造,并暴露 use、model 和 start 三个接口通过 start 方法完成store 的初始化models 和 effects 的封装,收集并运行 sagas运行所有的 model.subscriptions暴露 app.model、app.unmodel、app.replaceModel 三个接口dva-core create作用: 完成 app 实例的构造,并暴露 use、model 和 start 三个接口//dva-core/src/index.jsconstdvaModel={namespace:'@@dva',state:0,reducers:{UPDATE(state){returnstate+1;},},};exportfunctioncreate(hooksAndOpts={},createOpts={}){const{initialReducer,setupApp=noop}=createOpts;//在dva/index.js中构造了createOpts对象constplugin=newPlugin();//dva-core中的插件机制,每个实例化的dva对象都包含一个plugin对象plugin.use(filterHooks(hooksAndOpts));//将dva(opts)构造参数opts上与hooks相关的属性转换成一个插件constapp={_models:[prefixNamespace({...dvaModel})],_store:null,_plugin:plugin,use:plugin.use.bind(plugin),//暴露的use方法,方便编写自定义插件model,//暴露的model方法,用于注册modelstart,//原本的start方法,在应用渲染到DOM节点时通过oldStart调用};returnapp;}dva-core start作用:封装models 和 effects ,收集并运行 sagas完成store 的初始化运行所有的model.subscriptions暴露app.model、app.unmodel、app.replaceModel三个接口functionstart(){constsagaMiddleware=createSagaMiddleware();constpromiseMiddleware=createPromiseMiddleware(app);app._getSaga=getSaga.bind(null);constsagas=[];constreducers={...initialReducer};for(constmofapp._models){//把每个model合并为一个reducer,key是namespace的值,value是reducer函数reducers[m.namespace]=getReducer(m.reducers,m.state,plugin._handleActions);if(m.effects){//收集每个effects到sagas数组sagas.push(app._getSaga(m.effects,m,onError,plugin.get('onEffect'),hooksAndOpts));}}//初始化Storeapp._store=createStore({reducers:createReducer(),initialState:hooksAndOpts.initialState||{},plugin,createOpts,sagaMiddleware,promiseMiddleware,});conststore=app._store;//Extendstorestore.runSaga=sagaMiddleware.run;store.asyncReducers={};//Executelistenerswhenstateischangedconstlisteners=plugin.get('onStateChange');for(constlisteneroflisteners){store.subscribe(()=>{listener(store.getState());});}//Runsagas,调用Redux-Saga的createSagaMiddleware创建saga中间件,调用中间件的run方法所有收集起来的异步方法//run方法监听每一个副作用action,当action发生的时候,执行对应的sagasagas.forEach(sagaMiddleware.run);//SetupappsetupApp(app);//运行subscriptionsconstunlisteners={};for(constmodelofthis._models){if(model.subscriptions){unlisteners[model.namespace]=runSubscription(model.subscriptions,model,app,onError);}}//暴露三个Model相关的接口,Setupapp.modelandapp.unmodelapp.model=injectModel.bind(app,createReducer,onError,unlisteners);app.unmodel=unmodel.bind(app,createReducer,reducers,unlisteners);app.replaceModel=replaceModel.bind(app,createReducer,reducers,unlisteners,onError);/***Createglobalreducerforredux.**@returns{Object}*/functioncreateReducer(){returnreducerEnhancer(combineReducers({...reducers,...extraReducers,...(app._storeapp._store.asyncReducers:{}),}),);}}}路由在前面的 dva.start 方法中我们看到了 createOpts,并了解到在 dva-core 的 start 中的不同时机调用了对应方法。import*asrouterReduxfrom'connected-react-router';const{connectRouter,routerMiddleware}=routerRedux;constcreateOpts={initialReducer:{router:connectRouter(history),},setupMiddlewares(middlewares){return[routerMiddleware(history),...middlewares];},setupApp(app){app._history=patchHistory(history);},};其中 initialReducer 和 setupMiddlewares 在初始化 store 时调用,然后才调用 setupApp可以看见针对 router 相关的 reducer 和中间件配置,其中 connectRouter 和 routerMiddleware 均使用了 connected-react-router 这个库,其主要思路是:把路由跳转也当做了一种特殊的 action。Dva 与 React、React-Redux、Redux-Saga 之间的差异原生 React按照 React 官方指导意见, 如果多个 Component 之间要发生交互, 那么状态(即: 数据)就维护在这些 Component 的最小公约父节点上, 也即是以及 本身不维持任何 state, 完全由父节点 传入 props 以决定其展现, 是一个纯函数的存在形式, 即: Pure ComponentReact-Redux与上图相比, 几个明显的改进点:状态及页面逻辑从 里面抽取出来, 成为独立的 store, 页面逻辑就是 reducer及都是 Pure Component, 通过 connect 方法可以很方便地给它俩加一层 wrapper 从而建立起与 store 的联系: 可以通过 dispatch 向 store 注入 action, 促使 store 的状态进行变化, 同时又订阅了 store 的状态变化, 一旦状态变化, 被 connect 的组件也随之刷新使用 dispatch 往 store 发送 action 的这个过程是可以被拦截的, 自然而然地就可以在这里增加各种 Middleware, 实现各种自定义功能, eg: logging这样一来, 各个部分各司其职, 耦合度更低, 复用度更高, 扩展性更好。Redux-Saga因为我们可以使用 Middleware 拦截 action, 这样一来异步的网络操作也就很方便了, 做成一个 Middleware 就行了, 这里使用 redux-saga 这个类库, 举个栗子:点击创建 Todo 的按钮, 发起一个 type == addTodo 的 actionsaga 拦截这个 action, 发起 http 请求, 如果请求成功, 则继续向 reducer 发一个 type == addTodoSucc 的 action, 提示创建成功, 反之则发送 type == addTodoFail 的 action 即可Dva有了前面三步的铺垫, Dva 的出现也就水到渠成了, 正如 Dva 官网所言, Dva 是基于 React + Redux + Saga 的最佳实践, 对于提升编码体验有三点贡献:把 store 及 saga 统一为一个 model 的概念, 写在一个 js 文件里面增加了一个 Subscriptions, 用于收集其他来源的 action, 比如键盘操作等model 写法很简约, 类似于 DSL(领域特定语言),可以提升编程的沉浸感,进而提升效率约定大于配置app.model({namespace:'count',state:{record:0,current:0,},reducers:{add(state){constnewCurrent=state.current+1;return{...state,record:newCurrent>state.recordnewCurrent:state.record,current:newCurrent,};},minus(state){return{...state,current:state.current-1};},},effects:{*add(action,{call,put}){yieldcall(delay,1000);yieldput({type:'minus'});},},subscriptions:{keyboardWatcher({dispatch}){key('+up,ctrl+up',()=>{dispatch({type:'add'})});},},});Dva 背后值得学习的思想Dva 的 api 参考了choo[17],概念来自于 elm。Choo 的理念:编程应该是有趣且轻松的,API 要看上去简单易用。We believe programming should be fun and light, not stern and stressful. It's cool to be cute; using serious words without explaining them doesn't make for better results - if anything it scares people off. We don't want to be scary, we want to be nice and fun, and thencasually_be the best choice around._Real casually.We believe frameworks should be disposable, and components recyclable. We don't want a web where walled gardens jealously compete with one another. By making the DOM the lowest common denominator, switching from one framework to another becomes frictionless. Choo is modest in its design; we don't believe it will be top of the class forever, so we've made it as easy to toss out as it is to pick up.We don't believe that bigger is better. Big APIs, large complexities, long files - we see them as omens of impending userland complexity. We want everyone on a team, no matter the size, to fully understand how an application is laid out. And once an application is built, we want it to be small, performant and easy to reason about. All of which makes for easy to debug code, better results and super smiley faces.来自 Elm 的概念:Subscription,订阅,从源头获取数据,数据源可以是当前的时间、服务器的 websocket 连接、keyboard 输入、geolocation 变化、history 路由变化等等。参考资料[1]redux: https://github.com/reduxjs/redux[2]redux-saga: https://github.com/redux-saga/redux-saga[3]react-router: https://github.com/ReactTraining/react-router[4]fetch: https://github.com/github/fetch[5]redux entry: https://github.com/ant-design/antd-init/blob/master/boilerplates/redux/src/entries/index.js[6]配合 umi 使用: https://umijs.org/guide/with-dva.html[7]dva-loading: https://github.com/dvajs/dva/tree/master/packages/dva-loading[8]babel-plugin-dva-hmr: https://github.com/dvajs/babel-plugin-dva-hmr[9]dva@3 前年提出计划后,官方几乎不再维护: https://github.com/dvajs/dva/issues/2208[10]redux-sagas: http://superraytin.github.io/redux-saga-in-chinese[11]generator 的相关概念: http://www.ruanyifeng.com/blog/2015/04/generator.html[12]elm: https://elm-lang.org/news/farewell-to-frp[13]react-router: https://github.com/reactjs/react-router[14]组件设计方法: https://github.com/dvajs/dva-docs/blob/master/v1/zh-cn/tutorial/04-%E7%BB%84%E4%BB%B6%E8%AE%BE%E8%AE%A1%E6%96%B9%E6%B3%95.md[15]dva: https://github.com/dvajs/dva/blob/master/packages/dva/src/index.js[16]dva-core: https://github.com/dvajs/dva/blob/master/packages/dva-core/src/index.js[17]choo: https://github.com/choojs/choo[18]Why dva and what's dva: https://github.com/dvajs/dva/issues/1[19]支付宝前端应用架构的发展和选择: https://www.github.com/sorrycc/blog/issues/6[20]React + Redux 最佳实践: https://github.com/sorrycc/blog/issues/1[21]Dva 概念: https://dvajs.com/guide/concepts.html#%E6%95%B0%E6%8D%AE%E6%B5%81%E5%90%91[22]Dva 入门课: https://dvajs.com/guide/introduce-class.html#react-%E6%B2%A1%E6%9C%89%E8%A7%A3%E5%86%B3%E7%9A%84%E9%97%AE%E9%A2%98[23]Dva 源码解析: https://dvajs.com/guide/source-code-explore.html#%E9%9A%90%E8%97%8F%E5%9C%A8-package-json-%E9%87%8C%E7%9A%84%E7%A7%98%E5%AF%86[24]Dva 源码实现: https://www.yuque.com/lulongwen/react/ixolgo[25]Dva 源码分析: https://www.shymean.com/article/dva%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 会员注册

本版积分规则

QQ|手机版|心飞设计-版权所有:微度网络信息技术服务中心 ( 鲁ICP备17032091号-12 )|网站地图

GMT+8, 2025-1-15 14:31 , Processed in 0.556682 second(s), 25 queries .

Powered by Discuz! X3.5

© 2001-2025 Discuz! Team.

快速回复 返回顶部 返回列表