在京東,就任滿五年的老員工被稱做「大佬」,若是滿了十年,那就要被稱之爲「超級大佬」了。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,項目中間不斷試錯,不斷去解決問題,學習新知識,收穫良多。在之後的工做中,還要注重基礎知識的廣度,不斷積累,也許學習的時候並不清楚應用場景,可是終有一天會發現,每一個知識都有其存在的理由。