年後一直處於秣馬厲兵的狀態,上週接到了一個緊急需求,爲38婦女節作一個活動頁,主要功能是生成海報,第一次作這種需求,我也是個半桶水前端,這裏將碰到的問題、踩的坑,如何解決的分享給你們,講的不到位的地方還望斧正。javascript
目前活動仍是在線狀態,這裏是最後生成海報的效果,掃描二維碼就能夠進入頁面。css
起初實現的方案是展現的時候直接使用canvas,計算手機屏幕大小,讓canvas充滿整個屏幕,用戶編輯完以後直接用展現的canvas生成圖片,最後發現這種形式很麻煩,碰到適配問題,canvas計算起來比較麻煩。html
最終方案,展現的時候使用html、css,這樣用戶看到的展現、編輯頁面適配起來容易。最後生成圖片的時候使用canvas,這個canvas是隱藏的,用戶不可見,這樣還有一個優勢,最終生成的海報大小是固定的,跟手機屏幕大小無關。前端
方案看着很簡單,實現的時候各類細節問題。vue
H5海報活動,就像一個小型的APP,體驗必定要好,最主要的就是資源預加載了,整個應用大小有30個圖片,還有字體文件,一個字體文件就有3MB多,如何作好資源預加載很大程度上影響了此次活動的體驗。java
圖片預加載的原理就是使用http協議中的緩存,這裏主要指的是強緩存(協商緩存還要去服務器,有網絡交互)。在活動首頁以前加個loading頁面,將全部用到的圖片加載一遍,等到後面加載的時候就只有幾ms。ios
圖片預加載,使用let image = new Image()
建立一個圖片標籤,在image.src
中加入圖片連接,加載成功調用image.onload
事件。一張圖片還好,大量圖片的話如何優雅的作出進度條呢?git
還好有Promise
這個銀彈,咱們能夠輕鬆的實現進度條效果。github
class Preloadedr { /** * * @param images array 要加載的圖片,數組 * @param processCb function 回調函數,加載中進度有變化就調用 * @param completeCb function 回調函數,加載完成調用 */ constructor(images, processCb, completeCb) { this.imagesElement = [] this.loaded = 0 this.images = images this.total = images.length this.processCb = processCb this.completeCb = completeCb } /** * 開始預加載緩存圖片 * * @returns {Promise<any[]>} Promise 包含全部圖片的promise */ preloadImage() { let me = this let promises = [] me.loadedAction() me.images.forEach((img) => { let p = new Promise((resolve, reject) => { let image = new Image() image.src = img this.imagesElement.push(image) image.onload = () => { me.loadedAction(img) resolve(image) } image.onerror = () => { resolve("error") } }) promises.push(p) }) return Promise.all(promises) } /** * 進度變化的時候回調,private * * @param key string 加載成功的圖片地址 */ loadedAction(key) { if (key) { this.loaded++ } this.processCb(this.total, this.loaded) if (this.total == this.loaded) { this.completeCb() } } }
每一個要加載的圖片都是一個Promise,將全部圖片Promise包裝爲一個大的Promise,當這個大的Promise狀態爲fulfilled的時候,代表圖片加載完成。要注意,包裝圖片Promise的時候onerror
也是返回成功,這是由於Promise.all
會包裝出一個新Promise,這個Promise只要出現一個失敗,就直接返回報錯了,因此失敗了也返回成功(resolve),就算有少數圖片未加載成功也影響不大。web
用起來也很簡單:
(async () => { let imgLoader = new Preloadedr([ "//avatar-static.segmentfault.com/606/114/606114310-5c10a92c87f07_huge256", "//image-static.segmentfault.com/203/994/2039943300-5c515b79c91f1_articlex", ], (total, loaded) => { console.log("process: 圖片" + Math.floor(100 * loaded / total) + "%") }, () => { console.log("complete: 圖片" + 100 + "%") }) await imgLoader.preloadImage() console.log("加載完成") })()
能夠看到輸出以下:
process: 圖片0% Promise {<pending>} process: 圖片50% process: 圖片100% complete: 圖片100% 加載完成
至此,圖片預加載就實現了。接下來咱們看看字體的預加載,字體也是一種http靜態資源,也可使用緩存,但在實現預加載上卻遠沒有圖片這麼簡單。
字體預加載,沒有像Image
那麼方便的函數回調使用,查了下資料,有個document.fonts
實驗性的屬性,試了下基本支持,但在ios上可能會出現一點兒小問題,加載過一次有緩存了,第二次加載時候onloadingdone
事件可能不會觸發,另外這個屬性、事件仍是一個實驗性的屬性,瀏覽器支持程度未知,可能不好。
查了不少資料,無心中看到有人說webfontloader這個項目經過一種比較trick的方法實現了,原理就是下面這兩句話:
不一樣字體,在將 fontSize 設置到很大的時候(好比300px),同一段文字,他展現的寬度是不同的。給兩個div,一樣的文字內容,第一段設置兩種字體,待加載字體首選,默認字體備選,第二種只設置默認字體,定時器去掃描,當兩段文字長度不一樣的時候就說明新字體加載成功可以使用。
大概看了下webfontloader,代碼寫的比較凌亂,命名奇怪,註釋少、沒翻譯(😂,多是我能力還不夠),但考慮的狀況比較完善,實現字體實現除了trick的方法外,也用了上面提到的document.fonts
,有興趣的能夠詳細閱讀下。下面看看我實現的簡易代碼:
class Fontloader { constructor(fontFamily) { this.fontFamily = fontFamily } /** * 返回Promise,監測字體 * * @returns {Promise<any>} */ watcher() { if ("object" == typeof document.fonts) { // 使用默認的document.fonts,兼容性可能有問題,我作的過程當中發現ios上可能會出現問題 return this.defaultWatcher() } else { // 使用trick法監測 return this.trickWatcher() } } /** * 返回trick法監測的Promise * * @returns {Promise<any>} */ trickWatcher() { let me = this /** * 生成一個獲取字體展現寬度的span元素 * @param font * @returns {HTMLSpanElement} */ let genSpanWithFont = (font) => { let span = document.createElement("span") span.style.cssText = ` display:block; position:absolute; top:-9999px; left:-9999px; font-size:500px; width:auto; height:auto; line-height:normal; margin:0; padding:0; font-variant:normal; white-space:nowrap; font-family:${font} ` span.innerHTML = "BESbswy" if (typeof document.body.append == "function") { document.body.append(span) } else if (typeof document.body.appendChild == "function") { document.body.appendChild(span) } return span } /** * 用來比較的字體 * @type {string[]} */ let fontDefault = ["serif", "sans_serif"] let defaultWidth = [] let fontWidth = [] fontDefault.forEach(font => { let spanDefault = genSpanWithFont(font) defaultWidth.push(spanDefault) let spanFont = genSpanWithFont(me.fontFamily + `,${font}`) fontWidth.push(spanFont) }) let clearUp = () => { defaultWidth.forEach(e => { document.body.removeChild(e) }) fontWidth.forEach(e => { document.body.removeChild(e) }) } return new Promise((resolve, reject) => { let check = () => { for (let i = 0; i < fontDefault.length; i++) { console.log(defaultWidth[i].offsetWidth, fontWidth[i].offsetWidth) if (defaultWidth[i].offsetWidth !== fontWidth[i].offsetWidth) { return true } } return false } let times = 1 let maxTimes = 10000 let loop = () => { if (times > maxTimes) { clearUp() reject("load fonts error") } times++ if (check()) { clearUp() resolve([me.fontFamily]) } else { window.setTimeout(loop, 1000) } } loop() }) } /** * 支持原生方法的使用原生方法 * @returns {Promise<any>} */ defaultWatcher() { return new Promise((resolve, reject) => { let loadedFamily = [] document.fonts.onloadingdone = (e) => { e.target.forEach((font) => { if (font.status == "loaded") { loadedFamily.push(font.family) } }) resolve(loadedFamily) } document.fonts.onloadingerror = (e) => { reject("load fonts error") } }) } }
封裝以後,兩種形式都統一返回Promise,在調用方經過異步函數await watcher()
,等待字體加在完成以後在繼續流程。這裏惟一有個缺點就是,字體可能要好幾MB,加載很慢,進度條很不均勻,這裏我將加載分爲2段,一段是圖片,一段是字體,進度條分開展現,各位看官有更好的方法,不妨一塊兒討論。
繪製canvas的時候我是用了pixi.js類庫,實際使用的時候並不必定方便不少o(╯□╰)o,若是是簡單的繪製,原生的也是很好用的。若是用了某些類庫,碰到問題由於文檔少,翻譯更少,解決起來可能更麻煩。
繪製這張海報的時候,大部分圖片都是本身的,設置容許跨域,只有用戶圖像這個圖片,是拿的其餘部門獲取的實時用戶頭像,不讓跨域,這可把我整慘了,試了不少辦法都不行,最後使用服務器中轉解決了這個問題,步驟以下:
這樣就解決了來自別人服務器不讓跨域圖片的繪製
toDataURL
導出圖片不全海報由10個sprite組成,繪製完以後,立刻調用toDataURL
,發現生成的圖片沒內容,或者圖片缺失某些sprite,這是由於繪製還沒完成我就導出了,何以見得呢?當我延時幾秒以後導出就沒問題了。
爲了保險起見,圖片我一張張的繪製,每次繪製都是一個Promise
,等待狀態爲fullfield
以後在進行下一張圖片的繪製,最後一張繪製完以後,等待幾百毫秒以後在進行導出,實際效果挺好,沒再出現過導出圖片不全或者空白的問題,下面是對繪圖的封裝:
async drawImage(sprite) { return new Promise((resolve, reject) => { let img = new Image() img.setAttribute("crossOrigin",'Anonymous') img.onload = () => { console.log("yes") let item = new PIXI.Sprite.from(new PIXI.BaseTexture(img)) item.x = sprite.x item.y = sprite.y item.width = sprite.width item.height = sprite.height this.app.stage.addChild(item) resolve("0") } img.src = sprite.image }) }
我這裏使用的是pixi.js
,sprite 表示一個精靈,裏面包含了圖片地址、座標、寬高信息。onload以後進行繪製,而後resolve
。
用的這個類庫不支持漢字折行,漢字折行問題須要本身去計算,這裏使用canvas的measureText
方法,這個方法會根據字體大小樣式計算字體正常渲染須要多少寬度,我只須要根據這個寬度一行行渲染漢字就好了,須要本身控計算控制繪製起點。
做爲一個後端,半桶水前端,每次碰到這種奇葩問題都很頭疼,但做爲後端又有一絲慶幸,不用常常面對這些問題,哈哈哈哈。
此次碰到的問題是ios上鍵盤彈起不正常、收起鍵盤卡頓的問題,具體就是用戶點擊按鈕以後展現輸入框,軟鍵盤不彈起,和點擊ios軟鍵盤肯定按鈕以後卡頓,須要滑動一下才能繼續觸摸的問題。
碰到這問題真是老虎吃天,沒處下爪。最後各類查資料、各類嘗試,解決方案以下:
$nextTick()
包一層,下個渲染回合在進行渲染。document.body.scrollTop = 0
便可。document.body.scrollTop = 1000
,將滾動條滾到底部便可。碰到相似問題的能夠沿着這個思路去解決,延時觸發了、下個週期執行了、滾動之類的。
通過此次開發,對海報這種活動算是有了完整的瞭解,學習、鞏固了不少知識。相信讀着朋友們看完以後,也能夠輕鬆實現海報製做了。
最後請你們玩兒玩兒這個活動,不妨關注下個人微博,哈哈哈。