小鑫啊丶

小鑫啊丶 查看完整档案

填写现居城市  |  填写毕业院校  |  填写所在公司/组织填写个人主网站
编辑
_ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 该用户太懒什么也没留下

个人动态

小鑫啊丶 赞了文章 · 10月15日

JS算法之深度优先遍历(DFS)和广度优先遍历(BFS)

JS算法之深度优先遍历(DFS)和广度优先遍历(BFS)

背景

在开发页面的时候,我们有时候会遇到这种需求:在页面某个dom节点中遍历,找到目标dom节点,我们正常做法是利用选择器document.getElementById(),document.getElementsByName()或者document.getElementsByTagName(),但在本文,我们从算法的角度去查找dom节点,同时理解一下深度优先遍历(DFS)和广度优先遍历(BFS)的原理。

准备

假设页面上的dom结构如下:

<div id="root">
    <ul>
        <li>
            <a href="">
                <img data-original="" alt="">
            </a>
        </li>
        <li>
            <span></span>
        </li>
        <li>
        </li>
    </ul>
    <p></p>
    <button></button>
</div>

让我们来把这个dom结构转化成树的样子

clipboard.png
这样之后,dom结构似乎清楚了不少。

深度优先遍历(Depth-First Search)

该方法是以纵向的维度对dom树进行遍历,从一个dom节点开始,一直遍历其子节点,直到它的所有子节点都被遍历完毕之后在遍历它的兄弟节点。即如图所示(遍历顺序为红字锁标):

clipboard.png
js实现该算法代码(递归版本):

function deepFirstSearch(node,nodeList) {  
    if (node) {    
        nodeList.push(node);    
        var children = node.children;    
        for (var i = 0; i < children.length; i++) 
        //每次递归的时候将 需要遍历的节点 和 节点所存储的数组传下去
        deepFirstSearch(children[i],nodeList);    
    }    
    return nodeList;  
} 

非递归版本:

function deepFirstSearch(node) {
    var nodes = [];
    if (node != null) {
        var stack = [];
        stack.push(node);
        while (stack.length != 0) {
        var item = stack.pop();
        nodes.push(item);
        var children = item.children;
        for (var i = children.length - 1; i >= 0; i--)
            stack.push(children[i]);
        }
    }
    return nodes;
}

deepFirstSearch接受两个参数,第一个参数是需要遍历的节点,第二个是节点所存储的数组,并且返回遍历完之后的数组,该数组的元素顺序就是遍历顺序,调用方法:

let root = document.getElementById('root')
deepTraversal(root,nodeList=[])

控制台输出结果

clipboard.png

广度优先遍历(breadth-first traverse)

该方法是以横向的维度对dom树进行遍历,从该节点的第一个子节点开始,遍历其所有的兄弟节点,再遍历第一个节点的子节点,完成该遍历之后,暂时不深入,开始遍历其兄弟节点的子节点。即如图所示(遍历顺序为红字锁标):

clipboard.png
js实现算法代码(递归版本):

function breadthFirstSearch(node) {
    var nodes = [];
    var i = 0;
    if (!(node == null)) {
        nodes.push(node);
        breadthFirstSearch(node.nextElementSibling);
        node = nodes[i++];
        breadthFirstSearch(node.firstElementChild);
    }
    return nodes;
}

递归版本的BFS由于层级太深,会导致堆栈溢出:Maximum call stack size exceeded,但遍历的顺序依旧没有问题,可以在遍历过程中进行操作,不返回遍历数组即可。
非递归版本:

function breadthFirstSearch(node) {  
    var nodes = [];  
    if (node != null) {  
        var queue = [];  
        queue.unshift(node);  
        while (queue.length != 0) {  
            var item = queue.shift();  
            nodes.push(item);  
            var children = item.children;  
            for (var i = 0; i < children.length; i++)  
                queue.push(children[i]);  
        }  
    }  
    return nodes;  
}

控制台输出结果:

clipboard.png

总结

BFS和DFS都是图的算法之一,本文所阐述的版本较为简单,为无向且非连通图,在日后会更新更多基于JavaScript的算法。

查看原文

赞 21 收藏 9 评论 0

小鑫啊丶 发布了文章 · 5月3日

h5 小游戏总结及踩坑记录(全是干货~)

这是近期的一个 h5 项目,由于某些原因,预览地址不能放出来。不过这不是重点,没有 demo 不就可以好好看文章了吗 哈哈哈~

文中提到的 pixi 是 pixiJs,精灵是 pixiJs 中的概念。阅读本文假设你已经知道了这些东西,不过这在本篇文章中并没有太多关于这个库的内容

觉得这篇文章有帮助到自己,就让它去收藏夹吃灰;觉得没用或觉得写的不好的,可以留下你的足迹;觉得我的代码或文字可以改善的,咱们可以进行多人运动一起交流~

下面就开始正经的东西了:

适配方案

这个项目里用的是 rem 适配方案,通过计算 设备宽度/设计稿宽度 的比例,来设置 html 的 font-size 属性,达到适配的目的,代码如下:

function setSize() {
  // 设备宽度
  let deviceWidth = window.screen.width;
  // 设计稿宽度
  const baseValue = 750;
  // html的字体大小 = (设备宽度 / 设计稿宽度) * 100
  document.documentElement.style.fontSize = (deviceWidth / baseValue) * 100 + 'px';
}

// DOM树加载完执行
window.addEventListener("DOMContentLoaded", function () {
  setSize();
})

// 屏幕变化就执行
window.addEventListener("resize", function () {
  setSize();
})

setSize();

资源的预加载

资源的预加载用的是 preloadjs

中文官网:http://www.createjs.cc/preloadjs

用法也是极其简单:

  var queue = new window.createjs.LoadQueue(true);
  queue.on("complete", this.allLoadComplete);    // 所有文件加载完成时触发
  queue.on("fileload", this.aloneLoadComplete);    // 单个文件加载完成时触发
  queue.on("progress", this.fileProgress);    // 加载进度
  queue.loadManifest(allImg);    // 需要加载的资源数组
  queue.load();

如何解决需要引入很多图片的问题

