本系列分三部曲:《框架实现》 《框架使用》 与 《跳出框架看哲学》,这三篇是我对数据流阶段性的总结,正好补充以前过期的文章。前端
本篇是 《框架使用》。node
如今咱们团队也在从新思考数据流的价值,在业务不断发展,业务场景增多时,一个固定的数据流方案可能难以覆盖全部场景,在全部业务里都用得爽。特别在前端数据层很薄的场景下,在数据流治理上花功夫反却是本末倒置。react
业务场景一般很复杂,可是对技术的探索每每只追求理想状况下的效果,因此不少人草草阅读完别人的经验,给本身业务操刀时,会听到一些反对的声音,而实际效果也差强人意。git
因此在阅读文章以前,应该先认识到数据流只是项目中很是微小的一环,并且每一个具体方案都很看场景,就算用对了路子,带来的提效也不必定很明显。github
2017 年 Redux 依然是主流,可能到 18 年仍是。你们吐槽归吐槽,最终活仍是得干,Redux 仍是得用,就算分析出 js 天生不适合函数式,也依然一条路走到黑,由于谁也不知道将来会如何发展,redux 生态虽然用得繁琐,但普适性强,忍一忍,生活也能继续过。typescript
Dob 和 Mobx 相似,也只是数据流中响应式方案的一个分支,思考也是比较理想化的,所以可能也摆脱不了中看不中用的命运,谁叫业务场景那么多呢。redux
不过相对而言,应该算是接地气一些,它既没有要求纯函数式和分离反作用,也没有 cyclejs 那么抽象,只要入门的面向对象,就能够用好。后端
使用 redux 时,不少时候是傻傻分不清要不要将结构化数据拍平,再分别订阅,或者分不清订阅后数据处理应该放在组件上仍是全局。这是由于 redux 破坏了 react 分形设计,在 最近的一次讨论记录 有说到。而许多基于 redux 的分形方案都是 “伪” 分形的,偷偷利用 replaceReducer
作一些动态 reducer 注册,再绑定到全局。安全
讨论理想数据流方案比较痛苦,并且引言里说到,不少业务场景下收益也不大,因此能够考虑结合工程化思惟解决,将组件类型区分开,分为普通组件与业务组件,普通组件不使用数据流,业务组件绑定全局数据流,能够避免纠结。数据结构
使用 Mobx 时,文档告诉咱们它具备依赖追踪、监听等许多能力,但没有好的实践例子作指导,看完了 todoMvc 以为学完了 90%,在项目中实践后发现无从下手。
所谓最佳实践,是基于某种约定或约束,让代码可读性、可维护性更好的方案。约定是活的,不遵照也没事,约束是死的,不遵照就没法运行。约束大部分由框架提供,好比开启严格模式后,禁止在 Action 外修改变量。然而纠结最多的地方仍是在约定上,我在写 dob 框架先后,总结出了一套使用约定,可能仅对这种响应式数据流管用。
使用数据流,第一要作的事情就是管理数据,要解决 Store 放在哪,怎么放的问题。其实还有个前置条件:要不要用 Store 的问题。
首先,最简单的组件确定不须要用数据流。那么组件复杂时,若是数据流自己具备分形功能,那么可用可不用。所谓具备分形功能的数据流,是贴着 react 分形功能,将其包装成任具备分形能力的组件:
import { combineStores, observable, inject, observe } from 'dob'
import { Connect } from 'dob-react'
@observable
class Store { name = 123 }
class Action {
@inject(Store) store: Store
changeName = () => { this.store.name = 456 }
}
const stores = combineStores({ Store, Action })
@Connect(stores)
class App extends React.Component<typeof stores, any> {
render() {
return <div onClick={this.props.Action.changeName}>{this.props.Store.name}</div>
}
}
ReactDOM.render(<App /> , document.getElementById('react-dom'))
复制代码
dob 就是这样的框架,上面例子中,点击文字能够触发刷新,即使根 dom 节点没有 Provider
。这意味着这个组件不论放到任何环境,均可以独立运行,成为任何项目中的一部分。这种组件虽然用了数据流,可是和普通 React 组件彻底无区别,能够放心使用。
若是是伪分形的数据流,可能在 ReactDOM.render
须要特定的 Provider
配合才可以使用,那么这个组件就不具有可迁移能力。若是别人不幸安装了这种组件,就须要在项目根目录安装一个全家桶。
问:虽然数据流+组件具有彻底分形能力,但若此组件对 props 有响应式要求,那仍是有对该数据流框架的隐形依赖。
答:是的,若是组件要求接收的 props 是 observable
化的,以便在其变化时自动 rerender,那当某个环境传递了普通 props,这个组件的部分功能将失效。其实 props 属于 react 的通用链接桥梁,所以组件只应该依赖普通对象的 props,内部能够再对其 observable
化,以具有完备的可迁移能力。
React 虽然能够彻底模块化,但实际项目中模块必定分为通用组件与业务组件,页面模块也能够看成业务组件。复杂的网站由数据驱动比较好,既然是数据驱动,那么能够将业务组件与数据的链接移到顶层管理,通常经过页面顶层包裹 Provider
实现:
import { combineStores, observable, inject, observe } from 'dob'
import { Connect } from 'dob-react'
@observable
class Store { name = 123 }
class Action {
@inject(Store) store: Store
changeName = () => { this.store.name = 456 }
}
const stores = combineStores({ Store, Action })
ReactDOM.render(
<Provider {...store}>
<App />
</Provider>
, document.getElementById('react-dom'))
复制代码
本质上只是改变了 Store 定义的位置,而组件使用方式依然不变:
@Connect
class App extends React.Component<typeof stores, any> {
render() {
return <div onClick={this.props.Action.changeName}>{this.props.Store.name}</div>
}
}
复制代码
有一个区别是 @Connect
不须要带参数了,由于若是全局注册了 Provider
,会默认透传到 Connect
中。与分形相反,这种设计会致使组件没法迁移到其余项目单独运行,但好处是能够在本项目中任意移动。
分形的组件对结构强依赖,只要给定须要的 props 就能够完成功能,而全局数据流的组件几乎能够彻底不依赖结构,全部 props 都从全局 store 获取。
其实说到这里,能够发现这两点是难以合二为一的,咱们能够预先将组件分为业务耦合与非业务耦合两种,让业务耦合的组件依赖全局数据流,让非业务耦合组件保持分形能力。
若是有更好的 Store 管理方式,能够在个人 github 和 知乎 深刻聊聊。
对于 Mvvm 思想的库,Connect 概念不只仅在于注入数据(与 redux 不一样),还会监听数据的变化触发 rerender。那么每一个组件须要 Connect 吗?
从数据流功能来讲,没有用到数据流的组件固然不须要 Connect,但业务组件保持着将来不肯定性(业务不肯定),因此保持每一个业务组件的 Connect 便于后期维护。
并且 Connect 可能还会作其余优化工做,好比 dob 的 Connect 不只会注入数据,完成组件自动 render,还会保证组件的 PureRender
,若是对 dob 原理感兴趣,能够阅读 精读《dob - 框架实现》。
其实个议题只是很是微小的点,不过现实就是讽刺的,不少时候多会纠结在这种小点子上,因此单独花费篇幅说几句。
Store 扁平化有很大缘由是 js 对 immutable 支持力度不够,致使对深层数据修改很是麻烦致使的,虽然 immutable.js
这类库能够经过字符串快速操做,但这种使用方式必然会被不断发展的前端浪潮所淹没,咱们不可能看到 js 标准推荐咱们使用字符串访问对象属性。
经过字符串访问对象属性,和 lodash 的 _.get
相似,不过对于安全访问属性,也已经有 proposal-optional-chaining 的提案在语法层面解决,一样 immutable 的便捷操做也须要一种标准方式完成。实际上不用等待另外一个提案,利用 js 现有能力就能够模拟原生 immutable 支持的效果。
dob-redux 能够经过相似 this.store.articles.push(article)
的 mutable 写法,实现与 react-redux
的对接,内部天然作掉了相似 immutable.set
的事情,感兴趣能够读读个人这篇文章:Redux 使用可变数据结构,介绍了这个黑魔法的实现原理。
有点扯远了,那么数据流扁平化本质解决的是数据格式规范问题。好比 normalizr 就是一种标准数据规范的推动,不少时候咱们都将冗余、或者错误归类的数据存入 Store,那维护性天然比较差,Redux 推崇的应当是正确的数据格式化,而不是一昧追求扁平化。
对于前端数据流很薄的场景,也不是随便处理数据就完事了。还有许多事可作,好比使用 node 微服务对后端数据标准化、封装一些标准格式处理组件,把很薄的数据作成零厚度,业务代码能够对简单的数据流彻底无感知等等。
Redux 天然而然用 action 隔离了反作用与异步,那在只有 action 的 Mvvm 开发模式中,异步须要如何隔离?Mvvm 真的完美解决了 Redux 避而远之的异步问题吗?
在使用 dob 框架时,异步后赋值须要很是当心:
@Action async getUserInfo() {
const userInfo = await fetchUser()
this.store.user.data = userInfo // 严格模式下将会报错,由于脱离了 Action 做用域。
}
复制代码
缘由是 await
只是伪装用同步写异步,当一个 await
开始时,当前函数的栈已经退出,所以后续代码都不在一个 Action
中,因此通常的解法是显示申明 Action
的显示申明大法:
@Action async getUserInfo() {
const userInfo = await fetchUser()
Action(() => {
this.store.user.data = userInfo
})
}
复制代码
这说明了异步须要小心!Redux 将异步隔离到 Reducer
以外很正确,只要涉及到数据流变化的操做是同步的,外面 Action
怎么千奇百怪,Reducer
均可以高枕无忧。
其实 redux 的作法与下面代码相似:
@Action async getUserInfo() { // 类 redux action
const userInfo = await fetchUser()
this.setUserInfo(userInfo)
}
@Action async setUserInfo(userInfo) { // 类 redux reduer
this.store.user.data = userInfo
}
复制代码
因此这是 dob 中对异步的另外一种处理方法,称做隔离大法吧。因此在响应式框架中,显示申明大法与隔离大法均可以解决异步问题,代码也显得更加灵活。
响应式框架的另外一个好处在于能够自动触发,好比自动触发请求、自动触发操做等等。
好比咱们但愿当请求参数改变时,能够自动重发,通常的,在 react 中须要这么申明:
componentWillMount() {
this.fetch({ url: this.props.url, userName: this.props.userName })
}
componentWillReceiveProps(nextProps) {
if (
nextProps.url !== this.props.url ||
nextProps.userName !== this.props.userName
) {
this.fetch({ url: nextProps.url, userName: nextProps.userName })
}
}
复制代码
在 dob 这类框架中,如下代码的功能是等价的:
import { observe } from 'dob'
componentWillMount() {
this.signal = observe(() => {
this.fetch({ url: this.props.url, userName: this.props.userName })
})
}
复制代码
其神奇地方在于,observe
回调函数内用到的变量(observable 后的变量)改变时,会从新执行此回调函数。而 componentWillReceiveProps
内作的判断,实际上是利用 react 的生命周期手工监听变量是否改变,若是改变了就触发请求函数,然而这一系列操做均可以让 observe
函数代劳。
observe
有点像更自动化的 addEventListener
:
document.addEventListener('someThingChanged', this.fetch)
复制代码
因此组件销毁时不要忘了取消监听:
this.signal.unobserve()
复制代码
最近咱们团队也在探索如何更方便的利用这一特性,正在考虑实现一个自动请求库,若是有好的建议,也很是欢迎一块儿交流。
若是你在使用 redux,能够参考 你所不知道的 Typescript 与 Redux 类型优化 优化 typescript 下 redux 类型的推导,若是使用 dob 或 mobx 之类的框架,类型推导就更简单了:
import { combineStores, Connect } from 'dob'
const stores = combineStores({ Store, Action })
@Connect
class Component extends React.PureComponent<typeof stores, any> {
render() {
this.props.Store // 几行代码便得到了完整类型支持
}
}
复制代码
这都得益于响应式数据流是基于面向对象方式操做,能够天然的推导出类型。
复杂的数据流必然存在 Store 与 Action 之间相互引用,比较推荐依赖注入的方式解决,这也是 dob 推崇的良好实践之一。
固然依赖注入不能滥用,好比不要存在循环依赖,虽然手握灵活的语法,但在下手写代码以前,须要对数据流有一套较为完整的规划,好比简单的用户、文章、评论场景,咱们能够这么设计数据流:
分别创建 UserStore
ArticleStore
ReplyStore
:
import { inject } from 'dob'
class UserStore {
users
}
class ReplyStore {
@inject(UserStore) userStore: UserStore
replys // each.user
}
class ArticleStore {
@inject(UserStore) userStore: UserStore
@inject(ReplyStore) replyStore: ReplyStore
articles // each.replys each.user
}
复制代码
每一个评论都涉及到用户信息,因此 ReplyStore
注入了 UserStore
,每一个文章都包含做者与评论信息,因此 ArticleStore
注入了 UserStore
与 ReplyStore
,能够看出 Store 之间依赖关系应当是树形,而不是环形。
最终 Action 对 Store 的操做也是经过注入来完成,而因为 Store 之间已经注入完了,Action 能够只操做对应的 Store,必要的时候再注入额外 Store,并且也不会存在循环依赖:
class UserAction {
@inject(UserStore) userStore: UserStore
}
class ReplyAction {
@inject(ReplyStore) replyStore: ReplyStore
}
class ArticleAction {
@inject(ArticleStore) articleStore: ArticleStore
}
复制代码
最后,不建议在局部 Store 注入全局 Store,或者局部 Action 注入全局 Store,由于这会破坏局部数据流的分形特色,切记保证非业务组件的独立性,把全局绑定交给业务组件处理。
比较优雅的方式,是编写类级别的装饰器,统一捕获 Action 的异常并抛出:
const errorCatch = (errorHandler?: (error?: Error) => void) => (target: any) => {
Object.getOwnPropertyNames(target.prototype).forEach(key => {
const func = target.prototype[key]
target.prototype[key] = async (...args: any[]) => {
try {
await func.apply(this, args)
} catch (error) {
errorHandler && errorHandler(error)
}
}
})
return target
}
const myErrorCatch = errorCatch(error => {
// 上报异常信息 error
})
@myErrorCatch
class ArticleAction {
@inject(ArticleStore) articleStore: ArticleStore
}
复制代码
当任意步骤触发异常,await 以后的代码将中止执行,并将异常上报到前端监控平台,好比咱们内部的 clue 系统。关于异常处理更多信息,能够访问我较早的一篇文章:Callback Promise Generator Async-Await 和异常处理的演进。
准确区分出业务与非业务组件、写代码前先设计数据流的依赖关系、异步时注意分离,就能够解决绝大部分业务场景的问题,实在遇到特殊状况可使用 observe
监听数据变化,由此能够拓展出好比请求自动重发的功能,运用得当能够解决余下比较棘手的特殊需求。
虽然数据流只是项目中很是微小的一环,但若是想让整个项目保持良好的可维护性,须要把各个环节作精致。
这篇文章写于 2017 年最后一天,祝你们元旦快乐!
若是你想参与讨论,请点击这里,每周都有新的主题,每周五发布。