132019.7

TySheMo 2.0 发布

TySheMo 2.0 主要更新了两个功能,一个是输出的错误日志,另一个是新增试验中的 Parser 功能。

上一个版本在进行数据类型校验的时候,在遇到第一个校验失败时,就会停止继续校验,这对于对象类型的校验会导致开发者不知道真实的错误包含了那些,只知道第一个错误,因而不能有针对性的一次性修改完毕。而 2.0 则增强了日志输出功能,在进行对象、数组校验时,会遍历所有的节点,把每一个节点上如果存在的错误一次性全部返回,并且提供了 format 方法来让开发者可以自定义输出的内容。

第二个功能是新增的试验性功能:Parser。这个功能的主要作用是对数据类型进行文本描述。在上一个版本中,我们必须通过内置的方式创建一个类型容器来进行数据类型校验。但是在一些需要跨端通信的情况下,我们无法提供这种能力。Parser 则是提供这种能力的一个尝试。假设你的前端应用在某些情况下,需要从后台拉取一个类型配置,这个时候,就可以用上文本描述。后台返回一个基于字符串的 json 文件,在前端通过 Parser 对 json 进行解析之后,得到一个前端可用的类型容器。这就是新增的 Parser 功能。但这还是一个试验性的功能,虽然可用,但还未到非常标准的程度,希望后续可以将这个功能及其附带的需求完成。

22:01:16 已有0条回复
072019.7

学不动系列:新前端框架 Nautil,哇~

基于 react 的除了 Next.js 其他的所谓框架我都只想说,鸡你太美!React 实在太香了,但是实战开发起来却又不怎么好弄。让我们来看最新的 roadmap:

来自:react-developer-roadmap

我就想写个网页,何必这么残忍?面对这个画面,我都有贴大哥表情包的冲动。但是我忍住,毕竟我还是想写代码来丰富我的人生的。

在 react 的生态中,我们不难发现非常优秀的项目。例如,跨组件的通信怎么办?来 redux 吧!redux 组织好复杂?上 redux-saga 吧!异步咋解决呢?来 redux-thunk 真香!使用特定 state 也挺麻烦?搞 reselect 如何!哎呀呀,还是来 rematch 或来套国产的 dva 也行……

你让我写个应用,我除了要花精力去解决打包编译工具的问题之外,还要来纠结到底要用哪个方案。真的很烦唉!其实,就像当年用 jquery 用爽了,扛起 backbone 或更狠上 angular 就开干。我写了这么多年 react,想要的是一套框架,拿过来就开撸的那种。

从 roadmap 中,我们看这个区域,也就是 react 生态的状态管理和表单数据管理这个部分。是不是复杂!很复杂!那是因为 MobX 还没有扩展开来聊。害得我又想贴表包……

我们能不能简化 react 应用中的状态管理?能不能把数据请求这块处理的更加优雅?能不能提供更靠谱的 form 模型?有的。

Nautil 框架来了!!!

一款基于 react 的 js 前端框架。在我看来,一个前端框架需要具备应用开发中必不可少的部件。而且要好用,方便开发者理解和写代码。Nautil 一次性提供体验超爽,而且还有趣的:

  • UI 渲染
  • 路由
  • 状态管理
  • 数据仓库
  • 数据类型检查
  • 跨端开发解决方案
  • 多语言国际化

让我们来举个例子,就拿复杂的 state 管理来开刀吧。想想你在以往经验里面使用 redux 是怎么用的,有没有在准备方案阶段就很纠结和心累?如果你用 Nautil,不需要纠结,因为你没得选,只有一种全局的状态管理方案。

import { Component, Store, ObservableProvider } from 'nautil'
import { Section, Text } from 'nautil/components'

// create a store
const store = new Store({
   name: 'tomy',
   age: 10,
})

class App extends Component {
  render() {
    return (
      <ObservableProvider
        name="$store" value={store}
        subscribe={dispatch => store.watch('*', dispatch)} dispatch={this.update}
      >
        <Page1></Page1>
      </ObservableProvider>
    )
  }
}