这里用到了 webpack 的 api: require.context,当项目需要引入很多资源时,这项技术是必须要掌握的

可以阅读 使用require.context自动导入ES模块 - yeyan1996 这位大佬的文章,本文不做深入探讨

如何实现换装

如何让人物换装也是一个问题,经过尝试后发现,直接切换精灵的 texture 可以达到这个目的

this.personContainers[key].children.map((item, index) => {
    if (item.type == type) {
      item.texture = loadSources[name].texture
    }
})

pixi 多个容器的排序问题

在 pixi 里,要想动态的改变某个精灵或容器的层级,不能只改一个,,还需要把其他不相干的元素的层级设低

举个栗子:

personContainers.map((item, i) => {
    item.zIndex = 0;    // 不相干的元素 层级设为最低
    if (index == i) {
      item.zIndex = 99;    // 目标元素 层级设为最高
    }
})

精灵的 touchend 事件移动时会失效

具体为什么会失效还不知道,我的解决方法是:在给精灵绑定一个 touchendoutside 事件

touchendoutside:触摸开始、移出对象松开时触发

html2canvas 截取页面时图片模糊

如果截取的区域里有涉及到图片,不要用 background 设置图片,全部替换成 img 标签

这样可以大大提升图片的清晰度

ios 键盘会把页面顶上去 不会自动下来

问题描述:移动端 ios 键盘弹起后,会把页面顶上去,输入完成后页面不会自动下来

解决办法:

document.body.addEventListener('focusout', function () {
    window.scrollTo(0,0);
});

当监听到 body 里有元素失去焦点时,就把页面滚上去

focusout: 当元素即将失去焦点时,focusout 事件被触发。focusout 事件和 blur 事件之间的主要区别在于后者不会冒泡

—— MDN

如何监听 textarea 的输入

这个项目用了多行输入框 textarea ,发现直接在标签上加了监听事件后啥也没有...,后来找到了解决办法:

this.$refs.textarea.addEventListener('input', e => {
    console.log(e.target.value);    // textarea 输入的文字
}, false)

滑动加载以及获取body实际高度的坑

滑动加载的关键就是如果页面滑动到了底部,就进行数据的请求,要事先和后端沟通好数据怎么返回

滑动到页面底部的条件:滚动条离顶部的距离(document.body.scrollTop)+ 窗口的文档显示区的高度(window.innerHeight)>= 文档实际高度(document.body.scrollHeight)

这里有个坑,关于获取文档实际高度的:

document.body.scrollHeight 可以在手机上获取到实际文档高度,但在 chrome 里获取到的高度是 0

document.documentElement.scrollHeight 在 chrome 里可以获取到正常的,但在手机上获取的高度是 0

这样的话,还要判断当前设备是手机还是 pc 然后再去获取吗,No No No,有个优雅的写法:

// 获取文档实际高度/移动端
var bodyHeight = Math.max(
  document.documentElement.scrollHeight,
  document.body.scrollHeight
);

没有把基础的东西丢掉,此处应该有掌声 [啪啪啪]

vscode 返回上一个编辑点

当代码写的像又臭又长的意大利式代码时,需要去调试某个函数,发现另一个函数又有问题,看完之后还需要翻回来,这就很难顶了

后来发现了这个释放双手的快捷键 - 返回上/下一个编辑点:Alt + ←/→ (windows)

去除 iphone x 的小尾巴

大家管这玩意叫胡子,我比较喜欢叫小尾巴~,就是 iphone x 下面那一根玩意

想要了解更多请点击:https://imweb.io/topic/5baa38...

我在这里就说说我是怎么用的

meta 标签里加上 viewport-fit=cover

<meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no,viewport-fit=cover" />

然后在底部的样式里加上样式:

bottom: env(safe-area-inset-bottom);
bottom: 0;    // 这一句也要加上 没有小尾巴的机型需要用到这个样式

这里的 safe-area-inset-bottom 的意思是:在 Viewport底部的安全区域内设置量(CSS像素),更多方向看下图

img

