Giter Site home page Giter Site logo

blog's People

Contributors

maple-leaf avatar

Stargazers

 avatar

Watchers

 avatar  avatar

blog's Issues

Rx.js使用之结合node的读写流进行数据处理

前几天接到任务要使用第三方API处理几千张图片,得到结果集。我的做法就是使用Rx.js结合node的读写流来完成数据读入、接口请求、数据处理、数据写入这些操作。本篇就来分享这个代码和其逻辑。

Rx.js是什么

Rx.js是一个响应式编程库,能简化事件/异步处理逻辑代码。其视所有的事件/数据为__流__,提供各种流处理的operators,将输入与输出平滑的链接起来。可以类比为linux上的pipe操作符: ls | grep a*b | less

Node的读写流

  • readline模块提供readline.createInterface来创建行读取流,即读取文件的每一行作为持续的输入数据
  • fs模块提供fs.createWriteStream来创建写入流, 其返回的writerwriteend两个方法,来完成流式的写入与结束写入。

第三方接口的使用情况

  • 并发数有限制,3个是出现其出现并发错误概率最低的最大并发数
  • 接口请求过于频繁,会较大概率出现连续的并发错误, 大概延迟400秒效果尚可
  • 提供给第三方的图片是链接,其需要服务器自己下载,会出现操作超时或者长时间不返回的情况。

任务列表

  1. 从文件读取图片文件名
  2. 拼接url
  3. 发送3个并发请求
  4. 请求出现超时问题重试3次,最后如果失败则放弃
  5. 出现非超时错误(如并发错误等)则一直重试,直到成功
  6. 请求成功后延迟400秒继续发起下一个请求
  7. 处理返回的数据
  8. 写入文件

浏览器奔溃了。。。。泪奔,下面写好的要重写了😢 真的瞬间让人有不想写的冲动。。。。

撩测试MM神器cypress使用入门

不很久不很久以前

据说某家公司有两位前端,天天撸bug,为啥嘞?只怪测试MM倾人国,轻语哥哥有bug。✧(๑•̀ㅂ•́)و✧ 可是最近两位有点犯愁 Σ(っ °Д °;)っ。测试MM有几次提了紧急bug,都在旁边鼓励他们改bug了,可是线上bug重现排查比较麻烦,而且改了后还发现没改好,惹得测试MM潸然泪下,好生埋汰。怎么办呢?

前端君666某天发现了E2E测试神器cypress后,暗中偷练神功,改bug越来越6,测试MM每天笑着对他说,666你真6,MM好喜欢呀(๑•́ ₃ •̀๑) 另一位前端君555每天面对堆积如山的bug长吁短叹,测试MM提完新bug后都不理他了≡ ̄﹏ ̄≡

作为一个追求代码永无bug、顺带跟测试MM沟通产品的有理想的前端 (ง •̀_•́)ง,我觉得有必要学习一下怎么使用cypress来进行E2E测试,以此来提高代码质量。那么我们来看看怎么入门cypress测试框架。

cypress三问 - 你是谁

cypress是在mocha式API基础上构建的一套开箱可用的E2E测试框架,对比其他测试框架,它提供一套自己的最佳实践方案,无需其他测试工具库,配置方便简单但功能异常强大,可以使用webpack项目配置,还提供了一个强大的GUI图形工具。入门简单,上手方便,怎么舒服怎么来呀 (。→‿←。)

cypressGUI方式的测试使用真实浏览器,非GUI方式使用chrome-headless,不是用模拟方式进行测试,更真实的展现实际环境中的测试过程和结果。

cypress三问 - 你有啥优势