class Page1 extends Component {
  static injectProviders = {
    $store: true,
  }
  render() {
    const { state } = this.$store
    return <Section>
      <Text>Hi, I am {state.name}, and I am {state.age} years old.</Text>
    </Section>
  }
}复制代码

啥?你没看到怎么管理状态的?不怪你,因为它实在实在是太方便了,因为在 Nautil 里面,你可以没有全局的状态管理,但是你一定会有某个数据是全局的(准确的说是跨组件),你只需要用 ObservableProvider 这个组件去提供就可以了。然后在很深的组件里面去使用 injectProviders 来注入这个被提供的数据。恰巧的是,Nautil 提供的 Store 是一个可被观察的数据容器,使用 store.watch 来监听它的数据的变化,并在变化的时候触发更新操作。

还没明白?

这里的 store 就是你的状态管理器了啊!!!store 里面存着整个应用被共享的 state,你可以在任何地方去,任何地方改,任何地方删,都会通过 store.watch 的部分触发应用更新。也许你没听明白我的意思。我的意思是,你甚至可以在 react 应用之外去修改数据都是可以的,只要你在任何地方执行一下:

store.state.age ++复制代码

你的界面就会发生变化。是的,即使把你的 nautil 应用和 angular 应用混在一起,共享一个 store,也是可以的。同时,你还可以通过 watch 来收集每一次数据的变化,在必要时,把收集起来的数据通过 store.update 来复原数据。

它是不是完全超出了你对 react 状态管理的理解?没关系,还有一个东西会超出你的理解,那就是从后台 api 拉取数据。

你有没有想过,为什么那么优秀的 redux 会变得那么臃肿?因为数据是前端应用的命啊,一个不需要从后台 api 取数据的前端应用,除非是工具或游戏,否则就是没有灵魂的应用啊!所以,redux 出来之后,包括 react 本身,都必须面临异步数据请求的问题。以 react 本身而言,它一开始完全没有机制去处理,一个数据必然存在两种状态:数据还没有从后台拉回来的状态,已经拉回来的状态。在数据没有拉回来的时候,把界面显示出来,等数据回来了,再闪一下,哦豁,用户都可以化身产品经理给开发提 bug 了。

Nautil 怎么解决?

import { Component, ObservableProvider, Depository, Prepare } from 'nautil'
import { Text } from 'nautil/components'

// set data sources information
const datasources = [
  {
    id: 'articles',
    url: '/api/articles',
  },
  {
    id: 'tag',
    url: '/api/tags/{tag}',
  },
]

// create a data depository
const depo = new Depository({
   expire: 10000,
})

// register data sources into depository
depo.register(datasources)

class App extends Component {
  render() {
    return (
      <ObservableProvider
        name="$depo" value={depo}
        subscribe={dispatch => depo.subscribe('articles', dispatch).subscribe('tag', dispatch)}
        dispatch={this.update}
      >
        <Page1></Page1>
      </ObservableProvider>
    )
  }
}

class Page1 extends Component {
  static injectProviders = {
    $depo: true,
  }

  render() {
    const depo = this.$depo
    const some = depo.get('tag', { tag: 'some name' })

    return (
      <Prepare isReady={some} loadingComponent={<Text>loading...</Text>}>
        <Text>{some.name}</Text>
      </Prepare>
    )
  }
}复制代码

创建一个数据仓库来管理从后台 api 接口拉取的数据。在业务代码和后台 api 之间,不要直接打交道,而是通过数据仓库整合。业务代码,只需要从仓库中 get 数据即可,这个 get 是同步操作,不需要等待。同时,仓库是可观察的,通过一个 subscribe 方法对仓库进行观察,如果发现对应的数据发生变化,那么立即更新界面。对于仓库中还没有对应的数据时,使用 Prepare 组件来提供一个 loading 效果。

听上去好像还挺顺的对不对?但是,等等!!!我什么时候发 ajax 去请求数据?

