!https://github.com/journey-ad/pixel-paint

Demo

又是一个练手用的Web App,依然使用Vue进行开发。在此把遇到的一些场景和解决方法一并记录下来

移动端弹出框滚动穿透

有两个解决方法:

移动端滑动选择(模拟:hover)

移动端原生不支持滑动触发状态,touchmove回调传入的Touch事件对象target始终为触摸开始时的元素,但Touch事件对象包含pageXpageYclientXclientY等属性,值为滑动坐标偏移值,结合父容器的clientWidth和元素数量可计算出当前滑动元素的索引,进行相应处理

滑动事件触发频率较高,易产生性能问题,通常使用节流函数限制触发频次

元素移动性能

使用transformtranslate函数移动元素位置,而不是top、left、margin等,后者会触发页面回流,消耗更多性能

还可使用translate3d开启硬件加速进一步提高性能

移动端禁止下拉刷新

方法一

body应用css样式overscroll-behavior: none;

方法二

使body和容器脱离文档流,并设置元素堆叠

.body,
.wrapper {
  /* 脱离文档流 */
  position: absolute;
  top: 0px;
  /* 占满空间 */
  width: 100%;
  height: 100%;
  /* 移除边距 */
  margin: 0;
  /* 允许纵向滚动 */
  overflow-y: hidden;
}
.body {
  /* body在底部 */
  z-index: 1;
}
.wrapper {
  /* 容器在上部 */
  z-index: 2;
}

Canvas性能实践

使用requestAnimationFrame

setInterval可能会被阻塞造成的回调执行周期不可控,且无法应对刷新率不同的设备,requestAnimationFrame可以解决这两个问题,回调执行周期严格控制在浏览器发生刷新之前,且随设备刷新率变化而动态发生改变,这个周期在60Hz的屏幕上为1000/60 ≈ 16.67ms

使用cancel​Animation​Frame取消raf

Canvas分层

对于静态或更新频率较低的画面可以放到另外一层画布上,画布间使用z-index控制堆叠顺序,可以不必与需要频繁更新的画布一同绘制,减少性能开销,这种做法在游戏领域较为常见

使用“离屏绘制”

Canvas无需插入页面也能绘制,结合document.createElement()ctx.drawImage()方法,维护一个小尺寸的画布作为原始数据,画板会更容易实现缩放、平移等功能

Canvas禁用自动平滑(使用临近缩放)

对于像素画风的应用或游戏很有用,绘制前设置ctx.imageSmoothingEnabled = false,由于此属性仍为实验性质,使用时要加上前缀

ctx.mozImageSmoothingEnabled = false;
ctx.webkitImageSmoothingEnabled = false;
ctx.msImageSmoothingEnabled = false;
ctx.imageSmoothingEnabled = false;

随机数版本uuid生成JS实现

let uuid = () => {
  let d = Date.now()
  if (typeof performance !== 'undefined' && typeof performance.now === 'function') {
    d += performance.now()
  }
  return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
    let r = (d + Math.random() * 16) % 16 | 0
    d = Math.floor(d / 16)
    return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16)
  })
}

生成随机十六进制色值

getRandomColor() {
  let result = [];
  for (let i = 0; i < 16; i++) {
    result.push("#" + ((Math.random() * 0xffffff) << 0).toString(16));
  }
  return result;
}

node.js相关

图片裁剪、缩放

const sharp = require('sharp')

sharp('path/to/image.png')
  .extract({
    left: 0,
    top: 100,
    width: 1080,
    height: 1080
  })
  .resize(128, 128, {
    kernel: 'nearest' // 临近算法
  })
  .toBuffer((_err, data, info) => {
    console.log(info, data)
  })

简单命令行参数获取

const argv = require('optimist').argv

console.log(argv)

获取图片像素信息

const getPixels = require('get-pixels')

getPixels('path/to/image.png', function (_err, pixels) {
  let pixels = []

  for (let y = 0; y < pixels.shape[1]; y++) {
    for (let x = 0; x < pixels.shape[0]; x++) {
      const r = pixels.get(x, y, 0).toString(16)
      const g = pixels.get(x, y, 1).toString(16)
      const b = pixels.get(x, y, 2).toString(16)
      const hex = `#${r}${g}${b}`
      pixels.push(hex)
    }
  }
})

Vue-Cli 3.x使用moment.jslodash打包精简

安装webpack-bundle-analyzerlodash-webpack-plugin,配置vue.config.js文件

let webpack = require('webpack')
let BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
let LodashModuleReplacementPlugin = require('lodash-webpack-plugin')

module.exports = {
  chainWebpack: config => {
    config.plugin('ignore')
      .use(new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/))
    config.plugin('analyzer')
      .use(new BundleAnalyzerPlugin())
    config.plugin('loadshReplace')
      .use(new LodashModuleReplacementPlugin())
  }
}

Vue-Router按需加载页面,JS分包

组件定义为promise并import实现按需加载

使用魔术注释/* webpackChunkName: "chunkName" */实现JS分包

import Vue from 'vue'
import Router from 'vue-router'

const Artwork = (resolve) => {
  import(/* webpackChunkName: "artwork" */ './views/artwork.vue').then((module) => {
    resolve(module)
  })
}

Vue.use(Router)

export default new Router({
  routes: [
    {
      path: '/artwork',
      name: 'artwork',
      component: Artwork
    }
  ]
})