uni-app skyline worklet 纯 setup 写法分享 ts支持 全场景支持

发布于 1周前 作者 nodeper 来自 Uni-App

uni-app skyline worklet 纯 setup 写法分享 ts支持 全场景支持

微信小程序工作流工具与示例

仅在微信小程序测试,多端请自行添加条件编译

工作流工具 worklet.ts

declare const wx: {  
  worklet: {  
    timing: (value: number, options?: { duration?: number, easing?: any }, callback?: Function) => AnimationObject  
    runOnJS: any  
    shared: <T>(val: T) => SharedValue<T>  
    derived: <T>(val: () => T) => SharedValue<T>  
    Easing: {  
      in: any  
      out: any  
      inOut: any  
      ease: any  
    }  
  }  
}  

export interface SharedValue<T = any> { value: T }  
export type AnimationObject = any  

export const worklet = wx.worklet  

export function getMpInstance() {  
  return (getCurrentInstance()?.proxy as any).$scope  
}  

export interface MpInstance {  
  applyAnimatedStyle: (selector: string, callback: () => Record<string, any>) => void  
}  

export function workletValue<T>(val: T) {  
  return worklet.shared(val)  
}  

export function workletDerived<T>(val: () => T) {  
  return worklet.derived(val)  
}  

export function workletMethods<T extends { [key: string]: Function }>(methods: T) {  
  const mpInstance = getMpInstance()  

  for (const key in methods)  
    mpInstance[key] = methods[key]  

  return methods  
}  

export function useApplyAnimatedStyle() {  
  const mpInstance = getMpInstance()  
  return (selector: string, callback: () => Record<string, any>) => {  
    mpInstance.applyAnimatedStyle(selector, callback)  
  }  
}  

export const GestureState = {  
  POSSIBLE: 0, // 0 此时手势未识别,如 panDown等  
  BEGIN: 1, // 1 手势已识别  
  ACTIVE: 2, // 2 连续手势活跃状态  
  END: 3, // 3 手势终止  
  CANCELLED: 4, // 4 手势取消,  
}  

export const ScrollState = {  
  scrollStart: 0,  
  scrollUpdate: 1,  
  scrollEnd: 2,  
}

使用案例

<script lang="ts">  
export default {  
  options: {  
    virtualHost: true,  
  },  
}  
</script>  

<script setup lang="ts">  
const props = defineProps<{  
  open: boolean  
}>()  

const _height = workletValue(0)  

const applyAnimatedStyle = useApplyAnimatedStyle()  
const { getBoundingClientRect } = useSelectorQuery()  

onMounted(() => {  
  applyAnimatedStyle('.collapse-item', () => {  
    'worklet'  

    return {  
      height: _height.value < 0 ? 'auto' : `${_height.value}px`,  
    }  
  })  
})  

watch(() => props.open, () => {  
  getBoundingClientRect('.collapse-content')  
    .then(rect => rect.height ?? 0)  
    .then((height) => {  
      if (!props.open) {  
        if (_height.value === -1)  
          _height.value = height  
        _height.value = worklet.timing(0, { duration: 300, easing: worklet.Easing.inOut(worklet.Easing.ease) })  
      }  
      else {  
        _height.value = worklet.timing(height, { duration: 300, easing: worklet.Easing.inOut(worklet.Easing.ease) }, () => {  
          'worklet'  
          if (_height.value === height)  
            _height.value = -1  
        })  
      }  
    })  
})  
</script>  

<template>  
  <view class="collapse-item block overflow-hidden">  
    <view class="collapse-content block">  
      <slot />  
    </view>  
  </view>  
</template>

Worklet JS互调 Inject Popup 示例

popup.ts

const [useProvidePopupStore, usePopupStoreInner] = createInjectionState((close: () => void) => {  
  const _transY = workletValue(1000)  
  const _scrollTop = workletValue(0)  
  const _startPan = workletValue(true)  
  const _popupHeight = workletValue(1000)  

  return { _transY, _scrollTop, _startPan, _popupHeight, close }  
})  

export { useProvidePopupStore }  

export function usePopupStore() {  
  const popupStore = usePopupStoreInner()  
  if (popupStore == null)  
    throw new Error('Please call `useProvidePopupStore` on the appropriate parent component')  
  return popupStore  
}  

