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 的唯一特殊情况。
我来说一句反对typescript的话
世界上刚有typescript的时候,我就开始关注它了。我很明确,它是一门新语言,只是和javascript是近亲。在它的介绍文档中,我看到private关键字,甚是兴奋。但我开始去用它的时候,发现它的private的骗人的。除了private骗人外,它的所谓类型检查系统也有欺骗性。现在很多人已经叫嚣着,不懂typescript就是不会前端。说实话,除了对typescript不满意,我连对ES标准的#符也不满意。不是我挑剔,是有些东西人会天然不喜欢。
读完这个答案之后,我更加确信,原来自己的反感是有根源的,并非我有问题,而是typescript有问题。有什么问题呢?先来说一些旁的不喜欢点:
- 繁琐的类型定义方式,明明是一门新语言,非要按照js的语法来写,但是又发明一些莫名其妙的新语法
- 类型声明实在受不了,我知道使用 : 是受了其他语言的启发,但是真正好的类型声明很明显要放在变量前面
- 对象属性的类型声明?呵呵,太恶心。: 和 as 能让人写作烦躁10倍。
- any?哎。。。
- 结构类型检查!WTF
- 据说类型检查能减少bug,但是,你怎么能确保你编写的类型本身没有bug?
- 降低效率,用在解决(编写)一个类型问题上的时间,可能够我写完2个需求
- 使代码可读性降低,你需要在阅读代码过程中,跳到类型定义的部分去阅读,阅读完再跳回来来,一去一来,我是谁,在哪里?
- 增加运行难度,据说deno支持直接运行ts,但是实际上,还是先翻译为js后执行
我们来看上面那篇回答中的一个经典案例:
interface A { x: number; } let a: A = { x: 3 } let b: { x: number | string } = a; b.x = "unsound"; let x: number = a.x; a.x.toFixed(0);
恶心死你。你不是静态类型检查吗?给你查,查破了你能告诉我 bug 来自哪里?人生啊,不要相信所谓了“在准确和效率之间找平衡”,忽悠。别的类型系统,之所以成立,是因为别的语言需要先编译,后执行,有健全类型系统,类型声明不用慌,而且类型本身就是运行时的。从我的不成熟的想法来看,不在运行时的类型系统,都是扯虎皮。使用typescript嘛,和语句末尾使用使用;结束一样,技术不到位,有;也避免不了各种错,技术到家,没;照样优雅健壮。
我理想中的js变量类型声明:
int a = 1 bigint b = 299300002390809238n float c = 2.2 bigfloat d = 4.394085943789534809830543l string e = 'xxx' Date date = new Date() Promise p = new Promise(...) // 多层结构的对象,可以在内部属性上独立确定类型 object o = { string name: 'my name', int age: 10, // 用<>表达其内部结构 array<[ object<{ string name, number price, }> ]> books: [ // 纯值,当然,也可以在内部进行类型定义,类型系统自己推演 { name: 'book name', price: 6.6, }, ], } let x = 1 // 相当于 any const y = 'ok' // 不可变 var z = null // 带变量提升的 let
如果一个变量想要发生类型转化。。。不可以的。
int a = 10 string b = a + ''
这样操作是唯一的途径吧。
但是实际上,在运行时,你怎么知道后端接口会返回给你什么?大部分情况下,null 值无法避免。
// 用 type 关键字声明和定义类型 type ResponseType = object<{ int code, object data: { string name, number|null age, // 支持 null array<[ // array 内部可以有多种结构,只要元素满足其中之一,就可以通过类型检查 object<{ string name, number price, }>, object<{ string name, int pages, }>, ]> books, }, ?string error, // ? 开头表示可能不存在,存在的情况下才遵循类型 }>
现在我要使用它:
fetch('/api').then(res => res.json()).then(string (ResponseType data) => { // 如果data的类型检查不通过,直接抛出 TypeError })
看看函数:
function a(int x, int y) int { return x + y } object o = { a(int x, int y) int { return x + y }, } class Some { string _name = 'my name' static get name() string { return this._name } } // 直接使用原生 function 范型(借鉴 python 的 lambda 表达式) function<int, int: int> sum(a, b) { return a + b }
// 声明一个函数类型(借鉴 python 的 lambda 表达式) type func = int, int: int // 遵循将类型放在变量前面的规则,func 是类型名,sum 是函数(变量)名 func sum(a, b) { return a + b }
再搞一个泛型耍耍。我在这篇文章中已经说过了,泛型,实际上就是函数:
// 用 <> => 定义泛型,其中 book<a, b> 中的 book 是泛型的名字,<a, b> 是替代符,=> 后面是返回的形式化结果
type book<a, b> => object<{
a name,
b price,
}>
怎么用?这样子啊:
book<string, number> book = {
name: 'xxx',
price: 12,
}
看看泛型的演化:
// 定义一个泛型,泛型的结果是一个 object 描述,(而非一个类型) type a<x, y> => { x a, y b, } type b<z> => a<z, z> // 直接运行 a<z, z> 将得到的结果作为 b<> 泛型结果 // b<string> 的结果,作为 object<> 的参数 object<b<string>> o = { a: 'xxx', b: 'yyy', }
实际上,我不大推崇这种声明,因为通过这样的声明,我并不能马上看清楚在声明 book 这个变量时,它的结构,我更喜欢:
object<{ string name, number price, }> book
即使这个时候,我并不给 book 赋值,这样声明 book 这个变量我就知道它的内部结构是啥。虽然这样我可能需要写很多重复代码,但是相对来说,理解成本更低。
当然,在函数上,泛型确实还是有好处:
function a(int|string x, int|string y) int|string { return x + y }
这种声明明显不好,因为你怎么知道 x=1, y='a' 这种情况不会发生。所以,这种情况,要使用泛型:
// 通过泛型,实现函数参数和返回值统一类型 type f<T> => T, T: T f<int|string> a(x, y) { return x + y } // 或者通过一个匿名的泛型来简化写作 function<(T => T, T: T)<int|string>> a(x, y) { return x + y }
作为一个特殊结构,<> 内是一个复杂体。因为任何 <> 前面的东西,会被认为是一个范型而被运行,因此 (T => T, T : T) 将会被作为范型,传入 int|string 运行,得到的结果作为 function 范型的参数。
type some = function type some<T> => T, T: T
上面这段代码表示,我们可以直接使用 some 作为类型,也可以使用 some<xxx> 作为类型,some 既是类型,也是范型。
这一套东西,是在运行时做的,这样的定义,岂不是爽歪歪。当然,我依靠 tyshemo 还是有可能把这套东西给实现的。对了,有兴趣可以看下 C# 的类型系统。
-
不敢苟同,ts 的认知吾辈经历了反复的几次变化之后,只能说:真香!
-
whh#922 bigsir 2020-04-22 16:11
-
ts好的原因之一很簡單,弱類型IDE的輔助就是智障,輔助全靠猜,猜又猜不准,垃圾名詞一大串,ts用的多香,直接ts起跳誰還會js ts定義檔來來回回跳,根本就不會去看js了,唯一會坑人的就是ts定義檔寫太爛罷了。沒錯,ts只能保證編譯時期安全,就已經很夠用了,他確實就不保證執行時期類型安全,但是這絕對不是笑話,js寫的定義有的結構全都不清楚,模糊的要死,ts結構定義清晰看一下就明明白白的,定義檔直接寫清楚,還是js code/docs直接玩大家來找碴,二選一,明明白白地,誰比較香呢?
準確就是效率,先刷掉了一大把靜態檢查會產生錯誤,然後才是執行時期的錯誤再另外排除不就好了?
ts本來的任務就是輔助ts,所以在類型系統上本來就會有妥協,所以才會出現混合的類型,所以在實務上還是要做好類型把關,針對多類型混合狀態就做好執行時期檢查本來就是工程師的該做的事情,檢查沒做再把鍋甩改ts沒檢查到,幹嘛呢?#1108 blah 2021-11-16 18:28 -
ts简单?扯淡!就算你有10年java经验,要懂协变、逆变等等一大堆类型推演也是很难的,更别提一个普通程序员。什么?类型推演不重要,在工作中用不到?我只能呵呵了,随便写几个句子,都会得到与想象中不一致的类型判断,因为你根本没理解类型推演的实现逻辑。
除了复杂的类型系统,ts也不能辅助js开发,而是阉割js开发,说是js的超级,实际上只能用js的子集。在大部分情况下,你不仅要在为业务逻辑撰写代码过程中,浪费大量的时间在非业务之外的代码上(当然,收益也是有的,而且是持续性的),全是噪音,而且有的时候,为了解决一个IDE报错问题,你需要花费小半天的时间去搞明白逆变原理,再来思考我要做怎样的体操才能得到我想要的类型检查,说真的,扯淡!
当然,不得不承认在前端领域,ts占有一席之地,有好的地方,但是你要说有绝对地位统治地位,我觉得是瞎扯,es标准一升级,你ts可能就要靠边站,比如你private,我来一个#,你怎么办?你得private #?不是瞎扯淡吗?
我写了一套js的运行时类型检查系统,你可以通过 import { Dict } from 'tyshemo' 这样的形式来使用,虽然运行时类型检查消耗性能,但可以在特定情况下再做。可以说,表达类型更直观。#1110 回复给#1108 否子戈 2021-11-18 12:27 -
Typescript is a piece of fucking shit.#1298 Nichola 2023-12-22 17:19
重新理解 rxjs 的事件流
在做上一期 robust 的时候,受到这篇文章的启示,重新理解了 rxjs 的事件流这个概念。结合响应式编程范式的两个目标,对 rxjs 的理解又有了新的认识。
rxjs 是观察者模式、迭代器模式、函数式编程的结合体。其中,我一直对迭代器模式这个点不是很理解。在阅读上面那篇文章之后,我的思维豁然开朗了。在此之前,我只能强迫性的理解,new Observable 本质上是在决定什么时候进行迭代动作,也就是调用 next。但是,迭代器本身是怎么来的呢?我刚开始默认认为,迭代器需要用户自己构造。但在读完上面那篇文章之后,有了新的见解。rxjs 将一个对象的某个事件(例如 DOM 节点的 click 事件),在时间上的发生,抽象为一个虚拟的数组,它的示意图:
这个虚拟数组,和普通数组不一样,它不是在一开始就确定的,而是随着时间的流逝,不断往数组中新加元素,知道该对象被销毁,该数组也自动被销毁。而往数组中新增元素的过程,就是 next 被调用的时候。你可以想象成,这是一个无限个数坑位的列表,一个 click 发生,一个坑位就被填了,这个填坑位的过程,就是 next。而这个虚拟的数组,就是事件流的抽象表示。
既然是一个数组,那么就可以调用迭代器方法,rxjs 内置了非常多迭代器方法,比如常见的 map, filter, reduce。而 new Observable 本质上就是在构造这个迭代器数组。叫 Observable 很傻,还是叫 Stream 比较直观。