如何充分利用Composition API对Vue3项目进行代码抽离

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,
    }
}

从这个文件中我们发现,log1log2方法都是依赖于变量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是在某个组件中定义的,同时标签组件也需要获取这个变量来控制展示的状态,这之间用到了父子组件通信,那么我们不妨把这个变量写在一个公共的文件中,无论哪个组件需要用到的时候,只需要导入获取就好了,因为每次获取到的都是同一个变量

展示环节

对比1

抽离前

<template>
  <div class="import-config-container" v-show="isShow">
    <div class="import-config-alert">
      <div class="close-import-config-alert" @click="closeAlert"></div>
      <div class="import-config-alert-title">导入配置</div>
      <div class="import-config-alert-remind">说明:需要上传之前保存导出的xxx.json配置文件,文件中的信息会完全覆盖当前信息</div>
      <form action="" class="form">
        <label for="import_config_input" class="import-config-label">
          上传配置文件
          <i v-if="hasFile == 1" class="fas fa-times-circle uploadErr uploadIcon"/>
          <i v-else-if="hasFile == 2" class="fas fa-check-circle uploadSuccess uploadIcon"/>
        </label>
        <input id="import_config_input" type="file" class="select-file" ref="inputFile" @change="fileChange">
      </form>
      <lp-button type="primary" class="import-config-btn" @_click="importConfig">确认上传</lp-button>
    </div>
  </div>
</template>

<script>
import {ref, inject} from 'vue'
import lpButton from '../../public/lp-button/lp-button'
export default {
    props: {
      isShow: {
        type: Boolean,
        default: true
      }
    },
    components: {
        lpButton
    },
    setup(props, {emit}) {
        const result = ref('none')     // 导入的结果
        const isUpload = ref(false)    // 判断是否上传配置文件
        const isImport = ref(false)    // 判断配置是否导入成功
        const isLoading = ref(false)   // 判断按钮是否处于加载状态
        const inputFile = ref(null)    // 获取文件标签
        const hasFile = ref(0)         // 判断文件的传入情况。0:未传入  1: 格式错误  2:格式正确
        const $message = inject('message')
        // 导入配置
        function importConfig() {
          let reader = new FileReader()
          let files = inputFile.value.files
          if(hasFile.value == 0) {
            $message({
              type: 'warning',
              content: '请先上传配置文件'
            })
          }
          else if(hasFile.value == 1) {
            $message({
              type: 'warning',
              content: '请上传正确格式的文件,例如xx.json'
            })
          }
          else if(hasFile.value == 2) {
            reader.readAsText(files[0])
            reader.onload = function() {
              let data = this.result
              window.localStorage.navInfos = data
              location.reload()
            }
          }
        }
        // 关闭弹窗
        function closeAlert() {
          emit('closeImportConfigAlert', false)
          hasFile.value = 0
        }
        function fileChange(e) {
          let files = e.target.files
          if(files.length === 0) {
            $message({
              type: 'warning',
              content: '请先上传配置文件'
            })
          }
          else {
            let targetFile = files[0]
            if(!/\.json$/.test(targetFile.name)) {
              hasFile.value = 1
              $message({
                type: 'warning',
                content: '请确认文件格式是否正确'
              })
            } else {
              hasFile.value = 2
              $message({
                type: 'success',
                content: '文件格式正确'
              })
            }
          }
        }
        
        return {
          result, 
          isUpload,
          isImport, 
          isLoading,
          importConfig, 
          closeAlert,
          inputFile,
          fileChange,
          hasFile
        }
    }
}
</script>

抽离后