export function usePopupWorkletMethods() {  
  const { _popupHeight, _scrollTop, _startPan, _transY, close } = usePopupStore()  

  return workletMethods({  
    openPopup() {  
      'worklet'  
      _transY.value = worklet.timing(0, { duration: 200 })  
    },  
    closePopup() {  
      'worklet'  
      _transY.value = worklet.timing(_popupHeight.value, { duration: 200 })  
      worklet.runOnJS(close)()  
    },  
    shouldPanResponse() {  
      'worklet'  
      return _startPan.value  
    },  
    shouldScrollViewResponse(pointerEvent: any) {  
      'worklet'  
      if (_transY.value > 0)  
        return false  
      const scrollTop = _scrollTop.value  
      const { deltaY } = pointerEvent  
      const result = scrollTop <= 0 && deltaY > 0  
      _startPan.value = result  
      return !result  
    },  
    handlePan(gestureEvent: any) {  
      'worklet'  
      if (gestureEvent.state === GestureState.ACTIVE) {  
        const curPosition = _transY.value  
        const destination = Math.max(0, curPosition + gestureEvent.deltaY)  
        if (curPosition === destination)  
          return  
        _transY.value = destination  
      }  

      if (  
        gestureEvent.state === GestureState.END  
        || gestureEvent.state === GestureState.CANCELLED  
      ) {  
        if (gestureEvent.velocityY > 500 && _transY.value > 50)  
          this.closePopup()  
        else if (_transY.value > _popupHeight.value / 2)  
          this.closePopup()  
        else  
          this.openPopup()  
      }  
    },  
    adjustDecelerationVelocity(velocity: number) {  
      'worklet'  
      const scrollTop = _scrollTop.value  
      return scrollTop <= 0 ? 0 : velocity  
    },  
    handleScroll(evt: any) {  
      'worklet'  
      _scrollTop.value = evt.detail.scrollTop  
    },  
  })  
}

popup.vue

<script lang="ts">  
export default {  
  options: {  
    virtualHost: true,  
  },  
}  
</script>  

<script setup lang="ts">  
import { useProvidePopupStore } from './popup'  

const props = withDefaults(defineProps<{  
  open: boolean  
  fullScreen?: boolean  
  rounded?: boolean  
}>(), {  
  fullScreen: false,  
  rounded: true,  
})  

const emit = defineEmits<{  
  (e: 'update:open', val: boolean): void  
  (e: 'scrolltolower'): void  
  (e: 'leave'): void  
  (e: 'afterLeave'): void  
  (e: 'enter'): void  
  (e: 'afterEnter'): void  
}>()  

function onClose() {  
  emit('update:open', false)  
}  

const { _transY, _popupHeight } = useProvidePopupStore(onClose)  

const applyAnimatedStyle = useApplyAnimatedStyle()  

const { getBoundingClientRect } = useSelectorQuery()  

onMounted(() => {  
  getBoundingClientRect('.popup-container').then((res) => {  
    _transY.value = _popupHeight.value = res.height ?? 0  
  })  

  applyAnimatedStyle('.popup-container', () => {  
    'worklet'  
    return {  
      'transform': `translateY(${_transY.value}px)`,  
      'box-shadow': `_transY.value !== _popupHeight.value ? 10 : 0}px 0px rgba(0, 0, 0, 0.2)`,  
    }  
  })  

  applyAnimatedStyle(`.popup-mask`, () => {  
    'worklet'  
    return {  
      opacity: `${1 - _transY.value / _popupHeight.value}`,  
      display: `${_transY.value !== _popupHeight.value ? 'flex' : 'none'}`,  
    }  
  })  
})  

function onAfterEnter() {  
  emit('afterEnter')  
}  

function onAfterLeave() {  
  emit('afterLeave')  
}  

watch(() => props.open, () => {  
  if (props.open) {  
    emit('enter')  
    _transY.value = worklet.timing(0, { duration: 200 }, () => {  
      'worklet'  

      worklet.runOnJS(onAfterEnter)()  
    })  
  }  
  else {  
    emit('leave')  
    _transY.value = worklet.timing(_popupHeight.value, { duration: 200 }, () => {  
      'worklet'  

      worklet.runOnJS(onAfterLeave)()  
    })  
  }  
})  
</script>  

<template>  
  <root-portal>  
    <view class="popup-mask absolute left-0 top-0 h-screen w-screen" @tap="onClose" />  
    <view class="popup-container absolute bottom-0 w-screen overflow-hidden bg-white" :class="[rounded && 'rounded-t-5', fullScreen ? 'h-screen' : 'h-70vh']">  
      <slot />  
    </view>  
  </root-portal>  
</template>

<style>  
.popup-container {  
  z-index: 999;  
  transform: translateY(100%);  
  box-shadow: 0px 0px 10px 0px rgba(0, 0, 0, 0.2);  
}  

.popup-mask {  
  z-index: 998;  
  background-color: rgba(0, 0, 0, 0.5);  
  display: none;  
}  
</style>

popup-drag-view.vue

<script lang="ts">  
export default {  
  options: {  
    virtualHost: true,  
  },  
  behaviors: [virtualHostClassBehavior],  
}  
</script>  

<script setup lang="ts">  
import { usePopupWorkletMethods } from './popup'  

const { handlePan } = usePopupWorkletMethods()  

const virtualHostClass = useVirtualHostClass()  

const mergedClass = virtualHostClass  
</script>  

<template>  
  <pan-gesture-handler :worklet:ongesture="handlePan.name">  
    <view :class="mergedClass">  
      <slot />  
    </view>  
  </pan-gesture-handler>  
</template>

popup-scroll-view.vue

<script lang="ts">  
export default {  
  options: {  
    virtualHost: true,  
  },  
  behaviors: [virtualHostClassBehavior],  
}  
</script>  

<script setup lang="ts">  
import { usePopupWorkletMethods } from './popup'  

defineProps<{  
  type: string  
}>()  

defineEmits(['scrolltolower'])  

