18年年中的时候,笔者所在的公司让咱们开发一款微信小程序(马卡龙玩图)。主要的玩法是用户上传一张人像照片,图片通过后端的AI算法处理后识别出人物,将人物和周围环境进行分割(俗称抠图);前端将返回的抠像进行样式处理,包括设置大小位置旋转等;经过预设(或自定义上传)的一些主题场景以及点缀的贴纸或滤镜,用户对这些元素进行移动或缩放,能够衍生出不少好玩的修图玩法,好比更换动态背景,合成带有音频的动态视频等(文末有微信二维码)。css
开发初期,当时可选的成熟的微信小程序框架只有wepy,通过开发实践发现,wepy在多层嵌套列表渲染,组件化支持等方面体验不是很友好。后面美团的技术团队开源了一款基于vue的小程序框架mpvue,通过体验后感受上,虽然在组件化上体验和vue别无差别,可是在性能上并不占优点。html
直到某天有位朋友拉我进了一个Taro的开发群,原来京东的前端团队也在开发一款基于React规范的小程序框架,因为当时笔者担忧Taro尚处早期,功能上也许不足抑或bug,迟迟没有入手。直到最近更新到1.2.4的版本,群里有道友不吝溢美之词进行了一波安利,因此笔者决定对项目的部分模块进行了重构,发现Taro确实在开发体验和性能上都获得了很是好的提升,在此向taro的贡献者致以崇高的敬意。本着开源的精神,笔者也将这次重构的demo源码以及心得体会和你们一块儿分享。前端
关于Sticker组件的一些细节还包括:贴纸组件具备激活状态(点击当前组件显示控制器,而其余组件则隐藏);切换场景后,要缓存以前用户的操做,当切回到原先的场景时,则恢复到该场景下用户最后的操做状态。vue
用户点击保存后,将做图区的全部元素按照层级大小进行排序,而后经过微信提供的canvas接口进行绘制,最终返回所见即所得的合成美图。git
根据Taro的文档,安装CLI工具以及建立项目模板,建议选择Typescript开发方式。github
简要分析下项目结构算法
Taro-makaron-demo
├── dist 编译结果目录
├── config 配置目录
| ├── dev.js 开发时配置
| ├── index.js 默认配置
| └── prod.js 打包时配置
├── src 源码目录
| ├── assets 静态资源
| | ├── images 图片
| ├── components 组件
| | ├── Sticker 贴纸组件
| | ├── ... 其余组件
| ├── model Redux数据流
| | ├── actions
| | ├── constants
| | ├── reducers
| | ├── store
| ├── pages 页面文件目录
| | ├── home 首页
| | | ├── index.js index 页面逻辑
| | | └── index.css index 页面样式
| | ├── dynamic 做图页
| | | ├── index.js index 页面逻辑
| | | └── index.css index 页面样式
| ├── services 服务
| | ├── config.ts 全局配置
| | ├── api.config.ts api接口配置
| | ├── http.ts 封装的http服务
| | ├── global_data.ts 全局对象
| | ├── cache.ts 缓存服务
| | ├── session.ts 会话服务
| | ├── service.ts 基础服务或业务服务
| ├── utils 公共方法
| | ├── tool.ts 工具函数
| ├── app.css 项目总通用样式
| └── app.js 项目入口文件
└── package.json
复制代码
贴纸组件相较其余展现型组件,涉及手势操做,大小位置计算等,因此稍显复杂。json
// 使用
class Page extends Component {
state = {
foreground: { // 人像state
id: 'foreground', // id
remoteUrl: '', // url
zIndex:2, // 层级
width:0, // 宽
height:0, // 高
x: 0, // x轴偏移量
y:0, // y轴偏移量
rotate: 0, // 旋转角度
originWidth: 0, // 原始宽度
originHeight: 0, // 原始高度
autoWidth: 0, // 自适应后的宽度
autoHeight: 0, // 自适应后的高度
autoScale: 0, // 相对画框缩放比例
fixed: false, // 是否固定
isActive: true, // 是否激活
visible: true, // 是否显示
}
}
render () {
reuturn <Sticker
ref="foreground"
url={foreground.remoteUrl}
stylePrams={foreground}
framePrams={frame}
onChangeStyle={this.handleChangeStyle}
onImageLoaded={this.handleForegroundLoaded}
onTouchstart={this.handleForegroundTouchstart}
onTouchend={this.handleForegroundTouchend}
/>
}
}
// 组件定义
class Sticker extends Component {
...
render() {
const { url, stylePrams } = this.props
const { framePrams } = this.state
const styleObj = this.formatStyle(this.props.stylePrams)
return (
<View
className={`sticker-wrap ${stylePrams.fixed ? 'event-through' : ''} ${(stylePrams.visible && stylePrams.width > 0) ? '' : 'hidden' }`}
style={styleObj}
>
<Image
src={url}
mode="widthFix"
style="width:100%;height:100%"
onLoad={this.handleImageLoaded} // 图片加载后将原始尺寸信息通知给父组件
onTouchstart={this.stickerOntouchstart}
onTouchmove={this.throttledStickerOntouchmove} // touchmove比较频繁,须要节留
onTouchend={this.stickerOntouchend}/>
<View className={`border ${stylePrams.isActive ? 'active' : ''}`}></View>
<View className={`control ${stylePrams.isActive ? 'active' : ''}`}
onTouchstart={this.arrowOntouchstart}
onTouchmove={this.throttledArrowOntouchmove}
onTouchend={this.arrowOntouchend}
>
<Image src={scale} mode="widthFix" style="width:50%;height:50%"/>
</View>
</View>
)
}
}
复制代码
// services/cache.ts 缓存服务
function Cache (name) {
this.name = name
}
Cache.prototype = {
set: function (key, value) {
this[key] = value
return this[key]
},
get: function (key) {
return this[key]
},
clear: function () {
// 清空
Object.keys(this).forEach(v => {
this[v] = null
})
}
}
export const createCache = (name:string) => {
return new Cache(name)
}
// 使用
import {createCache} from '../../services/cache'
class Page extends Component {
cache = {
source: createCache('source'),
}
// 下载照片并存储到本地
downloadRemoteImage = async (remoteUrl = '') => {
const cacheKey = `${remoteUrl}_localPath`
const cache_source = this.cache['source']
let localImagePath = ''
if (cache_source.get(cacheKey)) {
// 有缓存
return cache_source.get(cacheKey)
} else {
try {
const result = await service.base.downloadFile(remoteUrl)
localImagePath = result.tempFilePath
} catch (err) {
console.log('下载图片失败', err)
}
}
return cache_source.set(cacheKey, localImagePath)
}
}
复制代码
因为微信小程序逻辑层和视图层各自独立,两边的数据传输是靠转换后的字符串。所以当setData频率过快,内容庞大时,会致使阻塞。因为本项目又涉及不少的手势操做,touchmove事件的频率很快,因此项目早期时候,在安卓系统下卡顿十分明显。canvas
优化方式有:经过作函数节流,下降setData频次;将页面无关的数据不要绑定到data上,而是绑定到组件实例上(牺牲运算效率换取空间效率)。小程序
使用微信的自定义组件,也是一个很大的提高因素,我的认猜想多是自定义组件内部data的改变不会致使其余组件或页面的data更新。项目早期采用的是wepy框架,因为历史局限性(当时微信还未公布自定义组件方案),因此效率问题一直非常头疼。好在Taro框架经过编译的方式完美的支持了这个方案。
例如,当图片加载,获取到原始尺寸后,须要计算出该图片在当前场景下的预设尺寸和位置。必须先计算出自适应后的宽高,而后才能计算出预设的偏移量。所以能够将尺寸和位置参数都计算完毕后,再调用setState更新视图,这样不只下降了频次,同时也解决了图片闪烁的bug.
前面有提到过利用缓存模块来存储组件状态或资源信息,在此再也不赘述。
Taro框架采起的是一种编译的方式,将源代码分别编译出能够在不一样端(微信/百度/支付宝/字节跳动小程序、H五、React-Native 等),所以能够在性能上与各个平台保持一致。
而mpvue的方案则是修改vue的runtime,将vue 实例与小程序 Page 实例创建关联以及生命周期的绑定。私觉得,这种经过映射的方式可能会致使通讯效率上的下降,而且vue和微信又各自独立迭代,后期的协调也愈来愈费劲,因此我的感受上,仍是Taro的方案略胜一筹。我的薄见,还请海涵。
Github
欢迎你们来这个demo项目下进行交流,项目地址 (github.com/HarryChen05…), 你的点赞将是我莫大的动力😊
线上项目
本demo项目的线上小程序可经过微信扫描下面的二维码前往体验👏
versa是一家致力于将人工智能技术应用于视觉影像领域的AI公司,诚招各种开发人员,感兴趣的小伙伴能够站内联系我。 (容许转载,请注明出处)