数据交互困境
客户端是可以直接连接数据库的, 在当时, 关于怎样获取数据, 怎样存数据的逻辑, 是放在客户端负责的,
之后 Web 的发展, 让更多的逻辑放在了后端, 后端负责连接数据库, 并且将更简单规范的 HTTP (以 Restful 为代表) 请求转换成对应的 SQL 等数据库语句,
在后端完成和数据库的交互 (此处的数据库指广义的数据库, 可能包含各种中间件, 各种形式的存储等),
前端只需要消费这些简单的接口即可, 看起来是降低了前端的负担 (无需思考如何和后端的数据库等服务交互, 后端已经封装好了).
以 Restful 为主的这种后端 API 思路仍然出现了很多问题,
我们发现实际的前端场景下, 前端往往需要对数据有更精细的控制:
前端显示一个评论列表, 这里只用到了每个人的头像和昵称, 但是后端提供的 profile 接口却会连着其它的手机号, 个性签名等等一系列信息全部返回了, 这个时候后端就返回了很多无用信息.
如果后端将用于操纵数据的接口封装的抽象层级过高, 会出现无法满足前端的灵活使用的问题
除此之外还有处理一对多关系, 比如一个班级里面包含很多学生, students 是 class 的一个属性, 那使用 GET 请求请求 class 的时候, 是否应该返回它的子属性 students 呢? 如果前端希望能够控制, 那往往又需要引入新的 query parameter 来控制, 这又增加了协商成本和文档成本.
由于接口本身的灵活性差, 导致前端程序员需要思考使用什么样的顺序和方式调用接口,
才能实现一个功能, 很多时候前端需要被迫拼接, 堆积接口调用,
甚至会出现在前端手动递归调用后端接口获得一个树状文件夹数据这种现象
语言也可能不同, 导致前端遇到接口问题时, 必须要和后端协商, 后端再做出改动,
根本原因:
1、第一在于 Restful 本身的灵活性问题,
2、其次在于简单的后端查询业务由于和前端的业务深度耦合
这部分工作应该收敛到一个工种上, 并且考虑到传统的后端 CRUD 的代码很大程度可以自动生成
所以我们接下来要做的事可以总结为:
把常规的后端业务实现的任务收敛到前端工种,
并且通过更好的 API + SDK + P/F/SaaS 让常规的后端业务尽可能自动化+服务化,
从而淘汰掉传统的后端 CRUD 工种, 提升整个系统的效率.
前后端桥梁:http?SQL?GraphQL
后端希望暴露给前端一个安全的操纵数据的接口, 何以满足这样的需求?
===> http的表述沟通语言不够,那设计下一代表述语言是否可行?
使用 HTTP Path + Method 的这种方式显然不够强,
关于数据关系模型是一个关于集合的数学理论
基于树状关系(HTTP Path 是一种树状的命名空间) + 方法(Get Post Delete Put 等) 的描述方式过弱,
远远无法支撑实际的数据操作;
直接使用 SQL 语言行不行??
问题:
1、一方面是安全性的问题(可以通过代理+一些权限验证方式解决, 不是问题的关键),
2、 SQL 语言这种模式和前端的语言环境太过割裂, 前端被迫进行字符串拼接,
与之相对的是 MongoDB 的查询语言, 其和 javascript 语言的贴合度较之 SQL 要好很多,
前端程序员可以用很自然的方式写出一个查询语句.
基于 Restful 这样的模式, 让很多的后端代码变成了非常简单的 CRUD 代码, 很多代码就是为了将 restful 接口转化成 SQL 语句, 大量的时间被耗费在了这些无聊的事情上, 降低了开发效率, 这些简单的操作应该被自动化.
GraphQL 应运而生
用一种更优雅的方式实现了声明式数据请求格式, 相较于 Restful, 它更像是 SQL 这种声明式的语言, 从 Restful 到 GraphQL 的转变, 对于前端来讲则是命令式到声明式的转变,
从思考 “what do I need to get I want“ 到直接思考 “what I want“
但是仅仅依靠 GraphQL 还是没解决这两个问题:
- Restful 时代的 CRUD 代码转变成了 GraphQL 的 resolver 代码, 后端还是需要手动写, 或者使用 codegen 工具来生成代码, 仍然没摆脱样板代码的桎梏.
- 前端对数据的请求的状态管理: 重复请求问题, 数据依赖更新问题…
前端对数据的请求的状态管理-状态绑定
GraphQL解决了 表述力度的问题,从思考 “what do I need to get I want” 到直接思考 “what I want”
前端对后端表述语言:要数据不再局限于 xx方法+xxx接口,而是直接声明我要什么数据
但,GraphQL还是没解决这两个问题:
1、后端还是要手写,只是减轻了语言沟通成本,本质还是需要后端搭建前端与sql沟通的桥梁
2、前端对数据的请求的状态管理: 重复请求问题, 数据依赖更新问题…
在前端的业务场景下, 数据依赖可以分为两类:
一类是纯前端的数据绑定,
一类则是涉及到后端数据的绑定
状态的绑定也是前端这种响应式系统和编译器这种转换式系统的最显要的差别,
这是 Web 要处理的最核心的问题之一.
响应式系统(reactive system) 和 转化式系统 (transformational system) 的最大区别在于: 响应式系统更像一个状态机, 输入与当前的状态才能决定输出;————前端 转化式系统(典型例子如编译器) 则更着重的是输入和输出; ——————编译器
我们可以看到几乎所有前端框架, 无论基于模板的, 还是 jsx 的, 都解决了一个核心问题就是状态之间的绑定, UI 状态和内部 js 变量的绑定等, 数据之间是有一个依赖关系的, 它们可以用一个依赖图表示.
纯前端数据绑定
前者的绑定只发生在前端, 比如一个 js 内部的变量和 的 value 的绑定
前后端数据绑定
后者则涉及到前端状态与后端状态的绑定, 比如, 一个评论列表与后端的评论数据做绑定
目前的思维方式是:input交互更改后,由于前端主动调用fetch请求,向后端要数据;——命令式做法
前端的input的value 与 js的内部变量,已经可以做到 dom data -> js data,相互映射 —— 声明式做法
未来前后端的绑定, 也应该是像纯前端绑定一样简单, 命令式, 消除显式的 http 请求代码
一个很有意思的点,作者把前端交互请求后端数据,调用api得到数据这个我们习以为常的场景,戳穿了本质:
===> 不也是一套jq的命令式编码思维吗?
评论框输入评论->发送->评论列表更新:
当新建评论后, 手动重新请求评论 api, 然后得到更新,
这种做法像极了使用原生 DOM 和 js 处理前端 UI 和数据的绑定关系, 手动维护状态, 手动调用 DOM 接口
所以:
# 前端的UI列表 与 后端的评论数据 可不可以也是 双向绑定 的关系呢?
社区还真有这样的解决方案!!
===> 前后端绑定 的相关工具: 代表性的有 React Query (2019 年建仓) 和 Apollo GraphQL Client (2016 年建仓).
假使今后的开发模式是:基于组件的协作流,一个复合型应用需要:将这些组件组合起来,填充到页面中,
下面来仔细探讨下,在这样的开发模型里,前后端绑定如何实现?
拿一个最简单的例子,在传统的 Web 思路中, 一个填写个人信息的表单的处理方式:
1、会将一个组件内部的 state 和表单的 <input> 的 value 属性做绑定,
2、当用户点击 "提交" 按钮时, 执行一个 onSubmit 方法, 方法内部将数据作为 body,
3、一个 POST 请求将数据传给后端.
使用”面向绑定”的方式理解这个问题的时候, 这个问题其实变得很简单:
1、绑定 后端的个人信息数据 <-> 组件内部的状态
2、用户点击提交前, 绑定处于 out of sync 的状态
3、点击 “提交” 进行同步, 进入 sync 状态
与此同时,因为页面的右上角可能有你的头像, 那个头像的组件也和你的个人信息的子属性 avatar 绑定, 这时, 当表单进行 sync 后, 由于头像组件也绑定了有依赖关系的数据源, 所以数据层会自动更新头像:
当个人信息点击提交后, 数据状态管理层会自动检测到 Avatar 组件所依赖的数据的父节点发生了 Mutation, 从而自动触发 refetch, 获得更新后的 Avatar.
下面问题拆解为:如何做到数据依赖变化监测,以及对这个原理设想建模型
1、数据的依赖变化问题
=> 如果我们能够保证所有的数据源的请求都是以 GraphQL 的话, 那么我们可以使用 GraphQL Query 作为前后端数据绑定的声明式语法, 而根据对 GraphQL + Endpoint 构成的实时数据图依赖分析
——前端完成
如何解决自动形成依赖管理的问题?
为了更好地在下面讨论组件, 我们先将组件从复用性的高低可以分为两类,
拿表单组件举例
1、通用型组件:
组件尽可能地让自己的能力通用化, 自己内部不维护网络请求等信息, 而是根据传入的 props 来动态地决定与后端数据的依赖, 以及数据源和组件内部状态的绑定关系
exp:
通用型表单组件从外界接受数据源, 以及请求后的数据的属性与组件内部的对应关系, 除此之外, 往往还需要提供一个数组用来生成相应的表单列表, 数据从哪来, 数据和表单怎么对应, 表单的 validation 函数, 都从外界传入, 组件本身只实现逻辑框架和设计样式.
2、自治型组件.
通常可以独立使用, 其自己内部实现了网络请求等逻辑, 但是通用型较差, 通常只能实现特定功能, 类似于 iframe.
exp:
很可能是一个飞书投票组件, 从飞书小程序中生成一个组件实例, 就可以直接使用, 它内部实现了相应的数据逻辑, 用户可以直接填写, 就可以在飞书投票后台看到该组件收集的投票信息.
对于纯前端的绑定来说, 本质上还是组件的树状结构组合, 很多页面可以通过代码的方式写成一个大组件,
不管是单一功能型组件还是页面, 都可以统一的视为组件, 使用统一的方式看待和处理.
如何解决后端程序员仍然要写很多 GraphQL Resovler 代码?
从数据库的 schema 出发, 是可以生成一些默认的 Rosovler 代码的,
但是需要程序员手写的原因在于, 默认生成的 Resolver 代码往往缺一些和具体业务相关的东西
===> 使用约定大于配置的思路
为用户提供默认的 Resolver 能力, 并给用户提供自定义的 Custom Resolver 的接口
先行者: https://hasura.io/
它们也是从数据库出发, 搭配权限验证策略, 自动生成统一的 GraphQL API 网关, 供各种形式的前端来调用, 大幅简化传统的 CRUD 代码:
我设想未来的 Web App 的开发, 可能会有很多的数据源提供提供商,
将常用的公开 API 转化成统一的 GraphQL API 网关供调用, 这些数据源会像是一个 Market, 组件有组件市场 (Component Market), 而数据源也有数据源市场 (Datasources Market).
对于数据源市场来讲, 每个人都能在其上发布数据源, 可以像 Github 一样做私有付费(Pay For Privacy) 的策略, 和组件一样可以供其它人消费, 然后很多程序员可以快速借助统一的数据源和组件市场搭建出一个功能完整的现代 web app:
数据源平台会依托于基础存储服务和 Serverless 服务, 借助在线的 Schema Editor 来编辑关系型数据 Schema, 借由 Schema -> GraphQL Generator 自动生成 GraphQL Endpoint, 再由平台提供的函数计算接口,
在在线 IDE 中完成函数的逻辑的开发, 这些函数可以在数据源中扮演 middleware, trigger, intercepter 等的角色, 为数据源能力提供一些补全和增强.
(看不懂了说实话)
目标:数据源平台会作为现代 web 开发的后台的最高层次抽象, 现代的业务侧开发者往往只需要在数据源平台上进行简单的操作即可配置出数据源供前端消费.
思路2:
采用 将服务端与客户端代码放在一起的开发模式
代码中没有显式的 http 调用, 直接通过函数调用的方式直接对数据库做 Query
构建困境
目前的开发模式:
DevOps 平台是一个资源消耗大户: 每当应用仓库的 release 分支发生 commit 的时候, 往往就会触发流水线的测试, 构建, 部署等一系列运维操作, 而目前的生态,
前端的构建涉及到依赖的拉取, 依赖图分析, 打包依赖, 打包产物优化等步骤,
一次完整的构建花费的时间可能是分钟级的:
总结一下, 目前上图的这种构建/发布模式存在这几个重大问题:
- 修改一个文件中的一行代码, 触发全量构建, 大量算力, I/O浪费.
- 上游的更新无法触发下游流水线更新, 或者说下游无法 “观察” 上游的更新
包和包之间的依赖, 是一个 有向无环图
一个包得到更新, 往往依靠迭代新的版本号来解决, 示意图:
当 Button 组件得到更新之后, 比如版本从 3.2.1 迁移到了 3.2.2, 这时候 Web App 应用本身是不会收到这个通知的, 它必须手动重新运行一次流水线, 才能将更新的依赖 3.2.2 打包近构建产物中.
解决方案1: cdn动态链接库
解决方案2:浏览器 esmodule
解决方案3:github submodule 子仓库,每次common更新了,会将整个应用进行更新(触发应用构建的流水线)
esm 能带来的潜在优势:
- 全局依赖缓存.
- 大幅降低流水线构建的计算和 I/O 负担, 甚至可以跳过构建这个步骤.
- 上游更新后, 用户加载页面时, 可以直接加载更新后的组件的代码, 达到了真正的敏捷更新.
基于源码的,意思是 common 和 activity还是在一个仓库里
下游如果想锁定上游的版本?
===》 可以直接用 commit hash 锁定, 这样就不会加载到更新的组件版本, 保证了下游的一致性.
esm带来的优势:全网范围内的cdn
比如浏览器层面对react进行缓存
这些网站都使用同样的 CDN, 这种 CDN 应该专为浏览器侧的 esm 做了优化, 支持 HTTP2/3 等新的协议, 支持 semver…
第一个将这个思路 built in mind 的, 应该是 deno, 它原生支持 http import, 为服务侧基于 cdn 的 import 的开发做了准备, 与之配套的就是相关的开发套件, 比如 VSCode 相关插件的支持, 以及 CDN for module, 对于这种 CDN, 已经有了类似的产品: Skypack: search millions of open source JavaScript packages.
===> 可以深入研究下
总结,未来大概率会:
CDN for module + http import 的开发模式
代码管理困境
没有包管理的时代, 人们的应用都包含了全部代码, 有了包管理后, 人们倾向于每个包都有自己独立的 git 仓库来管理, 但是有时候又想将一些包放在一起来开发, 于是又有了 monorepo:
Monorepo
我们引入 Monorepo 是因为我们想要同时对一些包做改动, 然后统一发布更新, 如果分开, 程序员需要每天在不同的仓库中辗转, 并且需要不断地 publish & update 才能在另外的包用到更新的包
但是引入了 Monorepo 后, commit history 就混入了各种包的 commit, 不方便追踪某个模块的改动,
git submodules子仓库
父仓库可以依赖于其它的子 git 仓库, 在父仓库做的 commit 不会进入到子仓库中, 同时在开发父仓库的时候, 又可以修改子仓库的代码, 甚至进行 commit, 它很好的平衡了 作为依赖引入 和 想要随时修改 的两个需求, 实测好用.
认为新的包管理模式, 应该是和 Git 仓库绑定的
我们使用 Git 来进行代码的管理, 发布到注册中心的包, 仍然是一个 Git 仓库, 只不过是一个 remote history, 当它作为依赖拉取的时候, 会连并构建产物一并拉取, (git clone -depth 1 拉取最新的 snapshot) 而如果想要即时修改该包并且 push 更新, 则可以使用提供的新的命令行工具进行 dependency 到 git submodule 模式的转化, 转化后, 会变成 git submodule, 你就可以即时的修改这个包了.
而构建步骤和 semver 相关的东西, 可以深度集成流水线和一些 tag, 下游使用上游依赖也仍然可以锁定版本, 这些都可以解决.
所有的包在 registry 中都是作为一个 git 仓库存在的, 而本地开发的时候, 既可以将其作为依赖, 也可以将其一个命令转化为 git submodule, 这样就可以灵活的协调依赖和快速修改反馈之间的矛盾了.