const { handlePan, shouldPanResponse, shouldScrollViewResponse, adjustDecelerationVelocity, handleScroll } = usePopupWorkletMethods()  

const virtualHostClass = useVirtualHostClass()  

const mergedClass = virtualHostClass  
</script>  

<template>  
  <pan-gesture-handler  
    tag="pan"  
    :worklet:should-response-on-move="shouldPanResponse.name"  
    :simultaneous-handlers="['scroll']"  
    :worklet:ongesture="handlePan.name"  
  >  
    <vertical-drag-gesture-handler  
      tag="scroll"  
      native-view="scroll-view"  
      :worklet:should-response-on-move="shouldScrollViewResponse.name"  
      :simultaneous-handlers="['pan']"  
    >  
      <scroll-view  
        :class="mergedClass"  
        scroll-y  
        :worklet:adjust-deceleration-velocity="adjustDecelerationVelocity.name"  
        :worklet:onscrollupdate="handleScroll.name"  
        type="list"  
        :show-scrollbar="false"  
        @scrolltolower="$emit('scrolltolower')"  
      >  
        <slot />  
      </scroll-view>  
    </vertical-drag-gesture-handler>  
  </pan-gesture-handler>  
</template>

使用方式

<u-popup v-model:open="open">  
  <u-popup-drag-view v-if="!roomDetail" class="flex-1">  
    <u-loading />  
  </u-popup-drag-view>  
  <u-popup-scroll-view v-else class="flex-1" type="list">  
    <view class="py-4">  
      <view  
        v-for="player in roomDetail?.players"  
        :key="player.steam"  
        class="mb-2 flex-row items-center px-4"  
        @click="onRoomPlayer(player.steam)"  
      >  
        <view>  
          <u-avatar class="h-8 w-8" :src="player.avatar" />  
        </view>  
        <view class="ml-2 flex-1 truncate text-sm">  
          {{ player.name }}  
        </view>  
        <view class="ml-2 text-xs text-neutral-500 font-mono">  
          {{ player.elo }}  
        </view>  
        <view class="ml-2" />  
      </view>  
      <view class="h-[var(--safe-bottom)]" />  
    </view>  
  </u-popup-scroll-view>  
</u-popup>

至此,便可以细粒度的调整popup的手势协商
在需要用到scroll-into-view的时候 slot中的元素无法被定位
便可以再次复用usePopupWorkletStore来再次实现定制化的popup-scroll-view

workletMethods的实现中,微信小程序会为具名worklet func 添加 _worklet_factory到包含他的object中 (非具名不会处理) 所以需要全部assign到mpinstance上


1 回复

uni-app 中使用 skyline worklet 可以显著优化页面渲染性能,特别是在处理复杂动画和大量计算时。结合 Vue 3 的 setup 语法和 TypeScript 支持,可以实现更高效、类型安全的代码。以下是一个简单的示例,展示了如何在 uni-app 中使用纯 setup 语法和 TypeScript 来集成 skyline worklet,并确保全场景支持。

首先,确保你的项目已经配置了 TypeScript 和 uni-app。接下来,我们将创建一个简单的 skyline worklet 并在组件中使用它。

1. 安装依赖

确保你已经安装了 uni-app 和相关依赖。对于 skyline worklet,你可能需要安装一些额外的库(具体依赖视项目需求而定)。

2. 创建 Worklet 文件

static 目录下创建一个 worklet.ts 文件,用于定义你的 worklet 代码。

// static/worklet.ts
self.addEventListener('message', (event) => {
  const { data } = event;
  // 假设我们在这里做一些复杂的计算
  const result = data * 2;
  postMessage(result);
});

3. 在组件中使用 Worklet

接下来,在你的组件中使用 setup 语法和 TypeScript 来加载和使用这个 worklet。

<template>
  <view>
    <text>{{ result }}</text>
    <button @click="runWorklet">Run Worklet</button>
  </view>
</template>

<script lang="ts" setup>
import { ref, onMounted, onUnmounted } from 'vue';

const result = ref<number>(0);
let worklet: Worker | null = null;

const runWorklet = async () => {
  if (!worklet) {
    // 加载 worklet 脚本
    worklet = new Worker(new URL('../static/worklet.ts', import.meta.url).toString());

    worklet.onmessage = (event) => {
      result.value = event.data;
    };

    worklet.onerror = (error) => {
      console.error('Worklet error:', error);
    };
  }

  // 发送数据给 worklet
  worklet.postMessage(10);
};

onUnmounted(() => {
  if (worklet) {
    worklet.terminate();
    worklet = null;
  }
});
</script>

4. 全场景支持

上述代码在 uni-app 的各个平台上(如 H5、小程序、App 等)应该都能运行,但请注意,Worker 的支持情况可能因平台而异。对于不支持 Worker 的平台,你可能需要采用其他策略,比如使用 setTimeout 模拟异步计算,或者使用平台特定的优化方案。

这个示例展示了如何在 uni-app 中使用 setup 语法和 TypeScript 集成 skyline worklet。实际项目中,你可能需要根据具体需求调整 worklet 的逻辑和组件的实现。

回到顶部