logo

低代码的“先进性”在哪?

2023-03-17

于我个人而言,很看低代码(含无代码)的未来,但现在业界普遍在实践的低代码却很难直说这就是未来。

但是最近在搞响应式开发框架的过程中突然想到的

对比于代码研发,低代码需要的门槛是一点都没少,但却没有带来实质性的改变,因为很多的低代码产品做的最多的事情就是加可视化编辑器,其核心主流思想内并没有跟编程有实质性的差别,

对于编程来说,编译器 和 执行环境,都面对的是终态的代码,无法感知到过程,所以代码框架所需要的逻辑都是得自己准备的,通常有2个时机:

  • 编译期
  • 运行时

比如svelte在编译期设定变量和view的响应函数,vue在运行时建立订阅关系,react也是运行时进行diff再渲染

这种需要自己全包的特性,造成了框架的复杂性,比如vue需要给所有的数据都做一层Proxy封装,每次的set/get都检查是否存在依赖Map

如果有一种方法,在运行前就能获得所有的依赖关系,通过静态注入的方式,直接注入view层,当一个变量改变的时候用修改方法来区分:比如 change 和 changeAndNotify。同时view框架可以只做到最小的颗粒度功能:渲染 和 重新渲染,就像react

不过在实际编程工作里这几乎不可能实现,因为开发人员各有不同,导致代码有各种风格写法,简单来说就是代码范式是不收敛的

但是如果转入低代码系统来思考, 这件事就具备可行性,因为低代码就意味着输入的约束,范式的收敛。当输入是可枚举的时候,那就意味着可静态分析,同时可以在用户输入的过程中就建立

低代码来说,由于是编辑,编译,运行的一体化,是可以充分关注到开发过程,比传统的开发代码的编译,运行,多了一个维度,那就多了一个可优化的操作空间

Y

知识体系的抽象

2023-02-27

背景

每个人的天花板是由接触信息的输入所决定的,其次是在天花板的基础上,对输入信息的整理,消化,形成自己知识体系内的一部分,才算是学到了“知识”

所以如何对这个学习知识的过程进行建模,从而实现有规可循的学习路径

初步阶段

最开始的阶段是从“知识图谱”相关的技术作为学习入口,用于以符号形式描述物理世界中的概念及其相互关系,希望从这个角度切入来构建最基础的知识模型

知识图谱的最基础的抽象单元就是三元组,三元组是由实体(Entity)、属性(Attribute)和关系(Relation)组成的,形式为<实体,关系,实体>或<实体,属性,属性值>

比如 <体育,包含,篮球>

但是光有三元组不够,过于底层了。如果要面向“工程师”,需要提供更具象的抽象概念

工程师的知识体系结构

这其中最难的是,有一种模糊的感觉可以感知到所谓知识体系应该是树状的,那么在树的顶点应该是什么呢?

其次既然面向的是工程师,那么工程师最关注的核心是什么?

都是“问题”

为了解决问题,就需要“解决方案”,但是方案不能凭空产生,需要工具,需要人,所以“知识”是解决问题的一种工具,得到初步的结论: 工程师的知识体系:问题 -> 方案 -> 工具(知识)-> 嵌套工具

用三元组表示:

  • <问题,解法,方案>
  • <方案,实现,工具>
  • <工具,依赖,工具>
Y

设计面向未来的组件库

2022-01-16

前言

在我不长的职业经历中,经历了多种多样的业务场景,涉及多种技术栈和框架

  • B端前台和中后台,BI平台
  • C端小程序,H5 和 运营工具
  • 通用技术基建,低代码,页面搭建

在以上的业务场景里,我意识到一个问题:一个小程序 Button,中后台 Button,H5 Button 它们并没有什么本质的不同,最多换个皮即可,但在现实中它们却完全来自于3个不同的组件库。

这问题困扰着我的,但似乎一直没有解决方案,即时是近来诞生的新组件库似乎还是在原地打转,没有新的突破,是太难还是根本无解 ?

最后的契机是在《web需求结构化》的探索中,逐渐发现也许是有解的,决定开始探索实践这样一个 web研发体系,并产出一个可用的组件库

组件库在前端工程架构中属于偏底层的位置(图),通过讨论组件库可以更好的说清楚问题,展示解决思路的推演过程,最终到达预期的目标。

重复造轮子

现在的开源组件库,中后台的各种design,移动端的各种mobile,小程序的各种mini,已经多如牛毛,为什么还得再造一个轮子呢?

首先上述的开源库很明显的是有自身的一些局限性:

  • 问题域太小,不适用于更大更通用的应用场景
  • 代码是超高内聚的,用户只能通过props 和 slot进行有限的配置,无法进行二次拓展。

由于上述的原因,这些库通常也不跨端和框架。

除此之外,业界还有另外一波更下沉的探索,比如各类headless库, hooks库

  • Radix UI
  • Reach UI
  • Headless UI
  • Downshift
  • React-aria

总结他们的共同特点是都只做最基本的事情,包含:

  • 布局结构
  • 状态逻辑

大家都认为一个组件库至少要包含上述元素,将其它的部分交给用户拓展,但这样的问题是:

  • 布局结构和状态逻辑依然是不可拓展的
    • 可拓展的部分限制在结构和状态逻辑之外
  • 用户按自己习惯开发出的组件依然是一个黑盒组件,后续依然是不可拓展的

说明类headless方案只减少了一层抽象,并且依然未提供一个有效的研发指导,让人能开发出一个具备高拓展的组件

最好的组件库

由于应用场景各有不同,没有统一的衡量标准,所以只有最合适的,没有最好的。

如果能提供一个组件库,能满足基础需求,并提供足够的定制拓展能力,经过用户二次开发之后,理论上这是一个称得上是最好的组件库

所以一个最好的组件库应该具备2个特点:

  • 能提供满足基本需求的原子组件
  • 体系化的拓展能力
    • 能够组合出新组件
    • 能充分覆写原子组件
    • 产生的新组件依然具备同样的拓展能力

思路

重点是不再是黑盒,能够让用户在编辑态或运行态就能获取到组件内的组成成分,那么整体的组件编写风格就会偏向声明式范式,同时也要为这种范式,提供统一的覆写规则。

首先是要突破现有组件架构的黑盒,让组件开发者能够在编译时或运行时获取到组件的内部信息。

现状的函数组件的问题,以React为例:

// JSX
function MyComponent () {
  const [state] = useState('')
  return <div id={state} ></div> // React.createElement('div')
}

在不改变现有代码的情况下,

  • 受限于编程语言,无法获取到函数内的局部变量的引用 (指针)
  • 受限于React,只能拿到经过构建的 React.Element 顶部接节点,无法获取原始的meta信息

为了避开框架和语言的限制,所以要稍微的调整下组件代码组织方式

整体思路是借鉴IOC,开发者只负责各个层的定义,将组件的逻辑流程交给runtime控制,再通过runtime拼装成完整的组件

所以整个实施思路就是:

  • 组件库要做的
    • 抽象一个runtime,负责控制组件的整个流程
    • 按照视图 / 逻辑 / 样式 为主的抽象进行分层
      • 组件包含拓展的逻辑,再加一个 “拓展层”
  • 用户要做的
    • 就尽可能“定义”每层抽象的代码

demo示例:

function logic () {
  const [state] = useState('')
  return { state } 
}

function view () {
  const { state } = useLogic(); 

  // 用JSON 描述视图结构的原子信息,不再是 React.createElement
  return {
    type: 'div',
    id: state,
    children: []
  } 
}

// 拆分后,在通过工厂函数拼装成一个函数组件
const MyComponent = createRuntime(logic).with(view)

进一步的,在有了中间抽象之后,剩下的拓展思路就比较清晰了,就是在runtime的内部对每个分层都提供对应的override函数

整体流程如下:

image.png

其中关于对 视图层的分层和拓展,可以参考前面2个视图文章

1.关于逻辑

相比HTML(JSX)在view层的统治力,逻辑的表达是非常多样化的。在现代图灵机抽象上,编程中的逻辑只围绕2个点:

  • 数据
  • 指令

编程语言的发展历史里已经总结了了非常多的编程范式,规范来更好的解决这些逻辑问题。

站在前人的肩膀上,我们无需自己凭空创造,只需要从历史里逐个找到“最适合”拓展的范式即可。这块值得详细展开,下个文章见

写在最后

本文主要提供一个启发思路,更好的范式和组件架构我也在持续探索和完善中。截止目前,通过上面的思路已经实现了一些基础组件的demo,

如果你觉得有用,欢迎点个 start 和交流

polymita(DEMO版): https://github.com/zhou-yg/polymita

Y

可定制视图组件

2023-01-05

现状

image.png

https://ant.design/components/input-cn/

antd 的 input ,一个非常常用的组件。它是组合了 Input 和 Button的组件

但在组合的时候写死了一个逻辑,那就是Button必须要在 Input的addonAfter 后面

https://github.com/react-component/input/blob/master/src/BaseInput.tsx#L139

image.png

这种写死的就会容易导致抽象不足,除非改代码否则就无法适应下面的使用场景

比如给search-input增加addonAfter

https://codesandbox.io/s/sou-suo-kuang-antd-5-1-2-forked-wl0hsy?file=/demo.tsx

image.png

要维持通用性,

  • 对于组件开发者,需要额外的开发抽象来支持
    • 新增配置项,专门用来配置前后顺序配置
    • 新增slot,让用户手动传入右侧的结构
  • 对于组件用户
    • hack样式
    • 基于Input和Button重新封装一下,调换一下数组元素的顺序
    • fork input ,修改下内部代码

总之现行框架里实现的方法也有很多种,实施起来的特点都需要break change<br />但其根本原因在于 类似antd的组件库 和 React框架 其内部对于开发者是一个黑盒,无法深入的定制

一些解法思路

通过参考开源界的一些做法,可以总结出大致的思路

  • 组件细粒度拆分
    • 代表: radix
    • 这个思路不解决问题,只是降低碰到问题的概率,通过降低封装性,提升组件的可复用性
  • 逻辑分层,“视图结构”交给用户
    • 代表:react-aria 等各类hooks库
    • 将视图交给彻底交给用户,消灭了导致问题的“视图结构”,但还是没解决“视图结构”的拓展问题

所幸在Web里已经提供足够的思路,就是 DOM API,可以精确的完成对于结构的修改,但是直接在React(或Vue)组件中使用DOM API 无疑是一场灾难,因为这类基于virtual dom 的框架是在渲染时才生成真实DOM,

Component Construtor (jsx) -> Virtual DOM ( react) -> DOM ( html)

如果是渲染完成后才进行 DOM API 修改,会引起页面的重绘,严重拉低体验。所以,合适的时机只能是在 jsx 之后, virtual dom 之前,将代码中写好的html结构进行拓展

JSX

很多react开发者容易将JSX跟react框架本身进行绑定,但完全可以从另一种视角来看待JSX:

  • 将JSX视作一种描述结构树的语言,入参是JSON, 生成 JSON Tree
  • React是存渲染用框架,接收一个JSON Tree,输出的是 DOM Tree

通过修改JSON Tree 实现视图结构的拓展,再调用 React.createElement 进行页面渲染

image.png

如何处理逻辑代码

