angularjs有什么点可以碾压vue, react之流
这里所指的angularjs是只angular早期的1.x版本。虽然vue, react在热度上已经碾压angularjs几条街,但是在面对web开发(以DOM为中心的开发)中,某些场景下面,angularjs可能比任何框架或库都做得好。这里,主要举例两个点。
Modal弹窗的隐现
现在是单向数据流和immutable的天下,其目的是为了数据流所蕴含的业务流清晰,但代价是代码编写变得复杂。举一个例子,用react写一个弹出框modal,按照单向数据流的范式,必须将modal的隐现通过一个prop来控制,而这个prop必须由外部组件传入,为了控制这个prop,就必须再配合一个回调函数来触发prop的改变。这是对modal组件而言,而对于外层调用modal组件而言,必须自建一个state传给对应的prop,同时,必须自建一个方法作为回调函数传给modal。这里面编程出错的几率远远大于保证业务流正确的几率,也就是成本的提升。因为,你必须在回调函数中,正确处理state的变化值。
而双向数据绑定则完全不需要,传入prop之后,不需要再在回调函数中修改state,双向绑定逻辑会自动修改state。(当然,这基于不同的数据思想,一个是immutable,一个是mutable,各自不同。)虽然基于的范式不同,但是一个modal的显示和隐藏在双向数据绑定范式下,更有利。
表格固定列
Web表格(grid table)是前端交互领域及其复杂的一个场景。其中的一个需求是,固定表格的前n列,剩下的可以左右滑动来查看。在基于 virtual dom 的 vue, react 编程中,自己手写是及其麻烦的,你需要手动在模板中拆分块,在脚本中拆分数据。总之,由于virtual dom的编程范式如此,本身就是要隔绝dom的操作。但是实际上,在前端领域,grid table的操作往往是virtual dom最早提出来的,基于数据变化,只修改小部分dom的需求。但是,实际上最终,virtual dom类编程,在这个点上,反而是最辣鸡的。辣鸡之点不在于性能,而在于编程的复杂。
而使用angularjs则太过便利。首先,angularjs也是基于数据响应更新界面的。其次,angularjs是基于DOM操作进行渲染的。所以,在angularjs中,如果要实现表格固定列的需求,只需要写一个directive,使用 template() 函数来动态返回模板,返回的模板是字符串,于是可以各种骚操作修改字符串来控制html结构,这比vue、react的结构编程快上10倍不止。接下来是对DOM的重新调整,当按照模板结构和样式完成渲染之后,你可能还需要调整列宽度、位置、滚动条是否显示、左右两边每行的点击联动、左右都可滚动(pointer-events: none;)等等操作,在不修改任何数据的情况下,瞬间通过DOM操作完成。而如果这些东西依赖virtual dom去做,可以说极其麻烦,代码量上翻10倍很正常。
除了上述这两个例子外,我在开发中还会碰到其他的一些场景,发现有的时候恨angular是垃圾,有的时候又觉得它diao。总之,在不同的场景下,不同的编程范式所带来的收益是不同的,但是问题点在于,我们无法真正的完全在同一套代码中享受不同范式的好处。这也是前端框架的一个不足之处吧。
react-tyshemo-form 发布
昨晚想到了很晚,睡不着,然后就实现了它,今天就把它发布了。react-tyshemo-form基于tyshemo的model实现响应式,基于react-tyshemo实现react和model的绑定,最终体现为一个Field组件。
<Field model={model} name="field" component={FieldComponent} />
整个包的核心就在这个Field组件上面,它基于我早前在robust中提出的表单范式理论,对于表单视图而言,实际上只需要 value, required, disabled, readonly, hidden, errors 这几个信息,以及一个 onChange 回调函数。所以,这这个 Field 组件的实现上,完全遵照这个理论去做,对于用于展示字段 UI 的组件,只需要接收这些 prop 就够了。当然,为了必要的扩展,Field 也支持一个 extend 属性,用来确保可以传入其他 prop。
以上面这段示例代码为例,<Field />
只是一个驱动器,主要作用是确保 model 上的 field 字段值发生变化时,可以重新渲染该字段。仅此而言,所以使用也超级简单。
领域模型 Domain Model
今天学到的新知识点,领域模型 Domain Model。简单总结一下,什么是领域模型?领域模型有哪些好处?如何在开发中运用领域模型?
什么是领域模型?
有两个领域对领域模型做出解释。
- 管理领域:领域模型是对整个行业的工作模式的抽象总结。
- 软件领域:领域模型是对对象普遍性的最高抽象。
虽然出发点不同,但是实际上表达意思的核心思想是一致的。要理解领域模型,首先看下“领域”的概念。横向比较,domain对应的是range,domain是定义域,range是值域。纵向对比,领域是比专业/职业/业务更高水平的概念,例如我们经常说“机器人领域”“水产品领域”“航空航天领域”。“领域模型”概念的核心思想,就是寻找某一宽泛范围内的行为模式的共性,抽象为可适用于这一范围内所有行为的普遍性原则、规律或方法。
在编程领域怎么去理解?从大的层面讲,电商类应用与OA办公应用是两个领域的应用,它们适用不同的业务领域模型。从小的角度讲,用户鉴权和订单跟踪这两个业务完全不同,适用不同的编程领域模型。这里你可以发现两个点:1)虽然鉴权和订单领域模型不同,但是这两个业务之间可能存在联系,比如是否得到订单信息基于鉴权的结果;2)虽然电商应用和OA应用业务场景不同,但是它们可能都需要鉴权。
所以,从编程角度看,领域模型实际上既要解决对象抽象的问题,也要解决对象间的关系问题,还要解决在什么事件下触发对象关系的转变问题。
领域模型有哪些好处?
这里的好处是指相对于业务模型来说。我们开发过程中,经常基于业务场景去进行设计。而一个应用系统的开发,必须应对业务需求的变化。特别是办公软件这种系统,一个企业使用办公软件,必然遵循企业管理的流程,而流程变更是常有的事,基于业务场景去设计和架构软件,并持续迭代,最终带来的后果就是在多次迭代之后,不得不进行重构。而如果该次重构仍然基于业务模型去重构的话,必然还会经历再一次的重构。因为一个业务模型是无法适应所有业务场景的。所以,一旦现实中的业务需求迭代了,给原有的技术架构造成必须重构的影响时,开发者没有任何回旋余地。
而如果基于领域模型设计软件,那么可能接受的代价更小。在分析业务本身时,不只为了实现业务去设计架构,而是从行业的普遍性出发去设计架构,同时结合自己的业务特征,再在设计好的架构上包上一层。哪一天自身的业务发生变化,往往只需要在上面包的那一层上做调整。在软件整体上,分不同的层“视图层-业务层-数据层”,在每一层中又具体的去划分,比如业务层“领域模型-业务模型-流转模型”。而且在设计之初,每一个领域(鉴权、订单等)都采用行业标准,再在行业标准基础上包一层实现定制。那么这一就可以以最小的成本,把知识沉淀在自己的真实业务代码中。
如何运用领域模型?
以下是我在这篇文章中看到的建议。
- 理解后端领域模型
- 建立前端领域模型
- 分离领域层
- 主导接口约定
- 开发中注意业务含义
- 实时同步
简单讲,就是要建立抽象类,这些类去定义领域对象的属性、方法,而且这些类要有一定的内在联系,可扩展。在视图层可以被很好的使用,并不需要在视图层去撰写业务逻辑。
一些相关话题的文章:
- https://juejin.im/post/5d3926176fb9a07ef161c719
- https://zhuanlan.zhihu.com/p/37904835
- https://juejin.im/post/5b1c71ad6fb9a01e5918398d
- https://zhuanlan.zhihu.com/p/109114670
- https://www.jianshu.com/p/fe45506ea358
- https://zhuanlan.zhihu.com/p/59886663
- https://www.cnblogs.com/luminji/p/3703082.html
另外,刚才在阅读的时候,发现“视图模型”“业务模型”“领域模型”这些概念。现在前端编程实际上大部分是杂糅“视图模型”和“业务模型”,也就是MVVM中的VM那一套东西,真正的MVC还非常少见。
var === undefined vs. typeof var === 'undefined'
全等号 === 已经在我日常开发中占据绝对位置,在我开发中所有判断里面,== 已经被当作奇淫巧技了,不在万不得已的情况下,不会使用,其地位和分号; 等同。所以,现在讨论如何判断一个值是否为 undefined 的时候,我只会用 ===。
现在的问题是,在 var === undefined
vs. typeof var === 'undefined'
这两种判断中,谁更好?先说结论,使用 typeof 的方法更好。最主要的原因有两点:
- var === undefined 的形式,你不能确保 var 被声明过,当 var 没声明过时,程序直接报错,而使用 typeof 可以用来判定一个变量是否声明过,这也是我们常用的 typeof window ... typeof global ... typeof self ... this 这个办法来搞定不同运行时环境下的处理。
- undefined 竟然是 window 的属性,按理来说作为 js 语言的基础类型,提供和 null 一样的关键字应该由语言解释器来做吧,但是在运行时中(浏览器),undefined 和 null 完全是两个层面的东西,null 是内置于解析器的空指针符号,而 undefined 是挂在 window 上的全局变量,竟然是挂在 window 上的变量,那么每次使用 var === undefined 时,实际上会去 window 上读取变量,读取的多了,也就让我们开始遐想有没有办法通过不断调用 undefined 变量使系统崩溃。不过值得庆幸的是,undefined 是不能重新赋值的,undefined = 1 虽然不会报错,但是没效果。而执行 null = 1 则会直接报错。就是这么奇妙。
虽然使用 typeof 要多写好多个字母,但是,抛开其他各种个人偏好问题,上面这两个理由足以让人选择 typeof 的形式来判断 undefined 了。
刚开始注册了codepen,用了一会儿就开始不舒服,咋只能用cdn连入包呢?然后转战codesandbox,这才是我要的呀。直接在线写项目,毫无压力,包管理,在线编译查看效果,一切都非常符合我的口味。顺带,看了 codesandbox 的实现原理,浏览器端实现编译,也是牛皮哄哄的。当然,说了这么多,我主要还是用它来实现 demo 效果,感觉也是嘴上说的好,实际上也是牛刀杀鸡用。
React 状态管理的另一个世界,mutable state 状态管理器 react-tyshemo 发布
在 react 状态管理领域,react-redux 可谓是只手遮天了,基于 flux 思想实现,小巧,immutable 的思想让数据变化可控。但 immutable 所带来的编程代价太大了,如果你要更新一个深层结构的对象的某个节点,写作将会是极其麻烦的一件事,而且还保不准会出错。为了保证 immutable,redux 的 reducer 机制让开发者掉光了头发。于是有了类似 dva、rematch 等这样的项目,这些项目基于 redux 再做了一层封装,让开发者少写了很多 reducer 相关的代码,但是很无奈,immutable 的特性,让开发需要付出更多的精力来控制每一个更新。
再另一个世界,mutable state 其实也非常优秀。知名的 mobx 推出了 mobx-react 和 react-redux 竞争。然而,原本非常优秀的 mobx 却只管把自己的想法强加于人,而忽视了代码写作的便捷性,总之,使用起来虽然不用再为 reducer 头疼,却对组件的侵入和让人很不适应。你需要了解它的概念,特别是基于观察者模式的很多概念,它提供的 api 的形式也很丰富,基于接口的、装饰器的,总之,你在掀开它的魔法盒子时,会忍不住“wo\cao/”,但当你真正在项目中尝试使用它时,确会不由但发出“wo^cao\”,但就在心智折腾上,就让人抓狂,还不知道会产生多少副作用 bug。
在 mutable 数据管理最优秀的,莫过于 vue。无论初识还是长久,都会与 vue 的响应式数据相看两不厌。它简介的用法,以及把基本原理告诉你,让你尽情去修改数据。Mutable 相对于 immutable 的最大好处,就是可以对对象任意节点上的对象进行修改,而无需仔细的把握这个节点在整个 state 的什么位置上,在 redux 的实践中,你可能都已经厌烦了写 ... 来解构对象/数组了,但在 vue 中,根本不需要担心这一个点,直接逮住一个对象,立即修改它的某个属性,完事走人,不需要先构造出一个新的数据,不需要调用某个接口把这个新数据传入进去。虽然 redux 那种每一个状态都是全新的思想很好,但是,你的状态不可能是全新的,每一个被认为是全新的状态,都包含了老状态的一部分(对象节点),而这些引用,可能带来后续的错误。在 react 生态中,你可以使用 immutable.js 来尽可能避免这个问题。
但是在 vue 生态中,用于管理全局状态的 vuex 确非得要引入 mutions, actions 的概念,这都是从 flux 借鉴过来的,而且很奇怪的是,在 mutions 中定义的修改,又要到 actions 中重做一遍。实在是有点自废武功啊。有没有一种方法,可以在 react 中真正享受 vue 式的数据管理?你不妨来试试下面的代码:
function MyComponent(props) { const { one } = props const { name, age, height, married, sex, changeSex, me, book, updateBook } = one return ( <> {!!me && ( <> <span>{me.user_name_zh}, {me.user_position}</span> <br /> </> )} <span>{name}: {age}, {height}, {married ? 'married,' : ''} {sex ? 'F' : 'M'}</span> <br /> <span>book: {book.price || 0}</span> <br /> <button type="button" onClick={() => one.age ++}>grow</button> <button type="button" onClick={() => changeSex(!sex)}>change</button> <button type="button" onClick={() => updateBook({ price: (Math.random() * 100).toFixed(2) })}>update</button> </> ) } const mapToProps = (contexts) => { const { one } = contexts return { one, } } export default connect(mapToProps)(MyComponent)
看这代码啊,和 react-redux 有点像,完全可以理解是不。
在数据开始对组件进行注入的时候,保持和 react-redux 一致的使用效果,这实在是无缝的思想过渡,你觉得从原来写 react-redux 的代码到写这样的代码,会有压力吗?没有,一点都没有。下面来看看这种全局状态管理的真面目:
import React from 'react' import { use, connect } from 'react-tyshemo' class Book {} use({ name: 'one', state: { name: 'one', age: 10, book: new Book(), }, computed: { height() { return this.age * 5 }, }, watch: { age({ value }) { if (value > 22) { this.married = true } }, }, methods: { changeSex(sex) { this.sex = sex }, updateBook(data) { Object.assign(this.book, data) this.dispatch('book') }, }, hooks: { onUse() { fetch('/api/me').then(res => res.json()).then((json) => { const { data } = json this.me = data }) }, }, })
看完什么感受?“wo/cao/” 绝对是抄 vue!连属性名字都和 vue 组件一毛一样(多了一个 hooks)。用一个 use 函数注册一个 state 的 namespace,并且这这个注册定义对象中,传入 state, computed, methods, watch 等来实现状态数据的处理。由于对数据的操作和 vue 是一摸一样,所以,你不用担心 ajax 请求的异步问题了,不需要考虑一大堆 redux 带来的“解决问题带来的问题”。而且,react-tyshemo 只提供几个函数接口,避免 mobx-react 那般复杂。
看上去是不是很舒服呢?
ESModule 和 commonjs module 混用
在一些项目中,使用 webpack 打包,难免会有 ESModule 和 commonjs 混用的情况,当然,全部用 ESModule 是最好的,符合标准。但是,在不得已或偷懒情况下必须混用时,要注意混用时的一些模块逻辑。
babel 会将 ESModule 导入进行处理,也就是说 import 的目标文件是 module.exports 导出的 commonjs 文件是可以的。但是前提是,必须先用 babel 进行编译。我们用 webpack babel-loader 的时候,为了获得 tree shaking 的效果,所以会关闭 modules 选项,这就导致在 webpack 进行打包时,babel 不会编译 ESModule,也就不会优化 import 逻辑,因此,这种情况下 import 的目标文件是 module.exports 时,运行时会报错,因为 webpack 认为 module 是不可写对象,你不能重写 module.exports,但你可以重写 exports。
这个逻辑是对的,webpack 按照标准进行了处理,并且是严格处理,这就要求模块输出者,必须输出 exports.xxx 这样的接口,从而可以通过 webpack 实现与 ESModule 完全对接。但是,很遗憾,由于历史原因,我们不可能把我们项目中全部的 module.exports 全部重新写过,而且对于 module.exports = function 的情况根本无解。
好消息是,我们也是有办法的,ESModule 和 commonjs 是可以混用的。关键就在 require 和 import 的区别。在 webpack 里面,目标文件究竟遵循 ESModule 还是 commonjs,完全由导入语句 require/import 来决定。例如,你的 a.js 是 ESModule 导出模块,但是你在外面用 require 导入,那么它可以很自然的被使用,可以说无缝对接。但是,如果你的 a.js 是 commonjs 导出模块,而外面是 import 进行导入,那就必须格外小心,module.exports 的导出方式不可以,但是 exports.xxx 的方式可以使用,当然,使用 export 导出是最优选择。
最终的结论是,不要用 import 去导入原来的 module.exports 的模块,而是要用 require,这在 webpack 中,你可以把这种操作当作日常操作,或者必须使用 require 的唯一特殊情况。