cypress有几大自带的强大功能:

  • 自带GUI工具,想测啥就点啥,还可以查看整个测试过程,想录屏还可以录屏哟(录屏可以发给测试MM看,保准她说哥哥真厉害哟。 一般人我不告诉他๑乛◡乛๑)
  • 测试的每一步都有snapshot,可以通过GUI工具查看每个过程的页面状态,不是截图而是真是的页面DOM环境哟!
  • 自带数据mock和请求拦截机制,还原线上数据引起的bug别提有多轻松了
  • 和wepbakc配置,实现无论修改测试文件还是被测试代码都可以自动重测
    • 小Tips:可以给测试用例加上only或者skip来避免重测测试文件里的所有用例: it.only('只测试这个哟); it.skip('不要测这个');

cypress三问 - 怎么用

安装

  • yarn add cypress 或者 npm install cypress
  • 安装完毕后,./node_modules/.bin/cypress install安装cypress环境(包括GUI工具)

配置

  • package.json: 配置GUI和非GUI(terminal)两种方式来运行cypress
    "scripts": {
        "cypress": "cypress run",
        "cypress-gui": "cypress open",

⚠️ 配置好后 先运行 yarn cypress[-gui] 或者 npm run cypress[-gui](中括号意思是可选)来初始化cypress生成默认配置和目录

  • cypress.json(与package.json同级目录): cypress提供比较灵活的配置,可以根据自己需要定制行为,以下列一下我对一个项目的配置
{
    "baseUrl": "http://localhost:8080", // 本地开发服务地址(webpack-dev-server)
    "integrationFolder": "src", // 自定义"src"为测试文件根目录,默认是"cypress/integration"
    "testFiles": "**/*.cypress.spec.js", // 自定义测试文件的匹配正则,默认是"**/*.*",即所有文件
    "videoRecording": false, // 关闭录屏功能, 如果开启录屏功能,记得将"cypress/screenshots"目录加入".gitignore",防止不小心将录屏加到git中
    "viewportHeight": 800, // 设置测试环境的页面视图的高度
    "viewportWidth": 1600 // 设置测试环境的页面视图的宽度
}
  • cypress/plugins/index.js: cypress运行环境配置,可以用来配置webpack等。以下是配置webpack别名范例。默认这里不需要配置。
// 参考官方例子地址 https://github.com/cypress-io/cypress-example-recipes/blob/master/examples/preprocessors__typescript-webpack/cypress/plugins/index.js
const wp = require("@cypress/webpack-preprocessor");
const path = require('path');

function resolve(dir) {
    return path.join(__dirname, "../..", dir);
}

module.exports = on => {
    const options = {
        webpackOptions: {
            resolve: {
                alias: {
                    "@": resolve("src"),
                    cypress: resolve("cypress")
                }
            }
        }
    };
    on("file:preprocessor", wp(options));
};

万事俱备,测测测

  • 简单的一个例子
describe('测试页面包含某元素', () => {
    it('有云 "前端哥哥们真帅,前端妹妹们真漂亮"', () => {
        cy.contains("前端哥哥们真帅,前端妹妹们真漂亮");
    });

    it('要有一个链接', () => {
        cy.get('a').should('have.length', 1);
    });

    it('不存在class含有abc的元素', () => {
        cy.get('.abc').should('have.length', 0);
    });
});
  • 互动的例子
describe('一起动', () => {
    it('获取输入框,输入文字并按enter键', () => {
        const text = 'not exist';
        // type api用法: https://docs.cypress.io/api/commands/type.html#Usage
        cy.get('input').type(`${text}{enter}`);
    });

    it('点击按钮', () => {
        cy.get('button').click();
    });
});
  • 网络请求mock例子

Tip1: cy.route的路径匹配是严格的,所以要注意是否需要加通配符。如 cy.route('/api/search', [])不会拦截/api/search?keyword=abc,只会拦截/api/search

Tip2: cy.route的method要注意,默认是GETcy.route('/api/posts')cy.route('POST', '/api/posts') 是不一样的。

describe('要啥给啥', () => {
     beforeEach(() => {
        cy.server(); // 一定要在 cy.route 前调用
        cy
            .fixture('/posts/list.json') // 我们在 cypress/fixtures 内创建mock用的数据
            .as('postsData'); // 给 mock 数据取别名,以后 cy.route 使用
        cy
            .route('/api/posts', '@postsData')
            .as('getPostsRoute'); // 给请求取别名,以供 cy.wait 使用
    })

    it('进入列表页,拦截列表请求接口', () => {
        cy.wait('@getPostsRoute'); // 等待被拦截的接口请求完成

        cy.get('.post').should('have.length', 10); // 要有10条数据被渲染到页面上
    });
})
  • 实际场景例子: 结合上面所有姿势,我们现在测试搜索页面的搜索、操作结果
describe('test search page', () => {
    // 几个 route 路径变量
    const searchRoutePath = '/api/items/activities?query=*';
    const deleteActivityRoutePath = '/api/activities/*/items/batch?num_iids[]=*';
    const undoActivityRoutePath = '/api/activities/*/items/undo';

    function search(keyword) {
        // 将搜索行为和等待搜索返回封装起来
        cy
            .fixture('items/activities.json')
            // 处理mock数据,只返回符合搜索结构的数据
            .then(data => data.filter(item => item.title.indexOf(keyword) !== -1))
            .as('searchResult');
        cy.server();
        cy.route(searchRoutePath, '@searchResult').as('searchRoute');

        const input = cy.get('input');
        input.clear(); // 清空输入框内文本

        input.type(`${keyword}{enter}`);

        cy.wait('@searchRoute');
    }

    before(() => {
        // 进行所有测试前,先访问搜索页
        cy.visit('/activities/search');
    });

    it('should show no data tip when search result is empty', () => {
        const text = 'not exist';
        search(text);
        cy.contains(`没有找到关于 ${text} 的结果`);
    });

    it('should remove activity from list when clean successful', () => {
        search('成功');

        cy
            .route('delete', deleteActivityRoutePath, {
                success: 0,
                fail: 0,
                waiting: 0,
            })
            .as('deleteActivityResponse');

        // within是让cy执行的context保持在'.activities-search'这个dom节点内
        // 默认cy的执行是以上一个cy命令结果作为context
        // 如 "cy.get('a'); cy.get('span')",cy会在上一个命令找到的'a'标签中查找'span'
        cy.get('.activities-search').within(() => {
            const items = cy.get('.result-item');
            items.should('have.length', 1);
            const applyList = items.get('.apply-list');

            applyList.should('not.be.visible'); // 每个数据项内详细内容区域是隐藏的

            const toggleBtn = items.get('.item-apply-count');
            toggleBtn.click(); // 点击显示详细内容区
            applyList.should('be.visible');
            applyList.children().should('have.length', 1); // 详细内容区内数据只有1条

            const cleanBtn = cy.contains('退出');
            cleanBtn.click(); // 点击详细内容区里的“退出”按钮

            cy.wait('@deleteActivityResponse'); // 等待“退出”请求返回
            cy.get('.apply-list').should('be', null); // 退出成功后,详细内容区数据减1,即空
        });
    });
});

几个必读文档

关于测试覆盖率

目前cypress没有内置测试覆盖率统计功能,github上有专门的issue在跟踪这个,后续应该会有。issue上也有几个临时方案,目前我倾向使用chrome自带的来查看。在GUI打开的测试的浏览器中打开devtools,切到Sources, 按下cmd+shift+p(windows用户按ctrl+shift+p),输入coverage,选择重新刷新并统计代码执行覆盖率。

untitled4

那么,high起来

为了高(撩)质(测)量(试)代(M)码(M),high起来。喜欢前端MM的可以手把手教起来了 (¬_¬)

微信小游戏体验之打飞机改造计划

微信小游戏推出已有几天了,这个功能对小程序和小游戏的推动影响不用多说,大家赶紧摩拳擦掌往上撸就可以了。关于如何开发官方文档已经说明了,这篇则是对官方的打飞机demo一些小改造。

开发预备式

  1. 下载最新版本的微信开发者工具(v1.02.1712280)
  2. 根据官方文档说明,目前不提供公开注册。因此目前只能使用无AppID模式进行体验
  3. 为了让HTML5游戏轻松接入,官方提供了Adapter。这个的作用就是提供HTML5写法和wx写法的全局转换层。

打飞机小游戏

使用无AppID模式创建一个微信小游戏后可以看到官方demo,其中入口文件和配置文件:game.jsgame.jsongame.js引入并初始化包含整个打飞机的游戏场景、参与者(玩家飞机和敌方飞机)、游戏逻辑的主函数的main.js。在main.js中我们可以发现由于Adapter的存在,这里的代码和我们平常的代码写法没什么差异了。游戏的主逻辑如下图:

image

在loop中,玩家每隔20帧射一次,每隔60帧生成新的敌机。每帧检查玩家和敌机是否死亡,玩家死亡游戏结束,敌机死亡分数+1。只有玩家可以射击,且射击方式固定,通过躲避敌机生存。接下来我们针对这些进行改造,提升游戏的可玩性和挑战性。

玩家升级计划

  1. 玩家初始等级为1,玩家可通过击杀敌机升级,每击落30敌机升级一次
  2. 玩家每升级一次,增加一个射击口
  3. 玩家最多升级两次

首先用编辑器打开player/index.js,将等级逻辑加入到玩家的类中。

export default class Player extends Sprite {
  constructor() {
    super(PLAYER_IMG_SRC, PLAYER_WIDTH, PLAYER_HEIGHT)

    // 玩家默认处于屏幕底部居中位置
    this.x = screenWidth / 2 - this.width / 2
    this.y = screenHeight - this.height - 30

    // 用于在手指移动的时候标识手指是否已经在飞机上了
    this.touched = false

    this.bullets = []

    // 初始化事件监听
    this.initEvent()

    this.playerLevel = 1;
  }

  get level () {
    return this.playerLevel;
  }
  set level (level) {
    this.playerLevel = Math.min(level, 3);
  }

接下来在main.jsupdate函数加入升级逻辑。

// 其他代码...

    update() {
        this.bg.update();

        databus.bullets.concat(databus.enemys).forEach(item => {
            item.update();
        });

        this.enemyGenerate();

        this.player.level = Math.max(1, Math.ceil(databus.score / 30));

        this.collisionDetection();
    }

// 其他代码...

好的,到此玩家已经可以正常升级了。那么该给予玩家奖励品了。在player/index.jsshoot函数中我们修改射击的逻辑。玩家1级时只有中间的射击口,2级有左边和中间的射击口,3级有左中右三个射击口。

// ...其他代码

    /**
     * 玩家射击操作
     * 射击时机由外部决定
     */
    shoot() {


      for(let i = 0; i < this.level; i++) {
        const bullet = databus.pool.getItemByClass('bullet', Bullet);
        const middle = this.x + this.width / 2 - bullet.width / 2;
        const x = !i ? middle : (i % 2 === 0 ? middle + 30 : middle - 30);
        bullet.init(
          x,
          this.y - 10,
          10
        )

        databus.bullets.push(bullet)
      }
    }

// ...其他代码

武器的最终形态如图, 这时候的玩家已经可以为所欲为了<_<,实际上都不需要躲避了。。。:

image

敌人的反击号角

为了对抗愚昧的玩家,不让他们为所欲为,最后没兴趣玩下去~~,敌机装备武器,反击开始。

首先敌机的子弹是向下,所以复制一份images/bullet.png,并颠倒保存为images/bullet-down.png, 然后我们重用js/player/bullet.js,在构造函数处增加敌机的子弹配置项,并修改敌人子弹更新逻辑。

const BULLET_IMG_SRC = 'images/bullet.png'
const BULLET_DOWN_IMG_SRC = 'images/bullet-down.png'
const BULLET_WIDTH   = 16
const BULLET_HEIGHT  = 30

const __ = {
    speed: Symbol('speed')
}

let databus = new DataBus()

export default class Bullet extends Sprite {
    constructor({ direction } = { direction: 'up' }) {
        super(direction === 'up' ? BULLET_IMG_SRC : BULLET_DOWN_IMG_SRC, BULLET_WIDTH, BULLET_HEIGHT)
       
        this.direction = direction;

// 其他代码...

    // 每一帧更新子弹位置
    update() {
        if (this.direction === 'up') {
            this.y -= this[__.speed] 
            
            // 超出屏幕外回收自身
            if ( this.y < -this.height )
                databus.removeBullets(this)
        } else {
            this.y += this[__.speed]

            // 超出屏幕外回收自身
            if ( this.y > window.innerHeight + this.height )
                databus.removeBullets(this)
        }
    }
}

接着在js/npc/enemy.js结尾部分为敌人装备武器, 子弹速度为敌人自身速度+5

import Animation from '../base/animation'
import DataBus   from '../databus'
import Bullet from '../player/bullet';

const ENEMY_IMG_SRC = 'images/enemy.png'
// 其他代码...

  update() {
    this.y += this[__.speed]

    // 对象回收
    if ( this.y > window.innerHeight + this.height )
      databus.removeEnemey(this)
  }

  /**
   * 敌机射击操作
   * 射击时机由外部决定
   */
  shoot() {
      const bullet = databus.pool.getItemByClass('bullet', Bullet);
      bullet.init(
          this.x + this.width / 2 - bullet.width / 2,
          this.y + 10,
          this[__.speed] + 5
      );

      databus.bullets.push(bullet);
  }
}

接下来,在js/main.js中加入敌机的射击逻辑,敌机移动5次、60次时设计。

// 其他代码...
 let ctx = canvas.getContext("2d");
 let databus = new DataBus();

const ENEMY_SPEED = 6;
// 其他代码...

    /**
     * 随着帧数变化的敌机生成逻辑
     * 帧数取模定义成生成的频率
     */
    enemyGenerate(playerLevel) {
        if (databus.frame % 60 === 0) {
            let enemy = databus.pool.getItemByClass("enemy", Enemy);
            enemy.init(ENEMY_SPEED);
            databus.enemys.push(enemy);
        }
    }

// 其他代码...

    // 实现游戏帧循环
    loop() {
        databus.frame++;

        this.update();
        this.render();

        if (databus.frame % 20 === 0) {
            this.player.shoot();
            this.music.playShoot();
        }

        databus.enemys.forEach(enemy => {
            const enemyShootPositions = [
                -enemy.height + ENEMY_SPEED * 5,
                -enemy.height + ENEMY_SPEED * 60
            ];
            if (enemyShootPositions.indexOf(enemy.y) !== -1) {
                enemy.shoot();
                this.music.playShoot();
            }
        });

        // 游戏结束停止帧循环
        if (databus.gameOver) {
            this.touchHandler = this.touchEventHandler.bind(this);
          canvas.addEventListener("touchstart", this.touchHandler);
            this.gameinfo.renderGameOver(ctx, databus.score);

            return;
        }

        window.requestAnimationFrame(this.loop.bind(this), canvas);
    }

这时候我们发现,由于不明宇宙的干扰射线的影响,玩家和敌机的子弹不受控制的乱飞。接下来我们就来恢复世界的秩序吧 ;

经侦测发现是对象池pool的获取逻辑问题导致子弹不受控问题,我们需要区分获取玩家、每个敌机的子弹

首先,对象获取我们加入对象属性的判断,当有传入对象属性时,我们获取所有属性值一致的已回收对象,若没有找到或者对象池为空时,则用属性创建新对象

  /**
   * 根据传入的对象标识符,查询对象池
   * 对象池为空创建新的类,否则从对象池中取
   */
  getItemByClass(name, className, properties) {
    let pool = this.getPoolBySign(name)

    if (pool.length === 0) return new className(properties);
   
    if (!properties) return pool.shift();
   
    const index = pool.findIndex(item => {
        return Object.keys(properties).every(property => {
            return item[property] === properties[property];
        });
    });
    return index !== -1 ? pool.splice(index, 1)[0] : new className(properties)
  }

相应的我们需要给每个子弹设置归属,在js/player/bullet.jsBullet类修改constructor

export default class Bullet extends Sprite {
    constructor({ direction, owner } = { direction: 'up' }) {
        super(direction === 'up' ? BULLET_IMG_SRC : BULLET_DOWN_IMG_SRC, BULLET_WIDTH, BULLET_HEIGHT)

        this.direction = direction;

        this.owner = owner;
    }

接着修改js/player/index.jsshoot,为其中创建的bullets提供归属

    /**
     * 玩家射击操作
     * 射击时机由外部决定
     */
    shoot() {
      for(let i = 0; i < this.level; i++) {
        const bullet = databus.pool.getItemByClass('bullet', Bullet, { direction: 'up', owner: this });

同样处理js/npc/enemy.jsshoot

  /**
   * 敌机射击操作
   * 射击时机由外部决定
   */
  shoot() {
      const bullet = databus.pool.getItemByClass('bullet', Bullet, { direction: 'down', owner: this });

最后处理js/databus.jsremoveBullets的回收逻辑

  /**
   * 回收子弹,进入对象池
   * 此后不进入帧循环
   */
  removeBullets(bullet) {
    const index = this.bullets.findIndex(b => b === bullet);

    bullet.visible = false

    this.bullets.splice(index, 1);

    this.pool.recover('bullet', bullet)
  }
}

这时候敌我的子弹就恢复正常了。不过这时候玩家中弹并不会死亡,现在来让玩家Go Die吧。在js/main.jscollisionDetection我们判断增加每一颗子弹如果是敌方的,就判断其是否打中玩家,是则游戏结束。玩家的子弹判断保持不变。

    // 全局碰撞检测
    collisionDetection() {
        let that = this;

        databus.bullets.forEach(bullet => {
            for (let i = 0, il = databus.enemys.length; i < il; i++) {
                let enemy = databus.enemys[i];
                if (bullet.owner instanceof Enemy) {
                    databus.gameOver = this.player.isCollideWith(bullet);
                } else if (!enemy.isPlaying && enemy.isCollideWith(bullet)) {
                    enemy.playAnimation();
                    that.music.playExplosion();

                    bullet.visible = false;
                    databus.score += 1;

                    break;
                }
            }
        });

到此整个简单改造计划就结束了,以后还可以添加武器系统,boss战等等。下面是改造后的游戏动图录屏

demo

Rx.js使用第二弹之拖拽与自动滚动

首先上demo地址:

Rx.js - DnD & autoScroll by tjf (@maple-leaf) on CodePen.

demo演示gif:
untitled

本次要实现的功能点有两个:

  1. 基本的拖拽
  2. 拖拽到顶部或底部的时候触发自动滚动

码代码开始--->>>

Html结构:

<div id="scrollTopTip">drag to here to auto scroll</div>
<div id="wrapper">
  <div id="handler">drag me</div>
  <div id="content"></div>
</div>
<div id="scrollBottomTip">drag to here to auto scroll</div>

Js初始化部分:

const wrapper = document.querySelector('#wrapper');
const handler = document.querySelector('#handler');

const initialStyleOfHandler = {
  left: 0,
  top: 0
};
const styleOfHandler = {
  left: 0,
  top: 0
};

const mousedown$ = Rx.Observable.fromEvent(handler, 'mousedown');
const mousemove$ = Rx.Observable.fromEvent(document, 'mousemove');
const mouseup$ = Rx.Observable.fromEvent(document, 'mouseup');

上面做好了拖拽目标的样式变量初始化,鼠标相关事件转化为Rx的流。

实现基本的拖拽 (删掉了自动滚动的代码)

mousedown$
.switchMap(mousedownEvt => {
  const startPoint = {
    x: mousedownEvt.pageX,
    y: mousedownEvt.pageY
  };
 
  const drag$ = mousemove$.throttleTime(10).takeUntil(mouseup$);  // 定义拖拽流为截流10ms鼠标移动的流,在鼠标释放时停止

  return drag$
  .map(mouseMoveEvt => {
    const diff = {
      x: mouseMoveEvt.pageX - startPoint.x,
      y: mouseMoveEvt.pageY - startPoint.y
    };  // 计算与初始位置的位移

    return diff;
  });
})
.subscribe(diff => {
  // 更新样式
  styleOfHandler.left = initialStyleOfHandler.left + diff.x;
  styleOfHandler.top = initialStyleOfHandler.top + diff.y;
  handler.style.left = `${styleOfHandler.left}px`;
  handler.style.top = `${styleOfHandler.top}px`;
});

mouseup$
.subscribe(() => {
  Object.assign(initialStyleOfHandler, styleOfHandler);  // 鼠标释放后将当前的样式作为下次的初始值
});

实现自动滚动功能 (注意注释部分)

const wrapper = document.querySelector('#wrapper');
const handler = document.querySelector('#handler');

const initialStyleOfHandler = {
  left: 0,
  top: 0
};
const styleOfHandler = {
  left: 0,
  top: 0
};

const mousedown$ = Rx.Observable.fromEvent(handler, 'mousedown');
const mousemove$ = Rx.Observable.fromEvent(document, 'mousemove');
const mouseup$ = Rx.Observable.fromEvent(document, 'mouseup');

mousedown$
.switchMap(mousedownEvt => {
  const startPoint = {
    x: mousedownEvt.pageX,
    y: mousedownEvt.pageY
  };
  const maxScrollTop = wrapper.scrollHeight - wrapper.clientHeight;
  const drag$ = mousemove$.throttleTime(10).takeUntil(mouseup$);
  
  const autoScroll$ = new Rx.Subject()
  .switchMap(({ action, mousemoveEvt }) => {

   // 定义自动滚动流,当流中有值时,根据`action`进行流转换
   // stop: 不做任何事情,直接转换为只发送一次`mousemove`事件的流
  // scrollTop 或 scrollBottom: 每10ms更新`wrapper`的scrollTop值,实现向上或向下自动滚动,然后将`interval`这个流的值转为`mousemove`事件值

    if (action === 'stop') return Rx.Observable.of(mousemoveEvt);
    const scrollDiff = 10;
    return Rx.Observable.interval(10)  // 定义间隔10ms的无限流
      .takeUntil(mouseup$)  // 无限流在鼠标释放时停止
      .do(() => {
        // 自动滚动。
        // 更新scrollTop, 由于这个是副作用,所以放在`do`内 (Rx.js 中的do是在流中会产生副作用使用)
        // 这里将自动滚动定位为副作用是因为这个流最终是为了向后面的拖拽流发送`mousemove`事件的,
        // 使`handler`的位置跟随滚动
        if (action === 'scrollTop') {
          wrapper.scrollTop = Math.max(wrapper.scrollTop - scrollDiff, 0);
        } else {
          wrapper.scrollTop = Math.min(wrapper.scrollTop + scrollDiff, maxScrollTop);
        }
      })
      .map(() => {
        return mousemoveEvt;
      })
  });
  
  drag$
  .subscribe(mousemoveEvt => {
    // 监听拖拽流,每有一次值就检察`handler`的位置和外框顶部、底部的关系,向`autoScroll$`值发送指令和`mousemove`事件
    const rectOfHandler = handler.getBoundingClientRect();
    if (rectOfHandler.top < 10) {
      autoScroll$.next({
        action: 'scrollTop',
        mousemoveEvt
      });
    } else if (rectOfHandler.bottom - wrapper.clientHeight > -10) {
      autoScroll$.next({
        action: 'scrollBottom',
        mousemoveEvt
      });
    } else {
      autoScroll$.next({
        action: 'stop',
        mousemoveEvt
      });
    }
  });

  return drag$
  .merge(autoScroll$)  // 当自动滚动流发送值时也更新`handler`的位置,使其跟随滚动
  .map(mouseMoveEvt => {
    const diff = {
      x: mouseMoveEvt.pageX - startPoint.x,
      y: mouseMoveEvt.pageY - startPoint.y
    };

    return diff;
  });
})
.subscribe(diff => {
  styleOfHandler.left = initialStyleOfHandler.left + diff.x;
  styleOfHandler.top = initialStyleOfHandler.top + diff.y + wrapper.scrollTop;  // 计算Y方向时加上滚动值
  handler.style.left = `${styleOfHandler.left}px`;
  handler.style.top = `${styleOfHandler.top}px`;
});

mouseup$
.subscribe(() => {
  Object.assign(initialStyleOfHandler, styleOfHandler);
});

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.