一个组件除了视图部分,还有逻辑代码:逻辑描述,视图映射

组件的逻辑描述代码会包含:状态流转,事件响应,数据相关,不同于JSX的通用性,逻辑代码的实现往往会不可避免的跟渲染框架耦合

  • react 对应 react-hooks
  • vue 对应 composition-api

所以当视图部分被单独拆分出去时,需要将逻辑代码重新结合回来,结合的方式通常有2种:

  • HOC,嵌套调用
  • 组合函数

示例代码

// 用这个代替 react.createElement,不直接产生
import { createElement } from 'react-json'

function loigc () {
  const name = useState('hello')
  return {
	  name,
  }
}
function view (p: { name: string }) {
  return (
    <div>
      {p.name}
    </div>
  )
}
function addSpan (jsx) {
  jsx.query('div').appendChild(<span>hello</span)
}
export default createComponent(logic).render(view).override(addSpan)

新的问题

即时做到了代码分层也仅仅只是满足了“视图结构可拓展”的这个要求,很快就会碰到新的问题,最典型的2个:

  • 渲染性能问题
    • 在运行时加了个新JSON中间层,相当于每次re-render前需要遍历当前视图树,并执行override,造成了重复的性能开销
  • override风险
    • override的函数是严格依赖基础的视图结构,当下层组件的视图结构调整了,那么上层组件的拓展逻辑就会失效了,引发不预知的错误
Y

design-pattern

2022-11-23

现状和问题

长年的UI 开发,仿佛让人意识到 视觉样式是有一定的内在规律的,而且这个规律往往跟 布局结构,数据状态的有一定的联系,但似乎又不是特别紧密,是在design-token 基础之上的更高级抽象

现在很多的组件库的组件,都会在提供相应的props 来配置视觉,实现的方式是 提供场景语义或者直接的样式属性 比如 primary, font, color 相关,但是对于程序来说,在额外说明的情况下,无法识别出哪些 prop 其实属于视觉相关的,哪些是逻辑相关的

场景

由状态 驱动 出 视图样式,这属于“视觉逻辑”,即通过总结组件的状态,组件的语义构成,即很多组件可能是 layout结构不同,但他们在视觉上的语义结构是一致的

  • Button
    • 视觉结构:
      • 背景块
      • 边框
      • 文字
  • Checkbox
    • 视觉结构:
      • 背景块
      • 边框
      • 文字(勾的颜色)

对于这两种结构,在就可以使用一种设计模式(包含了3者的),对这些视觉元素进行统一的设置

  • 对应pattern
    • 状态入参:
      • 选中,hover,disable 等
    • 输出:
      • 背景:{ 背景色,透明度 }
      • 边框:{ 颜色,宽度 }
      • 文字:{ 大小,颜色 }

注意pattern的边界,它具备是纯粹的视觉逻辑,不影响布局结构,只对应前端的一小部分css<br />这样的解耦的好处在于:当组件layout结构发生了变化或修改时,只需要调整语义的部分,不再需要调整视觉逻辑。这个对于一个支持无限可拓展的组件库来说非常重要,这意味着即使用户完全修改了 旧组件的结构,也依然能保持视觉逻辑的正确应用。

缺点则是对于开发者 或 设计者来说,需要在编码或可视化编辑过程中,增加额外的视觉语义声明

视觉语义

是另一套基于 layout 结构的一种标记语言,建立 layout 到 vision 的映射,解决视觉语义缺失的问题<br />标记语言的2个职责:

  • 表明不同种类:块,文字,层次块
    • 特点:具备bbox
    • markup: is-text,is-block
  • 增加附加性视觉效果:边框,圆角
    • markup: has-border-round

为了避免过度抽象,is-block 只是做视觉语义不会在标签上添加 display: block 的css