你真的不需要关心 ajax 的问题,真的!你只要 get, get, get~ 我能理解你理解不了,只是现在。只要你用用,什么 thunk, saga, action, dispatch 统统一边去耍吧。不需要异步的好吗。

话说回来,即使有异步操作,我们还有 store,随时随地,时时刻刻,想改就改,毫无限制。

如果你再去了解一下 Nautil 的路由,你会发现一个规律:

Nautil 就是 react,nautil 不只是 react。

说的这么玄乎,意思是,它完全兼容 react 应用。比如你在其他地方写了一些纯 UI 的 react 组件,没关系,拿过来直接用。或者你想在其他的 react 应用中使用 nautil 编写的组件,没关系,直接拿去用。

你会发现,nautil 中强调“可观察的”这样一个概念。简单说就是有一个办法知道发生了变化。nautil 内置的 Observer 组件用于监听这些变化,并且在变化发生时执行传入的逻辑(一般是更新界面)。所以,在 nautil 中,数据、状态、路由都是“可观察的”对象,被注入到应用中。但本质上,它们是完全独立的,意思是,你可以在 react 应用之外的任何场景使用这些“可观察的”对象,也可以将整个体系之外的“可观察的”对象拿到 nautil 中直接使用。这也可以说是“渐进式”,可用可不用,当然,作为框架,你必须这样用才符合我写 nautil 的初衷。

不开源的都是耍流氓。

很遗憾,Nautil 到现在还没有发布。但你可以通过 github 关注或贡献代码。你也可以从 github 克隆下来跑跑看,也许会喜欢上呢~

github

最后补充一句。Nautil 还提供了内置的 Model,拥有结构化、数据校验、格式化、类型检查、可观察等特性,在表单开发时可能是你正在寻找的最好的解决方案,之一。

作者:否子戈
链接:https://juejin.im/post/5d20ce44f265da1ba328e6f7
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

09:59:43 已有0条回复
062019.7

自己写一个 react-native 的 checkbox

react-native 默认不能支持 checkbox,内置的 CheckBox 组件只能在安卓下工作。看了一堆第三方组件,都需要借助 Image 或 Icon 来实现。在 stackoverflow 看到一个帖子之后,觉得用 View 来实现最简单,而且不需要加载额外任何资源。

import { View } from 'react-native'
import { PureComponent } from 'react'

export class CheckboxButton extends PureComponent {
  render() {
    const { color = '#333333', checked, onChange, style = {} } = this.props
    return (
        <View style={{
          height: 24,
          width: 24,
          borderWidth: 2,
          borderColor: color,
          alignItems: 'center',
          justifyContent: 'center',
          ...style,
        }} onResponderRelease={onChange}>
          {
            checked ? <View style={{
              height: 12,
              width: 12,
              backgroundColor: color,
            }}/> : null
          }
        </View>
    )
  }
}
export default CheckboxButton

这个道理和通过 div 来画一个三角形一样,也是通过边框、背景组合得到。基于这种方法 RadioButton 就是增加一个 borderRadius,也是非常容易。

12:15:34 已有0条回复
042019.7

js new 做了什么?

js new 做了 4 步。以 var a = new A(options) 为例:

  1. var a = {}
  2. a.__proto__ = A.prototype
  3. var o = A.prototype.constructor.call(a, options)
  4. if (o !== undefined) a = o

这过程,如果某一部报错,都会直接弹出错误,中断进程。

09:55:28 已有0条回复
012019.7

动效:补间值缓动计算类

缓动是动效中不可避免要用到的东西,缓动函数是大部分动效计算的基础,其中就包括了 css 的动画属性。你可以通过这里来了解一些缓动函数和它的实际效果。缓动函数,说的简单点,就是通过时间计算当前时间点上的值。经过抽象之后,缓动函数的计算结果是一个比例,用这个比例,去乘以对应的实际值,就是该时间点上的应得值。

缓动函数

