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
经过上面这些步骤我们就算出了物体滑行的距离。
多端统一开发
这段时间关注了一些利用 react 实现跨平台开发的方案,几乎无一例外,都不能很好的满足我的需要。我的理想是,特别针对移动端开发,写一份代码,然后编译到不同平台,直接运行,除了修改编译工具配置和少量的全局覆盖以外,不对应用本身的代码做任何修改。
我写 Nautil 的目的也是这样。
目前我接触到的跨端开发方案有:
- taro 京东出品,相对比较成熟,需要理解其工作原理,在写法上需要开发者自己遵循一些规则
- kbone 微信小程序团队自己成员开发的针对微信小程序方案,理念上提供虚拟 DOM 挂载节点,从而让整个应用像运行在 web 内部一样,不需要考虑兼容性问题
- remax 理念上和 kbone 很像,同时提供自己的一整套构建、框架,不足在于需要根据不同的小程序引入不同的库,当然,这个可以用构建工具来 alias
目前我自己使用了 kbone 希望能出现更好的方案来解决我的需求。
Great minds discuss ideas;
Average minds discuss events;
Small minds discuss people——埃莉诺·罗斯福(Eleanor Roosevelt)
webpack umd library only for root, replace exports
webpack 导出的 umd 模块在传入了 output.library 的情况下,会输出四个条件语句,大概如下:
if(typeof exports === 'object' && typeof module === 'object')
module.exports = factory(require("a"), require("b"));
else if(typeof define === 'function' && define.amd)
define(["a", "b"], factory);
else if(typeof exports === 'object')
exports["o"] = factory(require("a"), require("b"));
else
root["o"] = factory(root["a"], root["b"]);
这样的 umd 模式。其中,关于依赖、导出接口名都可以定制,其中依赖部分通过 externals 配置来定制,导出接口名通过 library 来定制。
但是,现在的一个情况是,在第三个条件句,即红色部分,这个部分会在标准的 commonjs 中被引用。所谓标准的 commonjs 是由 commonjs 官方定义的,只有 exports 和 require 两个关键字的模块方案。而 nodejs 虽然遵循 commonjs,但是在其基础上实现了 module 关键字,将 exports 作为 module.exports 的引用,因此被成为 commonjs2。
我们现在去看这个部分,倘若一个模块在遵循标准 commonjs 的情况下,导出如下:
// a.js exports.a = function a() {}
// b.js exports.b = function b() {}
// main.js export.a = require('./a.js') export.b = require('./b.js')
外部使用这个包,实际上应该是:
const { a, b } = require('./main.js')
但是在 webpack 的 umd 模块下使用时变成了:
// bundle.js exports.o = factory()
那么在外面的其他程序去用这个包时就需要变成下面这种方式才可以:
const { o } = require('./bundle.js') const { a, b } = o
这显然不符合我们的期望,我们希望即使 webpack 打包之后,仍然保持原有的使用方式。所以,我写了一个方法来修改这个部分的输出,经过处理之后,webpack 这个部分的输出将会是:
else if(typeof exports === 'object') { var a = factory(require("a"), require("b")); for (var i in a) exports[i] = a[i]; }
即替换掉原来的输出形式,将原本的输出接口直接赋值到 exports 上,这样就保持了原本的逻辑。
具体做法如下:
// webpack.config.js const { bufferify } = require('webpack-bufferify') const plugins = [ bufferify(function(content, file, assets, compilation, compiler) { if (file.split('.').pop() !== 'js') { return } const { optimization } = compiler.options content = content.toString() content = optimization.minimize === true ? content.replace(/exports\[.*?\]=(.*?):/, `function(e,a){for(var i in a)e[i]=a[i]}(exports,$1):`) : content.replace(/exports\[.*?\](.*?);/, `{ var a$1; for (var i in a) exports[i] = a[i]; }`) return content }), ] module.exports = { ..., plugins, }
webpack-bufferify 是我写的一个组件,用以替换 webpack 输出的结果内容。通过上面的处理,就可以实现我们的目的。
收集 scroll 事件
scroll 事件是不支持冒泡的,那么怎么收集呢?当然是在捕获阶段进行收集了。
document.addEventListener('scroll', (e) => { const target = e.target const isTop = Object.getPrototypeOf(target).constructor.name === 'HTMLDocument' const scrollLeft = isTop ? window.scrollX : target.scrollLeft const scrollTop = isTop ? window.scrollY : target.scrollTop console.log({ target, scrollLeft, scrollTop }) }, true)
这样就可以通过兼容方式收集到滚动事件的信息了。
文字旅行者:一种文本加载(类似打字机)效果
我的博客首页会在一段时间后加上一个作品区域,每个区域都会点缀一点点动效。在 Nautil 的部分,我想实现一个效果,就是在 Nautil 这个单词后面,不断切换 Nautilidae, Nautilus, Nautil.js 这几个单词,效果就像用键盘输入、删除再输入的效果。
在谷歌搜索了一段时间后,我确信没有一个库能够完完整整的实现这个效果。它们都实现了输入效果,但是没有回删效果。于是,我只能自己写一个 js 库来实现这个效果。最终的效果你可以在我的博客首页看到。但是,后来我觉得,这是一个非常不错的效果,虽然在我的博客上看到的是一个输入的效果,但是实际上,对它重新思考,会发现它比这个效果的想象空间大很多。你可以用它来断句、给视频打字幕、利用文字翻语音接口朗读小说。它像一个具有生命的灵活工具,因此,我称它为“文字旅行者”(github:TextTraveler)。
遍历文本怎么才能做到对开发者友好呢?我们以“视频字幕”来举例,如果你想写一个视频字幕,你会怎样?对于我来说,我想象中的写字幕的方式,就是在剧本中打断点。那么什么样的方式可以让我们打断点呢?我想到了 ES6 的字符串模板标签。于是,一个字幕效果就出来了:
`你話三年。${[0,100,1]}三年之後又三年,${[0,100,1]}三年之後又三年!${[0,100,1]}十年都嚟緊頭啦老細!${[0,1000]}`
通过在字符串模板中加入占位符,就可以给字符串打断点了。每个断点的地方加入参数,用以控制该断点前面的文字,将会以怎样的时间或行为出现在字幕中。
如何利用这些参数呢?
使用标签函数即可。我写了 travelText
这个标签函数,它可以生成一个 text traveler 用以处理每个段落。
const traveler = travelText`你話三年。${[0,100,1]}三年之後又三年,${[0,100,1]}三年之後又三年!${[0,100,1]}十年都嚟緊頭啦老細!${[0,1000]}`
接下来所有的事情就交给 traveler 去做。但他实际上只是一个记录者,你可以通过 on
方法监听他记录的内容,并作出相应的界面修改。
而为了更方便的实现我想要的那个效果,我又写了 createTextWriter
函数,它直接在一个 DOM 元素中不断的根据 traveler 的记录情况重写 html 中的文本,从而达到我想要的效果。
一切的一切都在 github 上。