视觉语义的可对应的值的特点

  • 影响范围特征
    • 不影响bbox的
      • 颜色,透明度
    • 影响bbox(自身或父级
      • margin,padding,border,fontSize等
  • 值的类型
    • 绝对值:10px,100%等
    • 相对值:
      • +20% 基于当前值再加20%

在没有特别的边界清晰下,原则上就不过度抽象,pattern应支持所有的feature

pattern管理

在组件库的场景,pattern有非常多的结构,而且pattern之间可能还可以互相继承,组合。<br />pattern 本身也成了类似于 组件库的存在。至此,pattern 和 组件就形成一种互相交叉的 n: n 关系

既然pattern也可以有一个pattern库,那么pattern库该如何定义和分类呢?

  • 方式一:是否依然保持组件库的命名方式,比如 button-pattern
    • 问题:其它组件库也可能应用这个pattern,这有点过于先入为主了
  • 方式二:以视觉语义结构为核心,寻找新的定义,比如 character-active-pattern
    • 问题:容易迷惑

方式二看起来更合理,但难度很大,需要透过现象看本质,问题点在于,这好像触及了知识的盲区

  • 盲区1:根本不知道这个本质是什么,如何定义,如何描述
  • 盲区2:不确定这作为命题是否成立,属于什么域,应该看什么资料

web里的一切元素都似乎可以归类为:表格,表单,控件 3大类,在看不清楚的情况下,或许可以先实践后学习

应用

应用的时机在组件的“内部”产生还是”外部“产生,pattern依赖于组件,如果普通的组件库那就只应用于内部,但是在这里,pattern可以应用于“中部”,这样的层次结构就变得很合理起来

  • 状态 和 数据的逻辑
    • pattern
      • 最终注入到 layout

这样的好处在于,当 release出组件的时候,pattern产生的设置项就可以天然的标识出这个是视觉层的prop,并且开发组件dpattern的人就可以自主的预知和开放哪些prop会作为视觉相关的prop

pattern产生的视觉prop:

text FontColor = text (视觉语义)- Font (属性集) - Color (具体字段)

Y

设计下一代组件库

2022-10-19

概述

在M2V的背景下,逻辑和视图已经进行了客观上的分层:

M2V (model to view推导系统), 基于特定的范式特征从而推导出视图中的核心元素,具体设计持续完善中

M2V中,前端部分的代码层次结构被划分成:数据逻辑 > 视觉逻辑 > 视图结构 > 视图样式(从低到高)

在这个分层的基础上,复用一个组件时可以不是简单的 import jsx 并传递 props即可,在复用时,可以表达出更“精确”的复用意图,从而实现最极致的代码复用率

示例

示例1

https://ant.design/components/input-cn/ antd 的 search-input ,一个非常常用的组件。它是组合了 Input 和 Button的组件,但在组合的时候写死了一个逻辑,那就是Button必须要在 Input的addonAfter 后面

源码示例

这种写死的就会容易导致抽象不足,除非改代码否则就无法适应下面的使用场景

源码示例

要维持通用性,要么就需要新增props,要么hack样式,要么就用户自己基于Input和Button重新封装一下(复制也可以)调换一下数组元素的顺序,总之现行框架里实现的方法也有很多。

对比一下M2V的组合方式

组合

2个基础组件简述 input 组件

// input.model.ts 组件逻辑
// $props = { addonAfter: string }
export const inputText = signal('')
const [getInputText] = inputText

export const textLength = signal(() => {
  return getInputText().length
})
// input.react.ts
<div>
  <input value={inputText[0]} onChange={inputText[1]} />
  <span>{textLength[0]()}</span>
  <span>{$props.addonAfter}</span>
</div>

button组件

// button.model.ts 组件
// $props = { text: string, onClick: Function }
export const submit = action((e) => {
  $props.onClick(e)
})

// button.react.ts
<button onClick={submit} >{$props.text}</button>

1.数据逻辑

在写 search 组件时,则需要引入这2个组件

// search.model.ts
// 只import必要的元素
import { inputText } from 'input.model'
import { submit } from 'button.model'  

// 这里input和button只是单纯在视图层的放一起,没有逻辑上的交叉,只import就行

input原本有一个展示输入长度的功能,但是在写 search组件并没有引入“textLength" 必须的状态,所以这个feature 就被丢弃了。

2.视图相关

在确定了逻辑的组合之后,视图结构也就随之确定,通过M2V自动推导出一个新的结构 推导结果汇总不包含 input#textLength的直接关联的dom

 // search.react.ts
<div>
  <input value={inputText[0]} onChange={inputText[1]} />
  <span>{$props.addonAfter}</span>
  /* 开发者手动将button的位置调整到这里 */
</div>
<button onClick={submit} >{$props.text}</button>

注意,推导的新结构只是简单的排列,没法将button放置到”正确“的位置,需要开发者手动调整到具体位置

复用

通过上面的组合流程,现在已经有了一个InputSearch组件,现在开发者需要将其引入到自己的工程中,然后M2V又会生成新的HTML结构

// page.mode.ts
import * as Search from 'search.model'

// page.react.ts
<div>
  <input value={inputText[0]} onChange={inputText[1]} />
  <span>{$props.addonAfter}</span>
  <button onClick={submit} >{$props.text}</button>
</div>
<div>
  /* any other elements in page */
</div>

然后在引入工程可以发现,HTML是新生成的结构,所以可以直接调整 Button和addonAfter,都不影响原本的组件库内的组件结构。

但这会有一个问题是这种调整是一次性的,如果有多个地方在使用呢,如何统一?这相当于是对基础组件的定制需求,要做的事情是新建一个 NewSearch组件,通过NewSearch封装后再重新在页面中使用。

编译时的复用有个缺点是视图跟原组件脱节,如果需要保持原组件的引用,同时又能定制视图,这需要运行时的复用

另一个缺点是动态性缺失,如动态渲染,动态事件绑定

1.运行时

需要提供的新的api,该api能满足:

  • 渲染插入的新UI片段
  • 声明新UI与旧UI的结构关系
    • 父级节点,声明一个选择器
    • 插入顺序,声明数字(支持负数,-1 表示尾部)
  • 不修改原有结构
    • 如果要修改原结构,只能走上述的编译时复用

通过参考features


// page.mode.ts
import * as Search from 'search.model'

// page.react.ts (运行时复用)
<Search increment={
  <newUI attach={Search.div[0].span[0]} attach-order={number} />
} />
<div>
  /* any other elements in page */
</div>

运行时的布局结构同样也需要一份类型系统。在原组件迭代过程中,当父级节点有变更(删除或修改)时,可以通过类型系统提前检查

问题:如何为布局结构生成一套可用的类型系统?

问题

  • q:通过M2V复用跟直接复制组件有什么区别?
    • a:M2V只复制了视图(逻辑)结构这层,最底层的数据逻辑还是通过引用的形式,确保底层是稳固的,依然能referrence from 原始库,不会像复制整个组件库那样出现脱节。
    • 简单的说,数据逻辑层是组件化的,视图相关的是模板化的。需要考虑复用和演进的组件化,定制修改多的模板化。概念引用:https://www.zhihu.com/question/305406422
  • q:使用自己的人是否必须强绑定M2V
    • a:可以降级成为普通组件库引入来使用,体验跟antd一致,如果想要高级功能需要接入这套体系
Y

vscode中ts文件的类型提示实现原理

2022-10-18

背景

起因是在探索下一代组件库的过程中,发现对类型系统是强依赖的,所以需要一个”工具“能够获取结构化的类型数据。发现在vs code的编码中过程,当你hover在某个变量的时候,会发现ts插件总能帮你提示出这个变量的类型

如下:

image.png

所以就思考如果能直接获取这个提示后的结果再直接解析岂不事半功倍?

过程

官方文档里有一个简单的文档说到这个事情,https://github.com/microsoft/TypeScript/wiki/Standalone-Server-%28tsserver%29

从文档里可知,插件的实现是依赖于ts-server工具,但因为ts-server的command数量实在很多且文档稀少,而且存在break change。所以最好的方式是看官方插件里是怎么使用tsserver。

如果开发vsc插件知识应该可以了解到的,”hover变量“的行为是vsc提供的一个事件机制:registerDefinitionProvider

通过搜索看到了插件的调用方式:

image.png

通过代码盲猜 ,获取 ts的行为就是:

  • command = definitionAndBoundSpan | 'definition' | 'implementation' | 'typeDefinition'
    • 根据版本的不同有一些是之前的command
  • 参数 = { line: number; offset : number }
    • 注意:上面2个number都是从1开始,跟编辑器的显示保持了一致

在得知了如何调用ts-server就可以开始这个测试的可行性,参考:https://github.com/mmorearty/tsserver-example.git

但是结果太遗憾了,definitionAndBoundSpan等命令获取到的类型真的只能是文本提示,而且结果逻辑比较黑盒,在没有文档的情况下只能熟读源码才行

其次是通过上述的example工程测试的command的结果跟插件还是有出入,可能还是存在一些配置或调用的问题。

综合来说,通过ts-server的捷径是行不通,还是回归AST更靠谱

Y

【译】startup-checklist

2022-09-30

原文:https://www.defmacro.org/2019/03/26/startup-checklist.html

Product 产品

  1. Who are the users? 你的用户是谁

  2. What is the essence of their dissatisfaction? If they read this answer, would they say “thanks, I wish I’d thought of putting it that way”? 什么是他们真正的痛点,如果他们获得了这个答案,他们是否会说“感谢,这才是我真正需要的”

  3. What are you building for them? 你为他们提供的是什么

  4. Write a tweet from a hypothetical customer explaining the product and how it eliminates their dissatisfaction. 写条推特,向你的目标阐述你的产品并说明这个产品是如何解决他们的痛点的

  5. Write a blog post title for your product launch. Is it surprising? Is it new? Will your target customers want to click on it? Will they want to share the link? Will they still share it the next day? 为你的产品发布写一个博客标题。这个标题是否足够令人惊喜,感到新奇,能否引起你的目标用户的点击欲望。他们是否会分享这个链接,即时到第二天也会愿意再次分享

  6. Write the first paragraph of your product announcement blog post. Include the product name, an explanation of what the product is, the target market, the main benefit, and the call to action.为你的产品写下第一篇文章,包括产品名称,产品说明,目标市场,主要优势,号召的口号

  7. What “metrics of goodness” do your target customers care about? Does your product dominate every available alternative on these metrics? (i.e. what can you do that no one else can do?) 哪些是你的目标用户关心的“优质指标”?在这些“优质指标”中,你的产品是否领先于所有的竞品(什么是你能做的而竞品做不到的)

  8. Is your product as awesome as it could be? Probably not. 你的产品是否做到了极致

Growth 增长

  1. Fill in the bottom-up market size equation: NUM_USERS * ACV = MARKET_SIZE. Are your numbers credible? Find a good reference class if you’re building something completely new.在充满的自下而上的市场规模公式:市场规模= 用户数 * 平均价值,你的数字是可信的吗?如果是全新的产品要找到好的参考竞品

  2. Which subset of your target customers are so constrained by the status quo, they’ll welcome a buggy product? 目标用户中的哪些子集被现状困扰,他们是否能欢迎有bug的产品?

  3. List your first ten customers. 列出你的前10位用户

  4. Which playbook will you use to get customers after the first ten? 在获得前10位用户后会使用什么策略吸引用户

  5. What would need to be true in 18 months for you to get essentially unlimited cheap capital? How will you achieve that? 在18月内实现什么能让你获得真正无限的廉价资本,你会如何完成它?

Strategy 战略

  1. Why now? What’s true about the world that nobody else figured out yet? 为什么是现在,是什么导致了世界上的其他人还没有解决它
  2. What is the most ambitious achievable milestone for your company within a 25 year time horizon? 你公司在25年内可实现的最野心勃勃的里程碑是什么
  3. Is your product a credible advance toward this milestone? 你的产品是否是朝着这个里程碑中的一个可靠进展?Yes/no
  4. What’s the next credible advance toward this milestone? The one after that? The one after that? 什么是这个朝着这个里程碑迈进的下一个进展,再下一个,再再下一个
  5. How will you build a moat? 会如何建立自己的护城河

Meaning 意义

  1. What would reaching your 25 year milestone mean for the world? Is this future really exciting? How many years of your life would you give up to teleport there? If you found yourself in this counterfactual world, would you want to go back? 当到达了你的25年里程碑,对这个世界的意义是什么? 这个未来令人激动吗 ? 你愿意放弃你生命中的多少年传到这个未来 ? 如果你发现身处这个反事实的世界,你会想回去吗?

  2. If another company was working on this idea and not you, what would you think about it? Would you join them? 如果是其他公司已经在实施这个想法但不是你,你会怎么看待这个世界 ? 你会加入他们吗

  3. Imagine yourself standing in front of your team, investors, family, and friends. You’ve failed, and they’re waiting for you to speak. What will you say? Are you willing to work on this problem given that failure is the default? 想象你站在你的团队,投资人,家庭,朋友前,你失败了,而且他们正等你开口,你会说什么 ? 你会愿意继续在研究这个导致失败的问题吗

Bonus 回报

  1. What’s your company’s stock ticker symbol? 你的公司股票代码是多少

  2. Is it likely to be the most important company started this decade? 它可能是这10年里最重要的公司吗?

Y

debug响应式编程

2022-08-30

相关资料:

https://programming-group.com/assets/pdf/papers/2016_Debugging-for-Reactive-Programming.pdf

《Debugging for Reactive Programming》

论文对应的产出产品 https://guidosalva.github.io/reactive-inspector/

https://gousios.org/pub/debugging-dataflows-in-reactive-programs.pdf

论文对应的产出产品:https://rxfiddle.net/

在研发以响应式的框架的过程中体会到了响应式编程中对debug的不易,在产品复杂之后,一个数据的修改,会引发雪崩式的更新,响应式编程无法debug的几个问题:

  • 缺少全局视角的依赖网络
  • 无法断点
  • 无法追踪触发的范围

以及需要深刻的理解响应式编程中的其它debug问题

在思考查询解决方案的过程中,发现有一篇论文好像很贴切《Debugging for Reactive Programming》 因为之前从没有过通过论文来获得输入,所以尝试着体验下,结果一发不可收拾

ps:原本我以为这是德国大学生的毕业论文,后来查了下才发现原来是2个教授

相比常规文章,论文本身虽然很长但内容通常是精而不广,论证过程严谨,有实验有数据。用来对线令人信服。

一点收获

对于reactive programing的理解又加深一点点,把我模糊的感觉进一步的清晰化:

  • dependencies structure也是程序逻辑的组成部分
  • 心智模型在程序和reactive application是统一的。

基于刷新后的认知下再重新回顾了下之前的工具,平台。

而且通过论文这种方式获取的信息就很有安全感和“高级”感,也不用太担心获取到fake news,

Debugging for Reactive Programming 摘录

3.designing RP debugging

4.RP debugging at work

列举RP编程中的问题和解法

  • 4.1 missing dependencies
  • 4.2 bugs in signal expressions
  • 4.3 understanding the RP programs
    • tranditional debugging issues
      • erratic behavior
      • many-to-many relations
  • 4.4 memory and time leaks
  • 4.5 performance bugs
    • because of inside unneccessarily recomputing
  • 4.6 advanced RP debugging
    • inspecting history
    • conditional breakpoints and queries
      • 通过一个自定义拓展的查询语言,在图上设定断点或查看历史

5.implementation

image.png

plugin architecture

6.evalution

organized a controlled experiment with 18 subjects

  • expriment results
    • 在时间上对比,用RD的学生时间耗时更少 2572/4317
    • 验证了RD在程序开发的有效性

7.discussion

  • dynamic dependency graphs
    • 必须先执行一遍才行
    • 跟传统的指令式断点是不冲突,而是进一步完善
  • scalability of the visualization
    • dependency graph realitvly object graph
    • mitigate effect of large grah
      • inspect the graph associated single node
      • collapse the node in graph
      • search
  • limitations
    • 用以实现debug插件的scala语言抽象不足
    • 落地实现的特性太少了,比如时间回溯
Y

为什么无法用代码行数衡量产出

2022-07-16

问题?

众所周知,程序员的工作产出除了完成各种需求之外,就很难再有一个简单,客观的标准评判他的工作产出,从而实现像销售提成般那样的按劳分配的绩效制度

在一般的公司,有些管理者想作妖的时候,往往就会想统计每个程序员的代码量,从而基于代码量来给每个程序员打绩效,但这显然是可笑的,《神秘的程序员们》也有一期专门讽刺这个的漫画

漫画示例

这里问题潜藏的是另一个问题,为什么我们的总是推崇更加抽象的代码,更加通用的架构设计 ?直接说我的想法,那就是抽象的代码的生产力比具象代码更高,因为抽象代码的“可能性”更高,能解决更多的现实问题

可以尝试用数学函数的角度来推理一下

数学函数

数学里的函数是由连续的点组成,所以抽象程度高低的就是数学函数所包含的点数

y = { 0, 1 } 就只能代表2个点,而 y = x,就是可以包含这条斜线上的无数个点,明显后者的抽象程度更高

反推到我们的日常的代码, 最简单的例子:

// 方法1
function foo (prop) {
  return {
    name: 'fooObj',
    value: prop === 'foo'
  }
}
// 方法2
function foo2 (props) {
  return {
    name: 'fooObj',
    ...props,
  }
}

很明显方法2的抽象程度更高,因为方法2可以适配更多的情况,而方法1就相对固化。因为通过简单的有限的几组传参我们就能知道,

  • 方法1:只能有1个参数,2个返回结果,
  • 方法2:可以有无数个返回结果,并且这些无数结果里包含了方法1的,

从这个论证里就能得知我们衡量抽象的其中一个标准就是:一组代码在运行时能够映射出的静态代码的数量,数量越多,就约抽象

function add1 (a) {
  return a + 1
}

function add2 (a, b = 1) {
  return a + b
}

如果能把上面的函数,用number数字填充并展开,生成静态,add2多出add1,至少多出 (b - 1)行,所以add2 抽象程度更高

当然实际的开发过程中的代码比上面的示例复杂了非常多,还有各种语言特性,设计模式等,一个抽象函数能够映射出的静态代码几乎是不可衡量,只能各种最佳实践的参考,理论的推理,以及凭借程序员技术的感觉来判断,这是一个极其复杂的过程

所以除非这么一个AI,它可以识别出每个抽象函数的映射,甚至可以再简单一点,它能对比任意2个函数的抽象程度的大小,这时候就也许就能实现用代码给程序员打绩效了。但是用屁股想都能知道,这个能“理解”抽象的AI得有多厉害,拿来做这个事情,实在太小儿科了。

但是谁能说的准呢,毕竟几十年前的计算机科学家们也想不到在机器性能成千上万倍的爆炸之后最主要的用途之一是用来看美女跳舞

Y

再谈学习

2022-06-25

上周跟人聊天说了关于程序员为什么终身学习的事情,发现有很多地方值得总结

为什么现在的程序员都陷入了一种学习焦虑,一听到有“新框架”,“语言”就直呼“真学不动了”,但过了一段时间天就抱着资料开始啃了

原因我想主要是2个方面

1.知识更新迭代快

尤其是应用层相关的,那真是层出不穷,琳琅满目

应用型的知识更新迭代快,而且这个新知识往往是开源的,因为IT技术无国界,这是全世界的技术人一起推动的,没有任何准入门槛,更重要的最新的知识大概率比旧知识要好。

这里另外举例在另一个也需要终身学习的行业里,虽然他们的知识更新也许没那么快,比如医生或律师,他们往往是因为现有的知识都有用,而且实在是太庞大了,终身都学不完。

2.钱热人多

尤其是年轻人多,

一个10年的程序员并不一定比5年程序员的技术水平先进5年

因为年轻人的成长速度可以很快的,因为他们不需要学习“错误”的知识,不走弯路,花最少的时间,可以直接追赶最新,最正确的技术。

外加“知识更新快”会给已入行的中年人施加影响,让他们过去的经验遭遇了贬值,所以中年人也必须立刻跟进,不然就会被掌握了新知识的年轻人淘汰。

但是年轻人也要注意,一旦到达了最新知识的前沿,他们也就慢下来,接下来就进入了跟同样达到最新知识前沿的中年人进入同一个“时空”,然后一起等待下一轮的“新知识”。下一轮新知识来临的时候又会淘汰一批赶不上的中年人和年轻人

启发

所以对于日常要学习的知识的选择就很重要了,这些知识通常有一个很明显的特征:1.不容易过时,也没有学习捷径 2.围绕客观的“人性”

这类知识通常才是真正能拉开差距的知识,一旦学会就构建了对他人的错位优势

除了此外,还有相关学习的指导方法,我列举了在技术领域会做的事情

1.底层,原理性知识

尤其是很多知识看似是新增的,其实在技术思想或核心原理都是换汤不换药

但这些底层的知识的选择就关键,因为时间是有限的,比如我作为一个前端就不太能花大量时间投入对操作系统的研究,不如好好研究透chromium

学习知识的时候应该更多的关注于本技术领域的,不要限定职责

我会多学习类似关于:编程范式,设计模式,语言的设计实现等诸如 跟领域有关,但不限定于前端开发的基础知识。

另外对于热门技术除了使用之外,也要了解最原子的实现原理,比如docker,这个挺好用,我不知道他是怎么实现,但能了解它最底层是linux提供的各种隔离API层层叠加后实现的伪虚拟,这种隔离达到虚拟效果的思想,也间接启发了现在的微前端框架的设计实现

还有一些软件工程本身的经典著作理论就不一一列举了

2.跨领域的高性价比知识

不同于本领域你可能都学,但其他学科,可以只挑选最精选的知识。这些知识可以很好的指导我们完成领域职责的工作

比如基本设计原则,这可以培养前端在看UI不只是凭感觉,还能看排版,也可以协助日常的PPT制作,这些只需要花很少的时间就能学会

3.客观的人性

就像很多说的35岁要转管理一样,其中的管理学作为一种围绕组织和人性的知识技能,掌握了就能终身使用。

当然这些也不是万能的,说不会被淘汰只是以我的认知水平,结合业界做的一个有限判断

说不准哪一天又有新的技术升级,会带来全新的体系,到时候又是洗牌的一天。所以说还是GWY好,以不变应万变(狗头

Y

技术为谁服务

2022-06-25

“技术是为业务服务的”,相信很多做前端的人都非常笃定这句话。

现在很多的技术言论基本都是服务端发出来的,他们从业人员多,占比大,话语权大,容易并获得广大程序员圈里的人的认可。有没有可能有些言论观点并适用于以“大前端”为主的技术,就比如这句的“技术是为业务服务的,没有业务价值的技术毫无价值”,我想了下这个其实并不适用于“前端”这种以用户为主的技术体系,换成前端领域,应该改成“技术是为用户服务”(这个用户可以是客户,也可以是其他技术人)

这里很多要做反驳说,做好用户服务,体验好,评价高,最终也是还会提升业务价值。这想起了在内网看到的,有举例说淘宝,咸鱼全面APP化,逼迫用户从PC转到手机的场景是不是符合客户第一的解释说明,高层反馈说:虽然强奸用户转手机看起来是为了业务,但长远来看,用户到手机之后发现购物更好更方便,其实是也是客户第一,符合客(ye)户(wu)的长期价值。

很明显,这里将用户跟业务不是等号,因为在国内环境用户未必是真的第一,业务实际才是第一。举个例子:就像银行窗口和银行系统,窗口和柜员是直面服务储户的,柜员和窗口只是储户和银行系统的传话筒,银行系统才是银行的业务,里面有各种跟钱有关的规则,管理者,对于这个业务系统来说,作为用户的人不是Entity,只是是具有Money的Value Object。对于业务系统来说,对于用户就只有一句,那就是“亲爱的用户,我是你die”

但对于前端而言,我们的用户是一群形形色色的人,不只是value Object,有手或有脚或有眼,有喜怒哀乐,前端技术应该为他们服务,提供便捷,流畅,无障碍,协助他们在业务系统要做的事情,满足需求,提供快乐。做好用户服务,体验好,评价高,最终也许会提升业务价值,也可能不会。

比如APP的各种弹框,红点,点击离开页面还给你挽留一下。经常是一段时间框不弹,业务数据就明显的往下掉了。在这种大环境下,业务第一,客户第N,互联网高层现在早就远离一线,也许在早年可能会直面客户,做做冷启动,但现在只能沉浸了业务里的没的感情的数字里,每天碎碎念“低频/高频,高活/低活,GMV/营收/流水”,恨不得一个弹框贴你脸上直接就完成KPI。这想起了在内网看到的,有举例说淘宝,咸鱼全面APP化,逼迫用户从PC转到手机的场景是不是符合客户第一的解释说明,高层反馈说:虽然强奸用户转手机看起来是为了KPI,但长远来看,用户到手机之后发现购物更好更方便,其实也算是客户第一,符合客(ye)户(wu)的长期价值。

而那些本应注重于用户的“前端”的人,却信“业务第一”,再也不主动关心用户,转而去躬身入局深入业务,培养所谓的业务sense,最后跟服务端们一起坚持“业务第一”,做起来高并发/可用之类的“流量生意”。等做需求就发现,惊觉头顶的title其实是“前端”,该糊的页面一个不少,加上在业务系统里舔到的一些脏类活,最后匆忙糊完需求,补2句“用户体验”,“业务目标”之类的,提醒别人,也可能是自己,前端不是只会糊页面,还很注重用户体验和业务。虽然这没人在乎,但迫于互联网圈内的“政治正确”,大家纷纷亮起来大拇指,称赞2句,不愧有业务sense才能做出这么好的体验

Y

web工程师的门槛

2022-05-25

基于在web领域的多年探索的经验总结,我认为的是在于3点:

  • 能将具象事物进行抽象总结的抽象能力
  • 基于已知实事,对未知问题进行推理的逻辑能力
  • 对于已有的技术工具,生产力工具的使用经验

而且在编程生涯里,这2种能力会伴随着编程的深入,经验的积累,认知的提升,这两种能力会逐渐加深,最后呈现出的效果是:

一眼看透事物本质的人和用了半辈子才看透事物本质的人是不一样的

工程师们非常熟悉的工具,技巧,对于非工程师来说也有时间成本上的门槛,但这部分门槛并不高,而且会随着新工具的出现逐步降低,新的工具总是越来越简单,越来越强大

如果有工程师发现在现有工作中积累的是工具经验,而不是能力,那其实是很“危险”的

出于上述观点,如果有低(无)代码系统是号称给无编程技能和经验的人使用,但系统却无情的充满抽象和逻辑,全新的学习成本,那就陷入了一种悖论:

卡拉赞没毕业打什么卡拉赞

但随着技术的普及,普通人的思维其实也在持续的“提升”,像快手,抖音的风靡普及了“算法”,“AI”的概念,提升了大众的认知

在触及过web领域内的非工程师们,每天不可避免的会接触很多的技术,概念,数据,他们是比领域外的人有容易的机会,更低的成本能够去完成程序的内容。

那有没有编程范式和相应的工具,可以稍微降低上述的门槛,能将非工程师们一点点的带入到程序的世界?

这时有机会去完成这件事的,新的编程范式,工具应该只使用易于理解的数据结构和计算过程:

  • 数据结构:图/树/列表
  • 计算过程:函数式编程,算法模型
  • 工具:“常见&成熟”的可视化编辑器

参考:https://gmtc.infoq.cn/2021/shenzhen/presentation/4069

Y

React Hooks的实现细节

2022-05-19

概览

出于设计useServerless的需要,最好能参考React源码的实现

react Hooks的实现作为前端经典八股文之一,之前已经大致了解内部是基于循环链表的实现过程,但仔细回想后,发现虽然了解了它的原理(WHAT)还是有很多地方不明白为什么要这么写(WHY),比如

  • 为什么hook数据对象使用的是链表?数组可以吗?
  • hook的queue跟hook数据的链表不一样,反而是环状链表?
  • 为什么Dispatcher要分成 HooksDispatcherOnMount 和 HooksDispatcherOnUpdate 2类?
  • useState的计算的时机为什么是下次更新的时候才Merge ?

以useReducer为例,hooks的源码出处: ReactFiberHooks.new.js

基本原理说明

在Hooks实现里最重要的事情,就是useState和useEffect之类的hook函数,能够知道当前执行的上下文信息,包含:

  • 当前是哪个Component在调用这个hook函数
  • 现在这个hook函数,它调用的入参 和 上次存储的值是多少?

1.如何hook被哪个Component调用了?

受限于JS的语言特性,strict模式下的当前函数无法获取当前的caller是谁,参考:callee

所以一般情况下需要通过一点技巧来实现,即通过劫持函数执行前后的全局变量,维持一个调用栈

这样在render函数在执行的时候,当前全局变量里最新的Component就是当前正在执行的Component

为什么可以这么做?是因为JS是单线程执行模型,代码在执行是同步的在一个线程里共享内存变量,所以才能用这个“技巧”

拓展思考:如果是多线程里,如何实现这个功能呢?

let globalCurrent = []
function run() {
  pushGlobalCurrentStack(ReactComponent)
  ReactComponent.render()
  popGlobalCurrentStack()
}

2.获取上次的存储值

总所周知,React的hook的值是存储在Component对应的Fiber中,所以只有能找到Component就能找到Fiber

在hooks有2个数据的来源,hooks初始设定的参数作为默认值,上一次的提交的action和上一次的值

  • mount时
    • 关心初始参数,保存到 hook的memoizedState 和 baseState
  • update
    • 关心上次dispatch的action变更,和 hook的memoizedState,计算出新的值并保存

由于mount和update的完全不同,所以需要区别2者,React的判断方法是:通过Fiber

// renderWithHooks current:Fiber
ReactCurrentDispatcher.current =
  current === null || current.memoizedState === null
    ? HooksDispatcherOnMount
    : HooksDispatcherOnUpdate;

useReducer的数据结构

分成2步:

  1. 获取当前的hook对应的数据对象,通过 mountWorkInProgressHook
  2. 构建hook的返回 pair 结构 [state, setState]

为什么useReducer每次都需要从Fiber中取数据,不能直接跟自己关联,2个原因:

  1. useReducer本身是全局引用,不能被污染。
    1. 除非在useReducer在Component进行实例化,在实例化的useReducer上挂数据,但受限于React的 Function Component的特性,这行不通。因为每次渲染的时候,Component作为一个Function会重新执行一次,执行的时候,函数body是从0开始执行的,useReducer每次都会被重新实例化
  2. 受限于语言特性,函数无法在function body内保存数据,useReducer只能将自己依赖的数据,保存到一个hook对象,并挂在当前Fiber里,用到的时候再取出。

如果真不想的每次都从外部的Fiber中取数据,将数据内存在组件内部,就必须得做到2个方面:

  • Component 初始化只执行一次,重新渲染的时候,函数不再重新执行那么就不会重新实例化
  • Component 的render逻辑需要在第一次执行的时候就跟状态数据绑定“关系”,这样就即修改数据不用初始化,仅render 视图就可以了

嗯没错,这就是Vue.setup的核心思想了,条条大路通罗马

1.获取当前hook数据对象

mount的逻辑相对简单,只需要做2件事情

  1. 初始化最基本的数据结构接口
  2. 将当前hooh对象 append 或 init 到全局

hook对象的构建链表的过程:

  1. 第一个 hook1
    1. currentlyRenderingFiber.memoizedState = workInProgressHook(全局,此时为null) = hook1
      1. currentlyRenderingFiber.memoizedState = hook1(这个Fiber下的hook起点始终指向hook1,这样才能通过遍历链表找到所有的hook数据对象
  2. 第二个 hook2
    1. workInProgressHook = hook2, hook1.next = hook2
  3. 后面重复这个过程

经过mount的初始化之后,此时我们就得到了一条hook数据构建的单链表:

currentlyRenderingFiber.memoizedState(hook1) -> hook2 -> hook3 ...

update的逻辑会稍微复杂一点,并且已经它的逻辑是已经经过mount了,认为已经有一条hook的单链表的前提下

在update里有2种hook对象,分别是:

  • nextCurrentHook
    • 初始化时来自于 currentlyRenderingFiber.alternate.memoizedState
    • 此时正在更新中,currentlyRenderingFiber是当前workInProcess,是新Fiber,它的alternate指向的是旧FIber
  • nextWorkInProgressHook
    • 表示当前正在执行的hook数据对象,即新Fiber的hook数据对象
      • 如果第一个hook(nextWorkInProgressHook == null时)则来自于 currentlyRenderingFiber.memoizedState,指向的是新Fiber的链表的头部
      • 否则来自于 workInProgressHook.next (即上一个hook数据对象的next)
        • 新FIber的hook链表

对alternate的补充说明:update的逻辑点正是在于 alternate的差别,了解React源码会知道React在更新的时候会有一个“2棵树”逻辑,基于待更新的Fiber,构建一个新的Fiber链表,新的iew会使用新的Fiber链渲染,并替换旧FIber,它们之间通过alernate关联,所以 fiber.alternate 可以理解为最新的fiber节点

oldFiber.alternate === newFiber newFiber.alternate === OldFiber

在useReducer的update逻辑,在第一个hook执行会有一个迷惑点,即

  • nextCurrentHook来自于旧Fiber, currentlyRenderingFiber.alternate.memoizedState
  • nextWorkInProgressHook来自于新Fiber

React在这里主要是行为将旧Fiber的hook数据对象,复制到新Fiber的hook数据对象中,下面展示的是clone hook的过

update的取数据对象的过程中:

  1. 第一个 hook1
    1. 分别取出新旧Fiber的hook链表的第1个
      1. old-hook1: 此时currentHook不存在,所以得从旧Fiber中先取出第1个 currentlyRenderingFiber.alternate.memoizedState,并设置到全局作为currentHook
      2. new-hook1:nextWorkInProgressHook也不存在,从新Fiber的currentlyRenderingFiber.memoizedState取出作为第1个
    2. 将旧Fiber的hook链表头clone到新Fiber
      1. currentlyRenderingFiber.memoizedState = 从旧Fiber上克隆的 old-hook1
      2. 此时 workInProgressHook 就是执行新Fiber的hook链的头部,即new-hook1,同时把new-hook设置到全局workInProgressHook,作为下一个turn的启动点
  2. 第二个 hook2
    1. 还是分别取出新旧Fiber的hook链表的第2个hook数据对象
      1. 由于此时全局的currentHook已经存在(指向的是old-hook1),直接使用currentHook.next
      2. workInProgressHook已经存在(指向的是new-hook2),直接使用workInProgressHook.next
    2. 同上的克隆过程
  3. 后面重复这个过程

经过取hook对象的过程,也完成了旧Fiber到新Fiber的数据克隆过程,update的逻辑之所以复杂,是因为同时涉及取数据和克隆hook的过程

这么设计的原因,基于函数式的特点,所有的计算都是lazy的,React在内部的大部分编程代码里都遵循了这一个原则

2.执行并计算hook

计算hook也是一个lazy的过程,当setState的时候,对应React,这个只是触发更新的一个信号,此时并不会将setState的入参更新到Fiber.memoizedState里,

计算hook的有1个特点:会进行优先级判断,并且在判断后会进行会将剩余的低优先级action保存到hook中,等待下次的渲染,同时要保存的时候要维持之前action queue的顺序,

而queue用到的数据结构化是环状链表,queue.pending始终指向最后一个插入的update,即链表的尾部

queue.pending = upadete3 <-- update2 <-- update1 ( = update3.next)

用了“环”之后,它的巧妙之处在于就可以

  • 当有新的update插入时,可以直接插入到链表的末尾
  • 当在useReducer需要进行更新的时候,要按顺序进行遍历的时候,只要访问 queue.pending.next(因为是环状的,所以尾部的next就是头部)就可以立刻从头部开始

这种实现在JS里在,用数组当然也能实现,主要还是因为JS的数组非常“强大”。但使用“环状链表”确实非常的酷炫,另外一个可能的好处是queue.pending保存最后一个updaet就行,

遍历完update的之后后面就是简单的 newState = reducer(state, action)这一套,同样是遵循lazy的设计。

Y

Why serverless

2022-05-13

概览

基于响应式的,纯粹的,描述的业务逻辑执行模型(不限语言,环境),write once run any where<br />参考: remix,qwik,nextjs,dva,redux,react hooks,axii,vue setup,Proxy

现状

纯粹的前端状态管理不行吗?有什么是redux和vuex,hooks解决不了的

不行的原因是:

  • 自主性不足,普通web应用的前端的状态管理是二手的,基于数据逻辑的再封装和再计算,重复劳动
    • 导致现状:服务不存状态,角色分工
      • 模型体现在数据库表里
    • 特定领域的重型前端应用呢?如编辑器
      • 不是,UI侧数据模型,数据库表可能有或没有(存了原子操作
  • 数据的能力不足,前端管理的是
    • GUI所依赖的临时缓存,如loading,交互状态的缓存
    • 用户行为产生的数据,如输入,轨迹
    • 待更新的数据草稿
    • server端返回的数据

因为数据的流向是 db -> server -> client,下游的持久化需求最终都依赖db手动的支撑

要解决的是:业务模型单元,纯函数逻辑,保持简洁和防止出错

  • 纯UI的状态
    • useState,
      • disabled
  • 涉及服务
    • 响应式
      • useRequest
        • loading
        • deps
    • 命令式
      • sendRequest()

useRequest建立联系后就可以不管了,自动处理了订阅关系,包括loading。<br />所有的state都视作数据 + 计算结果,都视作乐观更新,这是Read

Update,Remove,Create怎么解?

  • 手动
    • 调用apis.Method().then( ...重新读取当前数据 )
  • 自动
    • 赋值即create
    • 修改属性则update
    • = null 则删除(删除比较危险,可以更“显性”一些

数据控制idea:由于编程 = 数据 + 指令,数据是指令的起点,指令的目的是生成下一份数据,<br />异常表示程序指令无法正确的执行,通常是由于指令依赖的数据不及预期<br />而逻辑则是一系列数据 + 指令的有序的集合

数据 & 计算 &副作用

之前在思考如何在serverless 避免处理客户端长连接的情况,然后长连接的逻辑一般都是写在 useEffect里的,即在函数里看来,由于长链接的场景只消费当前计算的结果,不会影响当前的计算结果,所以它可以视作一种副作用。<br />而在SSR的过程中,也不会去处理副作用的逻辑,免得计算结果不一致。

对于“计算”而言,要拆解成3种:

  • 参数Context
    • 原子参数
    • computed参数 (参数和参数之间的关系)
      • b = computed (a)
    • 明确数据的存储特性
      • state(内存,基本)
        • cache(缓存,服务端缓存 or 客户端缓存,内存更新后,同步更新缓存)
          • 客户端缓存 or 服务端缓存
            • 依据的是数据的逻辑依赖上下游
              • 被Model依赖,服务端缓存
              • 没有被Model依赖,客户端缓存
        • model(DB,内存更新后,异步更新DB)
      • 更新策略问题(参考分布式系统的缓存和数据一致性问题)?
        • 默认乐观更新(先更新缓存,再更新数据库)
          • 先更新数据库,再更新缓存
          • 先删除缓存,再更新数据库
          • 先更新数据库,再删除缓存
  • 输入计算 f(跟computed的差别在于是否有外部输入,即入参)
    • newContext = f ( input , currentContext)
    • f 和 f之间的关系
  • 副作用 (基于上述2者的变化
    • context
      • before, after
    • inputCompute
      • before, after
// V1
// V1
const serverlessUnit = {
  context: {
    a; new State(),
    b: new Cache()
    c: new Model('Entity')
  },
  init () {
    // init body
  },
  inputCompute: {
    inputCompute (arg) {
      const deps = init(contextDraft)
      // middle state ?
      return { result: newResult } 
    },
  },
  effect: {
    notify: [['result'] ,(prev, current) => {
      //sendToNotify()
    }],
    beforeCompute: [(prev, current) => {
      //sendToNotify()
    }, ['compute']],
  },
}

// V2
function serverlessUnit () {
  const a = useState()
  const b = useCache()
  const c = useModel('Entity')
  
  const d = computed(() => a().x + b().x)
  
  // must receive a parameter,有语义的合集
  // reducer的既视感
  const inputCompute = useInputCompute(async (parameter) => {
    // do something with draft:a,b,c
    
    // progress ? no 只有0,1的2种状态
    
    return { a, b, c } // commit draft
  })
  
  // before compute
  useEffect([(prev) => {
    // prev = [a, b, c]
  }, [inputCompute]])

  // after compute
  useEffect([[inputCompute], (prev) => {
  }])
  
  return { a,b,c, compute }
}

对比 redux

不同于redux的单向数据流思路,改变数据一定要dispatch(action)-> reducer -> state,因为整个unit的数据都是响应式的,所以如果要修改state可以更简单些

  • 直接修改
  • 通过 inputCompute

这2者好像是互相冲突的,简单来说外部的输入,一定得引发内部某个变量的修改才有意义,即入参最终是某个变量的某个值 。所以从效果来说 2者是等价的。但 inputCompute 还是有存在的必要,因为它可以存在“语义”,serverless最终目的是一个建设业务模型,所以保留计算合集并提供一个语意就很重要,这也是区别于它不是一个单纯的数据结构

原本的系统是个整体 + 纯函数逻辑,所以如果要修改unit,那必须有外部的参数输入才能引起变化。inputCompute也应该是响应式的,按照最新的调用进行执行。虽然中间不达预期,但至少达到了 最终一致性<br />unit stack

  1. modify, a.x = 1
    1. 直接更新(1)
  2. start recomputed d (耗时较久)
  3. inputeComute (入参)
    1. 获取 d (旧的值,d还没计算完)
    2. 更新(2)
  4. end recomputed d
    1. inputCompute重新计算
    2. 更新(3)
  5. 结束

是否要必要保留”直接修改“ ?如果去掉是不是会更加纯粹,同redux一样 compute -> state?简单的场景下:<br />page.value + 1 和 一个addPage(1) 在语义上没有区别

综合下来,最大的区别是响应式 + 依赖

状态同步传输

由于server端还是不保存状态,所以当进行计算时,会将所依赖的Context通过http传到server端,这样server就可以执行compute计算

Y

chrome & v8源码探路

2021-05-09

冷知识:chromium的源码虽然也是git,但是托管在自建服务上的

代码仓库: https://chromium.googlesource.com/chromium/src.git/

代码阅读器: https://source.chromium.org/chromium/chromium/src/+/master:third_party/blink/renderer/core/clipboard/;bpv=0;bpt=0

需要学一点C++才能看得清楚明白

目录结构

  1. third_party/ 第三方
    1. blink/ 排版渲染引擎(之前是webkit)
  2. v8/ 外链到v8自己的目录

v8是js的runtime,但是在浏览器有很多的API,如DOM,fetch等其实并不在v8里面,这些代码大部分在blink里

如 navigator.clipboard相关方法就在

https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/modules/clipboard/clipboard_promise.h

阅读姿势

  • 了解整体的生命周期
    • 功能模块的职责
    • 功能模块的之间的调用机制和调用关系
  • 针对具体功能再看具体代码
    • 要区分多平台

(to be continued)

Y

自身编码的极限

非业务代码,解决一个棘手数据驱动的动画问题,早10晚10,且在提前一天已经想得比较清楚的情况下

结论:约1000行不到的代码

写完后大脑基本宕机

Y

数据库"全量”索引

2022-04-28

在了解了一些服务端的高并发问题看到的,大部分都因为数据库会成为性能瓶颈,或者某些服务成为单点问题也<br />是因为数据库。换句话说,有没有办法直接解决数据库的性能瓶颈问题

  • 数据库总是瓶颈
    • 为什么数据库不能随意扩容
    • 分布式系统的缺陷:CAP 定理
  • 为什么不给数据库的数据建索引和缓存
    • 按需缓存,手动实现
    • 全量索引的存储空间是指数级爆炸
  • 自动100%按需缓存
    • 现有的开发模式是假定完全无信任
      • 数据库/服务端系统的输入无法预测
      • 面向未来拓展需要
    • roi,只有量大之后缓存才划算
      • 缓存的成本固定,扩容的成本线性,2者产生交叉后用缓存才划算
    • 是否缓存,实时性
      • 依赖业务的需求来判断
  • 重复计算问题
    • 即时是完全相同的请求还是要耗费服务端计算和数据库查询
  • 纯函数的可能性
    • 100%的覆盖度
    • 输入可预测,可枚举

人对数据的意图

要想实现输入的可预测,必须能够全部枚举产品在面向用户的都提供哪些数据功能,如:

  • 查询教室的学生数量
  • 更新学生的状态,更新教室
  • 登录/注册/评论/发布topic
  • 引入外部资源

一个产品能提供的数据交互始终是有限的,可枚举,用户在使用产品的使用,他能做的输入操作也是有限,可预测。( 除了“模糊搜索”的,但像搜索这种非常典型且垂直且通用的场景可以走另一套单独的技术方案)

所以一般的服务端提供的代码也是一个有限的集合,但我们的代码是专注于抽象的,我们在产品的程序开发中强调”抽象”就是为了在有限的代码里去承接尽可能多的功能,从而降低软件研发的成本。“复用”也是,降低的是下次研发的成本

现在的ER提供的只有CRUD以及相关的一些衍生方法,过于抽象,要想实现产品的提供的功能往往需要大量的中间计算。如果能够把中间空白补充上,那不是以后就不用再写基础的接口了,明显是yes,但是站在一个ORM框架的角度来看,这是不可能,因为它知道自己是被什么产品用了,不可能穷举。

自定义类型

参考了“存取能力设计”,意图往往跟数据的类型有相关性,在CRUD的基础上还可以加一层基于类型的操作,这个类型诸如有:

  • 颜色
  • 日期

除了常规的类型之外,还可以拓展自己的复杂类型,这有点类似业务层的底层通用接口

findByKey('color').whereIsWarm() // 查找暖色系
findByKey('money').equals('$1') // 查找等值于1美元
findByKey('schoolWorker').equals('teacher') // 找“老师”或班主任,递进关系

类型的解释权应该归对应的产品所有,相同的类型在不同的产品有可能对应不同的语义

但类型本身应该具备横向的功能,又能被功能层重新定义

功能层抽象

不同于类型的通用,功能层的抽象逻辑往往只注重于当前功能要解决的问题,只存在于整体功能的复用而不会是功能内个别代码的复用

Y

一个用户值多少钱

2022-03-01

在互联网有这么一群人,他们不发帖不评论,几乎不生产任何内容,他们在网上留下的最重的动作可能就只是“点赞”和“关注”,很多从业者把这群人称作白嫖用户,他们在网上微不足道,但聚在一起却成了所有互联人求知若渴的“流量”

所以这里就有一个很矛盾的点,对于商业主体而言:

  • 成本
    • 为了服务大量的普通用户,服务器成了大量的开销,是一个巨大负担
  • 利益
    • 大量的普通用户汇聚在一起之后成了公司不可或缺的流量,公司需要这股流量进行变现,获得收入

经典的极端的2个方向,有矛盾那就说明有“边界”,越过这个“边界”让普通用户具备了超过“成本”的商业价值,使得商业主体获得利润

Y

小“闭环”体系

2022-04-11

最近在思考前后端一体化单元时,想到了几种形式:框架,或者 语言形式,如果采用语言形式,需要设计一套新的DSL,如果使用声明式DSL,还可以在使用可视化编辑器 + DSL runtime,如果是过程式DSL,可以使用Web IDE 结合 代码生成,出码后得考虑是直接的代码,还是生成包含框架的代码,如果是框架是否可以使用最开始的框架

---- 上层

  • UI
    • 计算数据
    • 原始远端数据
  • serverless 中间抽象层
  • Model
    • 原始数据
    • 高性能缓存的数据

---- 下层

  • 底层:
    • 框架
      • 抽象概念
    • 语言
      • 抽象概念
  • 我的DSL
    • 声明式
      • DSL可视化编辑器
    • 过程式
      • 富文本编辑器
  • 出码
    • 中间产物 + runtime
      • 底层的承接
        • 框架
    • 代码生成
      • 底层的承接
        • 语言
Y

关注"读"数据

2022-04-24

原始的ORM是数据库的操作的映射,现在更进一步发展了像prisma这样的基于ER的操作,但是原始的CRUD和关联关系还是不够描述

在实现”版本“的过程中发现 写操作,可以针对id进行原子化操作,即写操作必须是完全建立“读”的基础上 读操作,才需要重点考虑各种复杂的拓展关系,既要满足查询的诉求,同时也要作为“写”操作的基础

读操作(R)可以拆解成几个步骤:

  • 参数
    • 必须是精确描述
    • 参数 不等于 “意图”
    • “函数”处理:求和,排序等
  • 运行查询
    • 根据“Entity”来找数据
    • 同时兼处理“函数”
  • 给出查询结果

读(R)操作附带有查询和处理,由于现在的数据库都默认自带了一些“函数”逻辑,如排序,合并,所以很难意识到处理的部分,假设数据库不带这些函数,那么用户就必须在给出查询结果,再用额外的函数进行手动处理,手动处理的部分也是意图的一部分

Y

“搭建系统”的困境和突破

2021-12-31

搭建系统是工具,工具是解决问题的,所以重点关注问题是什么,为什么搭建系统能解决。<br />第二关注工具是怎么解决问题的,解决的结果如何。<br />以及最后的剩下无法解决的问题怎么办?还有办法能解决吗?

基本纲要

  • 对齐认知
    • 同步“目标”
      • 业务
        • 快速上线
      • 技术
        • 原子化,沉淀复用
        • 原子组件的开发体验要好:少概念,渐进增强
    • 同步“搭建系统”的概念,定义,特点
      • 面向非技术为主
      • NoCode
      • 不需要可维护性
    • 同步个人的简单经历
      • 美店
      • 蚂蚁营销工作台
  • 代入问题
    • 能解决的问题域太小太垂直
      • 有限的输入和组合只能解决更加有限的问题
    • 受上游影响极大
      • 问题域内的是神器,问题域外的一文不值甚至负作用
  • 对破局的思考
    • 解决方向
      • 广度
        • 原子化,提升抽象,-> 重新发明html,css,js
      • 深度
        • 业务
          • 需求不可控 -> 需求全部适配 -> 人力填坑 -> 拓展边界 -> 广度问题
        • 技术
          • 极大增加搭建系统的数量 -> 提升搭建系统的开发效率 -> 搭建系统的搭建系统 -> 搭建系统的基础物料 + 问题域定制
    • 换个角度
      • 卷往上游,以需求逻辑为中心
        • 需求逻辑的描述是必须的,核心的,稳定的
        • 需求逻辑是可流程化描述,如流程图
        • 基于逻辑是可自动化
  • 落地的结局
    • 落地的困难
      • no,low,pro code之间的差异
    • 和解,放弃完全的NoCode,工程师你回来吧
      • 重点解决其中的需求的确定性的部分
        • 需求的确定性越高,需求的信息量就越大,编写程序就变得越简单,后续而二次消费也更容易
        • 确定边界
      • 流程图变成填空题

为什么搭建

C端营销场景:技术服务业务,业务KPI。

  • 目标:业务KPI
    • 承接:页面
    • 目标:快速上线
      • 承接:技术
      • 目标:页面开发效率 ( 效率 = 工程师 * 页面代码 / 时间)
        • 时间不变,提升工程师,提升页面代码
        • 目标1:提升工程师
          • 工程师很难提升,只能转移或者赋能
            • 转到外包
            • 降低门槛,让非工程师也能做
        • 目标2:提升页面代码
          • 代码不能凭空产生,要么由人(AI)编写,要么使用已经存在的代码
            • AI手段:各种 to code,Design to Code, Prd to Code, Flow to Code
            • 代码封装,复用

页面搭建系统是符合上述的2个目标的解决方案之一

问题域

搭建工具对应的问题域的特征:

  • 技术挑战类:
    • 流量大,时效性强
      • 活动特征,跟随大促节点
      • 读远大于写入
    • 状态简单
      • 流转关系可以通过逻辑流程图充分描述,并被一般人充分理解
      • 状态枚举通常不会超过3个值(如 0/1, 0/pending/1, pending/fulfiled/rejected)
  • 业务价值类
    • 无序
      • 缺少统一标准(技术标准,产品标准)
      • 缺少最佳实践,
      • 反复验证,AB实验
    • 效率优先
      • 开发资源不足

搭建系统最简单实现

基本元素:

  • 有限规则的输入端
    • 可视化
    • API
  • 中间抽象数据
    • DB
    • json
  • 输出端
    • runtime
    • 生成代码

page = runtime(json)

假设现在已经用这个公式解决问题了。

这个过程的本质就是一个DSL,所以DSL的局限性也包含了这里说的“搭建系统”的局限性,同时我们狭义的页面搭建系统也有局限性。

2个项目简介

用“搭建”的思路解决问题的过程中,主要做过2个搭建系统,他们除了都叫搭建之外,剩下的技术实现不同,用户不同,解决的问题也不同:

  • 电商营销会场H5页面搭建
    • 平台用户:
      • 万千电商运营
    • 技术架构:
      • react + jquery
      • java模版 + ssr
      • 搭建组件 = React组件 + 组件的描述 + 定制编写的编辑区
    • 搭建能力:强
      • 私有组件标准,组件之间可以嵌套,组合
      • 线性布局,绝对定位布局,且能相互组合
    • 页面不需要可维护性
  • 花呗营销活动H5页面搭建
    • 平台用户:
      • 花呗运营
      • 专业前端
    • 技术架构:
      • 全栈中中台(react + eggjs + mysql)
      • CDN html + 异步接口
      • 组件拓展性强
      • 搭建模块等价于React组件
    • 搭建能力:弱
      • 相当于 Array<React.Component> 的一维数组
    • 页面不需要可维护性

总结特点:

  • 核心目标是:效率
    • 相同的模块标准,便于沉淀复用
    • 充分灰度,快速上线
  • NoCode
    • 低门槛,少概念
    • 非技术为主
  • 不在乎产物的可维护性和可拓展性

可以看到这里做的2个搭建系统他们是相似又不相同:

  • 相似的:
    • 营销,活动,
    • 一次性,
    • 低门槛
    • 一套组件标准
  • 不同的:
    • 使用者逻辑水平,
    • 页面可交互复杂度
    • 平台搭建能力

这么相似能不能在保留核心的特性下只加一点的改造就能互换一下在对方的业务里直接使用呢?不能。因为环境,需求的不同导致了平台之间有显著差异,所以不能。

那有没有可能一个搭建系统就能解决2个业务呢?那肯定能,因为站在现在的视角来看,需求已经确定成产品了,只要再统一一下抽象就行。

合并:提升抽象,提升复杂度

  • 搭建能力对齐,都支持多布局
  • 组件标准升级,私有标准
  • 按业务域划分租户,分隔平台用户

解决之后

往广度思考:<br />一个搭建系统只能解决一个垂直域的问题,如果要跨垂直域,那就意味着要把提升搭建系统的抽象程度,拆解一下把视图结构的用一种DSL,样式的部分用一种DSL,逻辑的部分再用一种DSL 。(黑人问号,重新发明一下html,css,JavaScript ?

往业务的深度思考:<br />除了无法跨域,而且针对搭建系统的上游:需求,逻辑,无法充分收敛。因为上层是完全不可控的,即当技术侧发起的工具/服务无法解决满足上层需求时,就只剩下2个解法:

  • 人力填坑
  • 需求变形

这2个解法都会出问题

人力填坑相当于开了外挂,一旦开启则无法停下,搭建系统迅速失控名存实亡,沦为鸡肋,其存在的就变成了问题本身。

需求变形,即需求适应系统的短板,站在更上层的视角看,这是对业务的不可避免的伤害。

往技术的深度思考:<br />如果一个搭建无法高效的解决全部问题,那就增多搭建系统的数量,创建能搭建搭建系统的高阶搭建系统,抽象搭建系统本身的组成:输入,中间数据,页面。那么,搭建系统的抽象如何拆解,

搭建系统A -> 问题域X<br />搭建系统B -> 问题域Y

高阶搭建系统X -> A, B -> X, Y

如果:这时候来了新需求Z1,新需求Z1属于问题域Z。(比如:营销人传人)

如果A,B不能满足Z1,解决问题Z,那为什么X能解决问题Z ?可能性🈶2:

  • X很先进,提前预判到了Z的存在
    • 如何预判?怎么收集需求
  • X很灵活,有拓展机制能够让开发者继续开发解决Z
    • 本质是劳动力的转移(需要拓展多少?有没有可能是完全重写,那就X不就成了基础框架)
    • 问题Z = 新需求Z(1-> Infinity) = ( 新搭建系统 = 高阶搭建系统 + 人力procode )
      • 反问:如何衡量ROI,为什么不简化为“人力procode -> 新需求Z0”,
      • 反问2:再来个新的W问题如何?上游不可控

如何突破

看流程,视角也许可以从技术侧前移,看看能不能从需求侧解决,俗称:如果解决不了问题,可以尝试换个问题,看看在这整个过程,最核心的点是什么。不是最初的业务目标,而是基于目标描述的业务逻辑。

来自运营的需求目标: 1.提升MAU, 2.有一亿的预算,3.其它指标等<br />产品分析之后并输出之后形成产品PRD:里面描述了产品逻辑,最后开发面对的是这一整套产品逻辑,包含用户,行为,状态事件,最后形成页面。

但是在搭建页面的这些逻辑被分散到各个组件里,经典的场景:在评审需求往往会有一个流程图,但在页面完成后这个流程图就没用了。

再深入一点,充分的把流程图用起来,让静态的流程图动起来。

建设以“逻辑编排”为核心的搭建体系,

以“逻辑”为中心

2个需求示例:日常花呗抽奖活动,花呗五周年小纸条

以流程图为中心,流程图里要包含的基本结构:

  • 生命周期上下文
    • 内置变量,
    • 内置常量,
    • 基础自定义数据,如
      • 营销规则,人群规则
      • 开始时间,结束
  • 基本结构字段
    • 人物等客观条件,Value Object
      • 时间,
      • 某个业务实体(物品,优惠券)
    • 计算节点
      • 分支判断
      • 用户行为事件响应(通常是点击,浏览
    • 渲染节点
      • 渲染
        • 渲染整个页面,页面是搭建来的
        • 渲染局部,如弹框
      • 等待事件
        • 等待渲染的UI 抛出事件(用户行为,自定义描述的事件)
  • 拓展信息:
    • 逻辑覆盖率
    • 监控
    • 埋点
    • 流量分布
    • 自动化压测
    • 自动化性能优化
    • 业务转化率,漏斗,采集

逻辑怎么不行了

示例1:抽奖

示例2:小纸条翻页

1.是纯前端视角,格局没有打开

2.可覆盖的复杂度不够

“动态 & 前后可翻动的小纸条”让我破了防

NoCode无法覆盖纯逻辑的部分,但是NoCode依然保留有能力无限且复杂的逻辑确定下来。

对业务来说可以是一个黑盒,黑盒只暴露接口,“动态 & 无限可翻动的小纸条”有2个接口:翻页完成,翻页退出,借助Code的手段从而完成降维

和解,放弃完全的NoCode,重点解决其中的需求逻辑的确定性的部分

需求逻辑的确定性越高,需求逻辑的信息量就越大,编写程序就变得越简单,后续而二次消费也更容易,流程图变成填空题

3.无法很好的处理循环和循环退出<br />拓展一下,即语义的逻辑和实际的程序逻辑不是一一一对应的,中间是有抽象的,如:

点击tab offset=0 -> getList 点击pagination offset+=1 -> getList 程序:任意点击行为,offset changed -> getList(offset)

而且语义的逻辑经常会默认带一下脑内上下文,比如点击tab,默认就清空了前面的变量,或者意识人,非技术人员通常会分别 每个tab都是独立的逻辑,而程序逻辑则可能是为了性能,默认复用变量

逻辑的边界

当我意识到在逻辑上的产品经理(或需求方)不应该去关注如何底层的程序细节时,同时让产品描述逻辑是可行的,那就说明在这个产品 -> 逻辑的路径上存在一个边界。

最先开始:产品 -> 需求 -> 工程师 -> 逻辑

原本的设计:产品经理 -> 逻辑(这里的逻辑即包含了业务逻辑和程序逻辑,同时工程师参与但也负责逻辑的正确性,完备性,逻辑有问题就如同需求有问题,如同程序出了bug)

业务逻辑一定包含程序逻辑,但反过来程序逻辑不一定是业务逻辑。(参照声明式编程和指令式)

转账 = move(A.balance,B.balance) 借钱 = move(A.balance,B.balance)

加入工程师之后的设计:产品经理 -> 业务逻辑(声明式,关心用户,UI,业务状态) -> 工程师 -> 程序逻辑(类库或接口, 复用或者新开发,按业务逻辑进行填空,关心数据,稳定性)

当考虑到工程师角色参与之后,我意识到这个“边界”不是一个确定性静态的值,它应该是工程师和产品在认知上达成的统一共识,而且会随着产品不停迭代动态变化。“边界”的作用不再是对立性的,而是为了寻求最佳的解法。

再进一步抽象一下:

产品 -> 结构化后的需求 -> 工程师 -> 代码

结局

一个瑕疵明显的NoCode方案,不容易打动人心,落地阻力大。

人们工程师们对于NoCode和ProCode有很高的质疑,但对于lowCode则很包容

  • no code表面太完美,工程师天然直觉认为必定非code不可解决的地方,这将来是一个深坑
  • pro code本就是工程师的专业领域,这是专业领域的碰撞,语言只有2种被人喷的和没人用的
  • low code 则取决于视角和比例
    • 观点1:low的部分是帮助工程师减轻负担
    • 观点2:code的部分是增加了工程师的负担
Y

前端的边界

2022-04-11

探索一下”前端“这个岗位在技术,职能的天花板

背景

由“模型”中心衍生想到的,当我在尝试实践remix时,remix这种完全以form表单为中心,action处理的方式,跟很早之前的MVC(php,java servlet,其中的主要差别在于remix通过编译手段暴露很多中间状态,改善UI渲染,不能说很像,只能说是极其类似。

参考这个方式会突然想到,古典的MVC是很不适合做复杂前端应用的,所以remix也许这样,但为什么不适合

另外前端是从后端里产生的,但是为什么后端要把这个工作分离出来,仅仅是有人说的因为“后端不想干这个脏活累活”吗?

还原一下

如果不考虑其它因素,真的完全用php可以实现复杂前端应用吗?比如绘图工具,编辑器工具。明显地,应该是可以实现的,

比如把原本的js里的状态变量都设定为数据库表或者redis,每一次用户的行为都会触发form或ajax请求,在controller里处理状态并落库,之前刷新页面,重新读取数据库里的数据,最后渲染到页面

相比现在的前后端分离的模式,在技术唯一的差别:

  • 将原本的前端状态持久化到了数据库
  • 确保前端UI是完全受控于数据库

那么在这2个条件下,就能用后端实现一个完全复杂的编辑器应用,前端作为视图层需要完全的渲染HTML即可,如果使用form表单来提交数据,确实甚至都不需要js执行

为什么不

既然上述的方式存在确实可行,但现实是还是发生了拆分了,那到底是什么因素导致的呢?首先看下上面的弊端,统一在一起之后

  • 对人
    • 素质要求变高,写的人必须同时写MVC
    • 身兼多职,无法深入研究和解决单一领域问题
    • 单点问题,一个人从头撸到尾容易成为瓶颈,工业化生产讲究分工协作,
  • 对事
    • 数据冗余,很多用户的临时操作状态都必须存在数据库里,如果要节约资源就加上及时清理机制
    • 体验不流畅,每步操作都必须走网络请求到服务端,链路长,响应时间久
    • 计算资源浪费,状态的流转都必须借助服务端CPU,浪费了用户客户端的计算能力
    • 耦合问题,全流程绑定到了一起,无法局部复用,无法提升后续效率

也许还有更多原因,但总结大体是2个因素导致了现状:

  • (降本)节约(服务器)计算资源
  • (增效)生产效率

降本:

  • 将临时状态,用户状态转移到前端内存或浏览器缓存,减少存储
  • 状态的切换转移在js里计算,减少计算和网络请求
  • 由于上述的操作,状态的存取都在用户内存里进行,响应加快,也间接的导致了体验变好

增效

  • 分离产生的分工,专业的人做专业的事情,前后端分别负责V,C和M,在单点里提升专业度和效率
  • 复用成为可能,由于分离后各自成为单独的抽象层,使得层的复用成为可能

边界的产生

在分离后,并且随着互联网大发展,前后端在各自的领域里狂奔,在今天去区别一个前后端的人已经不是用“编程语言”来区分了,用js或java并不重要,重要前后职能的人在自己领域内的重点问题的探索,思考,实践,如:

  • 前端
    • 生产力工具:低代码,web ide,富文本编辑器,各类平台
    • 多端程序:DSL,跨多端多平台,小程序,BFF(SSR,serverless函数计算)
    • 传统视图特色:Web GL,A/VR,游戏引擎
  • 后端:
    • 计算性能:高并发,大数据,多媒体处理
    • 存储:各种数据库,中间件,缓存,云盘
    • devops:容器虚拟,云原生
    • (列举不全,参考阿里云,腾讯云卖的各种产品)

以上只是一些举例,当然还有很多没列举到的。由于前后端在区分的边界上已经更注重于问题领域的差别,而不是手段或者工具,所以熟练的掌握一些工具的经验可以帮助我们更好的探索问题域,但不能本末倒置的沉迷在工具里,不需要再问“学react还是vue”这种问题了

回顾一下更早的历史

在最原始的时期,那时候连后端都没有,古典时期的程序员们就是直接面向数据库(计算机的本地的存储数据)开发,通过接收原始的输入存储,然后通过指令文本打印到命令行界面:

命令行文本 <--- 程序处理 <--- 存储的数据

在传统架构下,计算机仅需考虑用户本地的情况下,当互联网产生的时候,C/S架构也产生了,命令行界面的数据不一定来自于本地,也有可能来自于其它ip的计算机,其它ip的作为数据和计算服务提供者,才称作服务器。逐步逐步演进到今天后,随着问题的解决,以及新的问题越来越大越复杂。

为了适应这种变化,职能的分工也就陆续产生了,虽然职能诞生了,但会发现整个程序的基本模式并没有大变化,变化的是每个流程里不断的细化分工,不断提升的是整个系统的计算能力,输出(传输)能力,存储能力

输出(多端) <--- 中间处理(服务器架构或客户端)<--- 底层数据(集群)

面向更极致的未来

由于基础设施的完善,生产力工具的爆炸,以及人员素质的提升,也许使得一个人已经可以负担足够多的复杂度。想想看,现在写一个登陆系统,博客系统比之前的时代有了多了多的提升。

继续迭代生产力工具,提升抽象,降低对用户的复杂度,使得之前的复杂应用降维成了现在的简单应用。

所以在今天的场景里,也许已经有办法让一个人或一个职能去负担一个完整且复杂的应用,如果有一个种新的工具具备描述一个完整的功能,通过功能的叠加和组合,再去诞生出一个应用。

Y

以“模型”为中心

2022-04-08

不想再写一次登录系统了

Nextjs

深度使用nextjs后的感悟,发现nextjs这种以serverless为中心的设计复用的,以路由即页面

这样的问题是由于serverless缺少模型的信息,导致在业务逻辑就只能在前端UI的逻辑里面处理,同时在设计时,需要区分前端的状态和模型的字段

这导致的一个问题,设计书写一个完整功能是被分割成了3个地方:ER模型,serverless或前端处理逻辑,UI

在架构上就处于3横多纵的模式,这样的好处是相同层容易互相复用逻辑(耦合),局部的横向造成了耦合,但却无法整体导出

如果以模型为中心,只构建纵向,横向的建联通过直接的引用。

// module2.jsx
import { useApi: useApi1, Component } from 'module1.jsx'

export function useApi () {
  const use1 = useApi1()
  return {
    findFoo () {    
      use1.findAndDoSomething('')
    }
  }
}

export function Component () {
  return (
    <newDialog>
      <Component />
    </newDialog>
  )
}

这样的模型,severless,UI为组合,那就像之前的MVC架构, M -> C -> V -> C -> M的流程

这里有个问题,当M -> C -> V之后,其实在V层面已经收到了来自于C层经过计算之后得到的状态,为什么当状态修改的时候,还需要显示的调用 V -> C -> M 把数据再还原回去呢,为什么不能逆向计算后,直接 V -> M?

  • 不是所有的计算都是可逆的,那哪些计算可逆?
  • 也不是所有的V都需要直接到M,有些是临时状态需要缓存,如何显示的区分这些状态

发散

http server 是无状态的或者只缓存状态,模型里只保存最原始的数据或者为了性能考虑保存需要大量计算的数据或索引

  • 怎么判断哪些状态字段需要持久到模型中?
    • 如状态不存在模型中,那么模型到状态之间总是需要一个计算,好像是serverless不可缺少?
  • “状态”如何在 server/website 中同步 ?

前后一体化单元

实现一个功能最小单位至少包含:UI,serverless或useHook,Model ,(其中UI和model至少有1个)

另外的场景:单独的后端很少会讲到复用,但当说到复用的时候,系统负责的领域足够大时,这就成了“中台”

  • 没有Model
    • 静态站点,博客
    • Sketch,PS工具(广义来说,ps文件作为输入,也应视作Model)
  • 没有UI
    • API gateway(这种是中间件,这个好像Model也没有,但这个不够广泛
    • CDN服务,Open API服务,各种后端服务系统

在不考虑“用户”的情况:Model最重要,数据是资产<br />在考虑“用户”的情况下:UI最重要,能用和好用

一体化单元之间的互相组合有2种形式:

  • 静态,需要版本管理
    • 就像类库的引入,之后2个单元就是相同的工程,
      • 问题:model层是一个大集合,同时无法拆分,存储成本
  • 动态,需要运行时
    • 就像微服务或中台,引入的其实是SDK,2个单元不能算是一个有机的整体,只是允许互相调用的服务
      • 问题:model层会成为单点
    • 一旦动态之后就需要:服务的发现,注册,订阅,销毁等生命周期,

一体化单元要暴露的是:

  • Serverless或hook
  • UI

微服务

  • 持久层隔离
    • 底层Model之间的关系的引用和处理,跨数据库,这个怎么索引和建联?
  • 公用Model
    • 单点问题
    • 大表索引问题
Y

快速理解next.js

2022-03-17

最近尝鲜真正投入在业务场景里使用了下nextjs,但在刚开始用不久,就产生了一个疑问:

想在next.js 初始启动的时候去连接数据库,应该怎么做呢?

在参考了官方示例之后(https://github.com/vercel/next.js/tree/canary/examples/with-mongodb

我有点悟了,发现完全误解了nextjs的真正形态,简单来说:nextjs不是一个带了node服务的前端框架,而是一个集成加强型view的node server

上述的疑问背后的真正问题是:我不了解nextjs,我在尝试寻找nextjs的server的“入口”,而实际上nextjs整个就是server

Y

探寻Low Code

2022-01-16

概览

low code(包含no code)一种通过提高抽象,隐藏程序细节的方式而设计出来的面向具体业务(领域)的平台工具,其目标一般是提效(原本角色做的更快更不容易出错)或者赋能(能做他们本做不到的事情)

在公司平台里有大大小小不同的low code平台,针对不同的业务域问题,设计了不同的组件,服务系统,接口,可视化界面等等,然后在自己的业务域里不断迭代着

但是这些low code系统在往往只根据具体问题,设计了具体功能,现有的技术进行代码堆砌,最终出来一个“平台” ,但背后的底层抽象却鲜少提及。

回溯历史

我们现在每天接触的JavaScript,java等,通通都是称为高级语言,在高级语言之下的是低级语言,如汇编,机器语言。现在已经不再写汇编了,我们已经可以通过更高抽象的高级语言来完成工作,因为这样更加高效也更准确。所以高级语言某种意义上也是一种low code。

但光有在JavaScript还不够,还不够快,通过语言的特性,上面诞生了各种各样的UI框架,vue,react。这些框架也通过更加高级的抽象,改变了前端工程师的开发方式。这也是一种low code。

在UI框架之上,还有更上层的抽象,如remix,next.js等,通过引入服务端的能力,让前端工程师的技术的纵深又深了一步而且学习的成本很低。

可以看到随着时代的发展,在编程界的工具都是不断提升的抽象,而且领域越来越细分 抽象层次

一个有意思的问题

问题:搭建系统的搭建系统(高阶搭建系统)是否有存在的意义?

辅助说明:只通过提供 library 的方式不能算高阶搭建系统 抽象程度:高阶搭建系统 > 搭建系统, 问题域范围:高阶搭建系统 > 单个搭建系统,但 高阶搭建系统 >= 子搭建系统的并集

Y

图形开发

2020-08-10

前端本质是一个图形开发的子类,而图形开发最终,最高级的形态应该就是游戏,3A游戏。

一个成功的游戏应该是要涵盖图形技术,编剧,音乐,美术,交互互动,是各大类型技术的集大成者。图形开发虽然只是其中的一个分支,但它是其它分支表达的出口,就像小数点前面的1。

Y

流程化系统设计

2019-09-15

描述

流程化的执行,是一个中心化的调度者。通过流程 + 服务调用的能力,可以把各种各样的服务进行串联的系统。

节点树 + 执行器

中心系统的前端UI界面经过用户的意志产生了节点树(Node Tree),上传到系统中,执行器(Runtime)去执行对应的节点树(Node Tree),就像手写js代码,然后放到服务器node环境中执行一样。

标准服务模块

中心系统本身基本没有服务的能力,只能执行流程,调用服务。所以外围注册的服务模块的决定了流程系统的上限。就像nodejs调后端接口一样

整个体验的过程就像是可视化编程一样,程序 = 逻辑 + 服务 + 执行

Y