前端通过类型系统实现mock数据,解决接口约定问题
前段时间写过一个库 tyshemo,主要用于在运行时对数据进行检查,特别针对后台接口返回的数据进行检查,做到对后台返回数据类型和格式的实时监控。后来,在工作中,遇到前端界面写完了,后台接口没出,不得不等着后台写完接口后在联调的尴尬场景,于是就想,能否前端同学先写好一个文档,后端同学按照该文档输出?(使用 graghql 的同学请撤离)
想到这个点之后,我就思考,既然我写了一个类型描述系统,为何不直接将这个描述系统的实例,转化为描述文本呢?例如:
import { Dict } from 'tyshemo' export const BookType = new Dict({ name: String, price: Number, }) // 使用方法:BookType.assert(book)
为什么我不能直接输出一个文本:
{ name: string, price: number, }
这样非常清晰,前后端,甚至任何人都能知道我的一个数据结果和它每个节点(字段)的类型都一目了然。
于是我开发了一个 Parser,用于将类型实例,转化为描述对象,通过 JSON.stringify 就可以转化为描述文本。同时 Parser 还可以将描述对象返回去转化为可以用来做类型检查的实例。这样,前后端实现了可传输类型检查的描述对象,如果后端是通过 nodejs 实现的,那么传输这个描述对象,就可以在前后端都运行它。
通过 Parser,我想到,可以把前端写好的类型实例,转化为文档输出,在后端同学没有动工之前,前端写好实例,输出描述文本,和后端同学一起过目讨论,确认无误后,前端按照这个结构开发即可,后端同学对照这个结构和类型约束输出接口数据。
但是,单纯有了约定,并不能满足前端同学对数据的需求,因为没有数据就不能渲染界面,无法进行下一步的操作,总不能在脑海中想象结果吧。于是,我想,既然数据的类型是确定的,那么,就可以通过一些随机算法,按照数据类型输出对应的值。于是,我撰写了 Mocker 用来生成这样的数据。
import { Parser, Mocker } from 'tyshemo' import some from './some.type.js' const parser = new Parser() const desc = parser.describe(some) const mocker = new Mocker() const mock = mocker.mock(some)
但是,这样的工作方法太过原始了,有没有一个办法,可以更好的表达一个工作的流程呢?
我决定写一个基于 express 的服务,配置所有 api 接口的详细信息,启动这个服务之后,就可以在浏览器中看到对应的接口文档,或者启动服务后,可以将前端的接口指向 mock 服务的地址,让后台返回 mock 数据。用了一个周末,我写完了 tyshemo-service,虽然界面不怎么好看,但是功能实现了。
import TyshemoService from 'tyshemo-service' import { BookRequestType, BookResponseType } from './book.type.js' const server = new TyshemoService({ basePath: '/api/v2', errorMapping: { 10001: '数据库连接错误', 10002: 'xx错误', }, data: [ { name: '第一组接口', items: [ { name: '第一个接口', description: '这个接口是用来干嘛的?', method: 'get', path: '/book/:id', request: BookRequestType, response: BookResponseType, }, ], }, ], }) server.doc({ port: 3001 }) // 启动文档服务器
其中 BookRequestType 描述了请求参数的类型。由于这里的 method 是 get,所以,请求参数实际上表达的是 searchQueryParams 的字段及其类型。如果是 post 则会是 postBody 的字段类型及其结构。BookResponseType 描述了返回结果的数据结构,以及每个字段的数据类型。需要注意的是,实际上,在前端代码中 BookRequestType 都是可以直接使用的,例如,在请求 book 的 ajax 返回结果处,调用 Ty.expect(data).to.be(BookResponseType) 来检查 ajax 返回结果是否符合预期。
上面的代码,让 nodejs 在本地 3001 端口起了一个文档服务,浏览器打开,就可以看到文档。让后端同学访问你本地起的这个页面,就可以让他对照文档进行接口输出啦。
开启 mock 服务,你需要使用下面的代码:
server.mock({ port: 3002 })
将本地开发的代理,指向 3002 端口,就可以 ajax 请求到 mock 服务器就可以了。具体配置,你可以去 tyshemo-service 的仓库阅读更多配置细节。
之后,我又想,我还可以起一个服务,让前端同学去监控后端同学写好的接口,是否符合类型和结构要求。因为我们传入了 request 对应的类型,所以用它来 mock 一个请求数据也很简单。
server.data[0].items[0].test = [ { frequency: 6000, // 6 秒一次 name: '测试用例1', params: { id: 123 }, // 替换 path 中的参数 request: { price: 12 }, // 指定请求参数中的部分字段 }, ] server.test({ port: 3003, target: 'http://some.com:8008' })
打开本地 3003 的网页,可以看到一个测试页面,这个测试页面虽然丑陋,但是它可以帮助前端开发去监测后台接口是否是按照文档输出内容的。
通过上述一通操作,就可以让前端在没有什么成本的情况下快速完成自己的开发了。
最后留个悬念,既然我们能把接口的配置都集中在这里了,我们是否还可以提供更集成的方案,把前端的 ajax 也集成进来,也就是说,对于单一一个接口,前端需要写好该接口的各种请求配置、类型、测试等信息。需要发 ajax 的时候,调用某个方法得到数据,同时干完数据类型的检查。需要文档和 mock 的时候,启用服务。这样的方案,也不是不可行。
小评 Svelte.js 框架
Svelte是2019年才火起来的新前端框架,它的热度虽然不如react和vue,但是确实很热,大有冲击RAV之后第四大框架之势。对它的文档进行阅读之后,我来说一说自己的一些看法。这些看法不一定成熟,毕竟没有使用它开发过项目,但是话说回来,目前在国内,使用它进行前端项目开发的可能性仍然停留在观望阶段,没有一家公司会尝试一个不一定发展很久的框架。
Svelte的第一眼真是非常爽。你说它像vue不错,但是它比vue更爽:
<script>
let a = 0
</script>
<div on:click="{() => a ++}">{a}</div>
如此漂亮的语法,简直像是重新定义了HTML。而且这个写法和我当初写virtual dom框架的初衷一样,感觉相见恨晚。
但是,当我发现它发明了新语法$:之后,就有些开始不是很舒服了。
$: b = a + 1
这个语法要实现的是vue里面的计算属性,它具有依赖响应的效果,当a发生变化时,b也会随之发生变化。但是,为了计算属性的效果,我觉得可以忍,毕竟不麻烦。
当发现它来发明了新的循环语法时,我有点开始不适应了。
{#each books as book}
<div>{book.name}</div>
{/each}
这……感觉回到了php模板时代,怎么说好呢?有点拒绝。
再到后面,它的事件冒泡居然要声明:
<div on:click>
<Inner />
</div>
如果不声明,事件不冒泡。此时,我内心有种MMP的感觉,这是反人类么。。。
随后的文档中,出现了各种奇淫巧技,我心想啊,未来这东西还能维护吗?其中store部分,蹭了rxjs的热度,提供了多种模式的存储器,把数据管理搞出了越来越难理解的高度,也是大写的服。
Svelte虽然想要做出点什么不一样的东西出来,但是我个人觉得,有点太过了,像玩具一样,不具备可长期维护的可能性。而且它依赖编译,和vue不同,vue可以直接运行时,在html文件中引入后就像jquery一样,马上可以撸。总体上而言,svelte除了一开始让人眼前一亮外,没有超出vue所提供的功能,很难说服开发者从vue转向svelte。更别提react了。
在我看来,react仍然是最好的框架,除了jsx之外,它完全遵循js本身的运行原理,没有解析器去自己实现一套语法(jsx是语法糖,可以不用,写原生代码)。也正因为如此,它才有可能是未来10年都可能持续可维护的代码,对于开发者而言,他可以很容易从一个文件中读懂所有的逻辑,因为它本身就是js,没有其他需要理解的知识(jsx除外),而vue即使很简单,也需要了解它的模板语法。
不过,svelte也不是没有可取之处,例如它的编译,竟然可以在浏览器中完成(看它的在线教程编辑器),可见他们团队在编译这个中间环节下了大功夫,才能在撰写语法上如此优雅。
在 nautil 中发明 react 双向绑定的功能,原来早有前人实践过了,而且还是 react 官方博客发布。从思路和用法上几乎一样,只是官方方法仅支持 input 等表单组件,而无法满足自定义组件的使用。不过,值得骄傲的是,我发明的 createTwoWayBinding 函数还是有不错的思想,而且借助了 Proxy 的力量。总之,自己以为很牛叉的东西,可能前人早都做过了,我们总是在炒冷饭中自鸣得意。
babel remove class static properties
在开发 nautil 过程中我发现,react 本身会在 production 模式下去掉 prop-types 的校验,但是在我们自己写的业务代码中,会给每个 class 增加 propTypes,于是找到了一个 babel 插件,这个插件可以去掉 react 组件类中声明的 propTypes 这个静态属性。于是我想直接用这个插件,可以删除 nautil 中的 props 属性吗?最后发现是不以的,这个插件写死了被移除的属性是 propTypes,所以,我必须自己写一个插件来实现这个功能。经过对该插件的改造,我增加了一个 properties 参数,可以实现移除任意类的静态属性。并且发布到了 npm,你可以在这里了解。
echarts x 轴最后一个刻度右对齐
在我们的数据图表中,x 轴的最后一个 label 应该是右对齐的。echarts 官方没有参数来配置这个功能,导致文字被截掉了半截。挺失望的,这种情况实在是太有需求了。在不能修改源码的情况下,我们只能通过自己手动来实现这个需求。
xAxis: { type: 'category', boundaryGap: true, data: xAxisData, axisTick: { show: false, }, axisLine: { lineStyle: { width: 1, color: '#ccc', } }, axisLabel: { color: '#89889C', showMinLabel: true, showMaxLabel: true, formatter: (() => { let currentYear = '' return (value, index) => { const year = value.substr(0, 4) const month = value.substr(4, 2) const shouldNotShowYear = index > 0 && currentYear === year const isLast = value === xAxisData[xAxisData.length - 1] const text = shouldNotShowYear ? month : year + '/' + month const label = isLast && !shouldNotShowYear ? `{a|${text}}` : text currentYear = year return label } })(), rich: { a: { width: 70, align: 'left', }, }, },
上面红色部分是关键,我们要让最后一个 label 靠右对齐,但是它受到宽度的限制,所以我们干脆来了一个宽度,直接将 label 的宽度和文字的整体高度相等,这样就可以让文字显示完整了。
但是,你可能会说,我怎么知道文字的总体宽度是多少?label 的字数可多可少,不可控啊。我会告诉你,有一个东西可以帮助你在 canvas 中获取文本的宽高,你听说过吗?通过 canvas 计算文字宽高之后,赋值给 rich 里面的 width。echarts 内部会根据你给定的宽度重新布局 x 轴文本的展示,由于我们设置了 showMaxLabel,所以,无论如何最后一个 label 都会展示,如果空间不够,它会把前一个 label 隐藏起来。
touches, targetTouches, changedTouches
在移动端 touch 事件中,有 touches, targetTouches, changedTouches 这三个属性,它们都是一个 Touch 对象的列表,但它们所代表的意思不同:
- touches: 当前在屏幕上的触点,因为同一时间,用户可能有多个手指同时触摸在屏幕上,touches 中保存了所有触点信息
- changedTouches: 触发当前这个 touch 事件的真正触点信息,当然,也可能有两个以上的手指同时触发,比如 touchmove 事件,可以由两个以上手指同时触发
- targetTouches: 列出 touches 中与 touchstart 事件时所在 target element 相同的 Touch 对象,如果由于 touchmove 移动出了 touchstart 时的 element,那么 targetTouches 不会包含不在 element 中的那些触点信息。
因为 touch 事件和 mouse 事件不同,touch 事件指出事件发生时屏幕上的所有触点信息,表明可能存在多个触点。但是由于事件是不连续的,为了区分触点,Touch 对象会有一个 identifier 属性,它是一个 0 开始的索引值。手指放到屏幕时,会用一个 identifier 标记它,这样开发者就可以知道当前 changedTouches 中每个触点和其他触点的具体对应关系了。
带摩擦因子的加速度距离计算公式
因为要写一个库,用以模拟现实世界的运动逻辑。在经典力学里,我们常常用匀速运动来说事,但现实中哪有那么好的事情,摩擦力等阻力会让速度慢下来,重力加速度会让速度越来越快等等。
已知:某物体以 v1 的初始速度被加速前进,t1 秒后,速度达到了 v2(v2 > v1)。
求:t2(t2 > t1)时停止加速,物体滑行多长距离之后停下来?
当看到这个题目,作为文科生,我完全懵逼,把毕生学到当物理学知识翻出来也找不到快速解决的办法。所以,只能通过自己的推演慢慢解决这道题目。
分析
这道题的特别之处在于受到了阻力影响,且运动过程分为两段。那么接下来我们来进行受力分析。
第 1 阶段由于受到比 f 大的 F 力的助推,所以呈实际受力为 F - f 的推力,匀加速运动。第 2 阶段由于只受 f 阻力,所以呈匀减速运动。
关于加速运动用到的公式有如下:
- 求加速度公式 a = (v2 - v1) / t
- 求距离公式 s = v1t + at2/2
且公式中如果 v1 为 0,那么更加简单。
由于第 2 阶段物体停下来,只受 f 阻力影响,因此,我们只需要知道 f 所带来的减速度,以及 t2 时的即时速度即可算出滑行需要的时间,进而算出滑行距离。
开始解题
1.计算第一阶段加速度
a1 = (v2 - v1) / t
2.计算到达 t2 时的速度
vx = a1 * t2 = (v2 - v1) * t2 / t
3.计算计算阻力减速度
即时速度比较好算,但是 f 所带来的减速度是多少呢?
根据牛顿第二定律 F = ma。因为质量固定,所以摩擦力 f 所产生的减速度 ax 是固定的。同时根据动摩擦力公式 f = µFn = µG = µmg(µ 为动摩擦因子)。质量和重力加速度都是固定的,所以最终 ax 的大小仅仅和动摩擦因子有关,也就是物质表面的粗糙程度,ax = ƒ(µ)。
现在,我们假设在没有阻力的情况下所产生的加速度为 a0:
kµ = ax / a0
即损失加速度与理想状态(无阻力)加速度之比,k 为一个常量系数,表示摩擦因子与加速度损失之间的某种特定关系,那么
ax = ƒ(µ) ≈ µa0
而对于同一个事物,µ 是固定不变的。又因为
ax = a0 - a1
所以
ax = µa0 = a0 - a1
=> a0 = a1 / (1 - µ)
=> ax = a0 - a1 = a1 / (1 - µ) - a1
=> ax = µa1 / (1 - µ) = µ(v2 - v1) / t(1 - µ)
现在我们得到了摩擦力带来的减速度。
4. 计算停下来要花的时间
我们就可以得到从 t2 开始减速到停止所需要花费的时间:
tx = vx / ax
5. 计算停下来之前滑行的路程
在该时间内物体前进的距离是多少呢?
sx = ax * tx2 / 2
经过上面这些步骤我们就算出了物体滑行的距离。