H5海报制作实践

时间: 2019-03-07阅读: 545标签: 海报

引言

年后一直处于秣马厉兵的状态,上周接到了一个紧急需求,为38妇女节做一个活动页,主要功能是生成海报,第一次做这种需求,我也是个半桶水前端,这里将碰到的问题、踩的坑,如何解决的分享给大家,讲的不到位的地方还望斧正。


效果展示

目前活动还是在线状态,这里是最后生成海报的效果,扫描二维码就可以进入页面。


实现方案

起初实现的方案是展示的时候直接使用canvas,计算手机屏幕大小,让canvas充满整个屏幕,用户编辑完之后直接用展示的canvas生成图片,最后发现这种形式很麻烦,碰到适配问题,canvas计算起来比较麻烦。

最终方案,展示的时候使用htmlcss,这样用户看到的展示、编辑页面适配起来容易。最后生成图片的时候使用canvas,这个canvas是隐藏的,用户不可见,这样还有一个优点,最终生成的海报大小是固定的,跟手机屏幕大小无关。

方案看着很简单,实现的时候各种细节问题。


资源预加载

H5海报活动,就像一个小型的APP,体验一定要好,最主要的就是资源预加载了,整个应用大小有30个图片,还有字体文件,一个字体文件就有3MB多,如何做好资源预加载很大程度上影响了这次活动的体验。


图片预加载

图片预加载的原理就是使用http协议中的缓存,这里主要指的是强缓存(协商缓存还要去服务器,有网络交互)。在活动首页之前加个loading页面,将所有用到的图片加载一遍,等到后面加载的时候就只有几ms。

图片预加载,使用let image = new Image()创建一个图片标签,在image.src中加入图片链接,加载成功调用image.onload事件。一张图片还好,大量图片的话如何优雅的做出进度条呢?

还好有Promise这个银弹,我们可以轻松的实现进度条效果。

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),就算有少数图片未加载成功也影响不大。

用起来也很简单:

(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绘制

绘制canvas的时候我是用了pixi.js类库,实际使用的时候并不一定方便很多o(╯□╰)o,如果是简单的绘制,原生的也是很好用的。如果用了某些类库,碰到问题因为文档少,翻译更少,解决起来可能更麻烦。


跨域图片如何解决

绘制这张海报的时候,大部分图片都是自己的,设置允许跨域,只有用户图像这个图片,是拿的其他部门获取的实时用户头像,不让跨域,这可把我整惨了,试了很多办法都不行,最后使用服务器中转解决了这个问题,步骤如下:

  1. 得到图片链接。
  2. 将图片链接通过接口传递给我们自己的服务器,服务器上获取图片base64,成功后返回给web。
  3. 将base64绘制到canvas。

这样就解决了来自别人服务器不让跨域图片的绘制


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上键盘弹起不正常、收起键盘卡顿的问题,具体就是用户点击按钮之后展示输入框,软键盘不弹起,和点击ios软键盘确定按钮之后卡顿,需要滑动一下才能继续触摸的问题。

碰到这问题真是老虎吃天,没处下爪。最后各种查资料、各种尝试,解决方案如下:

  1. 弹起问题,我用的是vue,输入框展示之后马上聚焦有问题,需要用$nextTick()包一层,下个渲染回合在进行渲染。
  2. 卡顿问题,每当输入框失去焦点的时候,将滚动条滚动到顶部document.body.scrollTop = 0即可。
  3. 弹起遮盖问题,有些情况会出现键盘弹起会遮盖输入框,类似的,这种情况发生后执行document.body.scrollTop = 1000,将滚动条滚到底部即可。

碰到类似问题的可以沿着这个思路去解决,延时触发了、下个周期执行了、滚动之类的。


总结

经过这次开发,对海报这种活动算是有了完整的了解,学习、巩固了很多知识。相信读着朋友们看完之后,也可以轻松实现海报制作了。

来自:https://segmentfault.com/a/1190000018426131


站长推荐

1.阿里云: 本站目前使用的是阿里云主机,安全/可靠/稳定。点击领取2000元代金券、了解最新阿里云产品的各种优惠活动点击进入

2.腾讯云: 提供云服务器、云数据库、云存储、视频与CDN、域名等服务。腾讯云各类产品的最新活动,优惠券领取点击进入

3.广告联盟: 整理了目前主流的广告联盟平台,如果你有流量,可以作为参考选择适合你的平台点击进入

链接: http://www.fly63.com/article/detial/2254

FabricJs:动态海报营销方案

Fabric.js是一个可以简化Canvas程序编写的库。 Fabric.js为Canvas提供所缺少的对象模型, svg parser, 交互和一整套其他不可或缺的工具。Fabric.js可以做很多事情,如下:

小程序如何生成海报分享朋友圈

项目需求写完有一段时间了,但是还是想回过来总结一下,一是对项目的回顾优化等,二是对坑的地方做个记录,避免以后遇到类似的问题。利用微信强大的社交能力通过小程序达到裂变的目的,拉取新用户。

小程序海报生成工具,可视化编辑直接生成代码使用

在做小程序时候,我们经常会有一个需求,需要将小程序分享到朋友圈,但是朋友圈是不允许直接分享小程序,那我们还有其他的办法解决吗?答案肯定是有的,即 canvas 生成个性化海报分享图片到朋友圈

小程序如何在不同设备上自适应生成海报

小程序canvas的API并没有像其他的一样支持小程序独有的 rpx 自适应尺寸单位,在绘制内容时所应用的单位仍然是 px,那么如何实现不同尺寸屏幕的自适应呢?们的在开发中常用的参考屏幕尺寸(iPhone6)为:375*667;

Vue中用canvas实现二维码和图片合成海报

在项目中经常会遇到需要将不同的二维码放到一张通用图片上,提供用户下载,简单来说,就是利用canvas将同等比例的二维码在图片上叠加,生成海报

小程序海报最佳实现思路,可视化编辑直接生成代码使用

在做小程序时候,我们经常会有一个需求,需要将小程序分享到朋友圈,但是朋友圈是不允许直接分享小程序,那我们还有其他的办法解决吗?答案肯定是有的,即 canvas 生成个性化海报分享图片到朋友圈

小程序生成海报:通过 json 配置方式轻松制作一张海报图

由于我们无法将小程序直接分享到朋友圈,但分享到朋友圈的需求又很多,业界目前的做法是利用小程序的 Canvas 功能生成一张带有二维码的图片,然后引导用户下载图片到本地后再分享到朋友圈

内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!

文章投稿关于web前端网站点搜索站长推荐网站地图站长QQ:522607023

小程序专栏: 土味情话心理测试脑筋急转弯幽默笑话段子句子语录成语大全运营推广