上面是一个 sin 函数的曲线,假如一个人的身高和年龄的关系按照 sin 函数来长,年龄小的时候长的慢,青年时期长的很快,年龄再大又不长。假设身高为 h,年龄为 t,那么 h = easeInSin(t). easeInSin 的 js 实现就是今天我们这篇文章的主角。这里可以告诉你答案,easeInSin = t => 1 + Math.sin(Math.PI / 2 * t - Math.PI / 2) 这便是一个缓动函数。

你一眼看过去,觉得有意思,但是不知道具体怎么应用。现在,你来想象,你现在就是用 canvas 来画这个人,他的身高和胖瘦都是按照 easeInSin 来变化,你的 canvas 画布高度为 200,你的人现在 20 岁,你这个人现在在画布中的身高为 172,现在,让你用 canvas 动画,实现这个人从一个受精卵长大成现在这个身高,你是不是就有思路了。假设时间每过 1 秒相当于这个人年长 1 岁,那么你需要通过 setTimeout 来得到一个 20 秒的计时器,每一个秒刻度时,需要通过 easeInSin 函数算出这个时候这个人应该多高,具体翻译为:h = easeInSin(t) * 172, 乘以 172 就得到当前岁数对应的身高了,然后在通过 js 重绘人物就行了。

现在回头看,是不是很简单。

接下来,我们都来了解一下都有哪些缓动函数。

一般来说,有 4 种形式的缓动函数,它们通过 h 值的增长速度(加速度)加以区分,只增不减的是 in 函数,只减不增的是 out 函数,先增后减的是 in-out 函数,先减后增的是 out-in 函数。你可以通过这里研究不同缓动函数的图形。一般来说,in 和 out 函数相对应,将 in 函数图形旋转 180 度之后,就得到 out 函数图形。in-out 和 out-in 函数相对,将 in-out 函数旋转 90 度之后,对称翻转,就得到 out-in 函数图形。一般,out-in 函数相对更难实现。

补间值计算

那么有没有现成的库呢?比较知名的 Tween.js 内置了缓动函数,但是,它太过复杂,并非我想要的。我从这里得到了很多高手无私贡献的缓动函数,经过整理之后,撰写了一个类,用来进行补间值的计算。

import Etx from 'etx'

class Motion extends Etx {
  constructor(options = {}) {
    super()

    const { type = 'linear', start = 0, end = 1, duration = 0, loop = false } = options

    this.type = type
    this.start = start
    this.end = end
    this.duration = duration
    this.loop = loop
    this.current = start

    this.status = -1
    this.time = 0
  }
  animate() {
    if (this.status < 1) {
      return
    }
    requestAnimationFrame(() => {
      const currentTime = Date.now()
      const t = (currentTime - this.time) / this.duration
      const tw = t > 1 ? 1 : t < 0 ? 0 : t
      const easing = Motion.easings[this.type]
      const scale = easing(tw)
      const end = this.end
      const start = this.start
      const value = (end - start) * scale + start
      this.current = value
      this.emit('update', value)

      if (tw === 1 && this.loop) {
        this.time = currentTime
      }
      else if (tw === 1) {
        this.stop()
        return
      }

      this.animate()
    })
  }
  start() {
    if (!easings[this.type] || this.duration <= 0) {
      const value = this.end
      this.current = value
      this.emit('update', value)
      this.stop()
      return
    }

    if (this.status > 0) {
      return
    }
    if (this.status < 0) {
      this.time = Date.now()
    }

    this.status = 1
    this.emit('start')
    this.animate()
  }
  pause() {
    this.status = 0
    this.emit('pause')
  }
  stop() {
    this.status = -1
    this.emit('stop')
  }
}

