手把手教你用Taro框架寫一個影象處理類微信小程式
前言
18年年中的時候,筆者所在的公司讓我們開發一款微信小程式(馬卡龍玩圖)。主要的玩法是使用者上傳一張人像照片,圖片經過後端的AI演算法處理後識別出人物,將人物和周圍環境進行分割(俗稱摳圖);前端將返回的摳像進行樣式處理,包括設定大小位置旋轉等;通過預設(或自定義上傳)的一些主題場景以及點綴的貼紙或濾鏡,使用者對這些元素進行移動或縮放,可以衍生出很多好玩的修圖玩法,比如更換動態背景,合成帶有音訊的動態視訊等(文末有微信二維碼)。
開發初期,當時可選的成熟的微信小程式框架只有wepy,經過開發實踐發現,wepy在多層巢狀列表渲染,元件化支援等方面體驗不是很友好。後面美團的技術團隊開源了一款基於vue的小程式框架mpvue,經過體驗後感覺上,雖然在元件化上體驗和vue別無差異,但是在效能上並不佔優勢。
直到某天有位朋友拉我進了一個Taro的開發群,原來京東的前端團隊也在開發一款基於React規範的小程式框架,由於當時筆者擔心Taro尚處早期,功能上也許不足抑或bug,遲遲沒有入手。直到最近更新到1.2.4的版本,群裡有道友不吝溢美之詞進行了一波安利,所以筆者決定對專案的部分模組進行了重構,發現Taro確實在開發體驗和效能上都得到了非常好的提高,在此向taro的貢獻者致以崇高的敬意。本著開源的精神,筆者也將此次重構的demo原始碼以及心得體會和大家一起分享。
需求分析
使用者上傳的人像經過摳圖處理後,將展示在作圖區,同時展示的元素還有背景圖片,可動或固定的貼紙。為了獲取更好的使用者視覺體驗,每個場景下,通過預設人像和貼紙的大小和位置(引數為作圖區域的百分比等)。人像和貼紙需支援單指和雙指手勢操作來改變大小和位置等樣式,因此可以將人像和貼紙都封裝為Sticker的元件,子元件Sticker向頁面父元件傳遞手勢操作變更後的樣式引數,觸發父元件setState來重新整理,最終通過傳遞props到子元件來控制樣式。關於Sticker元件的一些細節還包括:貼紙元件具有啟用狀態(點選當前元件顯示控制器,而其他元件則隱藏);切換場景後,要快取之前使用者的操作,當切回到原先的場景時,則恢復到該場景下使用者最後的操作狀態。
使用者點選儲存後,將作圖區的所有元素按照層級大小進行排序,然後通過微信提供的canvas介面進行繪製,最終返回所見即所得的合成美圖。
準備工作
根據Taro的文件,安裝CLI工具以及建立專案模板,建議選擇Typescript開發方式。
專案目錄
簡要分析下專案結構
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
複製程式碼
核心程式碼分析
- sticker貼紙元件
貼紙元件相較其他展示型元件,涉及手勢操作,大小位置計算等,所以稍顯複雜。
// 使用
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>
)
}
}
複製程式碼
- 快取服務 快取服務對提高效能非常有幫助,比如canvas繪圖需要圖片是本地圖片,可以通過資料字典的方式將圖片的遠端地址和下載到本地的地址進行一一對應,節省了大量的網路資源和時間
// 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)
}
}
複製程式碼
效能優化
- 避免頻繁setState
由於微信小程式邏輯層和檢視層各自獨立,兩邊的資料傳輸是靠轉換後的字串。因此當setData頻率過快,內容龐大時,會導致阻塞。由於本專案又涉及很多的手勢操作,touchmove事件的頻率很快,所以專案早期時候,在安卓系統下卡頓十分明顯。
優化方式有:通過做函式節流,降低setData頻次;將頁面無關的資料不要繫結到data上,而是繫結到元件例項上(犧牲運算效率換取空間效率)。
使用微信的自定義元件,也是一個很大的提升因素,個人認猜測可能是自定義元件內部data的改變不會導致其他元件或頁面的data更新。專案早期採用的是wepy框架,由於歷史侷限性(當時微信還未公佈自定義元件方案),所以效率問題一直很是頭疼。好在Taro框架通過編譯的方式完美的支援了這個方案。
- 歸併setState
例如,當圖片載入,獲取到原始尺寸後,需要計算出該圖片在當前場景下的預設尺寸和位置。必須先計算出自適應後的寬高,然後才能計算出預設的偏移量。因此可以將尺寸和位置引數都計算完畢後,再呼叫setState更新檢視,這樣不僅降低了頻次,同時也解決了圖片閃爍的bug.
- 利用快取
前面有提到過利用快取模組來儲存元件狀態或資源資訊,在此不再贅述。
心得
Taro框架採取的是一種編譯的方式,將原始碼分別編譯出可以在不同端(微信/百度/支付寶/位元組跳動小程式、H5、React-Native 等),因此可以在效能上與各個平臺保持一致。
而mpvue的方案則是修改vue的runtime,將vue 例項與小程式 Page 例項建立關聯以及生命週期的繫結。私以為,這種通過對映的方式可能會導致通訊效率上的降低,並且vue和微信又各自獨立迭代,後期的協調也越來越費勁,所以個人感覺上,還是Taro的方案略勝一籌。個人薄見,還請海涵。
寫在最後
-
Github
歡迎大家來這個demo專案下進行交流,專案地址 (github.com/HarryChen05…), 你的點贊將是我莫大的動力?
-
線上專案
本demo專案的線上小程式可通過微信掃描下面的二維碼前往體驗?
- 打波廣告
versa是一家致力於將人工智慧技術應用於視覺影像領域的AI公司,誠招各類開發人員,感興趣的小夥伴可以站內聯絡我。 (允許轉載,請註明出處)