<template>
  <div class="import-config-container" v-show="isShowImportAlert">
    <div class="import-config-alert">
      <div class="close-import-config-alert" @click="handleImportConfigAlert(false)"></div>
      <div class="import-config-alert-title">导入配置</div>
      <div class="import-config-alert-remind">说明:需要上传之前保存导出的xxx.json配置文件,文件中的信息会完全覆盖当前信息</div>
      <form action="" class="form">
        <label for="import_config_input" class="import-config-label">
          上传配置文件
          <i v-if="hasFile == 1" class="fas fa-times-circle uploadErr uploadIcon"/>
          <i v-else-if="hasFile == 2" class="fas fa-check-circle uploadSuccess uploadIcon"/>
        </label>
        <input id="import_config_input" type="file" class="select-file" ref="inputFile" @change="fileChange">
      </form>
      <lp-button type="primary" class="import-config-btn" @_click="importConfig">确认上传</lp-button>
    </div>
  </div>
</template>

<script>
/* API */
import { inject } from 'vue'
/* 组件 */
import lpButton from '@/components/public/lp-button/lp-button'
/* 功能模块 */
import importConfigFunction from '@/use/importConfig'
export default {
    components: {
        lpButton
    },
    setup() {
        const $message = inject('message')
        
        const { 
          isShowImportAlert,
          handleImportConfigAlert,
          result,  
          isUpload, 
          isImport, 
          isLoading, 
          importConfig, 
          closeAlert, 
          inputFile, 
          fileChange, 
          hasFile 
        } = importConfigFunction($message)
        
        return {
          isShowImportAlert,
          handleImportConfigAlert,
          result, 
          isUpload,
          isImport, 
          isLoading,
          importConfig, 
          closeAlert,
          inputFile,
          fileChange,
          hasFile
        }
    }
}
</script>

抽离出的代码文件

// 导入配置功能
import { ref } from 'vue'

const isShowImportAlert = ref(false),   // 导入配置弹框是否展示
      result = ref('none'),             // 导入的结果
      isUpload = ref(false),            // 判断是否上传配置文件
      isImport = ref(false),            // 判断配置是否导入成功
      isLoading = ref(false),           // 判断按钮是否处于加载状态
      inputFile = ref(null),            // 获取文件元素
      hasFile = ref(0)                  // 判断文件的传入情况。0:未传入  1: 格式错误  2:格式正确
      
export default function importConfigFunction($message) {
  
    // 控制弹框的展示
    function handleImportConfigAlert(value) {
        isShowImportAlert.value = value
        if(!value) hasFile.value = 0
    }

    // 上传的文件内容发生改变
    function fileChange(e) {
        let files = e.target.files
        if(files.length === 0) {
            $message({
            type: 'warning',
            content: '请先上传配置文件'
            })
        }
        else {
            let targetFile = files[0]
            if(!/\.json$/.test(targetFile.name)) {
                hasFile.value = 1
                $message({
                    type: 'warning',
                    content: '请确认文件格式是否正确'
                })
            } else {
            hasFile.value = 2
                $message({
                    type: 'success',
                    content: '文件格式正确'
                })
            }
        }
    }

    // 导入配置
    function importConfig() {
        let reader = new FileReader()
        let files = inputFile.value.files
        if(hasFile.value == 0) {
          $message({
            type: 'warning',
            content: '请先上传配置文件'
          })
        }
        else if(hasFile.value == 1) {
          $message({
            type: 'warning',
            content: '请上传正确格式的文件,例如xx.json'
          })
        }
        else if(hasFile.value == 2) {
          reader.readAsText(files[0])
          reader.onload = function() {
            let data = this.result
            window.localStorage.navInfos = data
            location.reload()
          }
        }
    }

    return {
        isShowImportAlert,
        result,
        isUpload,
        isImport,
        isLoading,
        inputFile,
        hasFile,
        handleImportConfigAlert,
        fileChange,
        importConfig,
    }
}

最后

本文所阐述的代码抽离方法是我改过很多遍后定下来的,不知道后面还会有什么问题,但目前看来,对于以后的维护和管理应该是会方便很多的,如果大家有更好的意见或想法,可以留下评论,或者加我vx:XFJ–123私底下交流

最后谢谢各位的耐心观看

写文章不容易,希望各位多多留言给我提提意见,别忘了点个赞👍哦~