// https://gist.github.com/gre/1650294
// https://easings.net/
Motion.easings = {
  // no easing, no acceleration
  linear: t => t,
  // accelerating from zero velocity
  easeInQuad: t => t*t,
  // decelerating to zero velocity
  easeOutQuad: t => t*(2-t),
  // acceleration until halfway, then deceleration
  easeInOutQuad: t => t<.5 ? 2*t*t : -1+(4-2*t)*t,
  // accelerating from zero velocity
  easeInCubic: t => t*t*t,
  // decelerating to zero velocity
  easeOutCubic: t => (--t)*t*t+1,
  // acceleration until halfway, then deceleration
  easeInOutCubic: t => t<.5 ? 4*t*t*t : (t-1)*(2*t-2)*(2*t-2)+1,
  // accelerating from zero velocity
  easeInQuart: t => t*t*t*t,
  // decelerating to zero velocity
  easeOutQuart: t => 1-(--t)*t*t*t,
  // acceleration until halfway, then deceleration
  easeInOutQuart: t => t<.5 ? 8*t*t*t*t : 1-8*(--t)*t*t*t,
  // accelerating from zero velocity
  easeInQuint: t => t*t*t*t*t,
  // decelerating to zero velocity
  easeOutQuint: t => 1+(--t)*t*t*t*t,
  // acceleration until halfway, then deceleration
  easeInOutQuint: t => t<.5 ? 16*t*t*t*t*t : 1+16*(--t)*t*t*t*t,
  // elastic bounce effect at the beginning
  easeInElastic: t => (.04 - .04 / t) * Math.sin(25 * t) + 1,
  // elastic bounce effect at the end
  easeOutElastic: t => .04 * t / (--t) * Math.sin(25 * t),
  // elastic bounce effect at the beginning and end
  easeInOutElastic: t => (t -= .5) < 0 ? (.02 + .01 / t) * Math.sin(50 * t) : (.02 - .01 / t) * Math.sin(50 * t) + 1,
  easeInSin: t => 1 + Math.sin(Math.PI / 2 * t - Math.PI / 2),
  easeOutSin: t => Math.sin(Math.PI / 2 * t),
  easeInOutSin: t => (1 + Math.sin(Math.PI * t - Math.PI / 2)) / 2,
}

使用方法:

const motion = new Motion({
  type: 'easeInElastic',
  start: 24,
  end: 60,
  duration: 2000,
})

function render() {
  const height = motion.current
  document.querySelecotry('#some').style.height = height + 'px'
}

motion.on('update', render)

motion.start()

上面定义了一个基于 easeInElastic 缓动函数的计算器,它的缓动起始位置为 24,结束位置为 60,整个动效过程时间是 2 秒。在定义好计算器之后,我们调用 start() 开始计算,通过 on 方法监听计算结果,并且修改某个元素的 height,这样,#some 这个元素的高度,就会从 24 开始缓缓涨到 60,完成整个动作过程。

比较秒的设计是,它提供了一个 pause 方法,可以用来暂停动画。

基于这样的方法,我们可以进行更为复杂的扩展,比如同时设置多个 css 属性之类的。

缓动效果

接下来我们来看看都有哪些缓动效果。总体上,我们将效果分为 4 大类:渐变、位移、转体、缩放。其中转体比较复杂,又可以分为:旋转、翻转、3D特效。

渐变相关的一般包括透明度、长宽、色值等。位移包括直线、曲线,可以通过 css 的位置相关属性来控制。

  • fade
  • to
  • resize
  • gradient
  • tween

这些单词是在处理缓动效果时经常使用的,比如 fadeIn, fadeOut 等。

如何将缓动函数应用到缓动效果呢?其实很简单,只需要通过上面的缓动计算器修改对应的 css 属性值即可。例如,fadeIn 的实现:

const motion = new Motion({
  duration: 0.5,
})

function fadeIn() {
  const opacity = motion.current
  $('#some').css({ opacity })
}

fadeIn() // 初始值
motion.on('update', fadeIn())
motion.start()

这样就完成了一个渐显的动画效果。但是实际上,我们很少直接用 js 去处理这种简单的效果,因为考虑到性能问题,每一次修改 css,都会让浏览器重绘,这会阻塞其他进程,如果计算过于复杂,就会造成卡顿。但是,在一些特殊的场合,我们无法通过简单的 css 来实现的时候,这也是一种方式。

23:41:19 , 已有0条回复

将css转化为react-native stylesheet

19:19:13 已有0条回复
012019.6

批量自动化删除新浪微博代码

