复制粘贴很困难?看看如何使用组合式API帮助你加快跨端同构的效率!

2,446 阅读11分钟

摘要
本文分享了一种组合式API方法论,通过3个原则和1个方法,可以有效帮助开发者更好地掌握组合式API,更好地发挥出组合式API的编程优势。并且结合一个跨小程序、App的案例,介绍如何遵循方法论,完成跨端的需求,实现客户想要的效果。

前情提要

vue3 option api的新玩法😂 - 掘金 (juejin.cn)
在vue3刚发布组合式API时,我们团队尚未完全掌握组合式API的最佳用法,强行跟风导致代码风格混乱,因此团队暂时退回了option API的风格,于是留下了上述这篇文章。

但今天,经过近三个月的研究、学习、分享、培训,以及在项目中的实践,我们团队已经能很好掌握组合式API的用法,并总结出了使用的方法论。此次,借本次项目中的场景,向大家介绍Vue3组合式API的用法。

组合式API开发方法论

此方法论适用于所有使用类似React Hook编程范式的开发框架。

1. 逻辑聚合原则

我们使用组合式API进行开发时,应当遵循逻辑聚合原则。所谓逻辑聚合,即将负责相同逻辑的变量、事件、方法、生命周期、computed、watch、provide、inject等,统统写在一起。

参考Vue官网提供的对比图,不同颜色代表不同的逻辑。对比我们发现,组合式API的颜色区块都是连在一起的,这样是非常有助于代码开发和维护的。 optionsAPI、CompositionAPI的对比

我们建议,在开发时,可以在逻辑模块上面加上一行注释,表明这块逻辑模块的目的,推荐使用jsdoc的todo标识:

<template>
    <div>
        会议时长:{{time}}
        <footer>
            <div v-for="item in tools" @click="onClickTool(item)"/>
        </footer>
    </div>
</template>
<script setup>
import {ref,onMounted,onBeforeUnmount} from "vue"

/**@todo 设置会议时长 */
const time = ref('00:00:00');
let duration = 0; //会议时长,ms;
let timeout = undefined;
onMounted(()=>{
    duration = 1000000
    onLoop();
});
onBeforeUnmount(()=>{
    clearTimeout(timeout)
})
function onLoop(){
    timeout=setTimeout(()=>{
        duration+=1000;
        setTime()
    },1000)
}
function setTime(){
    // duration 转 hh:mm:ss格式,具体代码略
    time.value = 'hh:mm:ss'
}


/**@todo 设置底部工具栏 */
const tools = ref([]);
onMounted(()=>{})
function onClickTool(item){}
</script>

2. 先开发后封装再拆分原则

我们使用组合式API进行开发时,应当遵循先开发后封装再拆分原则。

  • 先开发,即先不要考虑任何封装的问题,而是按照业务要求,该做什么就写什么,直接平铺直叙。并且,在vue里的直接使用javascript去开发,你的关注点应该是业务逻辑,而不是其他封装、ts类型体操等等。例子:
import {ref,onMounted,onBeforeUnmount} from "vue"

/**@todo 设置会议时长 */
const time = ref('00:00:00');
let duration = 0; //会议时长,ms;
let timeout = undefined;
onMounted(()=>{});
onBeforeUnmount(()=>{})
function onLoop(){}
function setTime(){}


/**@todo 设置底部工具栏 */
const tools = ref([]);
onMounted(()=>{})
function onClickTool(item){}
  • 后封装,即如果逻辑代码很长,且与其他逻辑模块耦合性不是很强的时候,可以考虑用一个use函数给逻辑封装起来。这样可以很好利用IDE的代码折叠功能,在阅读、重审其他逻辑时,将这一整块的逻辑隐藏起来。例子:
import {ref,onMounted,onBeforeUnmount} from "vue"

/**@todo 设置会议时长 */
const {time} = useTime();
function useTime(){
    const time = ref('00:00:00');
    let duration = 0; //会议时长,ms;
    let timeout = undefined;
    onMounted(()=>{});
    onBeforeUnmount(()=>{})
    function onLoop(){}
    function setTime(){}
    return {
        time
    }
}


