背景介绍
在2020年,Vue3
的学习一直被我鸽到了11月份,在学完以后,我自己做了一个Vue3
的小项目nav-url,也整理了我对于如何快速上手Vue3
的几篇博客,很高兴受到了大家的指点和喜欢。
在上一篇博客中,我详细介绍了一下我发的第一版项目的特色、亮点以及所有核心功能的实现,希望大家可以前往阅读体验一下(记得用电脑打开,因为这是一个PC端的项目)
然而,这项目只是实现了一些功能,但我感觉并没有很好地利用Composition API
去对代码进行整合管理。要知道,Composition API
的出现就是为了解决Options API
导致相同功能代码分散的现象,也有很多大佬对其做了很多的动画展示(这里我借用一下大帅搞全栈大佬精心制作的动画,他的这篇文章可以说是好评连连)
看了一下我项目初版的代码,简直是没有体现出Composition API
的优势,可以给大家看一下某个组件内的代码
<template> <aside id="tabs-container"> <div id="logo-container"> {{ navInfos.navName }} </div> <ul id="tabs"> <li class="tab tab-search" @click="showSearch"> <i class="fas fa-search tab-icon"/> <span>快速搜索</span> </li> <li class="tab tab-save" @click="showSaveConfigAlert"> <i class="fas fa-share-square tab-icon"></i> <span>保存配置</span> </li> <li class="tab tab-import" @click="showImportConfigAlert"> <i class="fas fa-cog tab-icon"></i> <span>导入配置</span> </li> <br> <li v-for="(item, index) in navInfos.catalogue" :key="index" class="tab" @click="toID(item.id)"> <span class="li-container"> <i :class="['fas', `fa-${item.icon}`, 'tab-icon']" /> <span>{{ item.name }}</span> <i class="fas fa-angle-right tab-icon tab-angle-right"/> </span> </li> <li class="tab add-tab" @click="addTabShow"> <i class="fas fa-plus"/> </li> </ul> <!-- 添加标签弹框 --> <tabAlert /> <!-- 保存配置弹框 --> <save-config @closeSaveConfigAlert="closeSaveConfigAlert" :isShow="isShowSaveAlert"/> <!-- 导入配置弹框 --> <import-config @closeImportConfigAlert="closeImportConfigAlert" :isShow="isShowImportAlert"/> </aside> </template> <script> import {ref} from 'vue' import {useStore} from 'vuex' import tabAlert from '../public/tabAlert/tabAlert' import saveConfig from './childCpn/saveConfig' import importConfig from './childCpn/importConfig' export default { name: 'tabs', components: { tabAlert, saveConfig, importConfig }, setup() { const store = useStore() let navInfos = store.state // Vuex的state对象 let isShowSaveAlert = ref(false) // 保存配置弹框是否展示 let isShowImportAlert = ref(false) // 导入配置弹框是否展示 // 展示"添加标签弹框" function addTabShow() { store.commit('changeTabInfo', [ {key: 'isShowAddTabAlert', value: true}, {key: 'alertType', value: '新增标签'} ]) } // 关闭"保存配置弹框" function closeSaveConfigAlert(value) { isShowSaveAlert.value = value } // 展示"保存配置弹框" function showSaveConfigAlert() { isShowSaveAlert.value = true } // 展示"导入配置弹框" function showImportConfigAlert() { isShowImportAlert.value = true } // 关闭"导入配置弹框" function closeImportConfigAlert(value) { isShowImportAlert.value = value } // 展示搜索框 function showSearch() { if(store.state.moduleSearch.isSearch) { store.commit('changeIsSearch', false) store.commit('changeSearchWord', '') } else { store.commit('changeIsSearch', true) } } // 跳转到指定标签 function toID(id) { const content = document.getElementById('content') const el = document.getElementById(`${id}`) let start = content.scrollTop let end = el.offsetTop - 80 let each = start > end ? -1 * Math.abs(start - end) / 20 : Math.abs(start - end) / 20 let count = 0 let timer = setInterval(() => { if(count < 20) { content.scrollTop += each count ++ } else { clearInterval(timer) } }, 10) } return { navInfos, addTabShow, isShowSaveAlert, closeSaveConfigAlert, showSaveConfigAlert, isShowImportAlert, showImportConfigAlert, closeImportConfigAlert, showSearch, toID } } } </script>
上述代码是我项目中侧边栏中所有的变量以及方法,虽说变量和方法都同时存在于setup
函数中了,但是仍看起来杂乱无章,若是这个组件的业务需求越来越复杂,这个setup
内的代码可能更乱了
于是,我便开始构思如何抽离我的代码。后来在掘金的沸点上说了一下我的思路,并且询问了一下其他掘友的建议
其实最后一位老哥的回答对我启发很大,因此我也借鉴了一下它的思路对我的项目代码进行了抽离
准备工作
首先我得思考一个问题:抽离代码时,是按照组件单独抽离?还是按照整体功能抽离?
最后我决定按照整体的功能去抽离代码,具体功能列表如下:
- 搜索功能
- 新增/修改标签功能
- 新增/修改网址功能
- 导入配置功能
- 导出配置功能
- 编辑功能
开始抽出代码
上述的每一个功能都会通过一个JS
文件去存储该功能对应的变量以及方法。然后所有的JS
文件都是放在src/use
下的,如图
就拿 新增/修改标签功能 来举例子,用一个动图给大家看看该功能的全部效果
很明显,我是做了一个弹窗组件,当点击侧边栏中的 + 号后,弹窗显示;然后我输入了想要新增标签的名称,并且选择了合适的图标,最后点击了确认,于是一个标签就添加好了,弹窗也随之隐藏;
最后我又去编辑模式下点击修改标签,弹窗再次显示,与此同时把对应标签的名称与图标都渲染了出来;待我修改了名字后,点击了确认,于是标签的信息就被我改好了,弹窗又随之隐藏了。
所以总结一下涉及到的功能就有以下几个:
- 弹窗的展示
- 弹窗的隐藏
- 点击确认后新增或修改标签内容
按照传统的写法,实现上述三个功能是这个样子的(我修改并简化了代码,大家理解意思就行):
- 侧边栏组件内容
<!-- 侧边栏组件内容 --> <template> <aside> <div @click="show">新增标签</div> <tab-alert :isShow="isShow" @closeTabAlert="close"/> </aside> </template> <script> import { ref } from 'vue' import tabAlert from '@/components/tabAlert/index' export default { name: "tab", components: { tabAlert }, setup() { // 存储标签弹框的展示情况 const isShow = ref(false) // 展示标签弹框 function show() { isShow.value = true } // 隐藏标签弹框 function close() { isShow.value = false } return { isShow, show, close } } } </script>
- 标签弹框组件内容
<!-- 标签弹框组件内容 --> <template> <div v-show="isShow"> <!-- 此处省略一部分不重要的内容代码 --> <div @click="close">取消</div> <div @click="confirm">确认</div> </div> </template> <script> export default { name: "tab", props: { isShow: { type: Boolean, default: false } }, setup(props, {emit}) { // 隐藏标签弹框 function close() { emit('close') } // 点击确认后的操作 function confirm() { /* 此处省略点击确认按钮后更新标签内容的业务代码 */ close() } return { close, confirm } } } </script>
看完了我上面举例的代码后可以发现,简简单单的一个功能的实现,却涉及到两个组件,而且还需要父子组件相互通信来控制一些状态,这样不就把功能打散了嘛,即不够聚合。所以按照功能来抽离这些功能代码时,我会为他们创建一个 tabAlert.js
文件,里面存储着关于这个功能所有的变量与方法。
tabAlert.js
文件中的大致结构是这样的:
// 引入依赖API import { ref } from 'vue' // 定义一些变量 const isShow = ref(false) // 存储标签弹框的展示状态 export default function tabAlertFunction() { /* 定义一些方法 */ // 展示标签弹框 function show() { isShow.value = true } // 关闭标签弹框 function close() { isShow.value = false } // 点击确认按钮以后的操作 function confirm() { /* 此处省略点击确认按钮后更新标签内容的业务代码 */ close() } return { isShow, show, close, confirm, } }
对于为何设计这样的结构,先从导出的方法来说,我把跟该功能相关的所有方法放在了一个函数中,最后通过return
导出,是因为有时候这些方法会依赖于外部其它的变量,所以用函数包裹了一层,例如:
// example.js export default function exampleFunction(num) { function log1() { console.log(num + 1) } function log2() { console.log(num + 2) } return { log1, log2, } }
从这个文件中我们发现,log1
和log2
方法都是依赖于变量num
的,但我们并没有在该文件中定义变量num
,那么可以在别的组件中引入该文件时,给最外层的exampleFunction
方法传递一个参数num
即可
<template> <button @click="log1">打印加1</button> <button @click="log2">打印加2</button> </template> <script> import exampleFunction from './example' import { num } from './getNum' // 假设num是从别的模块中获取到的 export default { setup() { let { log1, log2 } = exampleFunction(num) return { log1, log2 } } } </script>
然后再来说说为什么变量的定义在我们导出函数的外部。再继续看我上面举的我项目中标签页功能的例子吧,用于存储标签弹框展示状态的变量isShow
是在某个组件中定义的,同时标签组件也需要获取这个变量来控制展示的状态,这之间用到了父子组件通信,那么我们不妨把这个变量写在一个公共的文件中,无论哪个组件需要用到的时候,只需要导入获取就好了,因为每次获取到的都是同一个变量
这样一来,岂不是连父子组件通信都省了嘛?
我们把刚刚封装好的tabAlert.js
用到组件中去,看看是什么效果
- 侧边栏组件内容
<!-- 侧边栏组件内容 --> <template> <aside> <div @click="show">新增标签</div> <tab-alert/> </aside> </template> <script> import tabAlert from '@/components/tabAlert/index' import tabAlertFunction from '@/use/tabAlert' export default { name: "tab", components: { tabAlert }, setup() { let { show } = tabAlertFunction() return { show } } } </script>
- 标签弹框组件内容
<!-- 标签弹框组件内容 --> <template> <div v-show="isShow"> <!-- 此处省略一部分不重要的内容代码 --> <div @click="close">取消</div> <div @click="confirm">确认</div> </div> </template> <script> import tabAlertFunction from '@/use/tabAlert' export default { name: "tab", setup() { let { isShow, close, confirm } = tabAlertFunction() return { isShow, close, confirm } } } </script>
这时候再翻上去看看最初的代码,有没有感觉代码抽离后,变得非常规整,而且组件中少了很多的代码量。
这样通过功能来将变量和代码聚集在一起的方法,我个人认为是比较好管理的,倘若之后有一天想在该功能上新增什么小需求,只要找到tabAlert.js
这个文件,在里面写方法和变量即可
展示环节
我就是按照这样的方法,对我原本的代码进行了抽离,下面给大家看几组抽离前和抽离后的代码对比