因为一些黑历史而带来了烦恼,年轻的时候不懂事,以为留下的足迹是好的,如今去看却满目苍夷,鄙夷那时幼稚的自己。好不容易通过邮箱找回了密码,但是发现自己竟然几年之间发了 4000 多条微博。总不可能一条一条删吧,网上搜了一下,发现了一个自动删除的脚本。但是这个脚本在删除时,会将当前页全部删除,这回导致微博自动刷新页面,脚本也就不能继续执行了。于是做了改进,得到如下代码:

const sleep = (time) => new Promise(resolve => setTimeout(resolve, time))
const script = document.createElement('script')
script.setAttribute('src', 'https://lib.sinaapp.com/js/jquery/2.0.3/jquery-2.0.3.min.js')
script.onload = function() {
  var latestTime = Date.now()

  async function scrollToBottom() {
    $('html, body').scrollTop($(document).height())
    await sleep(800) // 等下一页加载完

    // 往下拉,拉到底部
    if ($('[node-type="lazyload"]').length && !$('a.page.prev').length) {
      scrollToBottom()
      return
    }

    console.log('加载完毕。')
    $('html, body').scrollTop(0)
    console.log('开始删除。')
    deleteWeibo()
  }

  async function deleteWeibo() {
    if ($('a[action-type="feed_list_delete"]').length > 1) {
      const $date = $('.WB_detail:first a[date]')
      const time = +$date.attr('date')
      // 设定一个时间,当删除程序发现要删除的微博发布时间大于这个时间时,删除程序直接停止
      const date = +new Date('2016-01-01 00:00:00')

      if (time > date) {
        console.log('到达了设定的时间,停止删除。')
        clearInterval(timer)
        return
      }


      $('a[action-type="feed_list_delete"]')[0].click()
      await sleep(500) // 等删除确认窗口打开
      $('a[action-type="ok"]').each(function() { this.click() })
      await sleep(500) // 等待删除请求完毕
      latestTime = Date.now() // 记录最后一次删除操作时间
      deleteWeibo()
    }
    else {
      // 处理最后一条
      var mid = $('div[mid]').last().attr('mid')
      // 进入下一页之后,上一页的最后一条没有被删掉,用 ajax 将它删除
      const deleteLastOne = () => $.post('https://weibo.com/aj/mblog/del', { mid: mid }).fail(async () => {
        console.log('删除最后一条失败,正在尝试重新删除。')
        await sleep(1000)
        deleteLastOne()
      })
      deleteLastOne()

      console.log('进入上一页。')
      $('a.page.prev')[0].click()
      await sleep(1000) // 确保下一页已经加载
      scrollToBottom()
    }
  }

  scrollToBottom()

  // // 心跳检测,是否还在正常跑,如果没有跑了,就重启服务
  const heartbeat = 30000
  var timer = setInterval(() => {
    const currentTime = Date.now()
    if (currentTime - latestTime < heartbeat * 1.5) {
      return
    }
    console.log('检测到任务失败,正在尝试重新启动。')
    $('[action-type="feed_list_page_morelist"] ul li:first a')[0].click() // 通过点击全部按钮来恢复加载微博
    $('html, body').scrollTop(0)
  }, heartbeat)
}
document.head.appendChild(script)

使用方法:

  1. 将上方红色字修改为你想要停止的日前,如果要删除全部微博,删除上面灰色部分
  2. 进入个人中心页面 https://weibo.com/{weiboid}/profile
  3. 通过下拉,拉到最底部,出现导航,选择页面,跳转到最后一页
  4. 打开开发者工具执行上面的代码。