/**@todo 设置底部工具栏 */
const tools = ref([]);
function onClickTool(item){}
  • 再拆分,即如果逻辑模块有复用功能时,才考虑将已经封装好的逻辑模块,以ts文件的形式拆分出去。拆分的方式非常简单,在已经封装的基础上,直接把封装好的函数剪切出去新的ts文件里,然后再利用ts的检查功能,将依赖补全。例子:
// ts文件
import {ref,onMounted,onBeforeUnmount} from "vue";// <-补全依赖
export default function useTime(){
    const time = ref('00:00:00');
    let duration = 0; //会议时长,ms;
    let timeout = undefined;
    onMounted(()=>{});
    onBeforeUnmount(()=>{})
    function onLoop(){}
    function setTime(){}
    return {
        time
    }
}

// -------------------------------------
// vue文件(开发时建议用js写,效率高)
import {ref,onMounted} from "vue"

/**@todo 设置会议时长 */
import useTime from './useTime.ts'
const {time} = useTime();


/**@todo 设置底部工具栏 */
const tools = ref([]);
onMounted(()=>{})
function onClickTool(item){}

一开始团队中就有同事很喜欢一开始就用一个use函数将逻辑块进行定义封装,或者直接创建一个js/ts文件去写逻辑。但是通过实践先开发后封装再拆分的原则后,我们发现了项目里九成以上的业务代码根本走不到“后封装”的阶段。

所以我们建议,应当把精力首先用在业务逻辑的实现上。

3. 页面核心逻辑不封装原则

我们使用组合式API进行开发时,应当遵循页面核心逻辑不封装原则。所谓页面核心逻辑,即在任何一个页面下,产品经理设计上最注重,后续改动最频繁,且没有其他页面复用的逻辑模块。

核心逻辑应当写在vue文件里,这样可以直接通过路由找到vue文件,然后直接在vue文件里面就可以直接维护了。

4. 逻辑解耦方法

使用组合式API开发时,各种逻辑耦合是非常常见的场景。我们分析了项目里面的代码,总结出了出现耦合的两种常见情况:

  • 初始化耦合:当前逻辑模块的响应变量创建时,就要需要立即读取其他逻辑模块的相关内容作为初始值;
  • 事件耦合:当某个事件、生命周期、响应式数据监听器触发时,当前逻辑模块需要访问其他逻辑模块的相关内容或方法去做相关处理。

针对两种情况,可以采用下面方法进行解耦处理:

  1. 在“先开发”阶段,不做任何处理,该怎么样访问其他逻辑模块,就怎么样访问其他逻辑模块;逻辑解耦应当在“后封装”的阶段进行,不管是当前逻辑需要封装还是耦合着的逻辑需要封装。
  2. 针对初始化耦合,可以在封装时暴露一个init方法,这个方法接受外部参数,然后实现初始化逻辑,最后在所有逻辑模块声明完之后进行调用。开发时,因为业务的不同,逻辑模块的声明顺序也是不一样的,我们强行去约束是不现实的。所以干脆采用初始化的方式,提供一个方法,放在代码的最后面执行:
/**@todo 逻辑1*/
const {init:init1, var1} = useLogic1();
function useLogic1(){
    const var1 = ref();
    return {
        var1,
        init({var2,var3}){
            // 做初始化工作
        }
    }
}

/**@todo 逻辑2*/
import useLogic2 from './useLogic2.ts'
const {init:init2, var2} = useLogic2();

/**@todo 逻辑3*/
import useLogic3 from './useLogic3.ts'
const {init:init3, var3} = useLogic3();

/**@todo 耦合逻辑初始化*/
init1({var2, var3})
init2({var1, var3})
init3({var1, var2})
  1. 针对事件耦合,可以在封装时,定义参数为一个包含事件回调钩子的对象。然后在当前逻辑相关事件触发时,调用参数里的钩子。其他逻辑模块只需要在当前逻辑模块的use函数里,找到对应的钩子,写入逻辑即可:
/**@todo 逻辑1*/
const {init:init1, var1,onClick} = useLogic1({
    onClickHappened(){
        return {var2,var3} // 事件发生时,var2和var3已经有声明了
    }
});
function useLogic1({onClickHappened}){
    const var1 = ref();
    function onClick(e){
        const {var2,var3} = onClickHappened(e);
    }
    return {
        var1,
        onClick,
        init({var2,var3}){
            // 做初始化工作
        }
    }
}