(ps: 图片来源 https://imweb.io/topic/5baa38... 侵删)

小程序跳转页面传值的问题

小程序跳转到其他组件时,如果需要带网址这类的参数的话,记得要转码,不然是传不过去滴

栗子:

let link = "https://www.baidu.com/"
wx.redirectTo({
  url: "/pages/index/index?url=" + encodeURIComponent(url),    // 解码: decodeURIComponent
})

为什么要转码呢?

假设现在需要获取用户信息后传个网址给 index 组件,这时候,这个链接里是带有中文或者其他符号的,如果不转码,小程序会觉得你这个不是个可以识别的地址,会直接把域名后面的值给你去掉

想要了解更多 转/解码 的知识请点击:https://www.w3school.com.cn/j...

ios 最新版系统 微信浏览器 html2canvas 生成图片失败

这个项目里有个生成图片的功能,需要保存用户操作过的一些东西,其他设备都正常,唯独 ios 最新版的系统有问题

debug 了一波后发现,这他瞄的根本没有执行这个函数,找一半天也没找到为什么,后来看 issues 发现有人说把版本换成 rc.4 的就可以了

issues:https://github.com/niklasvh/h...

emoji 表情转码

项目测试的时候,发现 textarea 标签里可以输入表情,紧接着,接口就报错了,一查看,虽然支持输入表情,但是不会自动对表情转码,所以提交接口的时候报错了,下面分享个 emoji 表情转字符的方法:

// 表情转字符
utf16toEntities(str) {
  var patt = /[\ud800-\udbff][\udc00-\udfff]/g // 检测utf16字符正则
  str = str.replace(patt, function(char) {
    var H, L, code
    if (char.length === 2) {
      H = char.charCodeAt(0) // 取出高位
      L = char.charCodeAt(1) // 取出低位
      code = (H - 0xd800) * 0x400 + 0x10000 + L - 0xdc00 // 转换算法
      return '&#' + code + ';'
    } else {
      return char
    }
  })
  return str
}

教训

  • 全局事件卸载组件时一定要注销事件
  • 自己写的东西要多测几遍
  • npm 包切换版本后要去文件里看下是不是真的切换了

参考文章

ps: 上述问题的答案基本都是看了这些文章的内容并融入了自己的思考,得到的答案。感谢各位大佬的付出[抱拳]

本文首发:

最后,祝大家,五一Happy~

查看原文

赞 0 收藏 0 评论 2

小鑫啊丶 赞了问题 · 1月16日

解决IOS微信长按图片保存不完整

苹果手机在微信上浏览图片是完整的 保存到本地后打开发现被剪切了周围一圈,各位大佬知道是怎么回事吗?

关注 1 回答 1

小鑫啊丶 赞了文章 · 2019-12-21

关于微信小程序的wx.request执行后sucess和fail的问题

做过小程序的朋友们应该都知道,小程序中调用ajax的命令是wx.request,其有两个回调,一个是success,一个是fail,
那么什么情况下会走success,什么情况下会走fail呢?

大多数人可能认为,success就是我成功请求到数据了,也就是请求返回的状态码是2XX,而返回4XX,5XX肯定是出错了,出错了就走fail呗。

那可就错啦,其实只要你的wx.request成功发出了请求,无论返回什么http状态码,都会走success。

说到这很多同学会问这也太不合理了吧,这样success和fail有什么意义? 答案是这样的设定是非常合理的,当我们遇到断网,域名解析有问题,或者尤其是我们去调用restful api时,可能会在url格式,参数类型上出些问题,这些情况下才会调用到fail。

反之4xx、5xx 等 response的异常状态不会进入fail回调,我们需要在success回调中检查statusCode,根据http状态码的不同去做相应的逻辑判断。

查看原文

赞 2 收藏 1 评论 0

小鑫啊丶 赞了回答 · 2019-12-17

解决js将一位数组分割成每三个一组

var data = ['法国','澳大利亚','智利','新西兰','西班牙','加拿大','阿根廷','美国','0','国产','波多黎各','英国','比利时','德国','意大利','意大利',];
var result = [];
for(var i=0,len=data.length;i<len;i+=3){
   result.push(data.slice(i,i+3));
}

关注 15 回答 8

小鑫啊丶 关注了问题 · 2019-12-09

HTML中音频或者视频的缓存进度的获取

现在的要求就是点击播放后,视频或者音频没有加载完成,显示加载中,并且显示加载的进度。加载的进度该怎么获取呢?

关注 5 回答 3

小鑫啊丶 赞了文章 · 2019-11-21

记一次游戏H5开发经验

快到年终的时候做了一个以游戏形式展示的h5活动页,第一次尝试使用js写小游戏,很有趣的过程,很宝贵的经验。

效果图

直接上个效果的gif图,游戏的一小部分效果,录出来有点卡
图片描述

结果页:
图片描述

起因

产品妹子突然给我拉进来一个群,跟我们讲做了这么久的制作平台(用户制作手机主题的平台),我们是不是应该反馈给用户点什么东西,就像之前特别火的微信年终总结那样。总之就是要打动用户,要特别酷。说特别酷的时候她回头朝我微微一笑,微笑中带着一点点,嗯,杀意。
活动形式,展现方式,什么数据反正就是统统都没想好,整个过程中大家讨论的热火朝天。当时不知道我为啥脑子一热,跟她说了一句:“没事儿,搞吧,你能想出来我就给你做出来。”而我也因为这句话把自己置身于水深火热之中。。
讨论的结果就是大家的idea感觉都不是特别酷,又不好玩儿,干脆就做个游戏形式的吧!所有人都转头看向我,我想了想之前说的话,只吐出来一个字,“搞”。而内心中五味杂陈,“游戏?有意思啊?搞!没搞过啊?能搞定吗?搞!”。最终敲定,两周时间,游戏方式,展现用户在魔秀的点点滴滴。

准备工作

游戏的形式大概类似一个滑雪大冒险和赛车的结合,以赛车的形式进行伪3d效果的展现,滑雪大冒险的样式作为我们的主题,同时大家还给我们的游戏起了个酷炫的名字----魔秀时光道。

游戏引擎

游戏的展现形式确定后,直觉告诉我,想要将游戏快速稳定的呈现,免去图片加载控制,动画控制之类的复杂处理,我需要一个JS游戏引擎。最终在EgretPhaserPixiJS中选定了PixiJS,虽然不像Egret一样有完善的中文文档,但是它提供了清晰易懂的examples可快速上手,没有复杂的生态,简单的几行代码就可以用js实现我想要以下几点功能:

容器渲染及背景描绘

我需要定制整个画布的大小和背景,我需要使用不同的容器来承接不同的内容,并且灵活控制每个容器的属性:

//画布
var app = new PIXI.Application(800, 600, {backgroundColor : 0x1099bb});
document.body.appendChild(app.view);

//定制container
var container = new PIXI.Container();

container.x = (app.renderer.width - container.width) / 2;
container.y = (app.renderer.height - container.height) / 2;

app.stage.addChild(container);

图片加载及动画处理

大家都知道,使用canvas进行图片绘制的时候,需要确定图片已经成功加载,而游戏中有着大量的图片资源需要去维护,PixiJS已经为我们提供此项服务:

var bunny = PIXI.Sprite.fromImage('required/assets/basics/bunny.png’);

bunny.x = app.renderer.width / 2;
bunny.y = app.renderer.height / 2;

app.stage.addChild(bunny);

同时,我们需要一个动画控制器,来控制各Sprite的运动和重绘,而不是生硬的对各项属性进行重新修改:

app.ticker.add(function(delta) {
  bunny.rotation += 0.1 * delta;
});

需要注意的是,我们会发现,此处的Sprite动画控制,相当于添加了运动的动画队列,并且实现了类似transformjs的效果,可直接对实例的属性进行操作。而我在写项目的时候官方的例子是通过统一animate函数进行操作,通过requestAnimationFrame进行帧动画控制,更推荐新的方式而不是如下:

function animate () {
  requestAnimationFrame(animate);
  
  bunny.rotation += 1;
  renderer.render(stage);
}

事件处理

游戏最重要的部分相当于用户的交互了,也就是所谓的事件处理,为Sprite添加事件监听,很简单,如下所示:

//元素可点击
sprite.interactive = true;

//鼠标移入cursor
sprite.buttonMode = true;

sprite.on('click', onClick); // mouse-only
prite.on('tap', onClick); // touch-only

function onClick () {
  sprite.scale.x *= 1.25;
  sprite.scale.y *= 1.25;
}

设计图

设计图当然也是很重要的,决定了我们如何去实现这个游戏,当我拿到设计图的时候,他是长成这样的,我的内心是崩溃的。我能怎么样,我也很无奈呀~ 开搞吧!
图片描述

实现思路

根据以上,PixiJS已经基本满足我们的需求,也就是说,工具准备和素材准备已经都完成了。在动手书写之前,我们需要把实现思路想好,才能保证书写过程的清晰,避免不必要的麻烦。

背景滑动效果实现

就像我们平时玩儿赛车游戏一样,我们感觉赛车在跑道上进行比赛,实际上赛车只进行左右移动而已,而运动的则是背景,如何规划好路线,让背景按照既定的场景去运动,并展现不同的视角,特意向央美的同胞咨询了下,他们是用一个叫“摄像机”的东西实现的。对于我们来说,不需要那么复杂的场景,只需让背景像前规律的“平移”,造成“树动我不动”的视觉效果,同时我们利用“透视”的原理,让背景以“近大远小”的方式进行变化,就会产生一种low low的立体效果。

关键词:透视 近大远小(偏移,大小,速度)

偏移路线

对于背景及物体的运动,大概路线规划如下:
图片描述

确定视觉焦点后,我们只需随机生成物体出现的位置,计算出a,b相对固定,使其y进行相应速度的增加,x根据运动轨迹进行对应偏移,则可实现往近跑的效果。针对运动轨迹, 假设物体向下偏移距离为N,则对应水平针对中轴线的偏移为:
图片描述

大小

同时,我们还需对物体进行近大远小的显示,这个比较简单,以焦点为0,页面最底端为1,进行对应比例放大即可:

scale = (curY - startY) / ( endY - startY);

运动速度

针对物体的运动速度,也应在远近有不同的体现。

背景树与碰撞物体的区别

针对背景树,我们需在最初对所有的树进行展现,铺满两边背景。每列树对应的运动路线一致,可直接让其进行循环展示,当树运动到最底时,让其出现在最顶点。因此只需确定一共有几行几列树,并设定其边界,根据行列确定初始唯一并对其进行运动。同时,可以让树进行小范围的随机偏移,使树错落有致。如下所示:

export default function Tree (row, col, direction) {
  this.cfg = {
    direction: direction, //方向
    col: col, //第几列
    row: row, //第几行
    MaxX: 440,
    minY: 210,
    maxY: 500,
    range: 10 //坐标浮动范围
  }
};

而针对物体,则需要随机生成它的初始x坐标,并计算出其对于的路线进行运动,在运动过程中,进行碰撞检测,检测是否与人物进行相撞。

人物滑动实现

人物滑动的操作,用了最简单的实现方式:按钮。当用户点击不同方向时,让人物向对应方向进行偏移。同时,为了让人物滑动不僵硬,在左右滑动过程中,人也应该随着运动有对应角度的倾斜,就像我们平时玩儿滑雪拐弯时,会改变中心一样。思路如下:

  1. 点击按钮时,改变方向

  2. 运动时检测方向,若向左,则x减小,向右,则增加

  3. 向右(左)运动时,人物对应rotation也进行增加(减小)

  4. 松开手时,人物对应rotation慢慢恢复成0;

碰撞检测

由于人物有吃东西的环节(不然这还叫什么游戏呢),因此碰撞检测肯定是必须的啦。我们可以通过两种方式进行碰撞检测

  1. 人物检测碰撞物体,需实时遍历物体坐标列表,进行检测

  2. 每个物体自身进行碰撞检测,检测自身与人物位置的对应差

我很机智的选择了第二个,毕竟每个物体的位置都是实时变动的,而每次碰撞检测都进行一次循环的方法,太笨重啦。在这里我们设置碰撞检测的区域(宽高),在物体运动时,针对人物的x,y坐标,与自身的x,y坐标加减形成的四条边界进行比较即可,若进行碰撞,则进行对应的操作即可,如播放音频,得分+1等。

架构设计

思路理清楚之后,后面的路就很明朗啦。接下来我们就可以着手设计下如何实现这个东西了,很显然,游戏中我们拥有许许多多的“角色”,使用“面向对象”的方式再好不过了。大概的划分如下

  • stage //舞台,进行基本场景渲染,游戏整体控制(开始,停止)等

  • player //玩儿家,也就是对应的人物

  • sprite //出现的物体,如蛋糕等,提供玩儿家吃。 包含碰撞检测等,会自己运动

  • tree //因为tree自身会运动,所以每个tree为一个类

  • score //进行分数控制及显示

  • cfg.js //包含整体游戏配置

对象内部划分

每个对象包含以下几点属性及功能:

1. 对象配置

每个对象包含其内部自身基本配置,包括位置,边界,图片等。直观,便于调试

export default function People (stage) {
  this.cfg = {
    img: require('./img/people.png'),
    anchor: {
      x: 0.5,
      y: 0.5,
    },
    position: {
      x: cfg.width / 2,
      y: 500
    },
    speed: 5,
  }
}

2. 其他方法

每个对象都包含其自身方法,如下所示:

  • render //进行图片等渲染

  • animate //动画function

  • init //一些初始化配置

实现

通过以上思路的设计和结构的设计,我很快的将这个游戏实现了。。。没错,理清思路和结构的重要性就是这样。当然,在实现过程中,也有一些小的点可以记录下:

资源加载器(图片)

为了游戏的进行效果,还是决定在加载完所有资源(尤其是图片资源)后,才停止loading页面。如何判断所有内容都加载完毕了呢?写了个小loader

var pics = [
  require('./img/bg-start.png'),
  require('./img/btn-start.png'),
  ...
];

function loadImages (pics, callback) {
  if(pics.length) {
    var img = new Image(),
      pic = pics.shift();

    img.onload = callback;
    img.src = pic;
    loadImages(pics, callback);
  } else{
    return;
  }
}

$(function() {
  loadImages(pics, function () {
    if (!pics.length) {
      $('.loading').hide();
    };
  })
});

强制横屏

游戏是横屏展示的,那就强制横屏好啦。这个当时还纠结挺久,还是自己功底不扎实脑子走私了,还在想是监听resize事件还是旋转屏幕事件,都没有这些事儿啊好吗!直接让它旋转就好。

if(window.orientation==180||window.orientation==0) {
  $('#main').height(winW);
  $('#main').width(winH);

  $(‘#main’).css({
    'transform': 'rotate(90deg)',
  });
} else{
  $(‘#main’).css('transform', 'rotate(0)');
}

timer控制

理清思路后,最乱的还是各种定时器啦。 为了实现物体随机出现的效果,让每个物体随机多少秒后开始出现;最后一个物体出现完,多少秒后出现结束画面等等,需要理清楚各个定时器的关系,并对其添加语义化良好的标记,及时对未完结的定时器进行清除,防止定时器带来的意想不到的问题。

写在最后

最终游戏的效果基本让大家满意啦,也是第一次尝试这方面的开发,周围也完全没有做过这东西的人。从开始的忐忑和一无所措,到过程中理清思路和结构,到书写中的各种未知的坑,自己在这两周感觉经历了很充实的一件事情。同时也对后续进行一些未知事物的探索和学习有了更丰富的经验,找对路子才是王道呀!

查看原文

赞 27 收藏 75 评论 18

小鑫啊丶 赞了文章 · 2019-09-03

移动端h5模拟长按事件

为啥写这篇文章

最近接了个需求,要求长按某个标签显示删除一个悬浮的删除按钮。这个需求其实在app上很常见,但是在移动端h5中,我们没有长按的事件,所以就需要自己模拟这个事件了。

大概效果如下:

图片描述

ps: 为了做个gif还下了app,还得通过邮件发到电脑上,脑瓜疼。。

思路

  • 放弃click事件,通过判断按的时长来决定是单击还是长按
  • 使用touchstart和touchend事件
  • 在touchstart中开启一个定时器,比如在700ms后显示一个长按菜单
  • 在touchend中清除这个定时器,这样如果按下的时间超过700ms,那么长按菜单已经显示出来了,清除定时器不会有任何影响;如果按下的时间小于700ms,那么touchstart中的长按菜单还没来得及显示出来,就被清除了。

由此我们可以实现模拟的长按事件了。

上代码

请把重点放在JS上,这里贴出来完整的代码是为了方便大家看个仔细,代码可以拷贝直接看效果
css中大部分只是做了样式的美化,还有一开始让删除按钮隐藏起来

HTML:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
    <link rel="stylesheet" type="text/css" href="./longpress.css" />
</head>
<body>
    <div class="container">
        <div class="label" id="label">长按我</div>
        <div class="delete_btn">删除</div>
    </div>
    <script data-original="./longpress.js"></script>
</body>
</html>

JS

let timer = null
let startTime = ''
let endTime = ''
const label = document.querySelector('.label')
const deleteBtn = document.querySelector('.delete_btn')

label.addEventListener('touchstart', function () {
  startTime = +new Date()
  timer = setTimeout(function () {
    deleteBtn.style.display = 'block'
  }, 700)
})

label.addEventListener('touchend', function () {
  endTime = +new Date()
  clearTimeout(timer)
  if (endTime - startTime < 700) {
    // 处理点击事件
    label.classList.add('selected')
  }
})

CSS

.container {
    position: relative;
    display: inline-block;
    margin-top: 50px;
}

.label {
    display: inline-block;
    box-sizing: border-box;
    width: 105px;
    height: 32px;
    line-height: 32px;
    background-color: #F2F2F2;
    color: #5F5F5F;
    text-align: center;
    border-radius: 3px;
    font-size: 14px;
}

.label.selected {
    background-color: #4180cc;
    color: white;
}

.delete_btn {
    display: none;
    position: absolute;
    top: -8px;
    left: 50%;
    transform: translateX(-50%) translateY(-100%);
    color: white;
    padding: 10px 16px;
    background-color: rgba(0, 0, 0, .7);
    border-radius: 6px;
    line-height: 1;
    white-space: nowrap;
    font-size: 12px;
}

.delete_btn::after {
    content: '';
    width: 0;
    height: 0;
    border-width: 5px;
    border-style: solid;
    border-color: rgba(0, 0, 0, .7) transparent transparent transparent;
    position: absolute;
    bottom: -9px;
    left: 50%;
    transform: translateX(-50%);
}

ps: touchstart和touchend只有在移动端设备上才有用,如果要看代码示例的话请:

  • 用chrome
  • F12打开调时窗
  • 切换到模拟移动设备

即点击如下图:

clipboard.png

最后

欢迎交流~

查看原文

赞 35 收藏 27 评论 3

小鑫啊丶 提出了问题 · 2019-09-03

html2canvas不能截取当前页面的canvas

最近有个需求是要点击按钮截取当前页面的图片

页面其他的元素截取了,canvas渲染的就没截到

用的这段代码

html2canvas(document.querySelector('#ad'), {
        scale:2,
        logging:false,
        allowTaint: true,
        useCORS:true
      }).then(
        function(canvas) {
          var src = canvas.toDataURL()
          document.querySelector('#img').src = src
        }
      );

麻烦有时间的大佬帮我看看 谢谢

关注 2 回答 1

小鑫啊丶 赞了文章 · 2019-08-12

React造轮系列:对话框组件 - Dialog 思路

本文是React造轮系列第二篇。

本轮子是通过 React + TypeScript + Webpack 搭建的,至于环境的搭建这边就不在细说了,自己动手谷歌吧。当然可以参考我的源码

想阅读更多优质文章请猛戳GitHub博客,一年百来篇优质文章等着你!

UI

clipboard.png

对话框一般是我们点击按钮弹出的这么一个东西,主要类型有 Alter, ConfirmModal, Modal 一般带有半透明的黑色背景。当然外观可参考 AntD 或者 Framework 等。

确定 API

API 方面主要还是要参考同行,因为如果有一天,别人想你用的UI框架时,你的 API 跟他之前常用的又不用,这样就加大了入门门槛,所以API 尽量保持跟现有的差不多。

对话框除了提供显示属性外,还要有点击确认后的回放函数,如:

alert('你好').then(fn)
confirm('确定?').then(fn)
modal(组件名)

实现

Dialog 源码已经上传到这里

dialog/dialog.example.tsx, 这里 state ,生命周期使用 React 16.8 新出的 Hook,如果对 Hook 不熟悉可以先看官网文档

dialog/dialog.example.tsx

import React, {useState} from 'react'
import Dialog from './dialog'
export default function () {
  const [x, setX] = useState(false)
  return (
    <div>
      <button onClick={() => {setX(!x)}}>点击</button>
      <Dialog visible={x}></Dialog>
    </div>
  )
}

dialog/dialog.tsx

import React from 'react'

interface Props {
  visible: boolean
}

const Dialog: React.FunctionComponent<Props> = (props) => {
  return (
    props.visible ? 
      <div>dialog</div> : 
      null
  )
}

export default Dialog

运行效果

图片描述

显示内容

上述还有问题,我们 dialog 在组件内是写死的,我们想的是直接通过组件内包裹的内容,如:

// dialog/dialog.example.tsx
...
<Dialog visible={x}>
  <strong>hi</strong>
</Dialog>
...

这样写,页面上是不会显示 hi 的,这里 children 属性就派上用场了,我们需要在 dialog 组件中进一步骤修改如下内容:

// dialog/dialog.tsx
...
return (
    props.visible ? 
      <div>
        {props.children}
      </div>
      : 
      null
)
...

显示遮罩

通常对话框会有一层遮罩,通常我们大都会这样写:

// dialog/dialog.tsx
...
props.visible ? 
  <div className="fui-dialog-mask">
    <div className="fui-dialog">
    {props.children}
    </div>
  </div>
  : 
  null
...

这种结构有个不好的地方就是点击遮罩层的时候要关闭对话框,如果是用这种结构,用户点击任何 div,都相当于点击遮罩层,所以最好要分开:

// dialog/dialog.tsx
...
<div>
    <div className="fui-dialog-mask">
    </div>
    <div className="fui-dialog">
      {props.children}
    </div>
 </div>
...

由于 React 要求最外层只能有一个元素, 所以我们多用了一个 div 包裹起来,但是这种方法无形之中多了个 div,所以可以使用 React 16 之后新出的 Fragment, Fragment 跟 vue 中的 template 一样,它是不会渲染到页面的。

import React, {Fragment} from 'react'
import './dialog.scss';
interface Props {
  visible: boolean
}

const Dialog: React.FunctionComponent<Props> = (props) => {
  return (
    props.visible ? 
     <Fragment>
        <div className="fui-dialog-mask">
        </div>
        <div className="fui-dialog">
          {props.children}
        </div>
     </Fragment>
      : 
      null
  )
}

export default Dialog

完善头部,内容及底部

这里不多说,直接上代码

import React, {Fragment} from 'react'
import './dialog.scss';
import {Icon} from '../index'
interface Props {
  visible: boolean
}

const Dialog: React.FunctionComponent<Props> = (props) => {
  return (
    props.visible ? 
      <Fragment>
          <div className="fui-dialog-mask">
          </div>
          <div className="fui-dialog">
            <div className='fui-dialog-close'>
              <Icon name='close'/>
            </div>
            <header className='fui-dialog-header'>提示</header>
            <main className='fui-dialog-main'>
              {props.children}
            </main>
            <footer className='fui-dialog-footer'>
              <button>ok</button>
              <button>cancel</button>
            </footer>
          </div>
      </Fragment>
      : 
      null
  )
}

export default Dialog

从上述代码我们可以发现我们写样式的名字时候,为了不被第三使用覆盖,我们自定义了一个 fui-dialog前缀,在写每个样式名称时,都要写一遍,这样显然不太合理,万一哪天我不用这个前缀时候,每个都要改一遍,所以我们需要一个方法来封装。

咱们可能会写这样方法:

function scopedClass(name) {
  return `fui-dialog-${name}`
}

这样写不行,因为我们 name 可能不传,这样就会多出一个 -,所以需要进一步的判断:

function scopedClass(name) {

  return `fui-dialog-${name ? '-' + name : ''}`
}

那还有没有更简洁的方法,使用 filter 方法:

function scopedClass(name ?: string) {
  return ['fui-dialog', name].filter(Boolean).join('-')
}

调用方式如下:

  ....
  <Fragment>
      <div className={scopedClass('mask')}>
      </div>
      <div className={scopedClass()}>
        <div className={scopedClass('close')}>
          <Icon name='close'/>
        </div>
        <header className={scopedClass('header')}>提示</header>
        <main className={scopedClass('main')}>
          {props.children}
        </main>
        <footer className={scopedClass('footer')}>
          <button>ok</button>
          <button>cancel</button>
        </footer>
      </div>
  </Fragment>
 ...

大家在想法,这样写是有问题,每个组件都写一个函数吗,如果 Icon 组件,我还需要写一个 fui-icon, 解决方法是把 前缀当一个参数,如:

function scopedClass(name ?: string) {
  return ['fui-dialog', name].filter(Boolean).join('-')
}

调用方式如下:

className={scopedClass('fui-dialog', 'mask')}

这样写,还不如直接写样式,这种方式是等于白写了一个方法,那怎么办?这就需要高阶函数出场了。实现如下:

function scopeClassMaker(prefix: string) {
  return function (name ?: string) {
    return [prefix, name].filter(Boolean).join('-')
  }
}

const scopedClass = scopeClassMaker('fui-dialog')

scopeClassMaker 函数是高级函数,返回一个带了 prefix 参数的函数。

事件处理

在写事件处理之前,我们 Dialog 需要接收一个 buttons 属性,就是显示的操作按钮并添加事件:

// dialog/dialog.example.tsx
...
<Dialog visible={x} buttons = {
  [
    <button onClick={()=> {setX(false)}}>1</button>,
    <button onClick={()=> {setX(false)}}>2</button>,
  ]
}>
  <div>hi</div>
</Dialog>
...

咱们看到这个,第一反应应该是觉得这样写很麻烦,我写个 dialog, visible要自己,按钮要自己,连事件也要自己写。请接受这种设定。虽然麻烦,但非常的好理解。这跟 Vue 的理念是不太一样的。当然后面会进一步骤优化。

组件内渲染如下:

<footer className={sc('footer')}>
  {
    props.buttons
  }
</footer>

运行起来你会发现有个警告:

clipboard.png

主要是说我们渲染数组时,需要加个 key,解决方法有两种,就是不要使用数组方式,当然这不治本,所以这里 React.cloneElemen 出场了,它可以克隆元素并添加对应的属性值,如下:

{
  props.buttons.map((button, index) => {
    React.cloneElement(button, {key: index})
  })
}

对应的点击关闭事件相对容易这边就不讲了,可以自行查看源码

接下来来看一个样式的问题,首先先给出我们遮罩的样式:

.fui-dialog {
  position: fixed; background: white; min-width: 20em;
  z-index: 2;
  border-radius: 4px; top: 50%; left: 50%; transform: translate(-50%, -50%);
  &-mask { position: fixed; top: 0; left: 0; width: 100%; height: 100%;
    background: fade_out(black, 0.5);
    z-index: 1;
  }
  .... 以下省略其它样式
}

我们遮罩 .fui-dialog-mask 使用 fixed 定位感觉是没问题的,那如果在调用 dialog 同级在加以下这么元素:

<div style={{position:'relative', zIndex: 10, background:'#fff'}}>666</div>
   
<button onClick={() => {setX(!x)}}>点击</button>
<Dialog visible={x}>
...
</Dialog>

运行效果:

clipboard.png

发现遮罩并没有遮住 666 的内容。这是为什么?

clipboard.png

看结构也很好理解,遮罩元素与 666 是同级结构,且层级比 666 低,当然是覆盖不了的。那咱们可能就会这样做,给.fui-dialog-mask设置一个 zIndex 比它大的呗,如 9999

效果:

clipboard.png

恩,感觉没问题,这时我们在 Dialog 组件在嵌套一层 zIndex 为 9 的呢,如:

<div style={{position:'relative', zIndex: 9, background:'#fff'}}>
  <Dialog visible={x}>
    ...
  </Dialog>
</div>

运行效果如下:

clipboard.png

发现,父元素被压住了,里面元素 zIndex 值如何的高,都没有效果。

那这要怎么破?答案是不要让它出现在任何元素的里面,这怎么可能呢。这里就需要引出一个神奇的 API了。这个 API 叫做 传送门(portal)

用法如下:

return ReactDOM.createPortal(
  this.props.children,
  domNode
);

第一个参数就是你的 div,第二个参数就是你要去的地方。

import React, {Fragment, ReactElement} from 'react'
import ReactDOM from 'react-dom'
import './dialog.scss';
import {Icon} from '../index'
import {scopedClassMaker} from '../classes'

interface Props {
  visible: boolean,
  buttons: Array<ReactElement>,
  onClose: React.MouseEventHandler,
  closeOnClickMask?: boolean
}

const scopedClass = scopedClassMaker('fui-dialog')
const sc = scopedClass

const Dialog: React.FunctionComponent<Props> = (props) => {

  const onClickClose: React.MouseEventHandler = (e) => {
    props.onClose(e)
  }
  const onClickMask: React.MouseEventHandler = (e) => {
    if (props.closeOnClickMask) {
      props.onClose(e)
    }
  }
  const x = props.visible ? 
  <Fragment>
      <div className={sc('mask')} onClick={onClickMask}>
      </div>
      <div className={sc()}>
        <div className={sc('close')} onClick={onClickClose}>
          <Icon name='close'/>
        </div>
        <header className={sc('header')}>提示</header>
        <main className={sc('main')}>
          {props.children}
        </main>
        <footer className={sc('footer')}>
          {
            props.buttons.map((button, index) => {
              React.cloneElement(button, {key: index})
            })
          }
        </footer>
      </div>
  </Fragment>
  : 
  null
  return (
    ReactDOM.createPortal(x, document.body)
  )
}

Dialog.defaultProps = {
  closeOnClickMask: false
}


export default Dialog

运行效果:

clipboard.png

当然这样,如果 Dialog 层级比同级的 zIndex 小的话,还是覆盖不了。 那 zIndex 一般设置成多少比较合理。一般 Dialog 这层设置成 1, mask 这层设置成2。定的越小越好,因为用户可以去改。

zIndex 的管理

clipboard.png

zIndex 管理一般就是前端架构师要做的了,根据业务产景来划分,如广告肯定是要在页面最上面,所以 zIndex 一般是属于最高级的。

便利的 API 之 Alert

上述我们使用 Dialog 组件调用方式比较麻烦,写了一堆,有时候我们想到使用 alert 直接弹出一个对话框这样简单方便。如

  <h1>example 3</h1>
  <button onClick={() => alert('1')}>alert</button>

我们想直接点击 button ,然后弹出我们自定义的对话框内容为1 ,需要在 Dialog 组件内我们需要导出一个 alert 方法,如下:

// dialog/dialog.tsx
...
const alert = (content: string) => {
  const component = <Dialog visible={true} onClose={() => {}}>
    {content}
  </Dialog>
  const div = document.createElement('div')
  document.body.append(div)
  ReactDOM.render(component, div)
}

export {alert}
...

运行效果:

图片描述

但有个问题,因为对话框的 visible 是由外部传入的,且 React 是单向数据流的,在组件内并不能直接修改 visible,所以在 onClose 方法我们需要再次渲染一个新的组件,并设置新组件 visibleture,覆盖原来的组件:

...
const alert = (content: string) => {
  const component = <Dialog visible={true} onClose={() => {
    ReactDOM.render(React.cloneElement(component, {visible: false}), div)
    ReactDOM.unmountComponentAtNode(div)
    div.remove()
  }}>
    {content}
  </Dialog>
  const div = document.createElement('div')
  document.body.append(div)
  ReactDOM.render(component, div)
}
..

便利的 API 之 confirm

confirm 调用方式:

<button onClick={() => confirm('1', ()=>{}, ()=> {})}>confirm</button>

第一个参数是显示的内容,每二个参数是确认的回调,第三个参数是取消的回调函数。

实现方式:

const confirm = (content: string, yes?: () => void, no?: () => void) => {
  const onYes = () => {
    ReactDOM.render(React.cloneElement(component, {visible: false}), div)
    ReactDOM.unmountComponentAtNode(div)
    div.remove()
    yes && yes()
  }
  const onNo = () => {
    ReactDOM.render(React.cloneElement(component, {visible: false}), div)
    ReactDOM.unmountComponentAtNode(div)
    div.remove()
    no && no()
  }
  const component = (
  <Dialog 
    visible={true} onClose={() => { onNo()}}
    buttons={[<button onClick={onYes}>yes</button>, 
              <button onClick={onNo}>no</button>
            ]}
  >
    {content}
  </Dialog>)
  const div = document.createElement('div')
  document.body.appendChild(div)
  ReactDOM.render(component, div)
}

事件处理跟 Alter 差不多,唯一多了一步就是 confirm 当点击 yes 或者 no 的时候,如果外部有回调就需要调用对应的回调函数。

便利的 API 之 modal

modal 调用方式:

<button onClick={() => {modal(<h1>你好</h1>)}}>modal</button>

modal 对应传递的内容就不是单单的文本了,而是元素。

实现方式:

const modal = (content: ReactNode | ReactFragment) => {
  const onClose = () => {
    ReactDOM.render(React.cloneElement(component, {visible: false}), div)
    ReactDOM.unmountComponentAtNode(div)
    div.remove()
  }
  const component = <Dialog onClose={onClose} visible={true}>
    {content}
  </Dialog>
  const div = document.createElement('div')
  document.body.appendChild(div)
  ReactDOM.render(component, div)
}

注意,这边的 content 类型。

运行效果:

图片描述

这还有个问题,如果需要加按钮呢,可能会这样写:

 <button onClick={() => {modal(<h1>
     你好 <button>close</button></h1> 
  )}}>modal</button>

这样是关不了的,因为 Dialog 是封装在 modal 里面的。如果要关,必须控制 visible,那很显然我从外面控制不了里面的 visible,所以这个 button 没有办法把这个 modal 关掉。

解决方法就是使用闭包,我们可以在 modal 方法里面把 close 方法返回:

const modal = (content: ReactNode | ReactFragment) => {
  const onClose = () => {
    ReactDOM.render(React.cloneElement(component, {visible: false}), div)
    ReactDOM.unmountComponentAtNode(div)
    div.remove()
  }
  const component = <Dialog onClose={onClose} visible={true}>
    {content}
  </Dialog>
  const div = document.createElement('div')
  document.body.appendChild(div)
  ReactDOM.render(component, div)
  return onClose;
}

最后多了一个 retrun onClose,由于闭包的作用,外部调用返回的 onClose 方法可以访问到内部变量。

调用方式:

const openModal = () => {
  const close = modal(<h1>你好
    <button onClick={() => close()}>close</button>
  </h1>)
}
<button onClick={openModal}>modal</button>

重构 API

在重构之前,我们先要抽象 alert, confirm, modal 中各自的方法:

alertconfirmmodal
onCloseonClose * 2onClose
componentcomponentcomponent
renderrenderrender
return api

从表格可以看出,modal 与其它两个只多了一个 retrun api,其实其它两个也可以返回对应的 Api,只是我们没去调用而已,所以补上:

alertconfirmmodal
onCloseonClose * 2onClose
componentcomponentcomponent
renderrenderrender
return apireturn apireturn api

这样一来,这三个函数从抽象层面上来看是类似的,所以这三个函数应该合成一个。

首先抽取公共部分,先取名为x ,内容如下:

const x= (content: ReactNode, buttons ?:Array<ReactElement>, afterClose?: () => void) => {
  const close = () => {
    ReactDOM.render(React.cloneElement(component, {visible: false}), div)
    ReactDOM.unmountComponentAtNode(div)
    div.remove()
    afterClose && afterClose()
  }
  const component = 
  <Dialog visible={true} 
    onClose={() => {
      close(); afterClose && afterClose()
    }}
    buttons={buttons}
  >
    {content}
  </Dialog>
  const div = document.createElement('div')
  document.body.append(div)
  ReactDOM.render(component, div)
  return close
}

alert 重构后的代码如下:

const alert = (content: string) => {
  const button = <button onClick={() => close()}>ok</button>
  const close = x(content, [button])
}

confirm 重构后的代码如下:

const confirm = (content: string, yes?: () => void, no?: () => void) => {

  const onYes = () => {
    close()
    yes && yes()
  }
  const onNo = () => {
    close()
    no && no()
  }
  const buttons = [
    <button onClick={onYes}>yes</button>, 
    <button onClick={onNo}>no</button>
  ]
  const close =  modal(content, buttons, no)
}

modal 重构后的代码如下:

const modal = (content: ReactNode | ReactFragment) => {
  return x(content)
}

最后发现其实 x 方法就是 modal 方法,所以更改 x 名为 modal,删除对应的 modal 定义。

总结

  1. scopedClass 高阶函数的使用
  2. <Fragment>
  3. 传送门 portal
  4. 动态生成组件
  5. 闭包传 API

本组件为使用优化样式,如果有兴趣可以自行优化,本节源码已经上传至这里中的lib/dialog

参考

方应杭老师的React造轮子课程

你的点赞是我持续分享好东西的动力,欢迎点赞!

clipboard.png

查看原文

赞 17 收藏 12 评论 0

认证与成就

  • 获得 0 次点赞
  • 获得 3 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 3 枚铜徽章

擅长技能
编辑

(゚∀゚ )
暂时没有

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2019-03-15
个人主页被 99 人浏览