这段代码的逻辑是,通过模拟滚动下拉和鼠标点击事件,逐条删除微博(删除太快会出现 416 报错,应该是微博后台的一个防 DDos 策略)。但是删除完一页之后,我们不能按照正常的逻辑,删除下一页,因为如果我们这个时候点击下一页,那么就会实际进入下下一页,而不是下一页,中间有一页会被漏掉。了解接口设计的同学应该很清楚,翻页的时候,当前页是按 limit 的规则输出的,所以在对待这个场景时,我选择通过模拟点击“上一页”的办法来解决。这也是为什么要求你在运行这段脚本的时候跳转到最后一页才开始执行。面对前文提到的,会自动刷新的问题,解决办法是,在留下最后一条的时候,不删,而是直接点击“上一页”,这样,页面不会自动刷新,而会把上一页的内容加载进来。但是留下的这条怎么办?自己发一个 ajax 请求去删除即可。

吐槽一下,写多了 ES6+ 的语法,再写 function 声明代码感觉不适应。

互联网的一个特点是开放,但是,比较恐怖的一点时,我们没有一个“删除键”。我们个人的信息,一旦上网,就永远留在了网络空间,无论你想不想。这有的时候是好事,可以起到备份作用,但有的时候是坏事。我想,如果我以后做一款应用,一定要提供“备份”“一键全删”的功能。

12:37:52 已有0条回复
302019.5

在 leveldb 的使用过程中,发现一个比较忧伤的问题,level 这个包是依赖 levelup 去起一个 leveldb 的,一旦一个实例生成,就会在数据库目录下生成一个 LOCK 文件,这种情况下,你不可以再用另外一个 levelup 去起一个实例。解决的办法是,创建一个函数,用以在多个程序之间共享由 levelup 起来的实例。而且由于 js 是单线程的,所以,只要是在同一个进程中(某些异步情况下要格外小心),就可以避免被锁问题。

17:41:07 已有3条回复
  1. 自己都不知道怎么来的你博客,大概把你所有的杂目录下随便翻了翻觉得up很有意思
    然后往简介里面翻了一下发现还是天健园的学长~
    向学长学习~
    #784 潘小安 2019-05-30 18:00 回复
  2. 相互交流~
    #785 回复给#784 否子戈 2019-06-01 12:30 回复
  3. ~
    #793 回复给#785 潘小安 2019-06-04 14:00 回复
082019.5

在页面闲时执行任务

在页面空闲的时候,执行一些计算或某些特殊的后台任务,这样可以避免对用户操作带来卡顿的问题。虽然js支持异步编程,但是即使某些任务是异步执行的,但是因为js是单线程程序,所以,如果一个任务需要花费比较长的时间去进行计算,那么即使它是在异步回调的时候执行,也会带来界面卡顿,而如果用户在这个卡顿期间进行交互操作,就会明显感到卡死状态,体验不好。

有没有一种办法避免这种情况发生?当然有,最好的方式是使用webworker,启用另外一个线程去执行这个需要消耗大量时间的运算。因为webworker和用户界面所在的线程相互不影响,所以,不会给用户带来卡顿感。当计算完毕之后,通过postMessage实现数据传递,对于用户而言,几乎无感。

但是在一些特殊情况下,我们需要在用户界面所在的主线程去执行这种程序。比如,我们要在这个任务中获取DOM的一些信息。这个时候,我们要想办法让这个任务不对用户的操作造成影响。

web标准提供了requestIdleCallback这个接口,它的用法有点像requestAnimationFrame,它主要用于在浏览器闲时执行某个任务。比如:

var a = 0
requestIdleCallback(() => {
  a ++
})

上面这段代码,给浏览器下了一个命令,当浏览器空闲的时候,执行a ++。

不过requestIdleCallback会有一些兼容性问题,我们只能通过一些手段来使它在低版本浏览器可以用:

