在京东,就任满五年的老员工被称做“大佬”,若是满了十年,那就要被称之为“超级大佬”了。javascript
从 2016 年 5 月 19 日开始,每年的这一天都被定为京东集团的“519 老员工日”。正所谓:五年砺银,十年锻金!在京东成长 10 年的员工,放在行业里的任何一家公司,都可以像金子般发光!css
在这 5 年或 10 年无数个奋斗的日夜里,你们是以怎样的姿式在工做呢?下面由我揭晓这些姿式是怎样修炼而成的吧~java
首先咱们用一张 gif 图来回顾一下效果node
玩法基本的步骤以下webpack
ok,拍完照就能够分享到朋友圈了。ios
能够看到这里用到了大量的图片,经过对图片的拖拽缩放等操做,摆放人物及配件,最终合成相应的图片。那么这一过程是怎么实现的呢?web
首先咱们采用 NUTUI 来搭建整个项目,其脚手架能够很好地处理图片优化打包等。底部操做菜单模块使用了 NUTUI 中的 Tab 组件,提高了开发效率。在主界面的部分选用了基于 canvas 的 creatjs 库,以及一个轻量级的触屏设备手势库 hammer.js 来开发。canvas
NUTUI 是一套京东风格的移动端组件库,开发和服务于移动 Web 界面的企业级前中后台产品。50+ 高质量组件,40+ 京东移动端项目正在使用,支持按需加载,支持服务端渲染(Vue SSR)...跨域
快扫码体验起来吧数组
Hammer 是一个开源代码库,能够识别由触摸,鼠标和 pointerEvents 作出的手势。它没有任何依赖性,而且很小,压缩后只有 7.34 kB。
它支持常见的单点和多点触摸手势,而且能够添加自定义手势
CreateJS 是基于 HTML5 开发的一套模块化的库和工具。基于这些库,能够很是快捷地开发出基于 HTML5 的游戏、动画和交互应用。
CreateJS 包含以下几部分
在本项目中主要是运用了 EaseJs,并结合 Tween.js 作了一些小动画。
了解完所用到的技术后,咱们来看看具体的实现过程:
这个项目主要包含了三大核心:加载图片、绘制姿式、手势操做,下面咱们分别来讨论一下。
因为这个项目 99%的模块是由图片构成,所以预加载图片这一功能必不可少。图片那么多,要一个个手动列出来去加载吗?固然不用!如今是机械化时代了,能交给工具的就不动手。
const fs = require("fs"); const path = require("path"); let components = []; const files = fs.readdirSync(path.resolve(__dirname, "../img/")); files.forEach(function (item) { components.push(`'@/asset/img/${item}'`); }); let data = `let imgList = [${[...components]}] module.exports = imgList;`; fs.writeFile(path.resolve(__dirname, "./imgList.js"), data, (error) => { console.log(error); });
依托于 nodejs 对文件的读写来完成自动生成图片列表文件,加载时对这个列表下的图片依次 load
便可。
EaselJS 在 Createjs 中承担 ‘画’ 的能力,这里用到了画图片和画文字的 API。EaselJS 通常的绘制步骤是:建立舞台 -> 建立对象 -> 设置对象属性 -> 添加对象到舞台 -> 更新舞台呈现下一帧
this.stage = new createjs.Stage(this.canvas); // 建立舞台 let bgImg = new createjs.Bitmap(imgSrc); // 建立对象 this.stage.addChild(bgImg); // 添加对象到舞台
CreateJs 提供了两种渲染模式,一种是用 setTimeout,一种是用 requestAnimationFrame,默认是 setTimeout,帧数是 20,这里咱们选用 requestAnimationFrame 模式,由于要对页面元素进行大量的操做,选此种方式会更加流畅。
createjs.Ticker.timingMode = createjs.Ticker.RAF; // RAF为requestAnimationFrame缩写
easeljs 事件默认是不支持 touch 设备的,须要手动开启
createjs.Touch.enable(this.stage);
实时刷新舞台
createjs.Ticker.addEventListener("tick", this.stage.update(event));
因为 hammer.js 默认是不开启 rotate 事件的,所以须要在选项中使用 recognizers 来设置一个识别器
let bodyHandle = new Hammer.Manager(this.canvas, { recognizers: [[Hammer.Rotate], [Hammer.Pan]], }); let bodyRotate = new Hammer.Rotate(); bodyHandle.add(bodyRotate);
准备工做完成,下面正式开始
为了保持文明的形象,就不支持站在桌子上办公了。所以场景分为背景和桌子两部分,经过设置桌子的层级在人物的上层来进行约束。
首先绘制背景
let Bg = new Image(); Bg.src = require("../asset/img/scene" + n + ".png"); Bg.onload = () => { let bgimg = new createjs.Bitmap(Bg); this.stage.addChild(bgimg); };
注意,若是不是首次绘制,须要将以前的内容清空
this.stage.removeAllChildren();
同理绘制桌子,须要注意的是,桌子绘制完之后,须要设置其层级
... this.stage.addChild(deskImg); this.stage.setChildIndex(deskImg, 1);
绘制角色与场景不一样,这里须要用到 Container。
Container 是一个容器,能够包含 Text、Bitmap、Shape、Sprite 等其余的 EaselJS 元素。例如,你能够将手臂、腿部、躯干和头部聚在一块儿,把它们转换为一组,同时还能够将各个部分相对彼此移动。在这里咱们将角色及其表情放在一个 Container 中方便统一管理,统一移动缩放旋转等。
绘制角色前,咱们先肯定绘制的位置:默认位置在画布的最中间
let pos = { x: this.canvasW / 2, y: this.canvasH / 2, };
若是已经选择过角色,须要更换时,须要保持以前角色的位置
pos = { x: joy.x, y: joy.y, };
下面是具体绘制步骤:
var joy = new Image(); joy.src = require("../asset/img/joy" + n + ".png"); // 加载角色图片 joy.onload = () => { var joyImg = new createjs.Bitmap(joy); // 建立图像 joyImg.name = "joy"; // 角色命名 joyImg.regX = joy.width / 2; // 移动x方向到中心点位置 joyImg.regY = joy.height / 2; // 移动y方向到中心点位置 joyImg.x = pos.x; // 设置初始位置 joyImg.y = pos.y; // 设置初始位置 let container = new createjs.Container(); // 建立容器 container.name = "joyContainer"; // 容器命名 container.addChild(joyImg); // 容器添加角色 this.stage.addChild(container); // 添加容器到舞台 };
在上面绘制角色时,建立了一个 name 为 joyContainer 的容器,咱们将表情也绘制进去
var face = new createjs.Bitmap(imgBg); ... joyContainer.addChild(face);
这样当咱们想移动这个角色时,经过移动容器,来保证总体性。不然会出现脑壳跟不上身体移动的状况。。。
从添加角色开始,就会记录下当前的操做对象 activeItem,当触发删除按钮时,只要找到 activeItem,并将其相关内容删除便可。
const ele = this.stage.getChildByName(this.activeItem.name); this.stage.removeChild(ele);
hammer.js 是用于检测触摸手势的 JavaScript 库,支持最多见的单点和多点触摸手势,而且能够彻底扩展以添加自定义手势。NUTUI中将会集成此功能并在下个版本中正式发布。
bodyHandle.on("rotate", (e) => { let ctrEle = this.activeItem; ctrEle.scaleX = ctrEle.scaleY = e.scale * this.nowScale; ctrEle.rotation = this.BorderBox.rotation = e.rotation + this.nowRotate; });
经过监听 rotate 事件,能够获得当次操做的缩放及旋转的数据,咱们再将其与以前的状态相结合,就能达到各类手势操做的效果了。
好了,一切准备就绪,开始你的表演吧~
首先,选择一个办公场景,而后来个角色扮演,站着有点累?不要紧,换个姿式坐下来吧,固然你想站着凳子上也不要紧。。表情是否是有点古板?那就吐吐舌头吧。电脑水杯安排上,最后再来个口号“在京东胖个 20 斤”。。
玩过瘾了吗?好了,收收心我们继续聊如何实现的吧。
当你点击“完成时”,咱们会进入分享页,分享页的底图是三种颜色随机选择。这里咱们须要建立一个临时的 canvas 来绘制分享图片,将分享的背景,定制好的姿式场景图(经过 canvas.toDataURL 方法转成图片),还有二维码,以及昵称,依次绘制到这个临时的 canvas 中,最后导出图片后赋值给分享图片的 url。
let tmpStage = new createjs.Stage(tmpCanvas); tmpStage.addChild(bg, share, code, text);
因为分享图片与分享页展现元素不彻底同样,所以展现给用户看到的是分享页,而分享图片设置了透明度为 0,只能保存不能被看到。
然而,事情没有这么简单,一大波 bug 正在快马加鞭的狂奔袭来。。
前面介绍过,这个项目是由加载页和主界面两个页面组成,中间是经过路由跳转(history 模式)。可是在一些手机中,经过路由跳转到另外一个页面时,底部会自动出现导航模块,这是咱们所不但愿看到的,本就捉襟见肘的空间里,凭空多了这么大一块,这是不可容忍的存在。
所以在权衡以后,选择了 replace 模式,可是这样用户在进入主界面之后,就不能回到加载页了,鱼与熊掌不可兼得。
在加载完成后,有个昵称的输入框,在 ios 下输入完成,键盘收起后页面底部会有一大片空白,呈卡死状。
可是当咱们在页面上随意滑动一下,这个白块就会消失。这是由于 ios 键盘弹出后,会把页面总体顶上去,所以咱们须要使用 scrollTo 函数,在 blur 键盘落下时滚动页面,使页面归位。
blur() { window.scrollTo(0, 0); }
因为系统更新后,白块变成了透明状态,这使得人更加琢磨不透,明明看不到任何东西,可是输入框就是没法选中。别觉得脱了马甲就不认识你了,上面的解决方案依旧是有效的。
本地开发完成,上传代码到服务器后,本来的世界静好全都消失不见,取而代之的是刺眼的红:
一番查阅后找到了以下这段话:尽管能够在画布中使用未经CORS批准的图像,但这样作会污染画布。一旦画布被污染,就不能再从画布中提取数据。例如,不能再使用canvas toBlob()、toDataURL()或getImageData()方法;这样作将引起安全错误。这能够防止用户在未经容许的状况下使用图像从远程网站获取信息,从而公开私有数据。
这就解释了上面报错的由来,那么如何解决呢?
var bg = new Image(); bg.crossOrigin = "Anonymous";
这就开启了图片加载过程当中的 CORS 功能,从而绕过了报错。
图片能够加载了,但是当我想作拖拽等操做时,又又又报错了。。。
createjs 提供了 hitArea 点击区域。能够设置另外一个对象 objB 做为显示对象 objA 的 hitArea,当点击到 objB 时就至关于点击到了 objA。 这个 objB 不须要添加到显示对象列表,也不须要可见,但它会在交互事件的触发中替代 objA。
var hitArea = new createjs.Shape(); hitArea.graphics.beginFill("#000").drawRect(0, 0, imgBg.width, imgBg.height); //这里的大小为图片大小,请本身调整 img.hitArea = hitArea;
给对象绑定一个点击区域,这样拖拽是操做这个区域,而不是本来的图像,这样就能够不报错了
在这个项目中的设定,角色在全部其余元素的底层,而元素切换选中时,也须要将当前选中元素置顶,这里用到了 createjs 的 setChildIndex 方法
setChildIndex 方法容许你向上或向下移动显示对象在显示列表内的位置。显示列表能够看做为一个数组,它的索引位置是从第 0 开始的。假如建立了 3 个元素,那么他们的位置就是第 0,1,2 层。第二层的对象在外面,第 0 层的在最里面。
若是想把某一元素移到全部元素的上面,这时就要用到 getNumChildren 属性,它的含义就是该容器内显示对象的数目。最外层的层深就是第 numChildren-1 层。其余本来层级高于置顶元素的元素,相应层级会减小一级。
if (ele.name === "joy") { this.stage.setChildIndex(ele, 1); } else { this.stage.setChildIndex(ele, this.stage.getNumChildren() - 2); }
在咱们选中或者新增一个元素时,触发层级设置,由于要保证当前操做的元素层级在上。因为有置顶的元素,所以在设置层级时,若是是角色元素,那么设置在第 2 层,仅仅高于场景背景层;若是是其余元素,则设置为次顶层。
在测试阶段发现,ios10 如下的手机,不能拖拽,真是个晴天霹雳!
在排查过程当中发现了蹊跷,不能拖拽居然是由于选中框上面的删除按钮没有加载到,这个按钮有什么特别之处呢,哦,原来是 webpack 配置中的 url-loader 自动将小图片转成了 base64 格式,顺着这个思路,将这个功能去掉之后,问题得以解决,但并无深究。
接下来的结果更糟,分享图片不知去向了,只剩下个背景框!
上面“生成图片”部分就讲过,图片都是将 canvas 经过 toDataURL 导出,导出格式正是上面有问题的 base64 格式。
咱们发现 base64 在 ios10 如下版本中,没法触发 onload 事件,而是走了 onerror。那么 base64 图片还能转成什么格式呢?答案就在这里:
dataURLToBlob(dataurl) { //dataurl: data:image/webp;base64,UklGRvAIAABXRUJQVlA4WAoAAAAQAAAAXwAAXwAAQUxQSC4CAAABkAXbtmlH+xmxn... var arr = dataurl.split(','); // ['data:image/webp;base64','UklGRvAIAABXRUJQVlA4WAoAAAAQAAAAXwAAXwAAQUxQSC4CAAABkAXbtmlH+xmxn...'] var mime = arr[0].match(/:(.*?);/)[1]; // 分离出mime类型 ——> image/webp var bstr = atob(arr[1]); // atob() 方法用于解码使用 base64 编码的字符串,转换为字符串中保存的原始二进制数据。 var n = bstr.length; var u8arr = new Uint8Array(n); // Uint8Array表示一个8位无符号整型数组,建立时内容被初始化为0。建立完后,能够以对象的方式或使用数组下标索引的方式引用数组中的元素。 while (n--) { u8arr[n] = bstr.charCodeAt(n); // 依次存储Unicode 编码 } return new Blob([u8arr], {type: mime}); // type:表明了将会被放入到blob中的数组内容的MIME类型 }
咱们先将 base64 图片转为 blob 格式
sharePhoto.src = window.URL.createObjectURL(this.dataURLToBlob(photo));
而后经过 URL.createObjectURL 方法生成 ObjectURL
window.URL.revokeObjectURL(sharePhoto);
因为 createObjectURL 返回的 url 一直存储在内存中,直到 document 触发了 unload 事件(例如:document close)。因此我们养成好习惯,在使用完成之后要记得随手释放一下哦~
那么 createObjectURL 究竟是何方神圣呢?咱们一块儿来学习下:
定义:URL.createObjectURL()方法会根据传入的参数建立一个指向该参数对象的 URL。这个 URL 的生命仅存在于它被建立的这个文档里。新的对象 URL 指向执行的 File 对象或者是 Blob 对象。
createObjectURL 返回一段带 hash 的 url,而且一直存储在内存中,直到 document 触发了 unload 事件(例如:document close)或者执行 revokeObjectURL 来释放。
浏览器支持状况以下,移动端基本能够放心使用~
在即将上线时,因为内部 app 对长按保存图片支持不太充分,所以临时决定在其中屏蔽此功能,这里尝试了三种方法:
document.oncontextmenu = (e) => { e.preventDefault(); };
在 web 浏览器中生效,可是在移动端无效
* { -webkit-touch-callout: none; /* 系统默认菜单被禁用*/ -webkit-user-select: none; /* webkit浏览器*/ -moz-user-select: none; /* 火狐*/ -ms-user-select: none; /* IE10*/ user-select: none; /* 用户是否可以选中文本*/ }
实践证实这种方式不可行,咱们依次来分析一下:user-select
控制用户可否选中文本,而咱们这里须要的是控制图片。-webkit-touch-callout
:当你触摸并按住触摸目标时候,禁止或显示系统默认菜单。适用于:连接元素好比新窗口打开,img 元素好比保存图像等等
乍一看,这不就是咱们所须要的吗?
可是,-webkit-touch-callout 是一个 不规范的属性(unsupported WebKit property),它没有出如今 CSS 规范草案中。
看一下支持状况就明白了:
最终选择了第一种方式,简单直接,不用考虑兼容性。
在解决了上面一系列的问题以后,要回到最初的分析:无论项目用了何种技术,最终呈现的本质都是图片。因此图片的大小不只影响加载速度,同时也影响着渲染速度,为了提供更优的用户体验,选择使用 NUTUI 中的图片压缩功能,它能够提供高压缩比的图片优化,而且能够自动转化成 webp 格式。你们都知道,webp 格式的图片比通常压缩过的图片还要小不少,依托于这么强大的靠山,想不出色都难!
无论你如今是大佬、超级大佬,仍是刚刚加入京东的 fresh blood,519 老员工日就是属于每一位 JDer 共同的节日!
在作项目的过程当中,从零开始学习 createjs,项目中间不断试错,不断去解决问题,学习新知识,收获良多。在之后的工做中,还要注重基础知识的广度,不断积累,也许学习的时候并不清楚应用场景,可是终有一天会发现,每一个知识都有其存在的理由。