/**@todo 逻辑2*/
import useLogic2 from './useLogic2.ts'
const {init:init2, var2} = useLogic2();

/**@todo 逻辑3*/
import useLogic3 from './useLogic3.ts'
const {init:init3, var3} = useLogic3();

/**@todo 耦合逻辑初始化*/
init1({var2, var3})
init2({var1, var3})
init3({var1, var2})

实战:跨端SDK适配

效果截图

下面分别是小程序和App的会诊页面:

小程序页面

App页面

项目选型

此次项目是一个医疗项目,其最复杂的交互场景,是由多个医生使用Web或App发起对患者的远程会诊。经过团队评估,移动端项目原型以及交互不复杂,且场景对于应用的性能要求并不高,因此团队选用uniapp这个框架来构建App。考虑因素有二:一方面考虑团队内有会iOS和Andorid的前端开发,可以在uniapp中使用第三方音视频厂商提供的原生插件;另一方面是预防甲方医院临验收时突然追加小程序的需求。

作为前端负责人,在uniapp版本的选型上,则大胆地选用了vue3版本。一方面是经过几次版本更新,uniapp+vue3的版本已经趋于稳定,在这几个月的开发中,没有发现关于uniapp的恶性bug。另一方面是使用setup的语法,真的可以让开发变得简单,更重要的是,我们团队已经掌握了编写组合式API的方法论。

而这次选型,成功地预判到甲方追加小程序需求。这让我们开发小程序时的工作量近乎没有,只剩下少部分跨端场景需要投入精力。

跨端场景

受第三方远程音视频SDK的限制,App端必须使用nvue(即weex)才可以支持视频通话功能。而小程序端有小程序端的SDK,使用方法与原生插件SDK的调用方法差异巨大:原生插件提供了自定义的weex组件来提供视频推流拉流功能;小程序则使用微信内置的live-push、live-player组件来提供视频推拉流的功能。

因此,我们利用Uniapp打包App时优先打包nvue文件、打包小程序时优先打包vue文件的特性,针对小程序的使用场景,拆分出多一个同名的vue文件进行开发,这样js逻辑和template结构可以做到即可复用又有差异性,可针对用户需求灵活处理。

组合式API处理跨端代码

1. 分析

在nvue文件里,我们已经按照逻辑聚合的原则,将页面的逻辑分成各个逻辑模块,分别是:

模块名称适用端说明
字体图标配置模块APP无法通过css注册字体图标
导航栏样式设置模块APP、小程序
样式配置模块APP、小程序适用js编写nvue的样式,这里小程序就复用了
SDK模块APP小程序有小程序的sdk,APP有App的sdk
视图控制模块APP、小程序
会议时间模块APP、小程序
工具栏模块APP、小程序
生命体征模块APP、小程序

同时遵循先开发后封装再拆分原则,上面8个模块都属于未封装的状态。其中查表可知,可以拆分出去的模块有6个;不需要管的模块有1个,即字体图标配置模块;完全无法复用、需要重写的模块有1个,即SDK模块。

2. 逻辑模块拆分

拆分工作很简单,参考上文案例,新建一个typescript文件,把代码剪切进去,然后用一个函数封装起来再暴露出去,最后再回来到nvue文件、vue文件里引入这个ts文件并调用。

image.png image.png 这部分花费的时间并不多,不到一个小时就弄好了。

3. 处理模块依赖

在工具栏模块中,需要调用sdk模块的相关方法,来制作静音、录音、打开摄像头、关闭摄像头的功能。而不同端的SDK不一样,所以这里需要使用逻辑解耦方法,制作事件回调钩。

image.png

image.png 这部分耗时并不多。

4. SDK逻辑重写

视频SDK部分是这个页面的核心模块,遵循页面核心逻辑不封装原则,这部分模块不需要封装,直接在vue文件上写即可,方便后续改逻辑。

image.png

本质上这里的工作量,是对一个新的SDK进行对接,后续如果有H5的需求,也照此处理。

总结

  1. 组合式API就是要你将逻辑聚合起来
  2. 业务第一,时间和精力不要消耗在过度封装上
  3. 善于利用JS自身的功能解决组合式API中相互依赖的问题。
  4. 组合式API真的对复制粘贴非常友好!
  5. 使用组合式API可以让你更加专注差异的部分。