export const requestIdleCallback = window.requestIdleCallback || function(cb, delay = 1000) {
  const start = Date.now()
  const action = () => cb({
    didTimeout: false,
    timeRemaining: function() {
      return Math.max(0, 50 - (Date.now() - start))
    },
  })
  const id = setTimeout(() => {
    timeout.id = setTimeout(action, delay)
    timeout.reset = () => {
      clearTimeout(timeout.id)
      timeout.id = setTimeout(action, delay)
    }
    document.addEventListener('keydown', timeout.reset, true)
    document.addEventListener('mousedown', timeout.reset, true)
    document.addEventListener('touchstart', timeout.reset, true)
    document.addEventListener('touchmove', timeout.reset, true)
    document.addEventListener('mousemove', timeout.reset, true)
    window.addEventListener('scroll', timeout.reset, true)
    window.addEventListener('resize', timeout.reset, true)
  })
  var timeout = { id, reset: null }

  return timeout
}
export const cancelIdelCallback = window.cancelIdelCallback || function(timeout) {
  if (!timeout) {
    return
  }

  clearTimeout(timeout.id)
  document.removeEventListener('keydown', timeout.reset)
  document.removeEventListener('mousedown', timeout.reset)
  document.removeEventListener('touchstart', timeout.reset)
  document.removeEventListener('touchmove', timeout.reset)
  document.removeEventListener('mousemove', timeout.reset)
  window.removeEventListener('scroll', timeout.reset)
  window.removeEventListener('resize', timeout.reset)

  timeout.reset = null
  timeout.id = null
}

cancelIdelCallback用于取消前面下达的命令,参数是requestIdleCallback的返回值。

不过有一个问题,就是requestIdleCallback是只执行一次的,如果我们想要做一个守护程序,在浏览器空闲的时候,就开始运行这个守护程序,应该怎么做呢?

/**
 * 创建一个在空闲时执行的任务
 * @param {*} fn
 */
export function autoidle(fn, interval = 1000, immediate = true) {
  var idle
  const run = () => {
    const action = () => {
      idle = requestIdleCallback(run, interval)
    }
    asyncx(fn)().then(action).catch(action)
  }
  const start = () => {
    cancelIdelCallback(idle)
    idle = requestIdleCallback(run, interval)
  }
  const stop = () => {
    cancelIdelCallback(idle)
  }

  if (immediate) {
    start()
  }

  return { start, stop }
}

这个函数可以创建一个守护程序,它可以在你的浏览器空闲的时候不断运行,但你的浏览器开始忙碌的时候,又不会运行的效果。另外,它返回两个函数,start 和 stop,用以在必要的时候停止和重启任务。

17:09:58 已有1条回复
  1. 技术人员的文章看不懂。
    #779 repostone 2019-05-13 17:33 回复
072019.5

监控用户在当前页面的停留时间

我们希望知道一个用户在当前这个页面停留的时间,这中间要考虑到用户通过切换浏览器tab的情况,把所有用户动作都记录下来。

const tracers = []
// 进入
tracers.push({
  type: 'enter',
  time: Date.now(),
})
// 切屏
const visibilitychangeFn = () => {
  if (document.visibilityState === 'hidden') {
    tracers.push({
      type: 'focusout',
      time: Date.now(),
      url: window.location.href,
      requestId: this.getRequestId(),
    })
  }
  else {
    tracers.push({
      type: 'focusin',
      time: Date.now(),
    })
  }
}
document.addEventListener('visibilitychange', visibilitychangeFn)
// 离开
const unloadFn = () => {
  tracers.push({
    type: 'leave',
    time: Date.now(),
    url: window.location.href,
    requestId: this.getRequestId(),
  })
  record({
    type: 'usertracer',
    tracers,
    stay: tracers[tracers.length - 1].time - tracers[0].time,
  })
}
window.addEventListener('beforeunload', unloadFn)

function record(info) {}

通过一个traces数组,记录了用户从进入这个页面,在这个页面停留、切换tab,到最后离开这个页面的时间。

10:19:56 已有3条回复
  1. 现实是你用的几个api好像有兼容问题~
    #780 JC 2019-05-24 14:57 回复
  2. 我博客大部分文章,都是不考虑兼容性的,实现一个东西,不可能面面俱到,最重要的是为读者提供可供参考的思路
    #781 回复给#780 否子戈 2019-05-28 18:17 回复
  3. 这个怎么说呢,
    大部分开发肯定都是要考虑兼容性的,
    就算是参考,
    也是片面的,
    唉,
    不说了
    #782 回复给#781 JC 2019-05-28 18:18 回复